<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>내일 한걸음 더</title>
    <link>https://annajin.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 16 Jul 2026 09:10:30 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Anna-Jin</managingEditor>
    <image>
      <title>내일 한걸음 더</title>
      <url>https://tistory1.daumcdn.net/tistory/4959393/attach/83617c08e6324d64949085068d903c1f</url>
      <link>https://annajin.tistory.com</link>
    </image>
    <item>
      <title>[CS]권한마다 보안 솔루션이 다르다면? 하이브리드 환경에서 우아하게 파일 보호하기</title>
      <link>https://annajin.tistory.com/241</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;시간이 좀 흘렀지만 작년 말 사내 기술블로그에 투고한 글을 드디어 블로그에 남겨본다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;백오피스를 주로 담당하면서 파일 보호 및 암호화와 관련 레거시 솔루션을 (나름 업계에서는 유명하다고 하는...) 모던 솔루션으로 전환하면서 고민한 내용을 담고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;어떻게 보면 기본적인 패턴과 방식일 수도 있겠지만 누군가에게는 도움이 되지 않을까하여 주제로 다뤄보았다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;레거시 솔루션과 하이브리드로 동작해야하는 환경이었는데 레거시 솔루션이 mac 환경에서는 동작하지 않아서 애를 좀 먹었던 일과 솔루션 전환 과정에서 QA 엔지니어 분들과 밤 늦게까지 수십개의 화면이 정상적으로 동작하는 지 테스트했던 과정들이 기억에 남는다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;누군가에게는 도움이 되기를 바라며...&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-12-26/protection-switch/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://oliveyoung.tech/2025-12-26/protection-switch/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1783426031240&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;권한마다 보안 솔루션이 다르다면? 하이브리드 환경에서 우아하게 파일 보호하기 | 올리브영 테&quot; data-og-description=&quot;전략 패턴과 AOP로 비즈니스 로직과 보안 정책 깔끔하게 분리하기&quot; data-og-host=&quot;oliveyoung.tech&quot; data-og-source-url=&quot;https://oliveyoung.tech/2025-12-26/protection-switch/&quot; data-og-url=&quot;https://oliveyoung.tech/2025-12-26/protection-switch/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eGQde/dJMb82MOJR5/yPTIfE6iyUbYzZirph1MCk/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024,https://scrap.kakaocdn.net/dn/cXzXYT/dJMb85v0Cib/ZgrFPQZTFyN6NEatpMbBWK/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024,https://scrap.kakaocdn.net/dn/wi1Ra/dJMb81f4bue/wXintcgvNOQAkw2L9djbR0/img.png?width=210&amp;amp;height=210&amp;amp;face=0_0_210_210&quot;&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-12-26/protection-switch/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://oliveyoung.tech/2025-12-26/protection-switch/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eGQde/dJMb82MOJR5/yPTIfE6iyUbYzZirph1MCk/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024,https://scrap.kakaocdn.net/dn/cXzXYT/dJMb85v0Cib/ZgrFPQZTFyN6NEatpMbBWK/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024,https://scrap.kakaocdn.net/dn/wi1Ra/dJMb81f4bue/wXintcgvNOQAkw2L9djbR0/img.png?width=210&amp;amp;height=210&amp;amp;face=0_0_210_210');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;권한마다 보안 솔루션이 다르다면? 하이브리드 환경에서 우아하게 파일 보호하기 | 올리브영 테&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;전략 패턴과 AOP로 비즈니스 로직과 보안 정책 깔끔하게 분리하기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;oliveyoung.tech&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 블로그에 투고한 글을 그대로 남긴 내용이므로 링크에서 읽어보는걸 추천드린다.&lt;/p&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;h1 id=&quot;1-들어가며-변화의-지침과-현실적인-제약&quot; style=&quot;color: #333e4c; text-align: start;&quot;&gt;&lt;b&gt;1. 들어가며: 변화의 지침과 현실적인 제약&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;올리브영의 오프라인 매장 운영을 책임지는 백오피스 시스템은 매일 수많은 엑셀과 PDF 문서를 생성하고 처리합니다. 그동안 우리는 이 문서들을 보호하기 위해 표준화된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;설치형 에이전트(Agent) 보안 솔루션&lt;/b&gt;을 사용하여 파일을 암호화해 왔습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;최근 전사 보안 정책이 고도화됨에 따라, 문서 보안 체계를&lt;span&gt;&amp;nbsp;&lt;/span&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 style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 분석 단계에서 중요한 현실적 제약에 직면했습니다. 신규 도입된 클라우드 보안 시스템은 라이선스 및 인프라 구조상 '본사 구성원'에게만 지원된다는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백오피스 시스템은 본사 구성원뿐만 아니라, 매장에서 근무하는 '매장 구성원'이나 외부 '협력사'들도 빈번하게 사용합니다. 결국, 우리는 두 가지 보안 체계를 동시에 운영해야 하는 상황이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333e4c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;본사 임직원: 신규 클라우드 기반 보안 (Type A)&lt;/li&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;그 외 (매장/협력사): 기존 에이전트 기반 보안 (Type B)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;로그인한 사용자의 권한(Role)에 따라 적절한 암호화 솔루션을 동적으로 선택하여 적용&quot;&lt;/b&gt;해야 하는 과제가 주어진 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;2-해결책-탐색-왜-전략-패턴인가&quot; style=&quot;color: #333e4c; text-align: start;&quot;&gt;&lt;b&gt;2. 해결책 탐색: 왜 전략 패턴인가?&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음부터 전략 패턴을 떠올린 건 아니었습니다. 가장 먼저 떠오른 건 역시 단순한 if-else 분기였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;21-첫-번째-시도-단순-if-else-분기&quot; style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2.1. 첫 번째 시도: 단순 if-else 분기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;일단 돌아가게 만들자&quot;는 생각으로 빠르게 프로토타입을 작성해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;java&quot;&gt;
&lt;pre class=&quot;smali&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;// 첫 번째 시도: 단순 if-else
public byte[] downloadSalesReport(User user) {
    byte[] excelData = excelService.createExcel();

    if (user.isHeadquarters()) {
        // 클라우드 API 호출
        return cloudSecurityApi.encrypt(excelData, user.getId());
    } else {
        // 에이전트 라이브러리 호출
        return agentSecurityLib.protect(excelData);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;간단해 보이지만, 실제로 적용하려니 문제가 보이기 시작했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백오피스 시스템에는 파일을 다운로드하는 API가 수십 개에 달했습니다. 매출 리포트, 재고 현황, 발주서, 정산 내역... 이 모든 곳에 동일한 if-else 로직을 복붙해야 했습니다. 그리고 만약 향후 제3의 보안 솔루션이 추가된다면? 수십 개의 파일을 다시 찾아다니며 else if를 추가해야 하는 악몽이 그려졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;22-두-번째-시도-별도-서비스-클래스로-분리&quot; style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2.2. 두 번째 시도: 별도 서비스 클래스로 분리&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; 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;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;java&quot;&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;// 두 번째 시도: 서비스 클래스 분리
@Service
public class FileEncryptionService {

    public byte[] encrypt(byte[] data, User user) {
        if (user.isHeadquarters()) {
            return cloudSecurityApi.encrypt(data, user.getId());
        } else {
            return agentSecurityLib.protect(data);
        }
    }
}

// 사용하는 쪽
public byte[] exportExccel(User user) {
    byte[] excelData = excelService.createExcel();
    return fileEncryptionService.encrypt(excelData, user);  // 호출 필요
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;if-else가 한 곳으로 모이긴 했지만, 여전히 두 가지 문제가 남았습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333e4c; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;b&gt;호출 누락 위험&lt;/b&gt;: 모든 다운로드 메서드에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;fileEncryptionService.encrypt()를 명시적으로 호출해야 합니다. 신규 API를 만들 때 깜빡하면? 암호화되지 않은 파일이 그대로 내려갑니다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;b&gt;OCP 위반&lt;/b&gt;: 새로운 보안 솔루션이 추가되면&lt;span&gt;&amp;nbsp;&lt;/span&gt;FileEncryptionService&lt;span&gt;&amp;nbsp;&lt;/span&gt;내부를 수정해야 합니다. &quot;확장에는 열려 있고, 수정에는 닫혀 있어야 한다&quot;는 개방-폐쇄 원칙(OCP)에 어긋납니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;23-세-번째-시도-팩토리-패턴&quot; style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2.3. 세 번째 시도: 팩토리 패턴?&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; 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;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;java&quot;&gt;
&lt;pre class=&quot;haxe&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;// 팩토리 패턴 검토
public class EncryptorFactory {
    public Encryptor create(User user) {
        if (user.isHeadquarters()) {
            return new CloudEncryptor();
        } else {
            return new AgentEncryptor();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;팩토리 패턴은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;객체 생성&lt;/b&gt;에 초점을 맞춘 패턴입니다. 반면 우리가 필요한 건 이미 존재하는 암호화 구현체 중 하나를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;선택&lt;/b&gt;하는 것이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;매번&lt;span&gt;&amp;nbsp;&lt;/span&gt;new로 객체를 생성하는 방식은 여러 측면에서 적합하지 않았습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333e4c; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;b&gt;Spring 컨테이너 외부의 객체&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;new로 생성한 객체는 Spring 컨테이너가 관리하지 않습니다. 이 말은&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Autowired를 통한 의존성 주입, AOP 프록시, 트랜잭션 관리 등 Spring이 제공하는 이점을 전혀 활용할 수 없다는 의미입니다. 실제로 우리의 암호화 구현체들은 내부적으로 다른 Spring 빈(API 클라이언트, 설정 정보 등)에 의존하고 있었기에 이는 치명적인 단점이었습니다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;b&gt;싱글톤 빈의 설계 철학과 불일치&lt;/b&gt;: Spring의 기본 빈 스코프는 싱글톤입니다. 애플리케이션 전체에서 하나의 인스턴스만 생성하여 재사용함으로써 메모리 효율성과 일관성을 보장합니다. 팩토리 패턴으로 매번 새 인스턴스를 생성하는 것은 이러한 Spring의 설계 철학에 역행하는 방식이었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로, 우리에게 필요한 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;객체를 생성&quot;&lt;/b&gt;하는 것이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;이미 Spring 컨테이너가 관리하고 있는 싱글톤 빈 중 적절한 것을 선택&quot;&lt;/b&gt;하는 것이었습니다. 이 요구사항에 가장 부합하는 패턴이 바로 전략 패턴이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;24-결론-전략-패턴--aop-조합&quot; style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2.4. 결론: 전략 패턴 + AOP 조합&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;고민 끝에 내린 결론은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;전략 패턴&lt;/b&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;AOP&lt;/b&gt;의 조합이었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333e4c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&lt;b&gt;전략 패턴&lt;/b&gt;: 암호화 방식을 인터페이스로 추상화하고, 각 구현체를 독립적으로 관리합니다. 새로운 보안 솔루션이 추가되면 기존 코드 수정 없이 구현체만 추가하면 됩니다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&lt;b&gt;AOP&lt;/b&gt;: 개발자가 명시적으로 암호화 메서드를 호출할 필요 없이, 어노테이션 하나로 자동 적용됩니다. 호출 누락의 위험이 사라집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방식중복 제거확장성누락 방지채택&lt;/p&gt;
&lt;table style=&quot;color: #333e4c; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;if-else 분기&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서비스 클래스 분리&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;팩토리 패턴&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;전략 패턴 + AOP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;&lt;b&gt;채택&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;3-기술적-설계-전략-패턴을-통한-추상화&quot; style=&quot;color: #333e4c; text-align: start;&quot;&gt;&lt;b&gt;3. 기술적 설계: 전략 패턴을 통한 추상화&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전략 패턴이 낯선 분들을 위해 설명드리자면, 전략 패턴은 '게임기'와 '게임 팩'의 관계로 비유할 수 있습니다. 게임기(비즈니스 로직)는 꽂는 팩(전략: 클라우드 보안 또는 에이전트 보안)의 종류에 따라 플레이할 게임(암호화 방식)이 달라지죠. 이와 유사하게 우리는 사용자가 누구냐에 따라 알맞은 전략을 자동으로 반영하는 시스템을 설계했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;31-전략-인터페이스와-구현체&quot; style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3.1. 전략 인터페이스와 구현체&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저&lt;span&gt;&amp;nbsp;&lt;/span&gt;FileProtectionStrategy라는 인터페이스(규격)를 정의하고, 각각의 보안 방식을 따르는 구현체(게임 팩)를 만들었습니다. 이렇게 하면 향후 또 다른 보안 솔루션이 추가되더라도 기존 코드를 수정할 필요 없이 새로운 구현체만 추가하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;java&quot;&gt;
&lt;pre class=&quot;cs&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;public interface FileProtectionStrategy {

    /** 전략 식별자 (예: &quot;CLOUD&quot;, &quot;AGENT&quot;) */
    String getType();

    /** 파일 보호(암호화) 처리 */
    byte[] protect(byte[] inputBytes, String fileName);

    /** 파일 보호 해제(복호화) 처리 */
    byte[] unprotect(byte[] protectedBytes);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각각의 구현체는 전략에 맞는 API 혹은 라이브러리를 호출하여 실제 암호화를 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;java&quot;&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;/**
 * 전략 A: 신규 클라우드 보안 구현체
 */
@Component
public class CloudFileProtectionStrategy implements FileProtectionStrategy {
    @Override
    public String getType() { return &quot;CLOUD&quot;; }

    @Override
    public byte[] protect(byte[] inputBytes, String fileName) {
        // GW API를 호출하여 클라우드 방식 암호화 수행
        return inputBytes; 
    }
    // ... unprotect 구현 생략
}

/**
 * 전략 B: 기존 에이전트 보안 구현체
 */
@Component
public class AgentFileProtectionStrategy implements FileProtectionStrategy {
    @Override
    public String getType() { return &quot;AGENT&quot;; }

    @Override
    public byte[] protect(byte[] inputBytes, String fileName) {
        // 기존 에이전트 방식 암호화 수행
        return inputBytes;
    }
    // ... unprotect 구현 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id=&quot;32-전략-선택기-resolver&quot; style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3.2. 전략 선택기 (Resolver)&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음으로, 런타임 시점에 필요한 전략을 빠르게 찾을 수 있도록 Resolver를 구성했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 주목할 점은 생성자에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;List&amp;lt;FileProtectionStrategy&amp;gt;를 주입받는 부분입니다. 이는 Spring의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;다중 빈 자동 주입(Autowiring Collections)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기능을 활용한 것으로, 동일한 인터페이스를 구현한 모든 빈을 한 번에 주입받을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전략의 개수가 적을 때는 리스트를 순회해도 문제가 없지만, 호출 빈도가 매우 높은 보안 모듈 특성상 확장 시에도 성능 저하가 없는 구조를 목표로 했습니다. 따라서 주입받은 전략 리스트를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;애플리케이션 시작 시점에 단 한 번만 Map으로 변환하여 캐싱&lt;/b&gt;해둠으로써, 런타임에는 별도의 조건문(if-else)이나 리스트 순회 없이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;O(1)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;시간 복잡도로 전략을 즉시 조회할 수 있도록 구현했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 접근법의 장점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;새로운 전략 구현체를 추가할 때 Resolver 코드를 전혀 수정할 필요가 없다&lt;/b&gt;는 것입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Component로 등록된 새 전략 빈은 애플리케이션 시작 시점에 Spring이 자동으로 감지하여 리스트에 포함시킵니다. 이는 OCP(개방-폐쇄 원칙)를 자연스럽게 만족하는 구조입니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #90949a; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고문서:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #333e4c;&quot; href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/autowired.html&quot;&gt;Spring 공식 문서 - Using @Autowired&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;java&quot;&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Component
public class FileProtectionStrategyResolver {

    private final Map&amp;lt;String, FileProtectionStrategy&amp;gt; registry;

    // 생성자 주입 시점에 모든 전략 구현체를 Map으로 변환하여 캐싱
    public FileProtectionStrategyResolver(List&amp;lt;FileProtectionStrategy&amp;gt; strategies) {
        this.registry = strategies.stream()
                .collect(Collectors.toMap(FileProtectionStrategy::getType, Function.identity()));
    }

    /**
     * 지정한 전략 식별자(Type)로 구현체를 O(1)로 조회
     */
    public FileProtectionStrategy resolve(String type) {
        FileProtectionStrategy strategy = registry.get(type);
        
        if (strategy == null) {
            throw new IllegalArgumentException(&quot;지원하지 않는 보안 타입입니다: &quot; + type);
        }
        
        return strategy;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;4-aop를-활용한-자동-적용&quot; style=&quot;color: #333e4c; text-align: start;&quot;&gt;&lt;b&gt;4. AOP를 활용한 자동 적용&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막 퍼즐은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;AOP(Aspect Oriented Programming)&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 전략 패턴을 게임과 게임 팩에 비유했다면, AOP는 공항 보안 검색대에 가깝습니다. 파일을 생성하는 모든 코드마다&lt;span&gt;&amp;nbsp;&lt;/span&gt;resolve()를 호출하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;protect()를 실행하는 방식은 번거로울 뿐 아니라, 호출을 누락할 위험도 있습니다. 이런 반복적이고 빠뜨리기 쉬운 보안 처리를 AOP에 맡기면 훨씬 안정적으로 관리할 수 있습니다. 마치 승객이 비행기를 타기 전에 반드시 보안 검색대를 지나가야 하듯, AOP는 비즈니스 로직으로 진입하기 전에 데이터를 자동으로 검사하고 통과시키는 관문 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 @ProtectFile이라는 커스텀 어노테이션만 붙이면, AOP가 자동으로 개입하여 보안 처리를 수행하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;java&quot;&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Aspect
public class ProtectFileAspect {

    private final FileProtectionStrategyResolver resolver;

    public ProtectFileAspect(FileProtectionStrategyResolver resolver) {
        this.resolver = resolver;
    }

    @Around(&quot;@annotation(protectFile)&quot;)
    public Object aroundProtect(ProceedingJoinPoint pjp, Object protectFile) throws Throwable {
        // 1. 비즈니스 로직 실행 (파일 생성/다운로드)
        // (예시를 위해 반환 타입이 byte[]라고 가정)
        byte[] raw = (byte[]) pjp.proceed(); 

        // 2. 보안 전략 결정
        // 실제 운영 코드에서는 사용자 세션 정보(SessionUser)를 기반으로
        // &quot;CLOUD&quot; 또는 &quot;AGENT&quot; 타입을 동적으로 결정합니다.
        String securityType = getCurrentUserSecurityType(); 

        // 3. 전략 조회 및 암호화 수행
        FileProtectionStrategy strategy = resolver.resolve(securityType);
        
        // 4. 암호화된 결과 반환
        // (전략 구현체 내부에서 처리)
        return strategy.protect(raw, &quot;sample_report.pdf&quot;);
    }

    // 사용자 권한에 따른 보안 타입 결정 로직 (예시)
    private String getCurrentUserSecurityType() {
        // 실제로는 SecurityContextHolder 등에서 사용자 권한을 확인합니다.
        // return userContext.isHeadquarters() ? &quot;CLOUD&quot; : &quot;AGENT&quot;;
        return &quot;CLOUD&quot;; // 데모를 위해 고정값 사용
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 비즈니스 로직 개발자는 파일 생성 코드에 집중하고, 아래처럼 @ProtectFile 어노테이션 하나만 붙여주면 됩니다.&lt;/p&gt;
&lt;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;java&quot;&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@ProtectFile // 이 어노테이션 하나로 암호화 로직이 자동 적용됩니다.
public byte[] downloadSalesReport() {
// 순수 비즈니스 로직 (파일 생성)
return reportService.createPdf(...);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;5-삽질기-실제-적용하면서-마주한-문제들&quot; style=&quot;color: #333e4c; text-align: start;&quot;&gt;&lt;b&gt;5. 삽질기: 실제 적용하면서 마주한 문제들&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설계는 깔끔했지만, 실제 적용 과정은 순탄치 않았습니다. 여기서는 개발 중 마주했던 문제들과 해결 과정을 공유합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;51-로컬-개발-환경에서-에이전트-솔루션이-동작하지-않는-문제&quot; style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5.1. 로컬 개발 환경에서 에이전트 솔루션이 동작하지 않는 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 부딪힌 벽은 로컬 개발 환경이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라우드 보안 API는 개발 환경에서도 문제없이 동작했지만, 기존 에이전트 기반 솔루션은 상황이 달랐습니다. 에이전트 솔루션은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;네이티브 라이브러리&lt;/b&gt;에 의존하고 있었는데, 이 라이브러리가 macOS 아키텍처를 지원하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;java&quot;&gt;
&lt;pre class=&quot;lasso&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;// 문제: macOS 로컬 환경에서 에이전트 솔루션 호출 시
agentSecurityLib.protect(data);
// -&amp;gt; UnsatisfiedLinkError: 네이티브 라이브러리 로드 실패&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 개발자가 macOS 환경에서 개발하고 있었기 때문에, 에이전트 방식 암호화를 테스트하려면 매번 개발 서버에 배포하거나 Docker 환경에서 실행해야 했습니다. 로컬에서 코드 수정 후 즉시 테스트하면 피드백 루프가 1~2분이면 충분하지만, 개발 서버 배포를 거치면 커밋 &amp;rarr; CI/CD 파이프라인 &amp;rarr; 서버 재시작까지 적지 않은 시간이 소요되어 개발 생산성이 크게 저하되는 문제가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책: 로컬 환경용 Mock 전략 구현체&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 로컬 개발 환경에서만 활성화되는 Mock 전략을 추가하여, 로컬 환경에서는 Mock 전략이 에이전트 전략을 대체하도록 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;java&quot;&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Component
@Profile(&quot;local&quot;)  // 로컬 환경에서만 활성화
public class MockFileProtectionStrategy implements FileProtectionStrategy {

    @Override
    public String getType() { return &quot;AGENT&quot;; }  // 에이전트 타입을 대체

    @Override
    public byte[] protect(byte[] inputBytes, String fileName) {
        // 실제 암호화 없이 원본 그대로 반환
        log.debug(&quot;[Mock] 에이전트 암호화 스킵: {}&quot;, fileName);
        return inputBytes;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 우리는 실제 암호화는 수행하지 않지만, 전략 패턴의 흐름은 그대로 테스트할 수 있게 되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 암호화 동작 검증이 필요할 때만 개발 서버에 배포하면 되니, 암호화 에이전트를 사용할 수 없는 macOS 환경에서 코드와 흐름에 대한 불확실성을 제거할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는 인프라의 종속성에서 개발 환경을 완전히 독립시킨 의미 있는 리팩터링었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;52-기존-코드에-어노테이션-일괄-적용&quot; style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5.2. 기존 코드에 어노테이션 일괄 적용&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로, 기존에 존재하는 수십 개의 다운로드 API에&lt;span&gt;&amp;nbsp;&lt;/span&gt;@ProtectFile&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션을 붙이는 작업이 남아있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단순 작업이라 생각했지만, 막상 시작하니 까다로운 케이스들이 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333e4c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;파일이 아닌 JSON을 반환하는 API에 실수로 어노테이션을 붙이면?&lt;/li&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;이미 다른 방식으로 암호화가 적용된 레거시 코드는?&lt;/li&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;암호화가 필요 없는 공개 문서(약관 등)는?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책: API 목록화 + 꼼꼼한 QA 테스트&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;무작정 적용하기보다, 먼저 모든 파일 다운로드 API를 목록화하고 각각의 특성을 파악했습니다.&lt;/p&gt;
&lt;div style=&quot;color: #333e4c; text-align: start;&quot; data-language=&quot;text&quot;&gt;
&lt;pre class=&quot;elm&quot; style=&quot;background-color: #f6f9fc; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;[파일 다운로드 API 체크리스트]
✅ /api/report/sales - 매출 리포트 (암호화 필요)
✅ /api/report/inventory - 재고 현황 (암호화 필요)
⬜ /api/template - 양식받기 (공개 문서, 암호화 불필요)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;목록화 이후에는 QA 엔지니어분과 함께 꼼꼼한 테스트 과정을 거쳤습니다. 사용자 권한별로 파일을 다운로드하고, 암호화가 정상 적용되는지 하나하나 검증했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 자리를 빌려 늦은 시간까지 수십 개의 API를 빠짐없이 테스트해주신 QA팀 여러분께 깊은 감사를 전합니다. &amp;zwj;♀️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 id=&quot;6-마치며&quot; style=&quot;color: #333e4c; text-align: start;&quot;&gt;&lt;b&gt;6. 마치며&lt;/b&gt;&lt;/h1&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 전사적인 보안 체계 전환이라는 미션을 수행함과 동시에, 현장의 다양한 사용자 환경을 모두 고려해야 하는 과제였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전략 패턴과 AOP의 조합으로 본사 구성원에게는 최신 클라우드 보안 환경을, 매장 구성원에게는 익숙한 기존 환경을 제공하면서도 코드의 복잡도는 최소화할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333e4c; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단순히 &quot;돌아가는 코드&quot;를 넘어, 변화에 유연하게 대처할 수 있는 &quot;좋은 구조&quot;를 고민했던 이번 경험이, 복잡한 레거시 환경과 새로운 요구사항 사이에서 고민하는 개발자분들에게 작은 힌트가 되기를 바랍니다.&lt;/p&gt;</description>
      <category>CS</category>
      <category>기술블로그</category>
      <category>백오피스</category>
      <category>올리브영</category>
      <category>올리브영기술블로그</category>
      <author>Anna-Jin</author>
      <guid isPermaLink="true">https://annajin.tistory.com/241</guid>
      <comments>https://annajin.tistory.com/241#entry241comment</comments>
      <pubDate>Tue, 7 Jul 2026 21:14:54 +0900</pubDate>
    </item>
    <item>
      <title>[AI] #1: Harness Engineering</title>
      <link>https://annajin.tistory.com/240</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Claude-ai-icon.svg.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;956&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpbXOd/dJMb997Eb6I/LV9OCxNaBK0UHwupL3TAC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpbXOd/dJMb997Eb6I/LV9OCxNaBK0UHwupL3TAC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpbXOd/dJMb997Eb6I/LV9OCxNaBK0UHwupL3TAC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpbXOd%2FdJMb997Eb6I%2FLV9OCxNaBK0UHwupL3TAC0%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;200&quot; height=&quot;199&quot; data-filename=&quot;Claude-ai-icon.svg.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;956&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금껏 Claude를 사용해서 개발을 해오면서도 claude가 어느 단계에서 어떤 Agent를 호출해서 사용하는지, 혹은 어떻게 Agent나 Skill을 구성해야하는 지에 대한 생각을 깊게 해오지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 아무것도 모를 때에는 Claude.md 파일에 모든 내용과 지침을 때려넣는 것도 해봤으니 말 다했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude를 '잘' 사용할 수 있게 되면 클로드가 MCP를 직접 호출하거나 Agent가 다른 Agent를 호출해서 여러 역할을 동시에 수행하게 하는 등의 일도 가능하고, 퇴근 시간에도 스스로 계속해서 일을 하는 노예(?)를 만들어 놓을 수도 있다.&lt;/p&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;이제는 프롬프트를 잘 쓰는 것 뿐만 아니라, &quot;AI가 스스로 일할 작업환경을 얼마나 잘 설계해두는가&quot;가 중요하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업환경을 설계해두는 개념을 &quot;&lt;b&gt;하네스 엔지니어링(Harness Engineering)&lt;/b&gt;&quot;이라고 부른다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 이 하네스 엔지니어링은 무엇이고, Claude를 더 잘 사용하기 위한 하네스를 공부해보고자한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 언어 모델과 에이전트는 다르다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 두 개념을 구분해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언어 모델(Language Model)&lt;/b&gt; 은 텍스트를 받아서 텍스트를 돌려주는 엔진이다. 질문을 하면 답을 한다. 코드를 보여주면 설명을 해준다. 하지만 모델 자체는 파일을 열거나, 터미널 명령어를 실행하거나, Slack에 메시지를 보내는 것을 직접 할 수 없다. 텍스트의 세계 안에만 존재한다.&lt;/p&gt;
&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;에이전트(Agent)&lt;/b&gt; 는 다르다. 에이전트는 목표를 받으면 스스로 계획을 세우고, 필요한 도구를 꺼내 쓴다. 결과를 보고 다시 판단한다. 작업이 끝날 때까지 이 과정을 반복한다. 파일을 실제로 열고, 코드를 실행하고, 에러 메시지를 확인하고, 수정하는 행동을 직접 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 차이를 한 문장으로 정리하면 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'말만 잘하는 언어 모델'을 '실제로 행동하는 에이전트'로 만드는 것은 바로 &lt;b&gt;하네스&lt;/b&gt;다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 하네스란 무엇인가&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하네스(Harness)&lt;/b&gt; 는 언어 모델 주위를 감싸서, 모델이 실제 세상과 상호작용할 수 있도록 &lt;b&gt;도구&amp;middot;환경&amp;middot;규칙&amp;middot;안전장치를 제공하는 인프라 골격&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 감이 잘 오지 않는다면 다음 비유를 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 유능한 시니어 개발자가 있다. 10년 경력에 모든 기술 지식을 갖추고 있다. 그런데 오늘 완전히 낯선 회사에 첫 출근을 했다. 이 사람에게 아래의 것들이 없다면 아무것도 할 수 없다.&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;Jira 같은 작업 관리 시스템&lt;/b&gt; (할 일 목록)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실수했을 때 되돌릴 수 있는 Git&lt;/b&gt; (안전장치)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 '업무 인프라'가 곧 하네스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 포인트가 있다. &lt;b&gt;개발자(모델)의 능력은 이미 충분하다.&lt;/b&gt; 우리가 설계해야 하는 것은 그 능력이 발휘될 무대, 즉 하네스다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 에이전트 루프&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하네스에서 가장 핵심적인 부분은 &lt;b&gt;에이전트 루프(Agent Loop)&lt;/b&gt; 다. 이 루프는 에이전트가 작업을 처리하는 반복 구조이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;사용자 입력
    │
    ▼
┌─────────────────────┐
│   컨텍스트 수집        │  &amp;larr; 관련 파일, 기억, 규칙 등을 불러온다
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│   모델 추론           │  &amp;larr; Claude가 &quot;다음에 무엇을 해야 할까?&quot;를 판단한다
└──────────┬──────────┘
           │
    도구를 써야 하나?
     예 /       \ 아니오
      /           \
     ▼             ▼
┌─────────┐   ┌──────────┐
│ 도구 실행 │   │  응답 출력 │  &amp;larr; 작업 완료
│ (파일,   │   └──────────┘
│  명령어, │
│  검색&amp;hellip;)  │
└────┬────┘
     │
     ▼
┌─────────────────────┐
│  결과를 컨텍스트에      │  &amp;larr; &quot;파일을 읽었더니 이런 내용이었다&quot;를 기록
│  추가                │
└──────────┬──────────┘
           │
      작업이 끝났나?
     예 /       \ 아니오
      /           \
     ▼             ▼
┌──────────┐   [모델 추론]으로 돌아가서 반복
│ 응답 출력  │
└──────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 루프에서 주목해야 할 점이 있다. &lt;b&gt;루프 자체는 매우 단순하다.&lt;/b&gt; &quot;모델에게 보내고 &amp;rarr; 결과를 받고 &amp;rarr; 도구를 실행하고 &amp;rarr; 다시 모델에게 보내고&quot;를 반복하는 구조다. 복잡한 AI가 따로 있는 게 아니다. 아주 뛰어난 모델이 이 단순한 루프 안에서 작동하는 것만으로 복잡한 작업을 해낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 하네스 엔지니어링의 목표는 &lt;b&gt;루프 자체를 복잡하게 만드는 것이 아니라, 루프가 잘 돌아갈 수 있는 환경을 잘 설계하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 하네스의 핵심 구성요소&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 만들어진 하네스에는 공통적으로 등장하는 구성요소들이 있다. Claude Code를 기준으로 살펴보면 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;height: 172px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;구성요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;하는 일&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;쉬운 비유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;모델 인터페이스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;언어 모델과 통신하는 창구&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;팀원에게 업무를 지시하는 팀장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;도구 레지스트리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;모델이 사용할 수 있는 도구 목록과 실행기&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;사무실 비품 창고 + 사용 설명서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;컨텍스트 관리자&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;모델에게 보낼 정보를 취사선택하고 정리&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;회의 전에 필요한 자료만 추려 브리핑하는 비서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;실행 엔진&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;도구 호출을 실제로 처리&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;설계도를 받아 실제로 시공하는 팀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;메모리 시스템&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;단기(현재 대화)&amp;middot;장기(이전 세션) 기억 관리&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;회의 메모장 + 업무 일지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;피드백 루프&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;도구 실행 결과를 다시 컨텍스트에 입력&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;실험 결과를 노트에 적고 다음 실험을 설계하는 과정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;안전 장치&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;위험한 작업 차단 및 사용자 승인 게이트&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;중요 결제 전 상사 결재를 받는 절차&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;오케스트레이션&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;여러 에이전트를 조율하는 상위 계층&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;여러 팀을 동시에 관리하는 프로젝트 매니저&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 역할을 조금 더 들여다보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;도구 레지스트리: 모델에게 '손'을 붙여주는 곳&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어 모델은 텍스트만 주고받는다. 하네스는 &lt;b&gt;도구(Tool)&lt;/b&gt; 를 통해 모델이 실제 세계와 상호작용할 수 있게 한다. Claude Code의 내장 도구는 파일 읽기&amp;middot;쓰기&amp;middot;편집, bash 명령어 실행, 웹 검색, 브라우저 제어 등이다. 도구 레지스트리는 이 목록과 각 도구의 사용법 설명을 모델에게 전달한다. 모델은 &quot;&lt;code&gt;read_file&lt;/code&gt;과 &lt;code&gt;bash&lt;/code&gt;를 쓸 수 있다&quot;는 것을 인식하고 적절한 상황에서 꺼내 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 포인트가 있다. &lt;b&gt;모델은 파일 시스템을 직접 건드리지 않는다.&lt;/b&gt; 모델이 &quot;이 파일을 읽어야겠다&quot;고 판단하면, 하네스의 실행 엔진이 실제로 파일을 열어 그 내용을 컨텍스트에 넣어준다. 모델은 항상 텍스트만 주고받고, 나머지는 하네스가 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;컨텍스트 관리자: 하네스에서 가장 까다로운 문제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어 모델에는 &lt;b&gt;컨텍스트 윈도우(Context Window)&lt;/b&gt; 라는 한계가 있다. 모델이 한 번에 볼 수 있는 정보의 양이다. 작업이 길어질수록 대화 기록, 파일 내용, 도구 실행 결과가 쌓여서 이 공간이 가득 찬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨텍스트 관리자는 이 공간을 효율적으로 쓰기 위해 무엇을 계속 보여주고, 무엇을 요약하거나 제거할지 끊임없이 판단한다. 컨텍스트를 잘 관리하지 못하면 모델이 이전에 받은 규칙을 잊거나, 작업이 중간에 멈추는 문제가 생긴다. 그래서 &lt;b&gt;컨텍스트 관리는 하네스 품질을 좌우하는 가장 중요한 요소 중 하나&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;안전 장치: &quot;이건 확인받고 하겠습니다&quot;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하네스가 없는 모델은 데모 수준에 머문다. 프로덕션 수준의 하네스에는 반드시 안전 장치가 있어야 한다. Claude Code가 파일을 삭제하거나 중요한 명령어를 실행하려 할 때 사용자에게 승인을 요청하는 것이 바로 이 장치다. 아무리 모델이 자신 있어도, 되돌릴 수 없는 행동 앞에서는 멈추고 확인을 받아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. Claude Code를 하네스로 다시 보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Claude Code를 하네스 관점으로 다시 본다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│                   Claude Code                    │
│                                                  │
│  ┌───────────────────────────────────────────┐   │
│  │              하네스 (Harness)              │   │
│  │                                           │   │
│  │   도구 레지스트리     컨텍스트 관리자            │   │
│  │   실행 엔진           안전 장치              │   │
│  │   메모리 시스템       오케스트레이션            │   │
│  │                                          │   │
│  │   ┌───────────────────────────────────┐  │   │
│  │   │         언어 모델 (Claude)          │   │   │
│  │   │      추론과 판단은 여기서 일어남         │   │  │
│  │   └───────────────────────────────────┘   │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘&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;공식 문서는 Claude Code를 이렇게 정의한다. &quot;Claude라는 언어 모델에 도구, 컨텍스트 관리, 실행 환경을 제공해 코딩 에이전트로 만들어주는 에이전트 하네스.&quot; &lt;code&gt;claude&lt;/code&gt; 명령어를 실행하는 순간, 이미 &lt;b&gt;잘 설계된 하네스를 갖춘 에이전트를 손에 쥔 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 그래서 하네스 설계가 왜 중요한가&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Claude Code가 이미 다 해주는데, 하네스를 내가 왜 알아야 하나?&quot;라는 의문이 생길 수 있다. 두 가지 이유가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫째, 같은 모델도 하네스에 따라 성능이 크게 달라진다.&lt;/b&gt; Anthropic의 엔지니어링 팀도 &quot;아무리 뛰어난 프론티어 모델이라도 하네스 설계가 나쁘면 성능이 크게 저하된다&quot;고 강조한다. 컨텍스트를 잘못 관리하거나 도구를 어설프게 제공하면, 세계 최고의 모델도 엉뚱한 결과를 낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘째, Claude Code는 하네스를 직접 확장할 수 있도록 설계되어 있다.&lt;/b&gt; 팀의 규칙, 반복 작업, 외부 서비스 연동, 자동화 스크립트를 하네스에 직접 연결할 수 있다. 이 기능들이 하네스의 어느 부분에 연결되는지 이해해야 제대로 활용할 수 있다. 2편에서는 이 확장 기능들을 하나씩 살펴본다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편의 핵심을 정리해보자.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개념&lt;/td&gt;
&lt;td&gt;한 줄 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;언어 모델&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;텍스트를 받아 텍스트를 돌려주는 추론 엔진&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;에이전트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;목표를 받아 스스로 행동을 반복하는 존재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;하네스&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모델을 에이전트로 만들어주는 인프라 골격&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;에이전트 루프&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;추론 &amp;rarr; 도구 실행 &amp;rarr; 결과 반영을 반복하는 심장부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Claude Code&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Claude 모델 + 잘 설계된 하네스&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 Claude Code가 제공하는 하네스 확장 기능 &amp;mdash; CLAUDE.md, Skills, MCP, Subagents, Hooks &amp;mdash; 을 하나씩 뜯어보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 기능이 하네스의 어느 구성요소에 연결되는지, 실제로 어떻게 쓰는지를 구체적인 예시와 함께 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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://news.hada.io/topic?id=28232&quot;&gt;https://news.hada.io/topic?id=28232&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1780285008559&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;코딩 에이전트의 구성 요소 | GeekNews&quot; data-og-description=&quot;코딩 에이전트는 LLM을 중심으로 코드 작성, 실행, 피드백을 반복 수행하는 제어 루프와 소프트웨어 하니스로 구성된 시스템에이전트 하니스는 컨텍스트 관리, 도구 접근, 프롬프트 구성, 상태 &quot; data-og-host=&quot;news.hada.io&quot; data-og-source-url=&quot;https://news.hada.io/topic?id=28232&quot; data-og-url=&quot;https://news.hada.io/topic?id=28232&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/9m9Ho/dJMb89ykHib/r9KTN6t4FY08Rgpk7oifiK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/Ul9JI/dJMb9aKL5MM/RksakweSJ0aKfAhRUJvwH0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=28232&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://news.hada.io/topic?id=28232&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/9m9Ho/dJMb89ykHib/r9KTN6t4FY08Rgpk7oifiK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/Ul9JI/dJMb9aKL5MM/RksakweSJ0aKfAhRUJvwH0/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;코딩 에이전트의 구성 요소 | GeekNews&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코딩 에이전트는 LLM을 중심으로 코드 작성, 실행, 피드백을 반복 수행하는 제어 루프와 소프트웨어 하니스로 구성된 시스템에이전트 하니스는 컨텍스트 관리, 도구 접근, 프롬프트 구성, 상태&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;news.hada.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=28966&quot;&gt;https://news.hada.io/topic?id=28966&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1780285015474&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;하네스 엔지니어링: 모델보다 중요한 작업 환경 설계의 시대 | GeekNews&quot; data-og-description=&quot;애디 오스마니(Addy Osmani)가 정리한 &amp;ldquo;하네스 엔지니어링(Harness Engineering)&amp;rdquo;에 관한 분석입니다. 그는 지난 2년 동안 업계의 관심이 &amp;ldquo;어떤 AI 모델이 더 똑똑한가&amp;rdquo;라는 질문에만 쏠려 있었다고 &quot; data-og-host=&quot;news.hada.io&quot; data-og-source-url=&quot;https://news.hada.io/topic?id=28966&quot; data-og-url=&quot;https://news.hada.io/topic?id=28966&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/itHGS/dJMb9iaX8k7/KCLiKaMJ6i1k4F0OeUM3q0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/0MZ6J/dJMb9kT9XLL/HMepx9x7qIasYleGNoguu0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=28966&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://news.hada.io/topic?id=28966&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/itHGS/dJMb9iaX8k7/KCLiKaMJ6i1k4F0OeUM3q0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/0MZ6J/dJMb9kT9XLL/HMepx9x7qIasYleGNoguu0/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;하네스 엔지니어링: 모델보다 중요한 작업 환경 설계의 시대 | GeekNews&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;애디 오스마니(Addy Osmani)가 정리한 &amp;ldquo;하네스 엔지니어링(Harness Engineering)&amp;rdquo;에 관한 분석입니다. 그는 지난 2년 동안 업계의 관심이 &amp;ldquo;어떤 AI 모델이 더 똑똑한가&amp;rdquo;라는 질문에만 쏠려 있었다고&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;news.hada.io&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>AI</category>
      <category>claude</category>
      <category>claude code</category>
      <category>harness</category>
      <category>harness engineering</category>
      <category>하네스</category>
      <category>하네스 엔지니어링</category>
      <author>Anna-Jin</author>
      <guid isPermaLink="true">https://annajin.tistory.com/240</guid>
      <comments>https://annajin.tistory.com/240#entry240comment</comments>
      <pubDate>Mon, 1 Jun 2026 12:38:31 +0900</pubDate>
    </item>
    <item>
      <title>[Spring boot] @ModelAttribute에 대한 이해</title>
      <link>https://annajin.tistory.com/229</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mPXEL/btsDJhCOV3L/fLbas0l546uEYJLlr0b0l0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mPXEL/btsDJhCOV3L/fLbas0l546uEYJLlr0b0l0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mPXEL/btsDJhCOV3L/fLbas0l546uEYJLlr0b0l0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmPXEL%2FbtsDJhCOV3L%2FfLbas0l546uEYJLlr0b0l0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;180&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Spring boot에서 Get방식(혹은 쿼리파라미터가 필요한) 요청을 받을 때 사용되는 어노테이션에는 @RequestParam, @PathVariable, @ModelAttribute가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터의 개수가 적은 경우에는 앞의 두개 어노테이션으로 충분하지만 파라미터가 많아질 수록 객체로 요청을 받는게 가독성 측면에도 관리 측면에도 수월해지기 때문에 @ModelAttribute를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종종 @ModelAttribute를 사용하면서 헷갈리거나 잊어버리는 개념이 있어 이번 포스팅에서는 @ModelAttribute 어노테이션에 대해 공부해보고자 한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;@ModelAttribute&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 받을 때 다음과 같이 파라미터의 개수가 적다면 @RequestParam 어노테이션으로도 무리없이 코드 작성이 가능하다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1705722038589&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/param&quot;)
public ApiResponse&amp;lt;StudyResponseDto&amp;gt; requestParam(
        @RequestParam(&quot;name&quot;) String name,
        @RequestParam(&quot;age&quot;) int age
) {
    return ApiResponse.success(studyService.requestParam(name, age));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 파라미터의 개수가 많아진다면 어떨까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1705722145411&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/param&quot;)
public ApiResponse&amp;lt;StudyResponseDto&amp;gt; requestParam(
        @RequestParam(&quot;name&quot;) String name,
        @RequestParam(&quot;age&quot;) int age,
        @RequestParam(&quot;email&quot;) String email,
        @RequestParam(&quot;phone&quot;) String phone,
        @RequestParam(&quot;nickname&quot;) String nickname,
        @RequestParam(&quot;hobby&quot;) String hobby,
        @RequestParam(&quot;job&quot;) String job
) {
    return ApiResponse.success(studyService.requestParam(name, age, email, phone, nickname, hobby, job));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 눈에 보더라도 가독성이 떨어질 뿐만 아니라 service로 파라미터를 전달할 때 실수할 가능성이 다분한 코드가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 파라미터들을 객체로 관리하면 어떨까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1705722420430&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/model&quot;)
public ApiResponse&amp;lt;StudyResponseDto&amp;gt; modelAttribute(
        StudyRequestDto requestDto
        ) {
    return ApiResponse.success(studyService.modelAttribute(requestDto));
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1705722414579&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
public class StudyRequestDto {

    private String name;
    private int age;
    private String email;
    private String phone;
    private String nickname;
    private String hobby;
    private String job;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 한결 간결해질 뿐만 아니라 코드를 수정할 때 DTO 객체만 관리하면 되기 때문에 코드 수정도 용이해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자, @ModelAttribute를 사용하는 이유는 이제 알겠는데, 이 어노테이션이 어떻게 동작하기에 요청 파라미터들을 자동으로 바인딩해주는 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adfit&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;728x90&lt;/div&gt;
    &lt;ins class=&quot;kakao_ad_area&quot; style=&quot;display: none;&quot; data-ad-unit=&quot;DAN-cyaHfyh79OG4eGqd&quot; data-ad-width=&quot;728px&quot; data-ad-height=&quot;90px&quot;&gt;&lt;/ins&gt;
    &lt;script type=&quot;text/javascript&quot; src=&quot;//t1.daumcdn.net/kas/static/ba.min.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;동작방식&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;@ModelAttribute는 기본적으로 setter 메소드를 사용하여 데이터를 바인딩한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드를 사용해서 간단하게 테스트코드를 짜서 테스트해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1705724966966&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class StudyControllerTest {

    @LocalServerPort
    private int port;

    @Test
    public void modelAttribute() {
        // given
        RestAssured.baseURI = &quot;http://localhost&quot;;
        RestAssured.port = port;

        given()
                .log().params()
                .param(&quot;name&quot;, &quot;name&quot;)
                .param(&quot;age&quot;, 10)
                .param(&quot;email&quot;, &quot;email&quot;)
                .param(&quot;phone&quot;, &quot;phone&quot;)
                .param(&quot;nickname&quot;, &quot;nickname&quot;)
                .param(&quot;hobby&quot;, &quot;hobby&quot;)
                .param(&quot;job&quot;, &quot;job&quot;)

        // when
                .when()
                .get(&quot;/api/v1/study/model&quot;)

        // then
                .then()
                .log().all();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 응답값&lt;/p&gt;
&lt;pre id=&quot;code_1705725019451&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결과
{
    &quot;code&quot;: &quot;200_0&quot;,
    &quot;message&quot;: &quot;정상 처리 되었습니다.&quot;,
    &quot;data&quot;: {
        &quot;name&quot;: &quot;name&quot;,
        &quot;age&quot;: 10,
        &quot;email&quot;: &quot;email&quot;,
        &quot;phone&quot;: &quot;phone&quot;,
        &quot;nickname&quot;: &quot;nickname&quot;,
        &quot;hobby&quot;: &quot;hobby&quot;,
        &quot;job&quot;: &quot;job&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;&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;하지만 하나 불편한 점이 있다. 나는 개인적으로 꼭 필요한 경우가 아니면 DTO에 setter 사용을 지양하고 있다.&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;setter 대신 생성자를 사용할 수는 없는걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 다음과 같이 변경하고 테스트해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1705725248731&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor
@AllArgsConstructor // 편의를 위해 Lombok 사용
public class StudyRequestDto {

    private String name;
    private int age;
    private String email;
    private String phone;
    private String nickname;
    private String hobby;
    private String job;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 응답값&lt;/p&gt;
&lt;pre id=&quot;code_1705725284651&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 데이터가 바인딩되지 않는다.
{
    &quot;code&quot;: &quot;200_0&quot;,
    &quot;message&quot;: &quot;정상 처리 되었습니다.&quot;,
    &quot;data&quot;: {
        &quot;name&quot;: null,
        &quot;age&quot;: 0,
        &quot;email&quot;: null,
        &quot;phone&quot;: null,
        &quot;nickname&quot;: null,
        &quot;hobby&quot;: null,
        &quot;job&quot;: null
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setter를 사용할 때와 달리 데이터가 바인딩되지 않는다.&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;원인을 찾기 위해 @ModelAttribute의 구현체를 찾아가보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ModelAttributeMethodProcessor&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ModelAttribute가 어떻게 동작하는 지 확인하고 싶을 때 디버깅을 찍을 수 있는 구현체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스에서 주목해야할 부분은 &lt;code&gt;constructAttribute()&lt;/code&gt; 메소드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 클래스의 &lt;code&gt;resolveArgument()&lt;/code&gt; 메소드부터 디버깅을 해서 차근차근 들어가보면 생성자에서 파라미터를 바인딩하는 메소드를 찾을 수 있는데, 이걸 결정하는 메소드가 바로 &lt;code&gt;contructAttribute()&lt;/code&gt; 메소드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1705744178403&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protected Object constructAttribute(Constructor&amp;lt;?&amp;gt; ctor, String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
    if (ctor.getParameterCount() == 0) {
        return BeanUtils.instantiateClass(ctor, new Object[0]);
    } else {
        String[] paramNames = BeanUtils.getParameterNames(ctor);
        Class&amp;lt;?&amp;gt;[] paramTypes = ctor.getParameterTypes();
        Object[] args = new Object[paramTypes.length];
        WebDataBinder binder = binderFactory.createBinder(webRequest, (Object)null, attributeName);
        String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
        String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
        boolean bindingFailure = false;
        Set&amp;lt;String&amp;gt; failedParams = new HashSet(4);

        //...
        
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서 &lt;code&gt;ctro.getParaneterCount()&lt;/code&gt;를 사용해서 생성자의 파라미터 개수를 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 그 개수가 0개라면 기본생성자로, 0개가 아니라면 인자가 있는 (우리의 경우에는 AllAgrsConstructor) 생성자를 인식하고 파라미터 바인딩을 처리하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;@AllArgsConstructor&lt;/code&gt;를 사용하더라도 &lt;code&gt;@NoArgsConstrutor&lt;/code&gt;에 의해 기본생성자가 사용되게 되므로, 만약 Setter 메소드를 사용하고 싶지 않다면 기본생성자를 명시하지 않고 특정 파라미터에 대한 생성자만을 명시해주면 된다.&lt;/p&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;그렇다면 왜 Setter 메소드가 없으면 데이터 바인딩이 되지 않는 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Setter 메소드가 없으면 안되는 이유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ModelAttributeMethodProcessor의 메소드 중 하나인 &lt;code&gt;resolveArgument()&lt;/code&gt; 메소드를 확인해보면 다음과 같이 &lt;code&gt;WebDataBinder&lt;/code&gt;&amp;nbsp;클래스를 사용하여 데이터를 바인딩 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1705757527287&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (bindingResult == null) {
    WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
    if (binder.getTarget() != null) {
        if (!mavContainer.isBindingDisabled(name)) {
            this.bindRequestParameters(binder, webRequest);
        }

        this.validateIfApplicable(binder, parameter);
        if (binder.getBindingResult().hasErrors() &amp;amp;&amp;amp; this.isBindExceptionRequired(binder, parameter)) {
            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
        }
    }

    if (!parameter.getParameterType().isInstance(attribute)) {
        attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
    }

    bindingResult = binder.getBindingResult();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WebDataBinder&lt;/code&gt;는 요청 파라미터를 객체에 바인딩해주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 클래스는 Java Beans 규약을 준수하여 동작하고 있는데, Java Beans 규약은 &lt;b&gt;getter와 setter를 사용하여 프로퍼티에 접근&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;따라서 setter 메소드를 명시하지 않는다면 WebDataBinder는 데이터를 바인딩할 방법이 없는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ModelAttribute를 사용하여 파라미터를 객체에 바인딩하기 위한 방법은 두가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Setter 메소드를 사용한다.&lt;br /&gt;기본 생성자는 명시하지 않는다면 default로 생성되므로 신경쓰지 않아도 된다.&lt;/li&gt;
&lt;li&gt;파라미터가 존재하는 생성자만 사용한다.&lt;br /&gt;이때, 기본생성자와 함께 사용하면 데이터 바인딩이 되지 않으므로 주의한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 '그냥 되는구나'라고만 생각하고 사용했던 @ModelAttribute 어노테이션에 대해 깊게 알아보는 시간을 가졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 동작하는 코드를 까보는게 생각보다 훨씬 공부에 도움이 된다는걸 한번 더 깨닫게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 그냥 쓰지 말고 동작원리를 알고 개발하는 습관을 가지기로 다짐하면서 이번 포스팅을 마무리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring boot</category>
      <category>@ModelAttribute</category>
      <category>JAVA Beans</category>
      <category>spring boot</category>
      <category>WebDataBinder</category>
      <category>모델어트리뷰트</category>
      <category>스프링부트</category>
      <category>스프링부트 어노테이션</category>
      <category>어노테이션</category>
      <author>Anna-Jin</author>
      <guid isPermaLink="true">https://annajin.tistory.com/229</guid>
      <comments>https://annajin.tistory.com/229#entry229comment</comments>
      <pubDate>Sat, 20 Jan 2024 17:54:30 +0900</pubDate>
    </item>
    <item>
      <title>[Spring boot] WebClient 사용해보기 - 비동기 통신 subscribe()</title>
      <link>https://annajin.tistory.com/228</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b51yS8/btsz9lvtpYS/GaHzQFAK0jLZU84mKc85gK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b51yS8/btsz9lvtpYS/GaHzQFAK0jLZU84mKc85gK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b51yS8/btsz9lvtpYS/GaHzQFAK0jLZU84mKc85gK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb51yS8%2Fbtsz9lvtpYS%2FGaHzQFAK0jLZU84mKc85gK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;180&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient는 RestTemplate의 대안으로서 급부상된 WebFlux의 라이브러리이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서도 지원하는 WebClient는 RestTemplate과 비교했을 때 가장 큰 장점이 `비동기 통신을 지원한다`는 점이 아닐까 싶다. 물론 그 외에도 함수형 프로그래밍 스타일을 지원한다는 점, 다양한 설정과 에러처리를 지원한다는 점 등도 장점으로 꼽을 수 있겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient의 최대 장점인 비동기 통신을 지금까지 사용하기 어렵고 쓸 데가 없다는 이유만으로 동기 방식을 고수해왔었는데 이번 회사 프로젝트를 하면서 비동기 통신이 필요해진 시점이 생겼다. 이번 기회에 WebClient의 비동기 통신을 구현해보았고 이번 포스팅의 주제로 삼아보고자 한다.&lt;/p&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;비동기 vs 동기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기와 동기의 차이점은 예전에 글로 정리한 적이 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://annajin.tistory.com/157&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.05.30 - [Study/CS] - [CS] 동기 &amp;amp; 비동기 vs 블로킹 &amp;amp; 논블로킹&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699677424309&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;[CS] 동기 &amp;amp; 비동기 vs  블로킹 &amp;amp; 논블로킹&quot; data-og-description=&quot;들어가며 예전에 Petpular 프로젝트 때 사용해본 Spring WebClient에서 동기 &amp;amp; 비동기 / 블로킹 &amp;amp; 논블로킹의 개념이 등장했었다. WebClient는 논블로킹방식으로 작동하고 block() 메소드를 이용해서 블로킹&quot; data-og-host=&quot;annajin.tistory.com&quot; data-og-source-url=&quot;https://annajin.tistory.com/157&quot; data-og-url=&quot;https://annajin.tistory.com/157&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bFA6Xt/hyUu4TBXkH/kJpGlNi2fxrO09n14QAaMk/img.png?width=564&amp;amp;height=564&amp;amp;face=0_0_564_564,https://scrap.kakaocdn.net/dn/biauJ9/hyUuWuucDZ/vdO00f7gIPUoT5SL7FBq5K/img.png?width=564&amp;amp;height=564&amp;amp;face=0_0_564_564,https://scrap.kakaocdn.net/dn/ZC5e4/hyUruzEM2U/OTPYmkix9KmkSbbiOgT2C1/img.png?width=470&amp;amp;height=472&amp;amp;face=0_0_470_472&quot;&gt;&lt;a href=&quot;https://annajin.tistory.com/157&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://annajin.tistory.com/157&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bFA6Xt/hyUu4TBXkH/kJpGlNi2fxrO09n14QAaMk/img.png?width=564&amp;amp;height=564&amp;amp;face=0_0_564_564,https://scrap.kakaocdn.net/dn/biauJ9/hyUuWuucDZ/vdO00f7gIPUoT5SL7FBq5K/img.png?width=564&amp;amp;height=564&amp;amp;face=0_0_564_564,https://scrap.kakaocdn.net/dn/ZC5e4/hyUruzEM2U/OTPYmkix9KmkSbbiOgT2C1/img.png?width=470&amp;amp;height=472&amp;amp;face=0_0_470_472');&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;[CS] 동기 &amp;amp; 비동기 vs 블로킹 &amp;amp; 논블로킹&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며 예전에 Petpular 프로젝트 때 사용해본 Spring WebClient에서 동기 &amp;amp; 비동기 / 블로킹 &amp;amp; 논블로킹의 개념이 등장했었다. WebClient는 논블로킹방식으로 작동하고 block() 메소드를 이용해서 블로킹&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;annajin.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동기&lt;/b&gt;는 한 작업이 완료될 때까지 다음 작업을 기다리는 방식이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비동기&lt;/b&gt;는 한 작업을 실행한 후에도 다음 작업을 기다리지 않고 계속 진행하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RestTemplate&lt;/b&gt;은 동기 방식으로 동작하는 하는 HTTP 클라이언트 라이브러리이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WebClient&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Subscribe()&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 포스팅의 &lt;b&gt;ConnectStep&lt;/b&gt; 부분을 보면 &lt;code&gt;block()&lt;/code&gt; 메소드를 사용하여 응답을 동기로 받는다고 작성해두었다.&lt;/p&gt;
&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://annajin.tistory.com/225&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2023.08.26 - [Study/Spring boot] - [Spring boot] WebClient 사용해보기 - 모듈화 1-2&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699678445819&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring boot] WebClient 사용해보기 - 모듈화 1-2&quot; data-og-description=&quot;들어가며 2023.08.25 - [Study/Spring boot] - [Spring boot] WebClient 사용해보기 2-1 이전글에서는 내가 어떻게 웹클라이언트를 사용하고 있는 지에 대해 간단하게 설명했다. 이번 글에서는 코드를 하나씩 보&quot; data-og-host=&quot;annajin.tistory.com&quot; data-og-source-url=&quot;https://annajin.tistory.com/225&quot; data-og-url=&quot;https://annajin.tistory.com/225&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vtZIC/hyUuWBgKr6/wisxYykh7VbxOJ2IuctKkk/img.jpg?width=510&amp;amp;height=458&amp;amp;face=0_0_510_458,https://scrap.kakaocdn.net/dn/bL7Ol7/hyUu6DS0Ml/38iYyIc0AFK96btbYVaZ6K/img.jpg?width=510&amp;amp;height=458&amp;amp;face=0_0_510_458,https://scrap.kakaocdn.net/dn/lt1Mt/hyUrxC7L9p/JCFK71KyphrQB4ThhIpkJ1/img.jpg?width=510&amp;amp;height=458&amp;amp;face=0_0_510_458&quot;&gt;&lt;a href=&quot;https://annajin.tistory.com/225&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://annajin.tistory.com/225&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vtZIC/hyUuWBgKr6/wisxYykh7VbxOJ2IuctKkk/img.jpg?width=510&amp;amp;height=458&amp;amp;face=0_0_510_458,https://scrap.kakaocdn.net/dn/bL7Ol7/hyUu6DS0Ml/38iYyIc0AFK96btbYVaZ6K/img.jpg?width=510&amp;amp;height=458&amp;amp;face=0_0_510_458,https://scrap.kakaocdn.net/dn/lt1Mt/hyUrxC7L9p/JCFK71KyphrQB4ThhIpkJ1/img.jpg?width=510&amp;amp;height=458&amp;amp;face=0_0_510_458');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring boot] WebClient 사용해보기 - 모듈화 1-2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며 2023.08.25 - [Study/Spring boot] - [Spring boot] WebClient 사용해보기 2-1 이전글에서는 내가 어떻게 웹클라이언트를 사용하고 있는 지에 대해 간단하게 설명했다. 이번 글에서는 코드를 하나씩 보&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;annajin.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adfit&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;728x90&lt;/div&gt;
    &lt;ins class=&quot;kakao_ad_area&quot; style=&quot;display: none;&quot; data-ad-unit=&quot;DAN-cyaHfyh79OG4eGqd&quot; data-ad-width=&quot;728px&quot; data-ad-height=&quot;90px&quot;&gt;&lt;/ins&gt;
    &lt;script type=&quot;text/javascript&quot; src=&quot;//t1.daumcdn.net/kas/static/ba.min.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 위의 게시글 내용을 베이스로&amp;nbsp;&lt;code&gt;subscribe()&lt;/code&gt;를 사용해서 응답을 비동기로 받을 수 있도록 구현해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ConnectStepImpl&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1700274928061&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Log4j2
@RequiredArgsConstructor
public class ConnectStepImpl implements ConnectStep {

    private final WebClient.RequestHeadersSpec&amp;lt;?&amp;gt; methodType;
    private Object response;

   
    /**
     * WebClient의 header와 response class 설정 후 subscribe로 호출&amp;lt;br&amp;gt;
     * 헤더와 응답값이 존재할 때 사용. (ex. 일반적인 api 호출)&amp;lt;br&amp;gt;
     * 예외처리 포함
     * @return {@link ResponseStep}
     */
    @Override
    public Mono&amp;lt;?&amp;gt; connectSubscribe(Map&amp;lt;String, String&amp;gt; headers, Class&amp;lt;?&amp;gt; responseType) {
        try {
            return this.methodType
                    .headers(httpHeaders -&amp;gt; httpHeaders.setAll(headers == null || headers.isEmpty() ? new HashMap&amp;lt;&amp;gt;() : headers))
                    .retrieve()
                    .onStatus(HttpStatusCode::isError, clientResponse -&amp;gt;
                            clientResponse.bodyToMono(String.class)
                                    .flatMap(msg -&amp;gt; Mono.error(new ApiException(ResponseCode.INTERNAL_SERVER_ERROR, msg))))
                    .bodyToMono(responseType)
                    .retryWhen(Retry.fixedDelay(3, java.time.Duration.ofSeconds(1))
                            .doBeforeRetry(before -&amp;gt; log.info(&quot;Retry: {} | {}&quot;, before.totalRetries(), before.failure())));
        } catch (Exception e) {
            throw new ApiException(ResponseCode.INTERNAL_SERVER_ERROR, e.getMessage());
        }
    }

    /**
     * WebClient의 header와 response class 설정 후 subscribe로 호출&amp;lt;br&amp;gt;
     * 헤더와 응답값이 존재하지 않을 때 사용. (ex. Google chat webhook api 호출)&amp;lt;br&amp;gt;
     * 예외처리 포함
     * @return {@link ResponseStep}
     */
    @Override
    public Mono&amp;lt;String&amp;gt; connectSubscribe() {
        try {
            return this.methodType
                    .retrieve()
                    .onStatus(HttpStatusCode::isError, clientResponse -&amp;gt;
                            clientResponse.bodyToMono(String.class)
                                    .flatMap(msg -&amp;gt; Mono.error(new ApiException(ResponseCode.INTERNAL_SERVER_ERROR, msg))))
                    .bodyToMono(String.class)
                    .retryWhen(Retry.fixedDelay(3, java.time.Duration.ofSeconds(1))
                            .doBeforeRetry(before -&amp;gt; log.info(&quot;Retry: {} | {}&quot;, before.totalRetries(), before.failure())));
        } catch (Exception e) {
            throw new ApiException(ResponseCode.INTERNAL_SERVER_ERROR, e.getMessage());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;subscribe()&lt;/code&gt;는 WebFlux에서 제공하는 객체인 Mono 혹은 Flux를 사용한다. 이번 포스팅에서는 Mono 객체만을 사용하기로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;block()&lt;/code&gt;을 사용할 때와 다른 점은 &lt;code&gt;ConnectStepImpl&lt;/code&gt; 클래스에서 &lt;code&gt;subscribe()&lt;/code&gt;를 호출하지 않는다는 점과 return값이 &lt;code&gt;ResponseStep&lt;/code&gt;이 아니라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 데이터의 후가공이나 logging이 필요하지 않다면 이 단계에서 subscribe()를 호출해도 상관 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 예제에서는 메소드의 모듈화를 위해 확장성을 고려해야했으므로 응답 호출을 후 단계로 미뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점에 주의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Mono가 뭐고 Flux가 무엇인지에 대해서 공부하려면 WebFlux의 세계까지 들어가야한다. 이번에는 단순한 api 비동기 호출에 대해 작성하고 있으므로 이 부분은 건너뛰도록 한다.&lt;br /&gt;&lt;br /&gt;여담이지만 처음 개발 공부를 시작했을 때 WebFlux를 완벽하게 공부한 다음 WebClient를 사용하려고 2주의 시간을 끙끙댄 적이 있었다. 지금 생각해보면 너무 비효율적인 공부방식이었던 것 같다. 개발은 하면서 공부하는게 최고라고 생각한다!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;WebClientConnector&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1700276061779&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class WebClientConnector {
    private final ApiWebClientBuilder webClientBuilder;
    private final ApiStatics statics;


    /**
     * Google Chat WebHook 메세지 전송
     * @param requestBody {@link WebHook}
     */
    public Mono&amp;lt;String&amp;gt; callGoogleChat(WebHook requestBody) {
        ApiStatics.GoogleChat googleChat = statics.getGoogleChat();

        return webClientBuilder.request()
                .post(googleChat.getUrl(), requestBody)
                .connectSubscribe();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClientConnector 객체는 API를 호출해주는 역할을 한다. 간단하게 Google Chat에 메시지를 보내는 Webhook API를 호출하는 예제를 작성해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현체를 보면 ResponseStep 메소드를 사용하지 않은 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 return값을 &lt;code&gt;ResponseStep&lt;/code&gt; 객체로 하지 않은 이유가, &lt;code&gt;.connectSubscribe()&lt;/code&gt;메소드 이후 체이닝에서는 &lt;code&gt;ResponseStep&lt;/code&gt;의 메소드를 사용할 수 없도록 강제하기 위함이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;GoogleChatService&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1700276349512&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Log4j2
@Service
@RequiredArgsConstructor
public class GoogleChatService {

    private final WebClientConnector webClientConnector;

    /**
     * 비동기로 Google Chat WebHook 메세지 전송&amp;lt;br&amp;gt;
     * retry 3회 시도
     * @param title         card header 타이틀
     * @param messageHeader card body 상단 메세지
     * @param message       card body 하단 메세지, 복수 메세지 - divider 추가
     */
    public void sendGoogleChatMultipleMessage(String title, String messageHeader, List&amp;lt;String&amp;gt; message) {
        ResponseCode responseCode = ResponseCode.GOOGLE_CHAT_ERROR;

        webClientConnector.callGoogleChat(WebHookMessageBuilder.createGoogleChatMultipleMessage(title, messageHeader, message))
                .flatMap(response -&amp;gt; Mono.just(ResponseEntity.ok().body(ApiResponse.success())))
                .onErrorResume(error -&amp;gt; Mono.just(ResponseEntity.status(responseCode.getStatus()).body(ApiResponse.error(responseCode, &quot;메세지 전송 실패 : &quot; + error.getCause().getMessage()))))
                .subscribe(
                        response -&amp;gt; log.info(&quot;메세지 전송 완료 : {}&quot;, response),
                        error -&amp;gt; log.error(&quot;메세지 전송 실패 : {}&quot;, error.getMessage())
                );
    }

    /**
     * 비동기로 Google Chat WebHook 메세지 전송&amp;lt;br&amp;gt;
     * retry 3회 시도
     * @param title         card header 타이틀
     * @param messageHeader card body 상단 메세지
     * @param message       card body 하단 메세지, 단일 메세지
     */
    public void sendGoogleChatMultipleMessage(String title, String messageHeader, String message) {
        ResponseCode responseCode = ResponseCode.GOOGLE_CHAT_ERROR;

        webClientConnector.callGoogleChat(WebHookMessageBuilder.createGoogleChatSingleMessage(title, messageHeader, message))
                .flatMap(response -&amp;gt; Mono.just(ResponseEntity.ok().body(ApiResponse.success())))
                .onErrorResume(error -&amp;gt; Mono.just(ResponseEntity.status(responseCode.getStatus()).body(ApiResponse.error(responseCode, &quot;메세지 전송 실패 : &quot; + error.getCause().getMessage()))))
                .subscribe(
                        response -&amp;gt; log.info(&quot;메세지 전송 완료 : {}&quot;, response),
                        error -&amp;gt; log.error(&quot;메세지 전송 실패 : {}&quot;, error.getMessage())
                );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 구현한 로직들을 호출하고, 응답을 받아 로깅하는 Service이다. &lt;code&gt;ConnectStepImpl&lt;/code&gt;에서 &lt;code&gt;subscribe()&lt;/code&gt;를 사용하지 않은 이유는 이 곳에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 응답 데이터를 비동기 스트림 내에서 가공하고 처리하기 위함이다. 만약 모듈화된 Step 내부에 &lt;code&gt;subscribe()&lt;/code&gt;가 존재한다면 각 서비스마다 connectStep을 새롭게 구현해야할 것이다.&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;flatMap()&lt;/b&gt; : 각 입력 요소를 새로운 스트림으로 변환한다음 각각의 스트림을 하나로 합쳐주는 역할을 한다. 쉽게 말하면 각각의 요청을 하나로 모아주는 역할을 한다고 생각하면 된다. 예제에서는 여러개의 스트림이 존재하지 않지만 비동기 응답에 대한 데이터를 처리하기 위하여 사용했다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onErrorResume()&lt;/b&gt; : 스트림 내에서 발생한 에러를 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;subscribe()&lt;/b&gt; : 비동기 응답을 받아 처리한다. 첫번째 인자로는 정상 응답, 두번째 인자로는 에러 응답을 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 간단하게 비동기 처리를 구현해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 WebFlux의 세계는 Spring MVC만큼이나 방대하고 알아야할 것, 사용할 수 있는 수단이 많지만 초보 개발자가 단기간에 습득할 수 있는 분야는 아니라고 생각하기에 차근차근 공부하고 포스팅해나가면서 알아가는게 좋지 않을까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring boot</category>
      <category>Asyncronize</category>
      <category>spring boot</category>
      <category>WebClient</category>
      <category>비동기</category>
      <category>비동기통신</category>
      <category>스프링부트</category>
      <category>웹클라이언트</category>
      <author>Anna-Jin</author>
      <guid isPermaLink="true">https://annajin.tistory.com/228</guid>
      <comments>https://annajin.tistory.com/228#entry228comment</comments>
      <pubDate>Sat, 11 Nov 2023 14:01:01 +0900</pubDate>
    </item>
    <item>
      <title>[ChatGPT] OpenAi API 사용해보기 - 프롬프트 엔지니어링</title>
      <link>https://annajin.tistory.com/226</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/35B44/btsv8fTp9x2/p56k4bCEAXAKZuoJfSkYp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/35B44/btsv8fTp9x2/p56k4bCEAXAKZuoJfSkYp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/35B44/btsv8fTp9x2/p56k4bCEAXAKZuoJfSkYp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F35B44%2Fbtsv8fTp9x2%2Fp56k4bCEAXAKZuoJfSkYp1%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;200&quot; height=&quot;200&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 많은 개발자들이 알고, 사용해보고 있을 ChatGPT. Open AI에서 만든 ChatGPT는 API로도 사용이 가능하다.&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;자세한 내용은 OpenAI의 ChatGPT API Docs를 참고하도록 하자&lt;/p&gt;
&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://platform.openai.com/docs/introduction/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ChatGPT API Docs&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1697530110947&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;OpenAI Platform&quot; data-og-description=&quot;Explore developer resources, tutorials, API docs, and dynamic examples to get the most out of OpenAI's platform.&quot; data-og-host=&quot;platform.openai.com&quot; data-og-source-url=&quot;https://platform.openai.com/docs/introduction/overview&quot; data-og-url=&quot;https://platform.openai.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eA05l/hyUdRajxbF/0KKCc2Y5dL9buA5Ax6o2O1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/TSHV3/hyUgHKC6n5/0l0KkGLnUm6GQaMHQevT60/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://platform.openai.com/docs/introduction/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://platform.openai.com/docs/introduction/overview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eA05l/hyUdRajxbF/0KKCc2Y5dL9buA5Ax6o2O1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/TSHV3/hyUgHKC6n5/0l0KkGLnUm6GQaMHQevT60/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;OpenAI Platform&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Explore developer resources, tutorials, API docs, and dynamic examples to get the most out of OpenAI's platform.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;platform.openai.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ChatGPT API&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사전준비&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. OpenAI 회원가입&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입은 어렵지 않다. 이미 ChatGPT를 사용해본 사람이라면 로그인을 하고, 사용해본 적 없는 사람이라면 회원가입을 진행하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://platform.openai.com/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenAI Website&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1695984837185&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;OpenAI Platform&quot; data-og-description=&quot;Explore developer resources, tutorials, API docs, and dynamic examples to get the most out of OpenAI's platform.&quot; data-og-host=&quot;platform.openai.com&quot; data-og-source-url=&quot;https://platform.openai.com/overview&quot; data-og-url=&quot;https://platform.openai.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sdie8/hyT2EBUmxS/tYCcsGNK0hTjCLtI9uJ631/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/BHkpy/hyT2wjx1x2/mRc98nVNs1UTSyRecrRkk0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://platform.openai.com/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://platform.openai.com/overview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sdie8/hyT2EBUmxS/tYCcsGNK0hTjCLtI9uJ631/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/BHkpy/hyT2wjx1x2/mRc98nVNs1UTSyRecrRkk0/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;OpenAI Platform&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Explore developer resources, tutorials, API docs, and dynamic examples to get the most out of OpenAI's platform.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;platform.openai.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adfit&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;728x90&lt;/div&gt;
    &lt;ins class=&quot;kakao_ad_area&quot; style=&quot;display: none;&quot; data-ad-unit=&quot;DAN-cyaHfyh79OG4eGqd&quot; data-ad-width=&quot;728px&quot; data-ad-height=&quot;90px&quot;&gt;&lt;/ins&gt;
    &lt;script type=&quot;text/javascript&quot; src=&quot;//t1.daumcdn.net/kas/static/ba.min.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-29 오후 7.53.04.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;935&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3UzaC/btsv5ltkzMK/6r4YejSqNA7pglZKHnSP60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3UzaC/btsv5ltkzMK/6r4YejSqNA7pglZKHnSP60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3UzaC/btsv5ltkzMK/6r4YejSqNA7pglZKHnSP60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3UzaC%2Fbtsv5ltkzMK%2F6r4YejSqNA7pglZKHnSP60%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;341&quot; data-filename=&quot;스크린샷 2023-09-29 오후 7.53.04.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;935&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 무료 크레딧 확인&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI에서는 신규회원가입 시 &lt;u&gt;3개월 동안 사용할 수 있는 $5 무료크레딧&lt;/u&gt;을 제공해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 클릭 &amp;gt; Manage account &amp;gt; Billing&lt;/p&gt;
&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-09-29 오후 8.13.46.png&quot; data-origin-width=&quot;1928&quot; data-origin-height=&quot;923&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPHCnJ/btswcI1tYBI/ZpPghNx7urzLsS8Hl5axIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPHCnJ/btswcI1tYBI/ZpPghNx7urzLsS8Hl5axIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPHCnJ/btswcI1tYBI/ZpPghNx7urzLsS8Hl5axIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPHCnJ%2FbtswcI1tYBI%2FZpPghNx7urzLsS8Hl5axIK%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;338&quot; data-filename=&quot;edited_스크린샷 2023-09-29 오후 8.13.46.png&quot; data-origin-width=&quot;1928&quot; data-origin-height=&quot;923&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Playground에서 아래와 같은 문구가 뜬다면 무료 크레딧이 만료된 것이므로 카드 정보 등록 후 크레딧을 구매해주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰량이 많지 않은 프로젝트라면 $5로도 충분하다고 생각되므로, 최소한의 금액 씩 구매하는걸 추천한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-29 오후 7.58.50.png&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wkXkm/btsv7poBa2E/ugheEhNqXqWzEkXo4Lysh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wkXkm/btsv7poBa2E/ugheEhNqXqWzEkXo4Lysh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wkXkm/btsv7poBa2E/ugheEhNqXqWzEkXo4Lysh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwkXkm%2Fbtsv7poBa2E%2FugheEhNqXqWzEkXo4Lysh0%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;69&quot; data-filename=&quot;스크린샷 2023-09-29 오후 7.58.50.png&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;86&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. API Key 등록&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크레딧까지 확인했다면 API를 사용하기 위해 API Key를 등록해주어야한다.&lt;/p&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;gt; USER &amp;gt; API key &amp;gt; Create new secret key&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-09-29 오후 8.20.00.png&quot; data-origin-width=&quot;1919&quot; data-origin-height=&quot;937&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PDAWa/btsv8zRS62i/szDtMbXiklIu7Ln0jySEkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PDAWa/btsv8zRS62i/szDtMbXiklIu7Ln0jySEkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PDAWa/btsv8zRS62i/szDtMbXiklIu7Ln0jySEkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPDAWa%2Fbtsv8zRS62i%2FszDtMbXiklIu7Ln0jySEkk%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;1919&quot; height=&quot;937&quot; data-filename=&quot;edited_스크린샷 2023-09-29 오후 8.20.00.png&quot; data-origin-width=&quot;1919&quot; data-origin-height=&quot;937&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secret key의 Name은 자유롭게 설정해도 된다. 필자는 [GPT secret key]라고 입력해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-09-29 오후 8.21.16.png&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXAUhZ/btswgXjQtc6/kSg0oN5hdKBiDjoRKvPfR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXAUhZ/btswgXjQtc6/kSg0oN5hdKBiDjoRKvPfR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXAUhZ/btswgXjQtc6/kSg0oN5hdKBiDjoRKvPfR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXAUhZ%2FbtswgXjQtc6%2FkSg0oN5hdKBiDjoRKvPfR0%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;213&quot; data-filename=&quot;edited_스크린샷 2023-09-29 오후 8.21.16.png&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Name을 입력하고 나면 다음과 같이 Secret key가 생성된다. Secret key는 해당 화면에서 &lt;u&gt;딱 한번만 확인가능&lt;/u&gt;하기 때문에 안전한 곳에 저장해두고 관리해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Secret key를 분실했다면 기존 Key를 삭제한 후 새롭게 발급받아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. Playground&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI가 제공하는 Playgound에서 ChatGPT API를 간편하게 테스트해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 탭에서는 GPT의 Model, Temperature, Maximum Token(Length) 등등을 설정할 수있고, 좌측 탭에서는 SYSTEM과 USER를 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒤에서 자세히 설명하겠지만 SYSTEM은 GPT의 역할을 정해주는 부분이고, USER는 발화(질문) 내용을 입력하는 부분으로 이해하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 Playground에서 테스트를 해보면 다음과 같이 ChatGPT와 동일하게 응답이 온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASSISTANT 부분은 GPT에 대한 답변인데, API를 이용할 때 이 ASSISTANT를 함께 Request로 보내주면 마치 대화의 맥락을 기억하는 듯이 구현이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-09-29 오후 8.30.29.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;937&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vb6ZL/btswcGvPdbS/gpZ8ZkXKPcaWqtTGUtp52k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vb6ZL/btswcGvPdbS/gpZ8ZkXKPcaWqtTGUtp52k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vb6ZL/btswcGvPdbS/gpZ8ZkXKPcaWqtTGUtp52k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvb6ZL%2FbtswcGvPdbS%2FgpZ8ZkXKPcaWqtTGUtp52k%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;342&quot; data-filename=&quot;edited_스크린샷 2023-09-29 오후 8.30.29.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;937&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 SYSTEM 부분에 적절한 역할을 부여해주면 해줄 수록 GPT는 개발자가 의도한 바를 명확하게 파악하고 대답을 하게 되고, 이 부분을 작성하는 과정이 바로 &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;이제 간단히 ChatGPT의 사전준비가 끝났으니 본격적으로 ChatGPT API를 사용해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;알아보기&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;라이브러리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI의 ChatGPT API 공식문서에서는 Node.js와 Python를 사용하여 가이드를 진행하고, 공식적으로 이 두가지 언어의 라이브러리만을 지원해준다. 하지만 수많은 기업과 개발자들이 각각의 언어로 된 라이브러리를 배포 및 유지보수하고 있으므로 너무 걱정하지는 말자!&lt;/p&gt;
&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;span style=&quot;color: #9d9d9d;&quot;&gt;이정도면 거의 모든 언어가 지원되는게 아닌가 할 정도로 많은 언어로 된 라이브러리가 배포되어있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;라이브러리 언어 목록&lt;/summary&gt;
&lt;div&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;C# / .NET&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/betalgo/openai&quot;&gt;Betalgo.OpenAI&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/betalgo&quot;&gt;Betalgo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a&gt;OpenAI-API-dotnet&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/OkGoDoIt&quot;&gt;OkGoDoIt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/RageAgainstThePixel/OpenAI-DotNet&quot;&gt;OpenAI-DotNet&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/RageAgainstThePixel&quot;&gt;RageAgainstThePixel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;C++&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/D7EAD/liboai&quot;&gt;liboai&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/D7EAD&quot;&gt;D7EAD&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Clojure&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/wkok/openai-clojure&quot;&gt;openai-clojure&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/wkok&quot;&gt;wkok&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Crystal&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sferik/openai-crystal&quot;&gt;openai-crystal&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/sferik&quot;&gt;sferik&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Dart/Flutter&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/anasfik/openai&quot;&gt;openai&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/anasfik&quot;&gt;anasfik&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Delphi&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/HemulGM/DelphiOpenAI&quot;&gt;DelphiOpenAI&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/HemulGM&quot;&gt;HemulGM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Elixir&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mgallo/openai.ex&quot;&gt;openai.ex&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/mgallo&quot;&gt;mgallo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Go&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sashabaranov/go-gpt3&quot;&gt;go-gpt3&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/sashabaranov&quot;&gt;sashabaranov&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Java&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/TheoKanning/openai-java&quot;&gt;openai-java&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/TheoKanning&quot;&gt;Theo Kanning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Julia&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rory-linehan/OpenAI.jl&quot;&gt;OpenAI.jl&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/rory-linehan&quot;&gt;rory-linehan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Kotlin&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Aallam/openai-kotlin&quot;&gt;openai-kotlin&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/Aallam&quot;&gt;Mouaad Aallam&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Node.js&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/openai-api&quot;&gt;openai-api&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/Njerschow&quot;&gt;Njerschow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/openai-api-node&quot;&gt;openai-api-node&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/erlapso&quot;&gt;erlapso&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/gpt-x&quot;&gt;gpt-x&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/ceifa&quot;&gt;ceifa&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/gpt3&quot;&gt;gpt3&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/poteat&quot;&gt;poteat&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/gpts&quot;&gt;gpts&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/thencc&quot;&gt;thencc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/@dalenguyen/openai&quot;&gt;@dalenguyen/openai&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/dalenguyen&quot;&gt;dalenguyen&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tectalichq/public-openai-client-js&quot;&gt;tectalic/openai&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://tectalic.com/&quot;&gt;tectalic&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;PHP&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://packagist.org/packages/orhanerday/open-ai&quot;&gt;orhanerday/open-ai&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/orhanerday&quot;&gt;orhanerday&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tectalichq/public-openai-client-php&quot;&gt;tectalic/openai&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://tectalic.com/&quot;&gt;tectalic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai-php/client&quot;&gt;openai-php clinet&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/openai-php&quot;&gt;openai-php&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Python&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/OthersideAI/chronology&quot;&gt;chronology&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.othersideai.com/&quot;&gt;OthersideAI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;R&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ben-aaron188/rgpt3&quot;&gt;rgpt3&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/ben-aaron188&quot;&gt;ben-aaron188&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Ruby&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nileshtrivedi/openai/&quot;&gt;openai&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/nileshtrivedi&quot;&gt;nileshtrivedi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/alexrudall/ruby-openai&quot;&gt;ruby-openai&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/alexrudall&quot;&gt;alexrudall&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Rust&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/64bit/async-openai&quot;&gt;async-openai&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/64bit&quot;&gt;64bit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lbkolev/fieri&quot;&gt;fieri&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/lbkolev&quot;&gt;lbkolev&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Scala&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cequence-io/openai-scala-client&quot;&gt;openai-scala-client&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/cequence-io&quot;&gt;cequence-io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Swift&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dylanshine/openai-kit&quot;&gt;OpenAIKit&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/dylanshine&quot;&gt;dylanshine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/MacPaw/OpenAI/&quot;&gt;OpenAI&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/MacPaw&quot;&gt;MacPaw&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Unity&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hexthedev/OpenAi-Api-Unity&quot;&gt;OpenAi-Api-Unity&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/hexthedev&quot;&gt;hexthedev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/RageAgainstThePixel/com.openai.unity&quot;&gt;com.openai.unity&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/RageAgainstThePixel&quot;&gt;RageAgainstThePixel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Unreal Engine&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; color: #353740; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/KellanM/OpenAI-Api-Unreal&quot;&gt;OpenAI-Api-Unreal&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/KellanM&quot;&gt;KellanM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&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;이 예제에서는 OpenAI가 기본으로 라이브러리를 제공해주는 Python 을 사용해서 구현해볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;파라미터 요소&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://platform.openai.com/docs/api-reference/chat/create&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://platform.openai.com/docs/api-reference/chat/create&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1697868162069&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;OpenAI Platform&quot; data-og-description=&quot;Explore developer resources, tutorials, API docs, and dynamic examples to get the most out of OpenAI's platform.&quot; data-og-host=&quot;platform.openai.com&quot; data-og-source-url=&quot;https://platform.openai.com/docs/api-reference/chat/create&quot; data-og-url=&quot;https://platform.openai.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Qpn4M/hyUgJW5I5o/jHCMiaZjWHWdi83oM3AfiK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/buWwaL/hyUgVwu5Tr/EuSSap6tqiMcci659cckoK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://platform.openai.com/docs/api-reference/chat/create&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://platform.openai.com/docs/api-reference/chat/create&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Qpn4M/hyUgJW5I5o/jHCMiaZjWHWdi83oM3AfiK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/buWwaL/hyUgVwu5Tr/EuSSap6tqiMcci659cckoK/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;OpenAI Platform&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Explore developer resources, tutorials, API docs, and dynamic examples to get the most out of OpenAI's platform.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;platform.openai.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;우리가 사용하게 될 Chat Completion API의 파라미터들 중 주로 사용하는 인자들은 다음과 같다.&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;model(필수)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하게 될 GPT의 모델이다. 사용 가능한 모델은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;GPT 모델 목록&lt;br /&gt;&lt;br /&gt;&lt;/summary&gt;
&lt;div&gt;
&lt;table style=&quot;color: #353740; text-align: start; border-collapse: collapse; width: 100%; height: 224px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;MODELS&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;DESCRIPTION&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;a style=&quot;color: #10a37f;&quot; href=&quot;https://platform.openai.com/docs/models/gpt-4&quot;&gt;GPT-4&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;GPT-3.5 업그레이드 버전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;a style=&quot;color: #10a37f;&quot; href=&quot;https://platform.openai.com/docs/models/gpt-3-5&quot;&gt;GPT-3.5&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;GPT-3 업그레이드 버전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;&lt;a style=&quot;color: #10a37f;&quot; href=&quot;https://platform.openai.com/docs/models/gpt-base&quot;&gt;GPT base&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;현재 사용하지 않는 베이스 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;a style=&quot;color: #10a37f;&quot; href=&quot;https://platform.openai.com/docs/models/dall-e&quot;&gt;DALL&amp;middot;E&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;이미지를 생성하고 편집할 수 있는 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;a style=&quot;color: #10a37f;&quot; href=&quot;https://platform.openai.com/docs/models/whisper&quot;&gt;Whisper&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;오디오를 텍스트로 변환할 수 있는 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;&lt;a style=&quot;color: #10a37f;&quot; href=&quot;https://platform.openai.com/docs/models/embeddings&quot;&gt;Embeddings&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;임베딩이 가능한 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;a style=&quot;color: #10a37f;&quot; href=&quot;https://platform.openai.com/docs/models/moderation&quot;&gt;Moderation&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;민감한 내용이나 안전하지 않은 지 여부를 감지할 수 있는 미세조정 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;&lt;a style=&quot;color: #10a37f;&quot; href=&quot;https://platform.openai.com/docs/models/gpt-3&quot;&gt;GPT-3&lt;/a&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;Legacy&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;가장 기본 모델. 3.5나 4로 대체되었지만 미세조정이 가능한 유일한 모델이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;a style=&quot;color: #10a37f;&quot; href=&quot;https://platform.openai.com/docs/deprecations&quot;&gt;Deprecated&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;deprecated된 모델 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/details&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;size18&quot;&gt;&lt;b&gt;messages(필수)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT와의 대화를 구성하는 메시지 목록이다. JSON형태로 제공하며 필수 구성요소는 다음과 같다.&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;role&lt;/b&gt;: 메시지 작성자의 역할을 정의한다. 가능한 인자로는 &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;assistant&lt;/code&gt;, &lt;code&gt;function&lt;/code&gt;이 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;system: GPT의 동작을 미리 설정할 수 있다. 예를 들어, GPT의 성격을 정한다거나 응답 형태를 지정하는 방식으로 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;user: 사용자가 입력하는 내용이다.&lt;/li&gt;
&lt;li&gt;assistant: GPT가 답변한 내용이다. GPT는 기본적으로 이전 대화내용을 기억하지 못하기 때문에, 이전 내용을 바탕으로 대화를 이어나가기 위해 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;content&lt;/b&gt;: 메시지의 내용을 작성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;max_tokens(옵션)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT가 응답으로 만들어내는 토큰의 최대 개수이다. 기본값은 &lt;code&gt;inf&lt;/code&gt;로 model에 따라 기본값이 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;temperature(옵션)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답변의 다양성을 설정한다. 0부터 2사이로 설정하도록 권장되며 소수점 첫째자리까지 입력가능하다.&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;일반적으로 가장 안정적인 숫자는 0.5 ~ 1.0 사이라고 하지만 유저의 요구사항에 따라 적절한 temperature를 찾아야한다.&lt;/p&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;size18&quot;&gt;&lt;b&gt;stream(옵션)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT처럼 한글자씩 응답이 출력되는 방식을 사용하고 싶다면 &lt;code&gt;true&lt;/code&gt;로 지정하면 된다. GPT를 Front end에서 구현한다면 느린 응답에 대한 대안이 될 수 있다.&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 개발 면접을 도와주는 AI 멘토를 주제로 간단하게 구현한 코드와 프롬프트이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT는 영어에 대해 더 정확한 응답을 해주지만 이번 예제에서는 한국어로 구현하기로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용한 언어는 OpenAI가 공식적으로 지원하는 라이브러리를 사용하기 위해 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Python으로 선택하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;최종 코드&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1697875549173&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import json
import os
from dotenv import load_dotenv

import openai

load_dotenv()

# key와 organization을 환경변수로 관리
openai.organization = os.getenv('OPENAI_ORGANIZATION')
openai.api_key = os.getenv('OPENAI_API_KEY')

lang = &quot;Java&quot;
user_content = &quot;Java는 객체지향 언어이다.&quot;

messages = [
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;너는 개발 면접자야. 오직 &quot; + lang + &quot;에 대해서만 질문을 물어봐.&quot;},
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;유저가 말하는 내용을 1점에서 100점까지 점수로 판단해야해. 문장 가장 처음은 무조건 점수부터 출력해.&quot;},
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;유저가 말하는 내용에 대한 점수는 데이터의 정확도로 판단해. 공백이거나 \&quot;모르겠음\&quot;이라는 의미가 포함되면 0점이야.&quot;},
    {&quot;role&quot;: &quot;system&quot;,
     &quot;content&quot;: &quot;유저가 말한 내용에 대해 '답변'과 '키워드'와 '꼬리질문'을 말해. 답변은 틀린 이유에 대해 설명해줘.\n점수 : {점수}점\n답변 : {답변}\n키워드: {키워드}\n꼬리질문: {꼬리질문}&quot;},
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;어조: 친절하게, 전문적이게&quot;},
    {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;꼬리질문: JAVA의 특징은 무엇이 있나요?&quot;}, # 첫번째 질문을 미리 세팅해두었다.
    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_content}
]

# assistant를 저장하는 jsonl 파일을 읽고, message에 추가
with open('gpt_msg_assistant.jsonl', 'r') as file:
    for line in file:
        assistant = json.loads(line)
        messages.append(assistant)


# GPT 호출
completion = openai.ChatCompletion.create(model=&quot;gpt-4&quot;, messages=messages, temperature=0.2, max_tokens=2048)

# GPT 응답에서 필요한 부분만 추출
assistant_message = completion[&quot;choices&quot;][0][&quot;message&quot;].encode(&quot;UTF-8&quot;).decode()

# 결과값을 확인하기 위해 출력
print(assistant_message)

# user의 답변을 jsonl 파일에 저장하기 위해 변수에 저장
user_message = {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_content}


# 파일에 쓰기
data_path = &quot;gpt_msg_assistant.jsonl&quot;

with open(data_path, 'a', encoding='utf-8') as f:
    json.dump(user_message, f, ensure_ascii=False)
    f.write('\n')

with open(data_path, 'a', encoding='utf-8') as f:
    json.dump(assistant_message, f, ensure_ascii=False)
    f.write('\n')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 세세한 코드에 대한 설명보다는 GPT 라이브러리를 사용하는 부분에 대해서만 설명하도록 하겠다.&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;1. key 세팅&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697875866779&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import os
from dotenv import load_dotenv

import openai

load_dotenv()

# key와 organization을 환경변수로 관리
openai.organization = os.getenv('OPENAI_ORGANIZATION')
openai.api_key = os.getenv('OPENAI_API_KEY')&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;openai 라이브러리를 호출하기 위한 기본 key값 설정이다. 여기서는 .env 파일로 관리하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 api_key는 위에서 등록해준 secret key를 입력하면 되며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;organization은 &lt;code&gt;프로필 클릭 &amp;gt; ORGANIZATION &amp;gt; Settings &amp;gt; Organization ID&lt;/code&gt;에 적혀있는 값을 사용하면 된다.&lt;/p&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;size18&quot;&gt;&lt;b&gt;2. messages 세팅&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697876146379&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lang = &quot;Java&quot;
message = &quot;Java는 객체지향 언어이다.&quot;

messages = [
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;너는 개발 면접자야. 오직 &quot; + lang + &quot;에 대해서만 질문을 물어봐.&quot;},
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;유저가 말하는 내용을 1점에서 100점까지 점수로 판단해야해. 문장 가장 처음은 무조건 점수부터 출력해.&quot;},
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;유저가 말하는 내용에 대한 점수는 데이터의 정확도로 판단해. 공백이거나 \&quot;모르겠음\&quot;이라는 의미가 포함되면 0점이야.&quot;},
    {&quot;role&quot;: &quot;system&quot;,
     &quot;content&quot;: &quot;유저가 말한 내용에 대해 '답변'과 '키워드'와 '꼬리질문'을 말해. 답변은 틀린 이유에 대해 설명해줘.\n점수 : {점수}점\n답변 : {답변}\n키워드: {키워드}\n꼬리질문: {꼬리질문}&quot;},
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;어조: 친절하게, 전문적이게&quot;},
    {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;꼬리질문: JAVA의 특징은 무엇이 있나요?&quot;}, # 첫번째 질문을 미리 세팅해두었다.
    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: message}
]

# assistant를 저장하는 jsonl 파일을 읽고, message에 추가
with open('gpt_msg_assistant.jsonl', 'r') as file:
    for line in file:
        assistant = json.loads(line)
        messages.append(assistant)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&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;코드를 보면 role을 system으로 설정하고 GPT의 역할, 어조, 응답 형태 등등을 지정해주고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 assistant를 직접 입력해주어 첫번째 질문을 미리 세팅해두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, GPT를 호출할 때마다 응답(assistant)을 jsonl(Json line)파일에 저장해둔다음 GPT호출 전에 미리 messages에 그 내용을 append하여 이전 대화 내용을 기억할 수 있도록 해주었다.&lt;/p&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;size18&quot;&gt;&lt;b&gt;3. GPT 호출&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697876305258&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# GPT 호출
completion = openai.ChatCompletion.create(model=&quot;gpt-4&quot;, messages=messages, temperature=0.2, max_tokens=2048)

# GPT 응답에서 필요한 부분만 추출
assistant_message = completion[&quot;choices&quot;][0][&quot;message&quot;].encode(&quot;UTF-8&quot;).decode()

# 결과값을 확인하기 위해 출력
print(assistant_message)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 생성한 messages를 사용하여 GPT를 호출해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는 가장 최신 모델인 gpt-4를 사용하였고, temperature는 다양한 질문을 받기 위해 0.2로, max_token은 2048으로 설정해주었다.&lt;/p&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;pre id=&quot;code_1697876498444&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;id&quot;: &quot;chatcmpl-8C1VQeMNq3vFfbCriutG5CC3YvK4i&quot;,
  &quot;object&quot;: &quot;chat.completion&quot;,
  &quot;created&quot;: 1697876416,
  &quot;model&quot;: &quot;gpt-4-0613&quot;,
  &quot;choices&quot;: [
    {
      &quot;index&quot;: 0,
      &quot;message&quot;: {
        &quot;role&quot;: &quot;assistant&quot;,
        &quot;content&quot;: &quot;\uc810\uc218 : 70\uc810\n\ub2f5\ubcc0 : \ub9de\uc2b5\ub2c8\ub2e4. Java\ub294 \uac1d\uccb4\uc9c0\ud5a5 \uc5b8\uc5b4\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc Java\uc758 \ud2b9\uc9d5\uc740 \uadf8\uac83\ub9cc\uc774 \uc544\ub2d9\ub2c8\ub2e4. Java\ub294 \ud50c\ub7ab\ud3fc \ub3c5\ub9bd\uc801\uc774\uba70, \uba40\ud2f0\uc2a4\ub808\ub4dc\uc640 \ubd84\uc0b0\ud658\uacbd \uc9c0\uc6d0, \uac04\ub2e8\ud55c \uba54\ubaa8\ub9ac \uad00\ub9ac \ub4f1\uc758 \ud2b9\uc9d5\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\n\ud0a4\uc6cc\ub4dc: \uac1d\uccb4\uc9c0\ud5a5 \uc5b8\uc5b4, \ud50c\ub7ab\ud3fc \ub3c5\ub9bd\uc801, \uba40\ud2f0\uc2a4\ub808\ub4dc, \ubd84\uc0b0\ud658\uacbd \uc9c0\uc6d0, \uba54\ubaa8\ub9ac \uad00\ub9ac\n\uaf2c\ub9ac\uc9c8\ubb38: Java\uc758 \uba54\ubaa8\ub9ac \uad00\ub9ac\uc5d0 \ub300\ud574 \uc880 \ub354 \uc790\uc138\ud788 \uc124\uba85\ud574\uc8fc\uc2e4 \uc218 \uc788\ub098\uc694?&quot;
      },
      &quot;finish_reason&quot;: &quot;stop&quot;
    }
  ],
  &quot;usage&quot;: {
    &quot;prompt_tokens&quot;: 353,
    &quot;completion_tokens&quot;: 204,
    &quot;total_tokens&quot;: 557
  }
}&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;이 중에서 우리는 message의 value를 가져와서 파일에 저장하기만 하면 되는데, content부분이 Unicode로 인코딩되어 출력되기 때문에 디코딩을 하는 과정이 필수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과값을 디코딩해보면 다음과 같이 출력된다.&lt;/p&gt;
&lt;pre id=&quot;code_1697876877270&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;점수 : 70점\n답변 : 맞습니다. Java는 객체지향 언어입니다. 하지만 Java의 특징은 그것만이 아닙니다. Java는 플랫폼 독립적이며, 멀티스레드와 분산환경 지원, 간단한 메모리 관리 등의 특징도 있습니다.\n키워드: 객체지향 언어, 플랫폼 독립적, 멀티스레드, 분산환경 지원, 메모리 관리\n꼬리질문: Java의 메모리 관리에 대해 좀 더 자세히 설명해주실 수 있나요?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 응답값 jsonl 파일에 쓰기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697876937578&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 파일에 쓰기
data_path = &quot;gpt_msg_assistant.jsonl&quot;

with open(data_path, 'a', encoding='utf-8') as f:
    json.dump(user_message, f, ensure_ascii=False)
    f.write('\n')

with open(data_path, 'a', encoding='utf-8') as f:
    json.dump(assistant_message, f, ensure_ascii=False)
    f.write('\n')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT가 대화 맥락을 기억할 수 있도록 하기 위해 위에서 jsonl파일을 읽어 messages에 추가해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 마지막 부분에서 사용자의 메시지와 GPT의 응답을 파일에 쓰게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 OpenAI의 라이브러리를 사용하는 과정에 대해 적어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT API에는 이렇게 간단한 대화 뿐만 아니라 직접 학습을 시키거나, 오디오를 인식하거나, 이미지를 만드는 등등의 파워풀한 기능이 굉장히 많아서 어떻게 사용하느냐에 따라 ChatGPT보다 더 좋은 서비스를 개발할 수 있을 것 같다.&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;</description>
      <category>Study/Etc</category>
      <category>ChatGPT</category>
      <category>chatgpt api</category>
      <category>openAI</category>
      <category>OpenAI API</category>
      <category>오픈AI</category>
      <category>챗GPT</category>
      <category>챗GPT API</category>
      <author>Anna-Jin</author>
      <guid isPermaLink="true">https://annajin.tistory.com/226</guid>
      <comments>https://annajin.tistory.com/226#entry226comment</comments>
      <pubDate>Sat, 21 Oct 2023 17:33:55 +0900</pubDate>
    </item>
    <item>
      <title>[Spring boot] SSH 터널링 구현하기</title>
      <link>https://annajin.tistory.com/227</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq0xQO/btsynBTCtcF/tWro1GKtLkvAVgkG9YiqK0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq0xQO/btsynBTCtcF/tWro1GKtLkvAVgkG9YiqK0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq0xQO/btsynBTCtcF/tWro1GKtLkvAVgkG9YiqK0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq0xQO%2FbtsynBTCtcF%2FtWro1GKtLkvAVgkG9YiqK0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;180&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 개발 중인 서비스가 통신하기 위한 서버가 특정 서버에서만 접근 가능한 상황이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드에서 동작하는 (유저가 접근하지 않아도 되는) 어플리케이션은 그 특정 서버에 서비스를 띄워서 서로 통신이 가능한 방식을 사용하고 있었지만 개발 중인 서비스가 동작 중인 서버는 별도의 서버로 분리가 되어있었고, 이 서버는 정책상 특정 서버에서 인바운드가 막혀있는 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 복잡한데 아무튼 개발 중 서비스 -&amp;gt; 타겟 서버로의 직접 접근이 안된다는 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해서는 타겟 서버로의 접속을 우회하는 방식을 사용해야하는데 이걸 &lt;b&gt;SSH 터널링&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;u&gt;SSH 터널링의 개념과 필요성, 구현 예시&lt;/u&gt;에 대해 작성해보고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SSH (Secure Shell)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;시큐어 셸(Secure SHell, SSH)&lt;/b&gt;은 네트워크 상의 다른 컴퓨터에 로그인하거나 원격 시스템에서 명령을 실행하고 다른 시스템으로파일을 복사할 수 있도록 해 주는 응용 프로그램 또는 그&amp;nbsp;프로토콜을 가리킨다.&lt;br /&gt;출처 - &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%8B%9C%ED%81%90%EC%96%B4_%EC%85%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;위키백과&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 &lt;u&gt;원격 서버에 접속하기 위한 도구&lt;/u&gt; 라고 생각하면 된다. 사이드 프로젝트의 서버를 담당해본 사람이라면 아마 대부분 알고있지 않을까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ssh 명령어는 아래처럼 사용 가능하다. &lt;code&gt;-i&lt;/code&gt; 옵션은 key 파일로 서버에 접근해야하는 경우 사용하고, password를 사용한다면 옵션 없이 입력해도 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1697093340714&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh {user}@{host} -i {idendtity_file}&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의 Default Port는 22번이다. 따라서 &lt;code&gt;telnet&lt;/code&gt;을 사용해서 22번 포트가 열려있는 지 확인하려면 아래처럼 작성하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1697093131927&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;telnet {host} 22&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;접근이 제한되어있는 서버에 접속하려면?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 서버인 A 서버와 원격 서버인 B 서버가 있다고 가정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 서버 -&amp;gt; B 서버로의 통신은 불가능한데, 또 다른 서버인 C 서버 -&amp;gt; B 서버로의 접근이 가능하다. 또, A 서버 -&amp;gt; C 서버로의 접근 또한 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에 A 서버는 C 서버에 접속한 다음 B 서버로의 접속이 가능하다.&lt;/p&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 명령어를 하나하나 입력해서 B 서버까지 도달하는 방법도 있지만 보다 간단하게 A 서버에서 B 서버로의 접근이 가능한 방법이 있다.&lt;/p&gt;
&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;SSH 터널링&lt;/b&gt; 혹은 &lt;b&gt;SSH 포트 포워딩&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;일반적으로 이 C 서버를 '&lt;b&gt;점프 서버(Jump Server)&lt;/b&gt;' 혹은 '&lt;b&gt;배스쳔 서버(Bastion Server)&lt;/b&gt;'라고 부르고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B 서버를 '&lt;b&gt;리모트 서버(Remote Server)&lt;/b&gt;' 혹은 '&lt;b&gt;타겟 서버(Target Server)&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SSH 터널링 / SSH 포트 포워딩&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘은 비슷한 개념이지만 사실 개념에서 약간의 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SSH 터널링 &lt;/b&gt;&lt;b&gt;(SSH Tunneling)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SSH 터널링&lt;/b&gt;은 보안이 강화된 특별한 &lt;u&gt;'터널'&lt;/u&gt;을 만들어서, 두 서버 사이에 안전하게 정보를 주고받을 수 있게 해주는 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SSH 포트 포워딩 &lt;/b&gt;&lt;b&gt;(SSH Port Forwarding)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SSH 포트 포워딩&lt;/b&gt;은 이런 &lt;u&gt;'터널'&lt;/u&gt;을 통해서 특정 포트를 열어주는 기술이다. 포트 포워딩을 통해 서버와 클라이언트 사이의 통신을 안전하게 하거나, 보안 정책으로 인해 직접 접근이 불가능한 서버에 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트 포워딩은 &lt;u&gt;로컬 포트 포워딩(Local Port Forwarding)&lt;/u&gt;과 &lt;u&gt;리모트 포트 포워딩(Remote Port Forwarding)&lt;/u&gt;, 그리고 &lt;u&gt;다이나믹 포트 포워딩(Dynamic Port Forwarding, SOCKS Proxy)&lt;/u&gt;으로 나뉜다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;로컬 포트 포워딩&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 시스템(127.0.0.1 or localhost)의 특정 포트(LOCAL_PORT)를 리모트 서버의 특정 포트(REMOTE_PORT)와 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 &lt;code&gt;localhost:{LOCAL_PORT}&lt;/code&gt; 로 요청을 보내면, 그 요청은 SSH 터널을 통해서 &lt;code&gt;{REMOTE_HOST}:{REMOTE_PORT}&lt;/code&gt; 로 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;리모트 포트 포워딩&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리모트 서버의 특정 포트(REMOTE_PORT)를 로컬의 특정 포트(LOCAL_PORT)와 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리모트 서버에서 &lt;code&gt;localhost:{REMOTE_PORT}&lt;/code&gt; 로 요청을 보내면, 그 요청은 SSH 터널을 통해서 &lt;code&gt;localhost:{LOCAL_PORT}&lt;/code&gt;로 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;다이나믹 포트 포워딩&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 설정한 동적 포트를 사용하여 SOCKS 프록시 서버를 구성한다. 이 SOCKS 프록시를 통해 여러 리모트 호스트와 포트로의 동적인 연결이 가능해진다.&lt;/p&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;결국 간단하게 말하면 SSH 터널링은 &lt;u&gt;안전한 '도로'&lt;/u&gt;를 만드는 거고, SSH 포트 포워딩은 그 &lt;u&gt;'도로' 위에서 특정 '출입구'를 열어주는 방식&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;구현 예제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring boot에서 SSH를 사용하기 위한 몇가지 라이브러리가 존재하는데 이 예제에서는 JSch 라이브러리를 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Dependency&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Maven&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1697099043090&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- JSch for ssh --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.jcraft&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jsch&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;0.1.55&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&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;Gradle&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1697099118980&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// JSch for ssh
implementation 'com.jcraft:jsch:0.1.55'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;최종 코드&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 코드는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1697096712248&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.jcraft.jsch.*;
import com.roomio.linkapiserver.exception.CustomException;
import com.roomio.linkapiserver.exception.errorCode.CommonErrorCode;
import lombok.extern.log4j.Log4j2;

import java.io.IOException;
import java.io.InputStream;

@Log4j2
class SshServiceExample {

    public static void main(String[] args) throws IOException {
        String jumpServerHost = &quot;점프 서버 IP&quot;;
        String jumpServerUser = &quot;점프 서버 USER&quot;;
        String jumpServerIdentityPath = &quot;점프 서버 key file path&quot;;

        String remoteServerHost = &quot;리모트 서버 IP&quot;;
        String remoteServerUser = &quot;리모트 서버 USER&quot;;
        String remoteServerPassword = &quot;리모트 서버 PASSWORD&quot;;

        String request = &quot;실행할 명령어&quot;;

        StringBuilder response = new StringBuilder();

        Session jumpSession = null;
        Session remoteSession = null;
        ChannelExec channel = null;


        try {

            // JSch 로그 설정
            JSch.setLogger(new Logger() {
                @Override
                public boolean isEnabled(int level) {
                    return true;
                }

                @Override
                public void log(int level, String message) {
                    log.debug(&quot;========== JSch Logger==========\n==========Level: {}, Message: {} ===========&quot;, level, message);
                }
            });

            // JSch 객체 생성
            JSch jsch = new JSch();

            // First SSH session: application server -&amp;gt; jump server
            jumpSession = jsch.getSession(jumpServerUser, jumpServerHost, 22);


            // Private Key 설정
            jsch.addIdentity(jumpServerIdentityPath);

            // SSH에 처음 연결할 때, 리모트 호스트의 공개키가 자신의 known_hosts 파일에 저장된 것과 일치하는 지 검사하고 저장할 지 확인
            // no로 설정하면 보안에 취약해지므로 신뢰할 수 없는 네트워크에서는 사용을 지양해야함.
            jumpSession.setConfig(&quot;StrictHostKeyChecking&quot;, &quot;no&quot;);

            // SSH 연결
            jumpSession.connect();


            log.info(&quot;SSH session1 connected&quot;);


            // Second SSH session: jump server -&amp;gt; remote server

            // 포트포워딩 설정을 해준다. Lport를 0으로 설정하면 자동으로 남는 포트에 바인딩 된다.
            /* 중요 */
            // jump server에는 사용 중이 아닌 포트가 (남는 포트)가 존재해야한다.
            int assigned_port = jumpSession.setPortForwardingL(0, remoteServerHost, 22); // 자동으로 바인딩된 포트번호
            // remote 서버를 열 때는 localhost로 열어야 한다.
            remoteSession = jsch.getSession(remoteServerUser, &quot;localhost&quot;, assigned_port);
            remoteSession.setPassword(remoteServerPassword);

            remoteSession.setConfig(&quot;StrictHostKeyChecking&quot;, &quot;no&quot;);

            remoteSession.connect();

            log.info(&quot;SSH session2 connected&quot;);

            // exec 타입의 채널 오픈. 이 채널에서 Command 사용 가능
            channel = (ChannelExec) remoteSession.openChannel(&quot;exec&quot;);

            // 실행할 Command 세팅
            channel.setCommand(request);

            // 채널 입력 스트림 설정
            // 입력 스트림 : 데이터가 프로그램으로 들어오는 통로. 여기서는 SSH 채널에서 명령어의 실행 결과를 읽어 오는 데 사용됨

            // 입력 스트림을 null로 설정하는 이유?
            // 단방향 통신: 이 예제에서는 서버에 명령을 내리고 그 결과만 읽어오면 된다. 즉, 명령 실행 후에 추가적으로 서버에 보낼 데이가 없고, 이런 경우 입력 스트림이 필요 없으므로 null로 설정한다.
            // 명령어 독립성: 명령어가 실행되는 동안에는 다른 입력을 받을 필요가 없음. 명령어는 이미 setCommand(command);로 설정되었기 때문에 추가적인 입력 스트림은 필요하지 않다..
            // 리소스 절약: 불필요한 스트림을 열지 않음으로써 리소스를 절약할 수 있다.
            // setInputStream(null);을 호출함으로써, JSch는 서버로부터 데이터를 읽어올 때만 스트림을 사용하고, 서버에 데이터를 보낼 때는 스트림을 사용하지 않게 된다. 이렇게 하면 명령어 실행과 결과 수집에만 집중할 수 있음.
            channel.setInputStream(null);
            channel.setErrStream(System.err);

            // 명령어의 출력을 받을 스트림을 가져옴
            InputStream input = channel.getInputStream();

            // 채널을 연결하여 명령어 실행
            channel.connect();

            log.info(&quot;channel.isConnected() : {}&quot;, channel.isConnected());

            // 결과값 가져오기
            byte[] buffer = new byte[1024];
            while (true) {
                int bytesRead = input.read(buffer, 0, buffer.length);
                if (bytesRead &amp;lt; 0) {
                    break;
                }
                response.append(new String(buffer, 0, bytesRead));
            }


        } catch (JSchException e) {

            throw new CustomException(CommonErrorCode.SSH_CONNECTION_FAILED.getCode(), CommonErrorCode.SSH_CONNECTION_FAILED.getMsg(), e.getMessage());

        } finally {
        
            // 채널과 세션 닫기
            // session은 자동으로 연결이 끊어지지 않기 때문에 finally 키워드로 직접 연결을 끊어줘야한다.
            if (channel != null &amp;amp;&amp;amp; channel.isConnected()) {
                channel.disconnect();
            }
            if (remoteSession != null &amp;amp;&amp;amp; remoteSession.isConnected()) {
                remoteSession.disconnect();
            }
            if (jumpSession != null &amp;amp;&amp;amp; jumpSession.isConnected()) {
                jumpSession.disconnect();
            }
        }

        // 응답값 출력
        System.out.println(response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계별로 코드를 살펴보도록 하자. 기본 변수 세팅 부분은 넘어가도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JSch 로그 설정 및 객체 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1697099206635&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// JSch 로그 설정
JSch.setLogger(new Logger() {
    @Override
    public boolean isEnabled(int level) {
        return true;
    }

    @Override
    public void log(int level, String message) {
        log.debug(&quot;========== JSch Logger==========\n==========Level: {}, Message: {} ===========&quot;, level, message);
    }
});

// JSch 객체 생성
JSch jsch = new JSch();&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;JSch는 자체적으로 Logger 인터페이스를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인터페이스를 상속 받아서 구현을 해도 되지만, 이번에는 익명클래스를 사용해서 간단하게 Logger를 구현했다.&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;code&gt;boolean isEnabled(int level)&lt;/code&gt; : 인자로 받은 로그 레벨에 대한 로깅이 활성화 되어있는 지 확인&lt;br /&gt;다음과 같이도 사용 가능하다.&lt;br /&gt;가능한 로그 레벨은 &lt;code&gt;DEBUG&lt;/code&gt;, &lt;code&gt;INFO&lt;/code&gt;, &lt;code&gt;WARN&lt;/code&gt;, &lt;code&gt;ERROR&lt;/code&gt;, &lt;code&gt;FATAL&lt;/code&gt;이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1697153863282&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public boolean isEnabled(int level) {
    return level == Logger.DEBUG;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;void log(int level, String message)&lt;/code&gt; : 로그 메세지 출력. level의 실제 값은 아래를 참고하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #abb2bf;&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int DEBUG = 0;
int INFO = 1;
int WARN = 2;
int ERROR = 3;
int FATAL = 4;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅을 설정한 다음에는 &lt;code&gt;new JSch();&lt;/code&gt;로 객체를 생성해주면 JSch를 사용할 준비가 끝났다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SSH Session 연결 및 포트 포워딩(터널링)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1697154200074&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// First SSH session: application server -&amp;gt; jump server
jumpSession = jsch.getSession(jumpServerUser, jumpServerHost, 22);
jsch.addIdentity(jumpServerIdentityPath); // Private Key 설정
jumpSession.setConfig(&quot;StrictHostKeyChecking&quot;, &quot;no&quot;);
jumpSession.connect(); // SSH 연결


log.info(&quot;SSH Jump session connected&quot;);


// Second SSH session: jump server -&amp;gt; remote server
int assigned_port = jumpSession.setPortForwardingL(0, remoteServerHost, 22); // 자동으로 바인딩된 포트번호
remoteSession = jsch.getSession(remoteServerUser, &quot;localhost&quot;, assigned_port);
remoteSession.setPassword(remoteServerPassword);
remoteSession.setConfig(&quot;StrictHostKeyChecking&quot;, &quot;no&quot;);
remoteSession.connect();


log.info(&quot;SSH Remote session connected&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를 사용하기 위한 Session 객체를 만들어 connect하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드는 현재 서버 구조상 &lt;span style=&quot;color: #f89009;&quot;&gt;local -&amp;gt; jump -&amp;gt; remote -&amp;gt; target&lt;/span&gt; 형태이기 때문에 Session을 두번 생성하고 있지만 일반적인 경우라면 Second SSH session 부분 하나로도 터널링이 가능하다.&lt;/p&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;code&gt;setPortForwardingL()&lt;/code&gt; 부분인데, 차근차근 따라가면서 이해해보자.&lt;/p&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;Application Server -&amp;gt; Jump Server&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1697165834630&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jumpSession = jsch.getSession(jumpServerUser, jumpServerHost, 22);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 서버(Spring application server)에서 점프 서버로 접속하기 위한 Session을 연다. 일반적인 경우, 점프 서버가 로컬 서버가 된다. 이점에 유의하고 헷갈리지 말자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697165820296&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jsch.addIdentity(jumpServerIdentityPath);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점프 서버는 Password 대신 key.pem 파일을 사용하여 서버에 접속한다. 따라서 setIdentity()를 사용하여 pem 파일이 저장되어있는 경로를 입력해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697165814549&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jumpSession.setConfig(&quot;StrictHostKeyChecking&quot;, &quot;no&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;StrickHostKeyChecking&lt;/u&gt;은 SSH에 처음 연결할 때, 접속하려는 서버의 공개키가 자신의 known_hosts 파일에 저장된 것과 일치하는 지 검사하고 저장할 지 확인하는 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션을 no로 설정하면 보안에 취약해지므로 신뢰할 수 없는 네트워크에서는 사용을 지양해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 각각의 서버가 특정 IP로 제한되어 있기 때문에 해당 옵션을 비활성화 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697165808901&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jumpSession.connect();&lt;/code&gt;&lt;/pre&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;&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;Jump Server -&amp;gt; Remote Server&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 SSH 터널링을 하는 작업을 진행해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 터널링 / 포트포워딩을 하기 위해 선행되어야하는 작업이 있는데, 바로 &lt;b&gt;사용하지 않는 포트&lt;/b&gt;를 확보하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 터널링은 사용되지 않는 포트를 잡아서 &lt;u&gt;localhost:{포트}&lt;/u&gt; 로 요청을 받고, 리모트 서버의 특정 포트(여기서는 22)로 &lt;u&gt;포트 포워딩 시키는 방식&lt;/u&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697165799786&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int assigned_port = jumpSession.setPortForwardingL(0, remoteServerHost, 22);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트 포워딩을 위한 설정 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 인자(Lport)에는 사용 가능한 포트 번호를 적어주면 되는데, &quot;0&quot;을 사용하면 자동으로 서버 내의 남아있는 포트를 찾아 바인딩해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 인자와 세번째 인자에는 터널링을 하고자하는 서버의 host와 port를 작성해주면 된다.&lt;/p&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;pre id=&quot;code_1697165792867&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;remoteSession = jsch.getSession(remoteServerUser, &quot;localhost&quot;, assigned_port);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서와 똑같이 리모트 서버로 접속하기 위한 세션을 연다. 이때, 주의할 점은 위에서 포트포워딩 설정을 해주었기 때문에 &lt;b&gt;localhost:{assigned_port} &lt;/b&gt;를 Host에 적어주어야 정상적으로 접속이 가능하다.&lt;/p&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;pre id=&quot;code_1697165785981&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;remoteSession.setPassword(remoteServerPassword);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서 리모트 서버는 Password를 사용하기 때문에 setPassword()를 사용해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Remote Server에서 Command 실행&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1697163784323&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 명령어 실행을 위한 Channel 열기
channel = (ChannelExec) remoteSession.openChannel(&quot;exec&quot;);
channel.setCommand(request);

channel.setInputStream(null);
InputStream input = channel.getInputStream();

channel.connect();

log.info(&quot;channel.isConnected() : {}&quot;, channel.isConnected());

// 결과값 가져오기
byte[] buffer = new byte[1024];
while (true) {
    int bytesRead = input.read(buffer, 0, buffer.length);
    if (bytesRead &amp;lt; 0) {
        break;
    }
    response.append(new String(buffer, 0, bytesRead));
}&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;이제 접속한 리모트 서버에서 Command를 보낼 수 있게 되었다.&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;pre id=&quot;code_1697165767512&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;channel = (ChannelExec) remoteSession.openChannel(&quot;exec&quot;);
channel.setCommand(request);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 직접 Command를 보내기 위해서는 Channel을 열어주어야한다. 이를 위해 리모트 서버의 세션에서 &lt;code&gt;openChannel()&lt;/code&gt;을 해주어야하는데, 이 메소드의 인자로 들어갈 수 있는 타입 중 주로 사용하는 타입은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;session: 기본 세션 채널, 특별한 동작 없음&lt;/li&gt;
&lt;li&gt;shell: 원격 쉘을 열어 명령어 실행. 일반적으로 대화식 작업에 사용&lt;/li&gt;
&lt;li&gt;exec: 단일 명령어 실행. 명령 실행이 완료되면 채널이 자동으로 닫힘.&lt;/li&gt;
&lt;li&gt;sftp: 파일 전송을 위해 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 단순히 커맨드를 실행하기만 하면 되므로 exec타입을 사용했다.&lt;/p&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;pre id=&quot;code_1697165756202&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;channel.setInputStream(null);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 채널 입력 스트림을 설정해주어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 이유로 입력 스트림은 null로 설정해준다.&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;code&gt;setCommand(request);&lt;/code&gt;로 설정되었기 때문에 추가적인 입력 스트림은 필요하지 않다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리소스 절약&lt;/b&gt;: 불필요한 스트림을 열지 않음으로써 리소스를 절약할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #abb2bf;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;InputStream input = channel.getInputStream();
channel.connect();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 명령어의 출력을 받을 스트림을 가져온 후 채널을 연결하여 명령어를 실행하면 inputStream에서 결과값을 가져와 가공할 수 있는 단계가 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;입력 스트림(Input Stream)?&lt;/b&gt;&lt;br /&gt;데이터가 프로그램으로 들어오는 통로. 여기서는 SSH 채널에서 명령어의 실행 결과를 읽어오는 데 사용된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Session 닫기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1697163825538&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;finally {
    if (channel != null &amp;amp;&amp;amp; channel.isConnected()) {
        channel.disconnect();
    }
    if (remoteSession != null &amp;amp;&amp;amp; remoteSession.isConnected()) {
        remoteSession.disconnect();
    }
    if (jumpSession != null &amp;amp;&amp;amp; jumpSession.isConnected()) {
        jumpSession.disconnect();
    }
}&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;위의 코드들이 try-catch로 묶여있었는데, 마지막 finally 키워드를 이용하여 채널과 세션을 모두 닫아준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session은 모든 과정이 끝나더라도 자동으로 연결이 끊어지지 않기 때문에 작업 중간에 에러가 나더라도 세션이 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 꼭 세션을 닫아주도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안이 철저한 서버의 구조가 많은 우리 회사의 특성상 이렇게 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;보안이 중요한 회사 도메인 특성상 여러 단계의 서버를 거칠일이 많기 때문에 기록해두고 자주 꺼내볼듯 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring boot</category>
      <category>Java</category>
      <category>spring boot</category>
      <category>SSH PortFowarding</category>
      <category>SSH Tunneling</category>
      <category>SSH 터널링</category>
      <category>SSH 포트포워딩</category>
      <category>다른서버 경유해서 서버 접속하기</category>
      <category>서버 여러번</category>
      <category>터널링</category>
      <category>포트포워딩</category>
      <author>Anna-Jin</author>
      <guid isPermaLink="true">https://annajin.tistory.com/227</guid>
      <comments>https://annajin.tistory.com/227#entry227comment</comments>
      <pubDate>Fri, 13 Oct 2023 12:05:01 +0900</pubDate>
    </item>
    <item>
      <title>[Spring boot] WebClient 사용해보기 - 모듈화 1-2</title>
      <link>https://annajin.tistory.com/225</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcrjMp/btssfZGVyxP/Sip1KUPVnq6F0e1YBiMIOK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcrjMp/btssfZGVyxP/Sip1KUPVnq6F0e1YBiMIOK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcrjMp/btssfZGVyxP/Sip1KUPVnq6F0e1YBiMIOK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcrjMp%2FbtssfZGVyxP%2FSip1KUPVnq6F0e1YBiMIOK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;180&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://annajin.tistory.com/224&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2023.08.25 - [Study/Spring boot] - [Spring boot] WebClient 사용해보기 2-1&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전글에서는 내가 어떻게 웹클라이언트를 사용하고 있는 지에 대해 간단하게 설명했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 코드를 하나씩 보면서 내가 짠 코드가 어떻게 작동하는 지에 대해 설명해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;2181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NvPtZ/btssla1r8q9/Kou9VdukEBP4JPEIcG5uPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NvPtZ/btssla1r8q9/Kou9VdukEBP4JPEIcG5uPk/img.png&quot; data-alt=&quot;메서드 체이닝 방식 구현&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NvPtZ/btssla1r8q9/Kou9VdukEBP4JPEIcG5uPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNvPtZ%2Fbtssla1r8q9%2FKou9VdukEBP4JPEIcG5uPk%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;300&quot; height=&quot;680&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;2181&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메서드 체이닝 방식 구현&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://annajin.tistory.com/224&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;지난 글&lt;/a&gt;에서 위의 그림과 같이 메서드 체이닝 방식을 직접 구현했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그림 순서에 따라서 차근차근 코드를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ApiWebClientBuilder&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1693033087405&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class ApiWebClientBuilder {

    private final WebClient.Builder webClientBuilder;

    // 체이닝 시작점
    public &amp;lt;T&amp;gt; MethodStep&amp;lt;T&amp;gt; request() {
        return new MethodStepImpl&amp;lt;&amp;gt;(this.webClientBuilder);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 체이닝의&lt;span&gt;&amp;nbsp;&lt;/span&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;일반적으로 WebClient를 Bean으로 등록할 때 WebClient 객체 자체를 등록해서 사용하는데, 여기서는 &lt;code&gt;WebClient.Builder&lt;/code&gt;를 &lt;code&gt;@Component&lt;/code&gt;어노테이션을 이용하여 Bean으로 등록한 후 이후 Build 과정을 동적으로 받을 수 있게 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스를 의존성 주입을 한 다음 &lt;code&gt;request()&lt;/code&gt;메서드를 호출하게 되면 다음 순서인 &lt;code&gt;MethodStepImpl&lt;/code&gt;객체를 리턴하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request 메서드는 시작점이기 때문에 이 메서드가 호출되기 전에는 다음 과정을 수행할 수 없고, 다른 메서드를 먼저 호출하려고 하면 컴파일 에러가 뜨게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;MethodStepImpl&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1693033087408&quot; class=&quot;dart&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Slf4j
public class MethodStepImpl&amp;lt;T&amp;gt; implements MethodStep&amp;lt;T&amp;gt; {

    private final WebClient.Builder webClientBuilder;
    private WebClient.RequestHeadersSpec&amp;lt;?&amp;gt; methodType;


    /**
     * WebClient의 baseUrl과 defaultHeader, encoding 설정
     *
     * @return {@link WebClient}
     */
    private WebClient setBase(String baseUrl) {
        // 인코딩 설정
        DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
        factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);

        // memory size 설정
        ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
                .codecs(configurer -&amp;gt; configurer.defaultCodecs().maxInMemorySize(50 * 1024 * 1024)) // to unlimited memory size
                .build();

        // timeout 설정
        ReactorClientHttpConnector httpConnector = new ReactorClientHttpConnector(
                HttpClient.create()
                        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 120000)
                        .responseTimeout(Duration.ofSeconds(120)));


        return this.webClientBuilder
                .exchangeStrategies(exchangeStrategies)
                .clientConnector(httpConnector)
                .uriBuilderFactory(factory)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .baseUrl(baseUrl)
                .build();
    }

    /**
     * WebClient의 HttpMethod를 POST로 설정.&amp;lt;br&amp;gt;
     * Path 사용
     *
     * @return {@link ConnectStep}
     */
    @Override
    public ConnectStep post(String baseUrl, String path, T requestBody) {
        this.methodType = this.setBaseUrl(!StringUtils.hasText(baseUrl) ? &quot;&quot; : baseUrl)
                .post()
                .uri(uriBuilder -&amp;gt; uriBuilder
                        .path(path)
                        .build())
                .bodyValue(requestBody);
        return new ConnectStepImpl(this.methodType);
    }

    /**
     * WebClient의 HttpMethod를 POST로 설정.&amp;lt;br&amp;gt;
     * queryParam 사용
     *
     * @return {@link ConnectStep}
     */
    @Override
    public ConnectStep post(String baseUrl, MultiValueMap&amp;lt;String, String&amp;gt; params, T requestBody) {
        this.methodType = this.setBaseUrl(baseUrl)
                .post()
                .uri(uriBuilder -&amp;gt; uriBuilder
                        .queryParams(params)
                        .build())
                .bodyValue(requestBody);
        return new ConnectStepImpl(this.methodType);
    }

    /**
     * WebClient의 HttpMethod를 GET으로 설정.&amp;lt;br&amp;gt;
     *
     * @return {@link ConnectStep}
     */
    @Override
    public ConnectStep get(String baseUrl, MultiValueMap&amp;lt;String, String&amp;gt; params) {
        this.methodType = this.setBaseUrl(baseUrl)
                .get()
                .uri(uriBuilder -&amp;gt; uriBuilder
                        .queryParams(params == null || params.isEmpty() ? new LinkedMultiValueMap&amp;lt;&amp;gt;() : params)
                        .build());
        return new ConnectStepImpl(this.methodType);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MethodStep Interface&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1693034014244&quot; class=&quot;arduino&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface MethodStep&amp;lt;T&amp;gt; {
    ConnectStep post(String baseUrl, String path, T requestBody);
    ConnectStep post(String baseUrl, MultiValueMap&amp;lt;String, String&amp;gt; params, T requestBody);
    ConnectStep get(String baseUrl, MultiValueMap&amp;lt;String, String&amp;gt; params);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MethodStep&lt;/code&gt;부터는 본격적으로 동적으로 값을 세팅하기 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 곳에서 &lt;u&gt;baseUrl을 세팅&lt;/u&gt;하고, Api를 어떤 &lt;u&gt;Http method로 호출&lt;/u&gt;할 것인지 선택하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Interface를 상속받게 함으로써 기본적으로 사용하게 될 메서드 구현을 강제하게 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 메소드는 개발자에 따라 다르겠지만 우선 가장 많이 사용하는 post와 get를 몇가지 방식으로 구현해두었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드를 하나씩 보면서 따라가보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;setBase()&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1693033087411&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private WebClient setBase(String baseUrl) {
        // 1. 인코딩 설정
        DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
        factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);

        // 2. memory size 설정
        ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
                .codecs(configurer -&amp;gt; configurer.defaultCodecs().maxInMemorySize(50 * 1024 * 1024)) // to unlimited memory size
                .build();

        // 3. timeout 설정
        ReactorClientHttpConnector httpConnector = new ReactorClientHttpConnector(
                HttpClient.create()
                        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 120000)
                        .responseTimeout(Duration.ofSeconds(120)));


        return this.webClientBuilder
                .exchangeStrategies(exchangeStrategies)
                .clientConnector(httpConnector)
                .uriBuilderFactory(factory)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .baseUrl(baseUrl)
                .build();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메소드에서는 baseUrl 세팅과 Api 호출에 필요한 여러가지 설정을 세팅한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정을 동적으로 받지 않은 이유는, 프로젝트 구조상 굳이 뺄 필요가 없는 설정들이라고 판단했기 때문에 자칫해서 개발자가 놓칠 수도 있는 부분을 고려하지 않아도 될 수 있게 하기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정을 살펴보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인코딩 설정 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://wpioneer.tistory.com/222&quot;&gt;다음글&lt;/a&gt;을 참고해서 인코딩 모드를 설정해주었다. 각 EncodingMode에 대한 설명은 링크에 설명되어있긴 하지만 간단하게 설명을 적어본다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;TEMPLATE_AND_VALUES&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: URI의 문자열과 변수가 모두 인코딩된댜. 이때 URI Template(고정된 문자열들 ex. host, 변수명 등) 가 먼저 인코딩 되고 이후 변수가 특정 기준에 맞춰 인코딩된다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;VALUES_ONLY&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: URI Template를 인코딩하지 않고 URI 변수만 인코딩한다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;URI_COMPONENT&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: URI 변수가 대입된 후에 각 구성요소들을 인코딩한다. 이때 path에 허용된 예약어들은 인코딩하지 않는다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;NONE&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 인코딩을 적용하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Memory size 설정 : 응답값의 최대 메모리 사이즈를 결정해주었다.&lt;/li&gt;
&lt;li&gt;Timeout 설정 : 현재 사용 중인 Api의 권장 timeout시간이 120초였기에 다음과 같이 설정했다. &lt;code&gt;tcpConfiguration()&lt;/code&gt;은 deprecated 되었으므로 &lt;code&gt;Option()&lt;/code&gt;을 사용하여 Connection timeout 설정을 해준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 끝났다면 의존성 주입으로 받은 &lt;code&gt;WebCliet.Builder&lt;/code&gt; 객체를 이용하여 본격적으로 WebClient 객체 빌드를 하고 이후 객체 사용을 위해서 리턴해준다.&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;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;post(), get()&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1693034481996&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * WebClient의 HttpMethod를 POST로 설정.&amp;lt;br&amp;gt;
 * Path 사용
 *
 * @return {@link ConnectStep}
 */
@Override
public ConnectStep post(String baseUrl, String path, T requestBody) {

    if (!StringUtils.hasText(baseUrl)) {
        throw new ApiException(ErrorCode.ETC_ERROR, &quot;[WebClient] baseUrl은 비어있을 수 없습니다.&quot;);
    }

    this.methodType = this.setBaseUrl(baseUrl)
            .post()
            .uri(uriBuilder -&amp;gt; uriBuilder
                    .path(path)
                    .build())
            .bodyValue(requestBody);
    return new ConnectStepImpl(this.methodType);
}

/**
 * WebClient의 HttpMethod를 POST로 설정.&amp;lt;br&amp;gt;
 * queryParam 사용
 *
 * @return {@link ConnectStep}
 */
@Override
public ConnectStep post(String baseUrl, MultiValueMap&amp;lt;String, String&amp;gt; params, T requestBody) {
    if (!StringUtils.hasText(baseUrl)) {
        throw new ApiException(ErrorCode.ETC_ERROR, &quot;[WebClient] baseUrl은 비어있을 수 없습니다.&quot;);
    }

    this.methodType = this.setBaseUrl(baseUrl)
            .post()
            .uri(uriBuilder -&amp;gt; uriBuilder
                    .queryParams(params)
                    .build())
            .bodyValue(requestBody);
    return new ConnectStepImpl(this.methodType);
}

/**
 * WebClient의 HttpMethod를 GET으로 설정.&amp;lt;br&amp;gt;
 *
 * @return {@link ConnectStep}
 */
@Override
public ConnectStep get(String baseUrl, MultiValueMap&amp;lt;String, String&amp;gt; params) {
    if (!StringUtils.hasText(baseUrl)) {
        throw new ApiException(ErrorCode.ETC_ERROR, &quot;[WebClient] baseUrl은 비어있을 수 없습니다.&quot;);
    }

    this.methodType = this.setBaseUrl(baseUrl)
            .get()
            .uri(uriBuilder -&amp;gt; uriBuilder
                    .queryParams(params == null || params.isEmpty() ? new LinkedMultiValueMap&amp;lt;&amp;gt;() : params)
                    .build());
    return new ConnectStepImpl(this.methodType);
}&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;WebClient가 호출하게 될 HttpMethod를 선택하고, &lt;code&gt;RequestBody&lt;/code&gt; 혹은 &lt;code&gt;RequestParam&lt;/code&gt;을 설정하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 요구사항에서 POST나 GET 이외의 방식은 사용할 일이 없었기 때문에 두가지 메소드만 선언을 해두었지만, 개인의 요구사항에 맞게 세팅하면 된다.&lt;/p&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;WebClient의 각 메소드를 간단하게 설명해보면 다음과 같다.&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;get(), post() : Http Method&lt;/li&gt;
&lt;li&gt;uri() : UriBuilder를 이용하여 호출하려는 uri의 scheme, host, path, param... 등등을 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;bodyValue() : 객체 자체를 RequestBody로 담아 보낼 수 있다. WebFlux에서는 비동기 타입인 Mono나 Flux를 사용하지만 bodyValue를 사용하면 실제 값을 담을 수 있다.&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;Method 선택 및 RequestBody(Param)의 세팅이 끝났다면 다음 단계인 &lt;code&gt;ConnectStepImpl&lt;/code&gt;을 return해주어 다음 체이닝 단계를 사용할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위의 코드처럼 호출 URL을 WebClient 객체를 build하기 전 baseUrl로 설정해주는 방식도 존재하지만, UriBuilder의 sheme(), host() 메소드를 사용해서 지정할 수도 있다. 하지만 이렇게 하게 되면 URL을 설정하는 코드가 중복되기 때문에 하나의 메소드로 분리하여 구현하였다.&lt;br /&gt;&lt;br /&gt;UriBuilder를 사용하여 URL을 지정하는 방법은 아래와 같다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693036594422&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;this.setBaseUrl(baseUrl)
                .post()
                .uri(uriBuilder -&amp;gt; uriBuilder
                        .scheme(&quot;https&quot;)
                        .host(&quot;example.com&quot;)
                        .queryParams(params)
                        .build())
                .bodyValue(requestBody);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ConnectStepImpl&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1693036746282&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
public class ConnectStepImpl implements ConnectStep {

    private final WebClient.RequestHeadersSpec&amp;lt;?&amp;gt; methodType;
    private Object response;


    /**
     * WebClient의 header와 response class 설정 후 block으로 호출&amp;lt;br&amp;gt;
     * 헤더와 응답값이 존재할 때 사용. (ex. 일반적인 api 호출)&amp;lt;br&amp;gt;
     * 예외처리 포함
     *
     * @return {@link ResponseStep}
     */
    @Override
    public ResponseStep connectBlock(Map&amp;lt;String, String&amp;gt; headers, Class&amp;lt;?&amp;gt; responseType) {
        try {
            this.response = this.methodType
                    .headers(httpHeaders -&amp;gt; {
                        httpHeaders.setAll(headers == null || headers.isEmpty() ? new HashMap&amp;lt;&amp;gt;() : headers);
                    })
                    .retrieve()
                    .onStatus(HttpStatus::isError, clientResponse -&amp;gt;
                            clientResponse.bodyToMono(String.class)
                                    .flatMap(msg -&amp;gt; Mono.error(new APIException(ResponseCode.INTERFACE_ERROR, msg))))
                    .bodyToMono(responseType)
                    .retryWhen(Retry.fixedDelay(3, java.time.Duration.ofSeconds(1))
                            .doBeforeRetry(before -&amp;gt; log.info(&quot;Retry: {} | {}&quot;, before.totalRetries(), before.failure())))
                    .block();
            return new ResponseStepImpl(this.response);
        } catch (Exception e) {
            log.error(&quot;Exception : {}&quot;, e.getMessage());
            throw new ApiException(ErrorCode.INTERFACE_ERROR, e.getMessage());
        }
    }


    @Override
    public ResponseStep connectBlock(Map&amp;lt;String, String&amp;gt; headers) {
        try {
            this.response = this.methodType
                    .headers(httpHeaders -&amp;gt; {
                        httpHeaders.setAll(headers == null || headers.isEmpty() ? new HashMap&amp;lt;&amp;gt;() : headers);
                    })
                    .retrieve()
                    .onStatus(HttpStatus::isError, clientResponse -&amp;gt;
                            clientResponse.bodyToMono(String.class)
                                    .flatMap(msg -&amp;gt; Mono.error(new APIException(ResponseCode.INTERFACE_ERROR, msg))))
                    .bodyToMono(Object.class)
                    .retryWhen(Retry.fixedDelay(3, java.time.Duration.ofSeconds(1))
                            .doBeforeRetry(before -&amp;gt; log.info(&quot;Retry: {} | {}&quot;, before.totalRetries(), before.failure())))
                    .block();
            return new ResponseStepImpl(this.response);
        } catch (Exception e) {
            log.error(&quot;Exception : {} | {}&quot;, e.getMessage(), e.getStackTrace()[0].toString());
            throw new ApiException(ErrorCode.INTERFACE_ERROR, e.getMessage());
        }
    }


    /**
     * WebClient의 header와 response class 설정 후 block으로 호출&amp;lt;br&amp;gt;
     * 헤더와 응답값이 존재하지 않을 때 사용. (ex. Google chat webhook api 호출)&amp;lt;br&amp;gt;
     * 예외처리 포함
     *
     * @return {@link ResponseStep}
     */
    @Override
    public ResponseStep connectBlock() {
        try {
            this.response = this.methodType
                    .retrieve()
                    .onStatus(HttpStatus::isError, clientResponse -&amp;gt;
                            clientResponse.bodyToMono(String.class)
                                    .flatMap(msg -&amp;gt; Mono.error(new APIException(ResponseCode.INTERFACE_ERROR, msg))))
                    .bodyToMono(Object.class)
                    .retryWhen(Retry.fixedDelay(3, java.time.Duration.ofSeconds(1))
                            .doBeforeRetry(before -&amp;gt; log.info(&quot;Retry: {} | {}&quot;, before.totalRetries(), before.failure())))
                    .block();
            return new ResponseStepImpl(this.response);
        } catch (Exception e) {
            log.error(&quot;Exception : {} | {}&quot;, e.getMessage(), e.getStackTrace()[0].toString());
            throw new ApiException(ErrorCode.INTERFACE_ERROR, e.getMessage());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ConnectStep&lt;/b&gt;은 본격적으로 API를 URL로 호출하여 응답을 받는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 각 메소드를 하나씩 살펴보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;retrieve()&lt;/b&gt; : WebClient에서 응답을 받는 방식은 &lt;code&gt;exchange()&lt;/code&gt;와 &lt;code&gt;retrieve()&lt;/code&gt; 두가지가 있다. &lt;code&gt;exchange()&lt;/code&gt;는 deprecated되었으므로 만약 exchange를 사용하고 싶다면 &lt;code&gt;exchangeToMono()&lt;/code&gt; 나 &lt;code&gt;exchangeToFlux()&lt;/code&gt;를 사용하자.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onStatue() &lt;/b&gt;: 상태코드를 캐치한다. 특정 상태코드 (404, 500 등등)나 4xx, 5xx 등 세분화하여 응답을 받을 수 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;여기서는 &lt;code&gt;isError&lt;/code&gt;를 사용하여 모든 에러를 캐치하여 Exception을 throw할 수 있도록 구현하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;bodyToMono()&lt;/b&gt; : ResponseBody를 원하는 객체로 변환한다. Mono 타입이므로 0 ~ 1개의 응답만 받을 수 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;만약 2개 이상의 응답을 받고 싶다면 &lt;code&gt;bodyToFlux()&lt;/code&gt;를, ResponseEntity를 받고 싶다면 &lt;code&gt;toEntity()&lt;/code&gt;를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;retryWhen()&lt;/b&gt; : 에러가 발생했을 경우 재시도 한다. Retry 객체로 재시도 횟수와 대기 시간, 재시도하기 전에 수행할 동작을 정의할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;block()&lt;/b&gt; : 응답을 동기 방식으로 받는다. block를 사용하면 비동기 타입인 Mono&amp;lt;Object&amp;gt;가 아닌 Object 객체를 가져올 수 있지만 WebClient의 장점인 비동기 방식이 아닌 동기 방식을 사용한다는 부분에 유의하자.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;내 경우에는 비동기 통신을 이용할 일이 없기 때문에 모든 메소드를 &lt;code&gt;block()&lt;/code&gt;을 사용하여 동기 방식을 사용하였다. 이 역시 개인의 요구사항에 맞춰 사용하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 살펴보면 오버로딩을 통해 여러가지 파라미터를 받는 걸 확인할 수 있다. 이는, 요청으로 header가 필요한 경우, 특정 응답 객체로 받는 경우, 아무 응답도 받지 않는 경우를 모두 구현해 놓았기 때문이다.&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;이렇게 API 호출 및 응답을 받는 과정을 마치면 최종적으로 객체를 원하는 형태로 만드는 &lt;code&gt;ResponseStepImpl&lt;/code&gt;을 리턴하여 최종 단계로 넘어갈 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ResponseStepImpl&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1693038449741&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
public class ResponseStepImpl implements ResponseStep {

    private final Object response;

    /**
     * WebClient의 response를 Object로 리턴&amp;lt;br&amp;gt;
     * 응답값을 response type class로 파싱 후 사용&amp;lt;br&amp;gt;
     * &amp;lt;br&amp;gt;
     * Example)&amp;lt;br&amp;gt;
     * TestDto test = (TestDto) webClientConnectorTest.post(url, path, requestBody)
     *                 .connectBlock(headers, TestDto.class)
     *                 .toObjectCall();
     * @return {@link Object}
     */
    @Override
    public Object toObjectCall() {
        return this.response;
    }

    /**
     * WebClient의 response를 JsonNode로 파싱하여 리턴
     * @return {@link JsonNode}
     */
    @Override
    public JsonNode toJsonNodeCall() {
        try {
            ObjectMapper om = new ObjectMapper();

            return om.readTree(this.response.toString());
        } catch (JsonProcessingException e) {
            log.error(&quot;JsonProcessingException : {}&quot;, e.getMessage());
            throw new ApiException(ErrorCode.INTERFACE_ERROR, e.getMessage());
        }
    }

    /**
     * WebClient의 response를 리턴하지 않음
     */
    @Override
    public void toVoidCall() {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 step은 간단하다. 어떤 형태의 객체를 return할 지를 결정하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 toJsonNodeCall()은 굳이 필요하지 않지만 팀원의 요청으로 넣어두었기 때문에 무시해도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 설명할 부분이 없으므로 이 부분은 코드만 보여주고 넘어가도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Result&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 과정을 마치면 이전 글에서 작성한 것처럼 아래와 같이 코드를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693038729410&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Object callApi(String baseUrl, String path, T request, Class&amp;lt;?&amp;gt; response) {
    return apiWebClientBuilder.request()
            .post(baseUrl, path, request)
            .connectBlock(this.getHeader(), response)
            .toObjectCall();
}

public ExampleResponseDto exampleCallApi(ExampleRequestDto request) {
	String baseUrl = &quot;https://example.com&quot;;
    String path = &quot;/api/test&quot;;
    
    return (ExampleResponseDto) this.callApi(baseUrl, path, request, ExampleResponseDto.class);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 코드는 아래 Github 링크에서 확인할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Anna-Jin/spring-boot&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Anna-Jin/spring-boot&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1700274278643&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;GitHub - Anna-Jin/spring-boot: spring boot 코드 정리 및 공부용 레파지토리&quot; data-og-description=&quot;spring boot 코드 정리 및 공부용 레파지토리. Contribute to Anna-Jin/spring-boot development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Anna-Jin/spring-boot&quot; data-og-url=&quot;https://github.com/Anna-Jin/spring-boot&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/djX7BG/hyUysN12XZ/uSbmC8kh8qz2ij7v501400/img.png?width=1200&amp;amp;height=600&amp;amp;face=976_140_1056_228&quot;&gt;&lt;a href=&quot;https://github.com/Anna-Jin/spring-boot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Anna-Jin/spring-boot&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/djX7BG/hyUysN12XZ/uSbmC8kh8qz2ij7v501400/img.png?width=1200&amp;amp;height=600&amp;amp;face=976_140_1056_228');&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;GitHub - Anna-Jin/spring-boot: spring boot 코드 정리 및 공부용 레파지토리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;spring boot 코드 정리 및 공부용 레파지토리. Contribute to Anna-Jin/spring-boot 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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 WebClient를 더 쉽게 라이브러리처럼 bean등록하여 사용할 수 있을까 고심하여 코드를 짜보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 내용이지만 WebClient에 대한 약간의 이해가 있는 상태에서 차근차근 따라가면 크게 어려울 것 없는 코드라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 초보 개발자로서 완벽한 코드라고 말하기는 어렵지만, 계속 코드를 업데이트하면서 더 완벽하게 만들어 모듈화시킬 예정이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring boot</category>
      <category>WebClient</category>
      <category>WebClient 사용법</category>
      <category>메서드 체이닝</category>
      <category>웹클라이언트</category>
      <category>웹클라이언트 사용법</category>
      <author>Anna-Jin</author>
      <guid isPermaLink="true">https://annajin.tistory.com/225</guid>
      <comments>https://annajin.tistory.com/225#entry225comment</comments>
      <pubDate>Sat, 26 Aug 2023 17:34:16 +0900</pubDate>
    </item>
    <item>
      <title>[Spring boot] WebClient 사용해보기 - 모듈화 1-1</title>
      <link>https://annajin.tistory.com/224</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q28NS/btssasb97dy/CFKmQWvnbaFVpecCBSmJwk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q28NS/btssasb97dy/CFKmQWvnbaFVpecCBSmJwk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q28NS/btssasb97dy/CFKmQWvnbaFVpecCBSmJwk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq28NS%2Fbtssasb97dy%2FCFKmQWvnbaFVpecCBSmJwk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;180&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무려 1년도 더 된 &lt;a title=&quot;이전글&quot; href=&quot;https://annajin.tistory.com/101&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전글&lt;/a&gt;이 항상 내 블로그 인기글 1위인걸 볼 때마다 마음에 걸렸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-14 오후 3.43.17.png&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cq2jvo/btsrgjercdw/GJ3x7ktRPdOkKJxDWXsKKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cq2jvo/btsrgjercdw/GJ3x7ktRPdOkKJxDWXsKKK/img.png&quot; data-alt=&quot;누가봐도 개발 갓 시작한 초보가 쓴 티가 나는 글..ㅜㅜ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cq2jvo/btsrgjercdw/GJ3x7ktRPdOkKJxDWXsKKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcq2jvo%2Fbtsrgjercdw%2FGJ3x7ktRPdOkKJxDWXsKKK%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;300&quot; height=&quot;76&quot; data-filename=&quot;스크린샷 2023-08-14 오후 3.43.17.png&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;누가봐도 개발 갓 시작한 초보가 쓴 티가 나는 글..ㅜㅜ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막 개발 공부하기 시작했을 즈음 webClient에 대한 자료가 별로 없어서 정리할 겸 작성했던 글인데 관련 키워드로 검색하면 저 글이 뜨는 걸 보고... 새롭게 알게된 내용들을 조금 더 추가하고, 현재는 어떻게 사용하고 있는 지에 대해 정리를 해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 개념이나 RestTemplate과의 차이점 등등은 &lt;a title=&quot;WebClient 공식문서&quot; href=&quot;https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;나 &lt;a title=&quot;검색결과&quot; href=&quot;https://www.google.com/search?q=Webclient&amp;amp;sca_esv=556671154&amp;amp;rlz=1C5CHFA_enKR1025KR1025&amp;amp;sxsrf=AB5stBgy3UDV4AoRrkhs-AtB8_i1z1uGOA%3A1691995767530&amp;amp;ei=d87ZZIKvH66B2roP0uClsAM&amp;amp;ved=0ahUKEwjCxry1x9uAAxWugFYBHVJwCTYQ4dUDCA8&amp;amp;uact=5&amp;amp;oq=Webclient&amp;amp;gs_lp=Egxnd3Mtd2l6LXNlcnAiCVdlYmNsaWVudDIHECMYigUYJzIHECMYigUYJzIHEAAYigUYQzIHEAAYigUYQzIHEAAYigUYQzIHEAAYigUYQzIHEAAYigUYQzIHEAAYigUYQzIFEAAYgAQyBRAAGIAESIIqUOwDWKgocAF4AZABAJgBfKAB5gKqAQMwLjO4AQPIAQD4AQHCAgoQABhHGNYEGLADwgIKEAAYgAQYFBiHAuIDBBgAIEGIBgGQBgo&amp;amp;sclient=gws-wiz-serp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;구글에 검색&lt;/a&gt;하면 깔끔하게 정리된 자료가 많기에 넘어가기로 하고, 내가 사용하면서 알게 된 개념들만 적어내려가보겠다!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;WebClient&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-14 오후 3.51.31.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3UIXx/btsq2l50CT7/kJjK3VHmlPCfF1xqEBias1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3UIXx/btsq2l50CT7/kJjK3VHmlPCfF1xqEBias1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3UIXx/btsq2l50CT7/kJjK3VHmlPCfF1xqEBias1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3UIXx%2Fbtsq2l50CT7%2FkJjK3VHmlPCfF1xqEBias1%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;393&quot; data-filename=&quot;스크린샷 2023-08-14 오후 3.51.31.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 완성될 구조는 위와 같다. 각 &lt;b&gt;step&amp;nbsp;&lt;/b&gt;이 뭐고 어떻게 사용하는 지는 천천히 설명하기로 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 사용하면 아래와 같이 코드를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1691997117492&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * API 호출
 *
 * @param baseUrl  특정 API 마다 baseUrl이 다를 수 있음.
 * @param path     path parameter
 * @param request  requestBody
 * @param response responseBody의 class
 * @return 응답 객체
 */
public Object callApi(String baseUrl, String path, T request, Class&amp;lt;?&amp;gt; response) {
    return apiWebClientBuilder.request()
            .post(baseUrl, path, request)
            .connectBlock(this.getHeader(), response)
            .toObjectCall();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이런 구조를 사용한 이유?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 각 step 구조로 만들게 된 이유는 회사 내의 OpenApi에서 &lt;u&gt;범용적으로 사용할 수 있도록&lt;/u&gt;하고, 메서드 체이닝 방식을 사용해서 마치 &lt;u&gt;라이브러리를 사용하는 것처럼 간편하게 사용&lt;/u&gt;할 수 있게 하기 위해서이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient를 각 요청마다 매번 새롭게 생성해서 사용하기에는 반복되는 코드가 많아지는 문제가 있었고, bean에 등록해서 사용하기에는 WebClient는 Immutable하기 때문에 한번 Build를 하고나면 기존 객체를 mutate해서 사용해야했기 때문에 번거롭다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Webclient 공식문서에는 다음과 같은 내용이 기술되어있다.&lt;br /&gt;Once built, a WebClient is immutable. However, you can clone it and build a modified copy as follows:&lt;br /&gt;
&lt;pre id=&quot;code_1691996672955&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WebClient client1 = WebClient.builder()
		.filter(filterA).filter(filterB).build();

WebClient client2 = client1.mutate()
		.filter(filterC).filter(filterD).build();

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD​&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;출처 : &lt;a title=&quot;공식문서&quot; href=&quot;https://docs.spring.io/spring-framework/reference/web/webflux-webclient/client-builder.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;WebClient 공식문서&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;최종 목표&lt;/b&gt;는 bean등록을 함으로써 &lt;u&gt;중복되는 코드를 줄이면서&lt;/u&gt;도 baseUrl은 &lt;u&gt;동적으로 변경할 수 있도록 구현하는 것&lt;/u&gt;이었다. 또, 메서드 체이닝을 할 때 &lt;u&gt;메서드 호출 순서를 강제하게 함으로써&lt;/u&gt; 예상치 못한 문제 또한 제거하고자 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현 예제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 체이닝은 각 체이닝 단계별로 Interface와 구현체를 만들고, 순차적으로 다음 단계의 구현체를 리턴하는 방식으로 구현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;메서드_체이닝_구조.png&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;2181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KOvQ3/btsq2mcKWuN/Xg9cvwZk3LD3HFdQ36nKPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KOvQ3/btsq2mcKWuN/Xg9cvwZk3LD3HFdQ36nKPk/img.png&quot; data-alt=&quot;이미지로 표현한 메서드 체이닝 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KOvQ3/btsq2mcKWuN/Xg9cvwZk3LD3HFdQ36nKPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKOvQ3%2Fbtsq2mcKWuN%2FXg9cvwZk3LD3HFdQ36nKPk%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;350&quot; height=&quot;794&quot; data-filename=&quot;메서드_체이닝_구조.png&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;2181&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지로 표현한 메서드 체이닝 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기존에는 Builder 패턴처럼 구현하려고 했었다. 위와 같은 방식을 사용하면 각 호출마다 새로운 객체를 생성하게 되어서 메모리 낭비가 될 우려가 있었기 때문이다. 하지만 Builder 패턴처럼 구현하게 되면 메서드 호출 순서를 강제할 수 없기 때문에 문제가 발생 가능성이 있었다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 간단하게 내가 어떻게 WebClient를 사용하고 있는지 설명해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 코드를 하나씩 살펴보면서 어떻게 구현했는 지를 설명해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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://annajin.tistory.com/225&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2023.08.26 - [Study/Spring boot] - [Spring boot] WebClient 사용해보기 2-2&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처 및 참고&lt;/p&gt;
&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://docs.spring.io/spring-framework/reference/web/webflux-webclient/client-builder.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;WebClient 공식문서&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring boot</category>
      <category>SpringBoot</category>
      <category>WebClient</category>
      <category>WebClient 사용법</category>
      <category>Webflux</category>
      <category>스프링부트</category>
      <category>웹클라이언트</category>
      <category>웹클라이언트 사용법</category>
      <author>Anna-Jin</author>
      <guid isPermaLink="true">https://annajin.tistory.com/224</guid>
      <comments>https://annajin.tistory.com/224#entry224comment</comments>
      <pubDate>Fri, 25 Aug 2023 18:44:27 +0900</pubDate>
    </item>
    <item>
      <title>[Intellij] 인텔리제이 TODO 코드태그(Code Tag) 커스텀</title>
      <link>https://annajin.tistory.com/223</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IntelliJ_IDEA_Icon.svg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2XSWb/btrWUdQje3k/orkjKvL1leUPq1DzLsZE20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2XSWb/btrWUdQje3k/orkjKvL1leUPq1DzLsZE20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2XSWb/btrWUdQje3k/orkjKvL1leUPq1DzLsZE20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2XSWb%2FbtrWUdQje3k%2ForkjKvL1leUPq1DzLsZE20%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;200&quot; height=&quot;200&quot; data-filename=&quot;IntelliJ_IDEA_Icon.svg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 3.49.28.png&quot; data-origin-width=&quot;354&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMqmvX/btrWU6XBpId/77YfaYQXMWMwVM7m4Pu4kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMqmvX/btrWU6XBpId/77YfaYQXMWMwVM7m4Pu4kK/img.png&quot; data-alt=&quot;인텔리제이 TODO&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMqmvX/btrWU6XBpId/77YfaYQXMWMwVM7m4Pu4kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMqmvX%2FbtrWU6XBpId%2F77YfaYQXMWMwVM7m4Pu4kK%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;354&quot; height=&quot;64&quot; data-filename=&quot;스크린샷 2023-01-24 오후 3.49.28.png&quot; data-origin-width=&quot;354&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인텔리제이 TODO&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다보면 사진처럼 주석을 하이라이팅 하거나 한눈에 모아보고 싶은 경우가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Intellij에서 제공하는 테마를 살짝 손봐서 주석 색상을 변경하는 방법도 있고, Javadoc 주석(&lt;code&gt;/** content */&lt;/code&gt;)을 사용는 방법도 있 지만 Intellij 자체에서 주석을 모아보는 TODO 기능이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Intellij에서 제공하는 TODO 코드 태그(Code Tag)기능을 알아보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;TODO 기능 사용하기&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기본 사용법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.23.39.png&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZbtyj/btrWUc4P7KF/3YKTvQzYNnFNQUA3QWiyP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZbtyj/btrWUc4P7KF/3YKTvQzYNnFNQUA3QWiyP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZbtyj/btrWUc4P7KF/3YKTvQzYNnFNQUA3QWiyP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZbtyj%2FbtrWUc4P7KF%2F3YKTvQzYNnFNQUA3QWiyP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;168&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.23.39.png&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;184&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO 기능의 사용방법은 간단하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석 코드와 함께&amp;nbsp;&lt;b&gt;대문자로&amp;nbsp;&lt;/b&gt;TODO를 입력하면 자동으로 주석이 하이라이팅이 된다. (!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 TODO와 작성하고자하는 내용은 한칸의 공백(space)가 있어야 한다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.32.53.png&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGqcKC/btrW2uQIdnk/8HbK3JH06hD4br5tY58RF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGqcKC/btrW2uQIdnk/8HbK3JH06hD4br5tY58RF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGqcKC/btrW2uQIdnk/8HbK3JH06hD4br5tY58RF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGqcKC%2FbtrW2uQIdnk%2F8HbK3JH06hD4br5tY58RF1%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;119&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.32.53.png&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 멀티라인으로도 사용할 수 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티라인 TODO는 TODO 태그 다음 줄에 한칸의 공백(space)을 입력 후 작성하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TODO 모아보기&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.40.55.png&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;1392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wGYmm/btrWX30qaFY/ezkP8fpb1D1diEVnuELAC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wGYmm/btrWX30qaFY/ezkP8fpb1D1diEVnuELAC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wGYmm/btrWX30qaFY/ezkP8fpb1D1diEVnuELAC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwGYmm%2FbtrWX30qaFY%2FezkP8fpb1D1diEVnuELAC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;676&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.40.55.png&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO 탭은 상단바에서 &lt;b&gt;View &amp;gt; Tool Windows &amp;gt; TODO &lt;/b&gt;를 클릴하면 다음과 같이 작성한 TODO들을 한 눈에 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.46.49.png&quot; data-origin-width=&quot;2832&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lcHer/btrWTJPm4it/2JvLATQEyXbdjzmAQ1IkAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lcHer/btrWTJPm4it/2JvLATQEyXbdjzmAQ1IkAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lcHer/btrWTJPm4it/2JvLATQEyXbdjzmAQ1IkAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlcHer%2FbtrWTJPm4it%2F2JvLATQEyXbdjzmAQ1IkAK%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;2832&quot; height=&quot;620&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.46.49.png&quot; data-origin-width=&quot;2832&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TODO 탭 기능&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.02.42.png&quot; data-origin-width=&quot;66&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdujR8/btrWVLFWHew/J0dvdBMWWjVr5gD2H4eM90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdujR8/btrWVLFWHew/J0dvdBMWWjVr5gD2H4eM90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdujR8/btrWVLFWHew/J0dvdBMWWjVr5gD2H4eM90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdujR8%2FbtrWVLFWHew%2FJ0dvdBMWWjVr5gD2H4eM90%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;40&quot; height=&quot;263&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.02.42.png&quot; data-origin-width=&quot;66&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 146px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 1.16279%; height: 17px;&quot;&gt;Icon&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%; height: 17px;&quot;&gt;Name&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%;&quot;&gt;Shortcut&lt;/td&gt;
&lt;td style=&quot;width: 12.5%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;Description&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 1.16279%; height: 17px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.11.18.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pC4zy/btrWUccJ0XO/kgTobGZLdV4kb7YVMugnk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pC4zy/btrWUccJ0XO/kgTobGZLdV4kb7YVMugnk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pC4zy/btrWUccJ0XO/kgTobGZLdV4kb7YVMugnk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpC4zy%2FbtrWUccJ0XO%2FkgTobGZLdV4kb7YVMugnk0%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;25&quot; height=&quot;25&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.11.18.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%; height: 17px;&quot;&gt;Previous TODO&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%;&quot;&gt;Mac : option + cmd + up&lt;/td&gt;
&lt;td style=&quot;width: 12.5%; height: 17px;&quot;&gt;이전 TODO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 1.16279%; height: 10px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.11.55.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bADKOb/btrWZXFEDq1/se9piKXNANKtmZanXgXsH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bADKOb/btrWZXFEDq1/se9piKXNANKtmZanXgXsH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bADKOb/btrWZXFEDq1/se9piKXNANKtmZanXgXsH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbADKOb%2FbtrWZXFEDq1%2Fse9piKXNANKtmZanXgXsH1%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;25&quot; height=&quot;25&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.11.55.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%; height: 10px;&quot;&gt;Next TODO&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%;&quot;&gt;Mac : option + cmd + down&lt;/td&gt;
&lt;td style=&quot;width: 12.5%; height: 10px;&quot;&gt;다음 TODO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 1.16279%; height: 16px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.12.11.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dP4aRG/btrW37VhFOw/UUUHjWY0lKB2GgOSaXa3K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dP4aRG/btrW37VhFOw/UUUHjWY0lKB2GgOSaXa3K1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dP4aRG/btrW37VhFOw/UUUHjWY0lKB2GgOSaXa3K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdP4aRG%2FbtrW37VhFOw%2FUUUHjWY0lKB2GgOSaXa3K1%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;25&quot; height=&quot;25&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.12.11.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%; height: 16px;&quot;&gt;Filter TODO Items&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 12.5%; height: 16px;&quot;&gt;필터 선택, 필터 편집&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 1.16279%; height: 17px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.12.49.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMr7XN/btrW0PAMA2f/K3tgkIAwFWKKTrA0ZWAE91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMr7XN/btrW0PAMA2f/K3tgkIAwFWKKTrA0ZWAE91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMr7XN/btrW0PAMA2f/K3tgkIAwFWKKTrA0ZWAE91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMr7XN%2FbtrW0PAMA2f%2FK3tgkIAwFWKKTrA0ZWAE91%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;25&quot; height=&quot;25&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.12.49.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%; height: 17px;&quot;&gt;Navigate with Sigle Click&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 12.5%; height: 17px;&quot;&gt;활성화 시 싱글클릭으로 TODO 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 1.16279%; height: 17px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.13.35.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHzKEb/btrW6qG1SZ3/rXMM08QhWuzVlslkwihHwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHzKEb/btrW6qG1SZ3/rXMM08QhWuzVlslkwihHwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHzKEb/btrW6qG1SZ3/rXMM08QhWuzVlslkwihHwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHzKEb%2FbtrW6qG1SZ3%2FrXMM08QhWuzVlslkwihHwK%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;25&quot; height=&quot;25&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.13.35.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%; height: 17px;&quot;&gt;Expand All&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%;&quot;&gt;Mac : cmd + '+'&lt;/td&gt;
&lt;td style=&quot;width: 12.5%; height: 17px;&quot;&gt;탭 모두 펼치기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 14px;&quot;&gt;
&lt;td style=&quot;width: 1.16279%; height: 14px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.14.09.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YMml3/btrWVKtvcdV/DUxzIybQe4vqaZQe2NwtOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YMml3/btrWVKtvcdV/DUxzIybQe4vqaZQe2NwtOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YMml3/btrWVKtvcdV/DUxzIybQe4vqaZQe2NwtOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYMml3%2FbtrWVKtvcdV%2FDUxzIybQe4vqaZQe2NwtOK%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;25&quot; height=&quot;25&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.14.09.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%; height: 14px;&quot;&gt;Collapse All&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%;&quot;&gt;Mac : cmd + '-'&lt;/td&gt;
&lt;td style=&quot;width: 12.5%; height: 14px;&quot;&gt;탭 모두 닫기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 1.16279%; height: 17px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.15.18.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T0tmy/btrWUcKAJeG/4VqXNkmGgXKBUTtNHdkc91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T0tmy/btrWUcKAJeG/4VqXNkmGgXKBUTtNHdkc91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T0tmy/btrWUcKAJeG/4VqXNkmGgXKBUTtNHdkc91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT0tmy%2FbtrWUcKAJeG%2F4VqXNkmGgXKBUTtNHdkc91%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;25&quot; height=&quot;25&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.15.18.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%; height: 17px;&quot;&gt;Group By&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 12.5%; height: 17px;&quot;&gt;TODO 항목 그룹화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 1.16279%; height: 21px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.15.39.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSZAOs/btrWR8ooO3m/jmvz8KOH5O1FaHVtLApf9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSZAOs/btrWR8ooO3m/jmvz8KOH5O1FaHVtLApf9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSZAOs/btrWR8ooO3m/jmvz8KOH5O1FaHVtLApf9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSZAOs%2FbtrWR8ooO3m%2Fjmvz8KOH5O1FaHVtLApf9K%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;25&quot; height=&quot;25&quot; data-filename=&quot;스크린샷 2023-01-24 오후 5.15.39.png&quot; data-origin-width=&quot;58&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%; height: 21px;&quot;&gt;Preview Source&lt;/td&gt;
&lt;td style=&quot;width: 18.1686%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 12.5%; height: 21px;&quot;&gt;TODO 미리보기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코드태그 추가하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본으로 사용할 수 있는 TODO와 FIXME 외에도 코드 태그를 커스텀하여 사용할 수도 있다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Preferences &amp;gt; Editor &amp;gt; TODO&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;스크린샷 2023-01-24 오후 4.26.49.png&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;1402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pcynF/btrWVKmHZA0/jVSugehGRUniFEIkVKOEdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pcynF/btrWVKmHZA0/jVSugehGRUniFEIkVKOEdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pcynF/btrWVKmHZA0/jVSugehGRUniFEIkVKOEdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpcynF%2FbtrWVKmHZA0%2FjVSugehGRUniFEIkVKOEdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;389&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.26.49.png&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;1402&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Patterns 아래의 + 버튼을 클릭한 후, 아래의 정규식을 사용하여 새로운 태그를 추가할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1674545529097&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;\b{TODO이름}\b.*


ex) \bwaring\b.*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.55.14.png&quot; data-origin-width=&quot;562&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBlJES/btrW57HuPFh/6vIAvZ3DZJCSCtmjD7aiNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBlJES/btrW57HuPFh/6vIAvZ3DZJCSCtmjD7aiNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBlJES/btrW57HuPFh/6vIAvZ3DZJCSCtmjD7aiNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBlJES%2FbtrW57HuPFh%2F6vIAvZ3DZJCSCtmjD7aiNK%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;300&quot; height=&quot;509&quot; data-filename=&quot;스크린샷 2023-01-24 오후 4.55.14.png&quot; data-origin-width=&quot;562&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Case sensitive &lt;/b&gt;버튼을 클릭하면 대소문자를 구분할 수 있다. 이 버튼을 클릭하지 않으면 기본으로 '대문자'로만 사용할 수 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, &lt;b&gt;Use color scheme TODO default colors&lt;/b&gt; 버튼 체크박스를 해제하면 원하는 형태의 TODO를 만들 수도 있으니 입맛에 맞게 사용해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study/Etc</category>
      <category>code tag</category>
      <category>intellij</category>
      <category>todo</category>
      <category>인텔리제이</category>
      <category>코드태그</category>
      <author>Anna-Jin</author>
      <guid isPermaLink="true">https://annajin.tistory.com/223</guid>
      <comments>https://annajin.tistory.com/223#entry223comment</comments>
      <pubDate>Tue, 24 Jan 2023 17:35:29 +0900</pubDate>
    </item>
    <item>
      <title>[후기] 항해99 수료 후기 - 항해99 과정 및 솔직 후기</title>
      <link>https://annajin.tistory.com/221</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;항해99 logo.jpeg&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;1600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJRaXj/btrJ2oPRiam/6q0fRg9aoAzAcjwAbhxktk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJRaXj/btrJ2oPRiam/6q0fRg9aoAzAcjwAbhxktk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJRaXj/btrJ2oPRiam/6q0fRg9aoAzAcjwAbhxktk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJRaXj%2FbtrJ2oPRiam%2F6q0fRg9aoAzAcjwAbhxktk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;250&quot; data-filename=&quot;항해99 logo.jpeg&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;1600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항해99에 승선한 5월부터 8월까지. 총 99일 +a간의 여정이 막을 내렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미니 프로젝트부터 알고리즘, 주특기, 실전프로젝트까지 다사다난했던 지난 날들을 회고하며 항해의 끝과 앞으로의 개발자로서의 여정의 첫 문장을 이렇게 시작하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;전직 호텔리어가 개발자로 전직하게 된 썰 푼다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;호텔리어가 개발자를 한다고?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국내 5성급 호텔에 취업해서 일을 하다가 갑자기 개발자가 되기로 마음을 먹었을 때, 사실 주위의 반대는 없었다. 어렸을 때부터 기계를 다루길 좋아했고, 취미가 여행경비 엑셀에 정리하기였기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩을 접하게 된 계기는 사소했다. 떠오르는 IT 시대에 발맞춰 학교에서 코딩 교양을 열어줬고, 흥미가 생겨 수업을 듣게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교양에서 사용한 프로그램은 스크래치였는데, 블럭 몇 개 가지고 고양이랑 춤추는 그 과정이 흥분되었다. 당연하게도 해당 수업은 A+를 받았고 그렇게 마음 속에 코딩의 싹을 틔웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0qtT0/btrJ0UonNCW/0Gu5ksBq6xtIfZAM4XXPnk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0qtT0/btrJ0UonNCW/0Gu5ksBq6xtIfZAM4XXPnk/img.jpg&quot; data-alt=&quot;스크래치 고양이&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0qtT0/btrJ0UonNCW/0Gu5ksBq6xtIfZAM4XXPnk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0qtT0%2FbtrJ0UonNCW%2F0Gu5ksBq6xtIfZAM4XXPnk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;284&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스크래치 고양이&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학을 졸업하고 나서 아무튼 호텔경영학과를 졸업했으니 호텔리어는 되어보자!라는 생각에 무작정 호텔에 취업을 하긴 했지만 마음 한 켠에는 여전히 채워지지 않는 무언가가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후에 우연한 계기로 접하게 된 언어가 &lt;b&gt;파이썬&lt;/b&gt;이었다. &lt;a title=&quot;점프투파이썬&quot; href=&quot;https://wikidocs.net/book/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;점프투파이썬&lt;/a&gt;을 보면서 차근차근 코딩을 공부하고 있으니 휴무의 1/4을 다 써도 지치지 않을 정도로 재미가 있었다. 지금까지 공부를 하면서 이렇게 몰입을 했던 적이 있었던가? 하는 생각에 과감하게 일을 그만두고 개발 공부를 시작하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;학원 수료 후, 항해99&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퇴사하자마자 학원에 등록해 6개월 간의 과정을 수료했음에도 불구하고 여전히 개발자가 되기에는 많이 부족하다고 느꼈다. 1인프로젝트로 서비스를 하나 만들어보는 경험도 했지만 협업 경험이 중요하다고 느꼈고, 아직 모르는게 많았기에 부트캠프를 다니자!라는 생각이 들어 파워 구글링에 들어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 많은 부트캠프가 있었다.&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://cocoon1787.tistory.com/674&quot;&gt;국내 코딩 부트캠프 가격 비교 (2021년 11월 기준)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1660807774152&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;국내 코딩 부트캠프 가격 비교 (2021년 11월 기준)&quot; data-og-description=&quot;  본 포스팅은 블로그들마다 혹은 연도마다 부트캠프들의 수강기간과 가격이 정확하지 않아 제가 보기 위해서 정리한 글입니다. 부트캠프 사이트에 직접 들어가서 메인 페이지나 QnA 페이지에&quot; data-og-host=&quot;cocoon1787.tistory.com&quot; data-og-source-url=&quot;https://cocoon1787.tistory.com/674&quot; data-og-url=&quot;https://cocoon1787.tistory.com/674&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/IsKA1/hyPuJHgl2t/y7nW0b8HOSguQ5FKlkBBO1/img.png?width=800&amp;amp;height=489&amp;amp;face=0_0_800_489,https://scrap.kakaocdn.net/dn/lnTUA/hyPuE61nvZ/4AOxrkrnatathzbvnktOP1/img.png?width=800&amp;amp;height=489&amp;amp;face=0_0_800_489,https://scrap.kakaocdn.net/dn/JEWDg/hyPuHbBsHm/pvxtN9W8eUh5nHqWEzPiB0/img.png?width=1286&amp;amp;height=742&amp;amp;face=0_0_1286_742&quot;&gt;&lt;a href=&quot;https://cocoon1787.tistory.com/674&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cocoon1787.tistory.com/674&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/IsKA1/hyPuJHgl2t/y7nW0b8HOSguQ5FKlkBBO1/img.png?width=800&amp;amp;height=489&amp;amp;face=0_0_800_489,https://scrap.kakaocdn.net/dn/lnTUA/hyPuE61nvZ/4AOxrkrnatathzbvnktOP1/img.png?width=800&amp;amp;height=489&amp;amp;face=0_0_800_489,https://scrap.kakaocdn.net/dn/JEWDg/hyPuHbBsHm/pvxtN9W8eUh5nHqWEzPiB0/img.png?width=1286&amp;amp;height=742&amp;amp;face=0_0_1286_742');&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;국내 코딩 부트캠프 가격 비교 (2021년 11월 기준)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  본 포스팅은 블로그들마다 혹은 연도마다 부트캠프들의 수강기간과 가격이 정확하지 않아 제가 보기 위해서 정리한 글입니다. 부트캠프 사이트에 직접 들어가서 메인 페이지나 QnA 페이지에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cocoon1787.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adfit&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;728x90&lt;/div&gt;
    &lt;ins class=&quot;kakao_ad_area&quot; style=&quot;display: none;&quot; data-ad-unit=&quot;DAN-cyaHfyh79OG4eGqd&quot; data-ad-width=&quot;728px&quot; data-ad-height=&quot;90px&quot;&gt;&lt;/ins&gt;
    &lt;script type=&quot;text/javascript&quot; src=&quot;//t1.daumcdn.net/kas/static/ba.min.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 부트캠프 중에서 항해99를 선택한 이유는 물론 가격도 있었지만 &lt;b&gt;99일 동안 9 to 9으로 최대 몰입 경험을 제공한다는 점&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금이라도 여유시간이 생기면 헤이해지거나 스트레스를 받는 내 성격상 &lt;b&gt;하루종일 앉아있어야하는 강제력&lt;/b&gt;이 필요했기 때문에 나랑 가장 잘 맞는 부트캠프라는 생각이 들어 지원하게 되었고, 좋은 기회로 합격 후 항해99에 승선하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;나만의 공부방법 찾기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항해99의 커리큘럼을 따라가면서 많은 우여곡절이 있었다. 특히 처음해본 알고리즘 공부는 &lt;b&gt;이렇게 가다간 무너진다&lt;/b&gt;라는 생각밖에 들지 않았다. 고민 끝에 내린 결론은 &lt;b&gt;&quot;나만의 공부방법을 찾자&quot;&lt;/b&gt; 였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 TIL에 적은 적이 있었는데, 나만의 알고리즘 공부방식을 정립하고 차근차근 따라가다보니 어렵더라도 차근차근 커리큘럼을 따라가고 있는 나를 발견할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-08-18 오후 4.36.14.png&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;1310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yFlD5/btrJZPm22q0/QExe1wk7Btsjj3BCsGhBtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yFlD5/btrJZPm22q0/QExe1wk7Btsjj3BCsGhBtk/img.png&quot; data-alt=&quot;https://annajin.tistory.com/114&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yFlD5/btrJZPm22q0/QExe1wk7Btsjj3BCsGhBtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyFlD5%2FbtrJZPm22q0%2FQExe1wk7Btsjj3BCsGhBtk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;498&quot; data-filename=&quot;스크린샷 2022-08-18 오후 4.36.14.png&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;1310&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://annajin.tistory.com/114&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실전프로젝트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;배슐랭 포트폴리오&quot; href=&quot;https://planet-punishment-427.notion.site/Bae-Chelin-53dfb6f29ec94416b943706053f5748a&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;배슐랭 포트폴리오&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1660810287331&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;배슐랭 (Bae-Chelin)&quot; data-og-description=&quot;&amp;ldquo;사회적 교통약자도 편하게 식사하세요&amp;rdquo;&quot; data-og-host=&quot;planet-punishment-427.notion.site&quot; data-og-source-url=&quot;https://bit.ly/3daAlEe&quot; data-og-url=&quot;https://planet-punishment-427.notion.site/53dfb6f29ec94416b943706053f5748a&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1SXUH/hyPuE0iazj/FXuP88MOJn65MdeaMCn1U1/img.png?width=600&amp;amp;height=300&amp;amp;face=0_0_600_300,https://scrap.kakaocdn.net/dn/qBufO/hyPuIBCKPq/jhyLT6AbWK8dg3Y4JetnN0/img.png?width=600&amp;amp;height=300&amp;amp;face=0_0_600_300&quot;&gt;&lt;a href=&quot;https://bit.ly/3daAlEe&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bit.ly/3daAlEe&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1SXUH/hyPuE0iazj/FXuP88MOJn65MdeaMCn1U1/img.png?width=600&amp;amp;height=300&amp;amp;face=0_0_600_300,https://scrap.kakaocdn.net/dn/qBufO/hyPuIBCKPq/jhyLT6AbWK8dg3Y4JetnN0/img.png?width=600&amp;amp;height=300&amp;amp;face=0_0_600_300');&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;배슐랭 (Bae-Chelin)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;사회적 교통약자도 편하게 식사하세요&amp;rdquo;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;planet-punishment-427.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;배슐랭 서비스 링크&quot; href=&quot;https://bae-chelin.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;배슐랭 서비스 링크&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1660810989003&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;배슐랭&quot; data-og-description=&quot;사회적 교통 약자도 편하게 식사하세요&quot; data-og-host=&quot;bae-chelin.com&quot; data-og-source-url=&quot;https://bae-chelin.com/&quot; data-og-url=&quot;https://bae-chelin.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bDPulZ/hyPuyF6n6a/dpOKm3uBXXhEryNmLgVNrK/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot;&gt;&lt;a href=&quot;https://bae-chelin.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bae-chelin.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bDPulZ/hyPuyF6n6a/dpOKm3uBXXhEryNmLgVNrK/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;배슐랭&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;사회적 교통 약자도 편하게 식사하세요&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bae-chelin.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;항해99 커리큘럼을 따라가면서 배운 모든걸 적용하는 최종 기간이었다.&lt;/p&gt;
&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;을 주제로 &quot;사회적 교통약자도 편하게 식사하세요&quot;를 슬로건으로 삼고있는 프로젝트였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 담당한 부분은 &lt;b&gt;Spring Security와 JWT, OAuth2.0을 사용한&lt;/b&gt;&amp;nbsp;&lt;b&gt;소셜로그인&lt;/b&gt;이었는데 처음에 만만하게 생각했던 것과는 달리 굉장히 어려웠다. 더불어 팀 리더도 맡고 있었기 때문에 생각해야할게 많아서 고생이 많았다. Spring Security와 JWT의 플로우를 이해하는 데에 많은 시간이 걸렸고, 지금도 완벽하게 이해하고 있다고는 말할 수 없지만 그래도 어느정도 다룰 수 있을 정도는 되었다고 자신할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;항해99 추천? 비추천?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 4개월간의 여정은 좋은 점도 아쉬운 점도 많았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 점은 &lt;u&gt;국비학원에서는 배우기 힘든 기술들을 다양하게 접할 수 있다는 점&lt;/u&gt;, &lt;u&gt;계속해서 팀을 꾸려서 공부를 했다는 점&lt;/u&gt;, &lt;u&gt;디자이너 + 프론트엔드 + 백엔드 간의 협업을 할 수 있었다는 점&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 협업 경험은 개인적으로 꾸리고 프로젝트를 진행하면서 가능한 경험이지만, 다양한 사람을 만나볼 수 있고 이미 열정이 가득한 사람들과 함께할 수 없다는 게 차별점인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉬운 점은 &lt;b&gt;다소 부족한 커리큘럼&lt;/b&gt;과 &lt;b&gt;짧은 기간&lt;/b&gt;, &lt;b&gt;학습 자료의 부족&lt;/b&gt;이었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 주차가 진행된 3주동안 1년동안 공부해야할 만큼의 내용을 공부하고 테스트를 봤어야 했는데 항해99가 지향하는 비전공자도 가능한 부트캠프가 아니라 &lt;u&gt;전공자 혹은 그에 준하는 공부를 해본 사람을 위한 부트캠프&lt;/u&gt; 같았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도 알고리즘 주차 커리큘럼을 따라가는건 쉽지 않았다. 오히려 이 기간동안 항해99를 그만둔 사람이 단 한명도 없다는 게 신기할 정도였다.&lt;/p&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;u&gt;그렇다면 항해99를 추천하는가 비추천하는가?&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 개발 공부를 &lt;b&gt;단 한번도 해본 적이 없는 사람들 중 자기주도학습에 적응할 자신이 없는 사람&lt;/b&gt;이라면 &lt;b&gt;비추천&lt;/b&gt;한다. 다른 부트캠프는 모르겠지만 항해99는 개발 공부 경험이 전무한 사람에게 친절한 부트캠프는 아니다. 모든걸 '스스로' 찾아서 학습해야하기 때문에 자기주도학습이 익숙하지 않은 사람은 어지간한 열정을 갖고있지 않다면 득보단 실이 많을거라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 중도포기한 타 반의 사람들의 이야기를 들어봤을 때, 개발이 처음인 사람이 대다수였고 국비지원학원에서처럼 A to Z로 하나하나 알려주면서 끌고가주기를 바랐으나 항해99는 그렇지 않아 불친절하다는 말이 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 분들을 나쁘게 말하려는 게 아니라 부트캠프는 생각했던 것 보다 더 '자기주도학습'에 초점이 맞춰져있다고 말하는 의도이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개월이라는 짧은 시간동안 한가지 언어를 배우고, CRUD와 그 외 처음 사용해보는 기술들을 습득하기는 웬만한 열정이 아니고서는 쉽지 않기 때문에 항해99의 커리큘럼을 따라가려면 정말 쉬지않고!! 공부해야한다. 전공자가 4년동안 공부해온 내용 중 핵심만 쏙쏙 뽑아서 단기간에 머리에 집어넣는 과정이기 때문에 당연하지만 참 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 한번 강조하지만 개발 공부를 단 한번이라도 '스스로' 해본 적이 없는 사람이라면 부트캠프보단 국비지원이든 독학이든 그 외의 경로를 통해서든 기초를 습득하고 오는 걸 추천한다!&lt;/p&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;b&gt;스스로 검색하고 알아내는 데에 경험이 있고 개발에 대한 열정이 있다면&lt;/b&gt; &lt;b&gt;추천&lt;/b&gt;한다. 오전 9시부터 오후 9시까지 혼자서 앉아서 공부하기는 어려운 일이기 때문에 약간의 강제력이 있는 건 몰입하기에 아주 좋은 환경이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 국비지원학원에서는 알려주지 않는, 요즘 많이 사용하는 기술들의 존재를 알게 되는 경험을 할 수 있다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;(위에서도 계속 말하지만 공부는 셀프이다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 항해99의 협력사에 대해 궁금해하는 사람들도 많을 것 같아 조금 얘기를 풀어보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면 &lt;u&gt;스타트업 ~ 중소 기업은 절대 싫다! 하면 기대하지 말자&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 발표회 때부터 계속해서 '스타트업' 협력사들의 채용 세션을 접할 기회가 종종 생긴다. 7기의 경우에는 삼성 사외벤처 프로그램에 선정된 스타트업도 있었고, 퀄리티 있어보이는 스타트업도 채용 세션을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적어도 주마다 한 개 이상의 채용 세션을 진행하면서 항해99 출신 개발자에게 관심이 있는 회사들을 접할 기회가 많다는건 좋지만 계속 언급하는 것처럼 '스타트업' 한정이기 때문에 중견 ~ 대기업 수준의 협력사를 바란다면... 항해99가 더 성장해서 유명해지기를 기도하거나 직접 발품팔아서 이력서를 넣고 다니는게 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항해99에서 서비스 중인 포트99에도 다양한 회사들이 채용공고를 올려두고 있는데, 이곳 역시 스타트업이 대부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트99를 이용하면 좋은 점은 &lt;u&gt;항해99 출신을 좋게 보는 회사들을 골라서 이력서를 넣어볼 수 있다&lt;/u&gt;는 정도? 그렇다고 협력사 서류 합격률이 높다고는 말하기 어렵다. 당연한 말이지만 서류 합격은 온전히 본인에게 달렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;총평&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 말했던 것처럼 우여곡절이 많은 기간이었지만 확실히 전보다는 성장했음을 느낄 수 있다. 몰랐던 기술들도 많이 알았고 배운 점도 많았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;21년 10월부터 22년 8월까지의 개발 공부. 이제 취준에 들어가지만 공부는 끝나지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;+추가&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이럴이라는 분들이 계셔서 3줄 요약 해드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 국비 나오는 것보다 배울 점은 많음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 개발 처음 시작하는 사람은 오지 마라. 개발 처음하는 사람에게 친절하지 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 개발 공부 독학이든 뭐든 해봤으면 나쁘지 않음&lt;/p&gt;</description>
      <category>Review</category>
      <category>부트캠프항해</category>
      <category>코딩부트캠프</category>
      <category>항해99</category>
      <category>항해99비추천</category>
      <category>항해99선택이유</category>
      <category>항해99솔직후기</category>
      <category>항해99실전프로젝트</category>
      <category>항해99추천</category>
      <author>Anna-Jin</author>
      <guid isPermaLink="true">https://annajin.tistory.com/221</guid>
      <comments>https://annajin.tistory.com/221#entry221comment</comments>
      <pubDate>Thu, 18 Aug 2022 18:26:38 +0900</pubDate>
    </item>
  </channel>
</rss>