<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ryukyungwoo1220 님의 블로그</title>
    <link>https://ryukyungwoo1220.tistory.com/</link>
    <description>ryukyungwoo1220 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 8 Jun 2026 19:21:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ryukyungwoo1220</managingEditor>
    <item>
      <title>CloudFront 배포 후 로그인 풀림 현상 해결 (세션 쿠키 미저장 이슈)</title>
      <link>https://ryukyungwoo1220.tistory.com/72</link>
      <description>&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 AWS 인프라 구축 중에 겪었던 &quot;로그인 세션이 자꾸 풀리는 문제&quot;에 대한 트러블슈팅 과정을 기록으로 남겨두려 합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;로컬 환경(Localhost)에서는 아무 문제 없이 잘 되던 로그인이, &lt;b data-index-in-node=&quot;40&quot; data-path-to-node=&quot;8&quot;&gt;AWS EC2에 배포하고 CloudFront(CDN)를 연결하자마자&lt;/b&gt; 로그인이 안 되거나 페이지 이동 시 바로 풀려버리는 현상이 발생했는데요.&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;범인은 바로 &lt;b data-index-in-node=&quot;7&quot; data-path-to-node=&quot;9&quot;&gt;CloudFront의 캐시 정책&lt;/b&gt;과 &lt;b data-index-in-node=&quot;26&quot; data-path-to-node=&quot;9&quot;&gt;브라우저의 쿠키 보안 정책&lt;/b&gt;이었습니다. 어떻게 해결했는지 정리해 보겠습니다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;10&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11&quot;&gt;1. 문제 상황 (Symptoms)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;개발한 Admin 페이지를 배포한 후 다음과 같은 증상이 나타났습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;  &lt;b data-index-in-node=&quot;3&quot; data-path-to-node=&quot;13,0,0&quot;&gt;로그인 풀림:&lt;/b&gt; Admin 로그인 성공 후, 다른 페이지로 이동하거나 뒤로가기를 누르면 로그인이 풀림.&lt;/li&gt;
&lt;li&gt;  &lt;b data-index-in-node=&quot;3&quot; data-path-to-node=&quot;13,1,0&quot;&gt;쿠키 실종:&lt;/b&gt; 브라우저 개발자 도구(F12) &amp;gt; Application &amp;gt; Cookies 탭을 확인해 보니 JSESSIONID가 저장되지 않음.&lt;/li&gt;
&lt;li&gt;  &lt;b data-index-in-node=&quot;3&quot; data-path-to-node=&quot;13,2,0&quot;&gt;401 에러:&lt;/b&gt; API 요청 시 인증 정보가 넘어가지 않아 401 Unauthorized 응답 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;14&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15&quot;&gt;2. 원인 분석&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;로그를 뜯어보고 설정을 확인해 본 결과, 두 가지 복합적인 원인이 있었습니다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17&quot;&gt;원인 1. CloudFront가 쿠키를 &quot;배달 사고&quot; 냄&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;기존 Terraform 코드에서 CloudFront의 설정을 forwarded_values (Legacy 방식)로 하고 있었습니다. 이 방식은 요청(Request) 시 쿠키를 백엔드로 보내주긴 하지만, &lt;b data-index-in-node=&quot;113&quot; data-path-to-node=&quot;18&quot;&gt;백엔드가 응답(Response)할 때 주는 Set-Cookie 헤더를 브라우저에게 제대로 전달하지 못하는 문제&lt;/b&gt;가 있었습니다. 즉, 중간 배달부(CloudFront)가 쿠키를 누락시킨 것이죠.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;19&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19&quot;&gt;원인 2. 브라우저의 깐깐한 보안 정책 (HTTPS)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;CloudFront를 통해 HTTPS로 통신하고 있었는데, 백엔드(Spring Boot)에서 세션 쿠키를 발급할 때 **보안 속성(Secure, SameSite 등)**을 명시하지 않았습니다. 최신 브라우저(Chrome 등)는 HTTPS 환경에서 보안 속성이 없는 쿠키는 &lt;b data-index-in-node=&quot;152&quot; data-path-to-node=&quot;20&quot;&gt;&quot;위험하다&quot;고 판단하여 저장을 거부&lt;/b&gt;해버립니다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;21&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;22&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22&quot;&gt;3. 해결 방법 (Solution)&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-path-to-node=&quot;23&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23&quot;&gt;Step 1. CloudFront 정책 변경 (Terraform)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;CloudFront의 구형 설정(forwarded_values)을 버리고, 최신 방식인 &lt;b data-index-in-node=&quot;49&quot; data-path-to-node=&quot;24&quot;&gt;Cache Policy&lt;/b&gt;와 &lt;b data-index-in-node=&quot;63&quot; data-path-to-node=&quot;24&quot;&gt;Origin Request Policy&lt;/b&gt;를 적용했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;25&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;25,0,0&quot;&gt;Cache Policy:&lt;/b&gt; API 응답은 캐시하지 않도록 설정 (min/max/default_ttl = 0)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;25,1,0&quot;&gt;Origin Request Policy:&lt;/b&gt; 쿠키(All), 헤더(AllViewer), 쿼리스트링(All)을 모두 백엔드로 통과시키도록 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;26&quot;&gt;  modules/cloudfront/main.tf&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwj2mI32mdWRAxUAAAAAHQAAAAAQ5gE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;Terraform&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 1. API용 캐시 정책 생성 (캐시 안 함)
resource &quot;aws_cloudfront_cache_policy&quot; &quot;api_no_cache&quot; {
  name        = &quot;homepage-dev-api-no-cache&quot;
  comment     = &quot;No caching for API&quot;
  min_ttl     = 0
  default_ttl = 0
  max_ttl     = 0

  parameters_in_cache_key_and_forwarded_to_origin {
    cookies_config {
      cookie_behavior = &quot;none&quot;
    }
    headers_config {
      header_behavior = &quot;none&quot;
    }
    query_strings_config {
      query_string_behavior = &quot;none&quot;
    }
  }
}

# 2. 오리진 요청 정책 생성 (쿠키/헤더 모두 통과)
resource &quot;aws_cloudfront_origin_request_policy&quot; &quot;api_all_cookies&quot; {
  name    = &quot;homepage-dev-api-all-cookies&quot;
  comment = &quot;Forward all cookies and headers to origin&quot;

  cookies_config {
    cookie_behavior = &quot;all&quot;
  }
  headers_config {
    header_behavior = &quot;allViewer&quot;
  }
  query_strings_config {
    query_string_behavior = &quot;all&quot;
  }
}

# 3. CloudFront Behavior에 적용
resource &quot;aws_cloudfront_distribution&quot; &quot;main&quot; {
  # ... (생략)

  ordered_cache_behavior {
    path_pattern     = &quot;/api/*&quot;
    target_origin_id = &quot;EC2-Backend&quot;

    # 새로 만든 정책 ID 연결
    cache_policy_id          = aws_cloudfront_cache_policy.api_no_cache.id
    origin_request_policy_id = aws_cloudfront_origin_request_policy.api_all_cookies.id
    
    # ... (나머지 설정)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-path-to-node=&quot;28&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28&quot;&gt;Step 2. Spring Boot 쿠키 속성 강화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;브라우저가 쿠키를 안전하게 저장할 수 있도록, docker-compose 환경변수를 통해 Spring Boot의 세션 쿠키 설정을 오버라이드했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;30&quot;&gt;  docker-compose.ec2.yml&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwj2mI32mdWRAxUAAAAAHQAAAAAQ5wE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;YAML&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;backend:
  environment:
    # ... 기존 환경변수 ...
    
    # HTTPS 환경 필수 설정
    SERVER_SERVLET_SESSION_COOKIE_SECURE: &quot;true&quot;      # HTTPS에서만 쿠키 전송
    SERVER_SERVLET_SESSION_COOKIE_SAME_SITE: &quot;lax&quot;    # CSRF 방지 및 사이트 간 이동 허용
    SERVER_SERVLET_SESSION_COOKIE_PATH: &quot;/&quot;           # 모든 경로에서 쿠키 허용
    SERVER_SERVLET_SESSION_COOKIE_HTTP_ONLY: &quot;true&quot;   # 자바스크립트로 쿠키 접근 불가 (보안)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-path-to-node=&quot;32&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;33&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;33&quot;&gt;4. 결과 및 배운 점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;위 설정을 적용하고 Terraform Apply 및 재배포를 진행한 결과,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;35&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저 Application 탭에 JSESSIONID가 정상적으로 생성되었고,&lt;/li&gt;
&lt;li&gt;페이지를 이동해도 &lt;b data-index-in-node=&quot;10&quot; data-path-to-node=&quot;35,1,0&quot;&gt;로그인이 풀리지 않는 것을 확인&lt;/b&gt;했습니다!  &lt;/li&gt;
&lt;/ol&gt;</description>
      <category>CS/INFRA</category>
      <author>ryukyungwoo1220</author>
      <guid isPermaLink="true">https://ryukyungwoo1220.tistory.com/72</guid>
      <comments>https://ryukyungwoo1220.tistory.com/72#entry72comment</comments>
      <pubDate>Wed, 24 Dec 2025 12:01:54 +0900</pubDate>
    </item>
    <item>
      <title>Lambda 외부 라이브러리 적용 가이드 (ZIP, Layer, Docker)</title>
      <link>https://ryukyungwoo1220.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Lambda에서 boto3 같은 기본 라이브러리 외에 다른 패키지를 사용하려면 직접 포함시켜야 합니다. 이 글에서는 Lambda에 외부 라이브러리를 추가하는 3가지 방법(ZIP, Layer, Docker)과 주의사항을 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. ZIP 업로드&lt;/h2&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;가장 기본적인 방법입니다. 코드와 라이브러리를 함께 압축해서 업로드하는 방식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디렉토리 구조&lt;/h3&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQiAI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;lambda.zip/
├── handler.py          # Lambda 코드
├── pypdfium2/          # 외부 라이브러리
├── PIL/                # 외부 라이브러리
└── ...
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 설치 방법&lt;/h3&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;Lambda는 Amazon Linux 환경에서 실행되기 때문에, Windows나 Mac에서 작업할 경우 반드시 &lt;b&gt;Linux용 바이너리&lt;/b&gt;를 받아야 합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방법 1: pip 옵션 사용&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQiQI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;pip install \
  --platform manylinux2014_x86_64 \
  --only-binary=:all: \
  -r requirements.txt \
  -t package/
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;옵션&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,1,0,0&quot;&gt;--platform manylinux2014_x86_64&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,1,1,0&quot;&gt;Linux용 바이너리 다운로드 강제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,2,0,0&quot;&gt;--only-binary=:all:&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,2,1,0&quot;&gt;소스 컴파일 없이 바이너리만 설치&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,3,0,0&quot;&gt;-r requirements.txt&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,3,1,0&quot;&gt;설치할 패키지 목록 파일&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,4,0,0&quot;&gt;-t package/&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,4,1,0&quot;&gt;라이브러리가 설치될 타겟 폴더 지정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방법 2: Docker 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;로컬 환경과 무관하게 리눅스 컨테이너 안에서 패키지를 설치하는 확실한 방법입니다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQjQI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;docker run --rm -v $(pwd):/var/task \
  python:3.14-slim \
  pip install -r /var/task/requirements.txt -t /var/task/package/
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;16&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;옵션&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,1,0,0&quot;&gt;--rm&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,1,1,0&quot;&gt;컨테이너 종료 후 자동 삭제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,2,0,0&quot;&gt;-v $(pwd):/var/task&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,2,1,0&quot;&gt;현재 폴더를 컨테이너 내부로 마운트&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,3,0,0&quot;&gt;python:3.14-slim&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,3,1,0&quot;&gt;Python 3.14 Linux 이미지 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,4,0,0&quot;&gt;-t /var/task/package/&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;16,4,1,0&quot;&gt;설치 경로 지정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ZIP 파일 생성&lt;/h3&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQkQI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;# handler.py를 package 폴더에 복사
cp handler.py package/

# ZIP 만들기
cd package
zip -r ../lambda.zip .
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장단점 요약&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;20&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;20,1,0,0&quot;&gt;구성이 간단함&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;20,1,1,0&quot;&gt;함수마다 라이브러리가 중복됨&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;20,2,0,0&quot;&gt;배포 과정이 직관적임&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;20,2,1,0&quot;&gt;코드 수정 시 전체(라이브러리 포함) 다시 업로드 필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Layer (계층)&lt;/h2&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;라이브러리를 별도의 Layer로 분리해서 여러 Lambda 함수에서 공유하는 방식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ZIP 방식 vs Layer 방식 구조 비교&lt;/h3&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ZIP 방식: 중복 발생]&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQlQI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Lambda 함수 A (zip)
├── handler.py
├── pypdfium2/
└── pillow/

Lambda 함수 B (zip)
├── handler.py
├── pypdfium2/    (중복)
└── pillow/       (중복)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Layer 방식: 공유]&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQlgI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Layer (공용 라이브러리)
├── pypdfium2/
└── pillow/

Lambda 함수 A
└── handler.py   (Layer 참조)

Lambda 함수 B
└── handler.py   (Layer 참조)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Layer용 ZIP 생성&lt;/h3&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;Layer는 반드시 특정 폴더 구조(python/)를 지켜야 인식됩니다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQlwI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;mkdir -p layer/python
pip install \
  --platform manylinux2014_x86_64 \
  --only-binary=:all: \
  -r requirements.txt \
  -t layer/python/

cd layer
zip -r ../layer.zip python/
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장단점 요약&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;32&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;32,1,0,0&quot;&gt;여러 함수에서 라이브러리 재사용 가능&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;32,1,1,0&quot;&gt;Layer 버전 및 권한 관리 필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;32,2,0,0&quot;&gt;소스 코드 수정 시 handler.py만 가볍게 업로드&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;32,2,1,0&quot;&gt;초기 설정이 ZIP 방식보다 복잡함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Docker 이미지&lt;/h2&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;컨테이너 이미지로 말아서 Lambda를 배포하는 방식입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dockerfile 예시&lt;/h3&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQmwI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM public.ecr.aws/lambda/python:3.14

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY handler.py .

CMD [&quot;handler.handler&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장단점 요약&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;38&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;38,1,0,0&quot;&gt;최대 10GB까지 패키지 포함 가능&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;38,1,1,0&quot;&gt;콜드 스타트(초기 구동) 속도가 느릴 수 있음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;38,2,0,0&quot;&gt;OS 의존성 등 복잡한 환경 구성 가능&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;38,2,1,0&quot;&gt;AWS ECR(Container Registry) 사용 필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;38,3,0,0&quot;&gt;로컬 테스트가 용이함&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;38,3,1,0&quot;&gt;설정 및 배포 파이프라인이 복잡함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;선택 가이드: 언제 무엇을 쓸까?&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;40&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;추천 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;40,1,0,0&quot;&gt;Lambda가 1~2개이고, 빠르게 개발하고 싶을 때&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;40,1,1,0&quot;&gt;&lt;b&gt;ZIP 업로드&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;40,2,0,0&quot;&gt;여러 Lambda가 동일한 라이브러리를 사용할 때&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;40,2,1,0&quot;&gt;&lt;b&gt;Layer&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;40,3,0,0&quot;&gt;라이브러리 용량이 크거나(250MB 초과), OS 의존성이 복잡할 때&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;40,3,1,0&quot;&gt;&lt;b&gt;Docker 이미지&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의사항: 플랫폼(OS) 호환성 문제&lt;/h2&gt;
&lt;p data-path-to-node=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;C언어 기반의 바이너리가 포함된 패키지(pillow, numpy, pypdfium2 등)는 OS별로 빌드된 파일이 다릅니다. 개발자의 PC 환경과 Lambda의 실행 환경이 다르면 오류가 발생합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;43&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;개발 환경 (내 PC)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;설치되는 파일&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Lambda (Linux) 실행 결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;43,1,0,0&quot;&gt;Windows&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;43,1,1,0&quot;&gt;Windows용 바이너리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;43,1,2,0&quot;&gt;&lt;b&gt;오류 발생&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;43,2,0,0&quot;&gt;Mac (M1/M2 등)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;43,2,1,0&quot;&gt;Mac(ARM)용 바이너리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;43,2,2,0&quot;&gt;&lt;b&gt;오류 발생&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;43,3,0,0&quot;&gt;Linux&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;43,3,1,0&quot;&gt;Linux용 바이너리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;43,3,2,0&quot;&gt;&lt;b&gt;정상 동작&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소스 코드 vs 바이너리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;45&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;소스 코드:&lt;/b&gt; 사람이 읽을 수 있는 텍스트 형태. 컴파일 과정이 필요하므로 Lambda 실행 시점에 바로 사용할 수 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;바이너리:&lt;/b&gt; 컴퓨터(OS)가 바로 실행할 수 있는 기계어 파일.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;Lambda 런타임에는 컴파일러(gcc 등)가 포함되어 있지 않습니다. 따라서 로컬에서 미리 &lt;b&gt;Linux 환경에 맞는 바이너리&lt;/b&gt;를 다운로드하거나 빌드해서 올려야 합니다. 앞서 설명한 --platform manylinux2014_x86_64 옵션이나 Docker를 사용하는 이유가 바로 이것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;48&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ZIP 업로드:&lt;/b&gt; 소규모 프로젝트에 적합한 기본 방법&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Layer:&lt;/b&gt; 중규모 이상, 함수 간 코드 공유가 필요할 때&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Docker:&lt;/b&gt; 대용량 라이브러리나 복잡한 환경이 필요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;49&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 ZIP 방식으로 시작하여 구조를 잡고, 프로젝트가 커짐에 따라 Layer나 Docker로 전환하는 것을 추천합니다.&lt;/p&gt;</description>
      <category>CS/INFRA</category>
      <author>ryukyungwoo1220</author>
      <guid isPermaLink="true">https://ryukyungwoo1220.tistory.com/71</guid>
      <comments>https://ryukyungwoo1220.tistory.com/71#entry71comment</comments>
      <pubDate>Wed, 26 Nov 2025 20:59:17 +0900</pubDate>
    </item>
    <item>
      <title>WSL2 + Docker 개발 환경을 외부에서 접속하기</title>
      <link>https://ryukyungwoo1220.tistory.com/70</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;로컬(WSL2 + Docker)에서 개발 중인 서비스를 모바일에서 테스트하거나, 외부에서 시연해야 할 때가 있습니다. 이번 글에서는 공유기, Windows 포트 프록시, 그리고 Docker Nginx를 활용해 외부 IP로 로컬 개발 환경에 접속하는 전체 흐름과 설정 방법을 정리합니다.&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;/h2&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;외부 요청이 내부의 Docker 컨테이너까지 도달하는 과정은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;7&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;외부 접속&lt;/b&gt;: http://[공인IP] 접속 (포트 80)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공유기 (Port Forwarding)&lt;/b&gt;: 외부 포트 80을 PC 내부 IP([내부IP])의 80 포트로 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Windows (Port Proxy)&lt;/b&gt;: Windows 80 포트로 들어온 요청을 &lt;b&gt;WSL의 IP&lt;/b&gt; 80 포트로 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Docker (Nginx)&lt;/b&gt;: 리버스 프록시가 요청 경로(/api 또는 /)에 따라 백엔드/프론트엔드로 분기&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1764157916269&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;외부 사용자 (http://[공인IP])
      │
      │ (포트 80)
      ▼
   공유기 (Port Forwarding)
      │
      │ (80 -&amp;gt; [내부IP]:80)
      ▼
   Windows (Port Proxy)
      │
      │ (80 -&amp;gt; WSL IP:80)
      ▼
 Docker Nginx (리버스 프록시)
      │
      ├─ /api ──▶ 백엔드 (8080)
      │
      └─ / ──▶ 프론트엔드 (5173)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Nginx 설정 (리버스 프록시)&lt;/h2&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;가장 안쪽인 Docker 환경부터 설정합니다. Nginx 컨테이너 하나가 모든 요청을 받아 백엔드(Spring Boot)와 프론트엔드(Vite)로 라우팅합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;nginx.conf 생성&lt;/h3&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;host.docker.internal을 사용하여 컨테이너 내부에서 호스트(WSL)의 포트를 바라보도록 설정합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764157944233&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    listen 80;

    # 백엔드 API 요청
    location /api {
        proxy_pass http://host.docker.internal:8080;
    }

    # 프론트엔드 페이지 요청
    location / {
        proxy_pass http://host.docker.internal:5173;
    }
}&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;Docker 컨테이너 실행&lt;/h3&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQ3AE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;docker run -d --restart always --name default-nginx-proxy -p 80:80 \
  -v $(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf \
  nginx&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Vite 설정 (Frontend)&lt;/h2&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;외부 IP나 Docker 호스트를 통해 들어오는 요청을 허용하기 위해 vite.config.ts에 설정을 추가합니다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQ3QE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// vite.config.ts
server: {
  allowedHosts: ['host.docker.internal'],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 백엔드 CORS 설정 (Spring Boot)&lt;/h2&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;application-dev.yml (혹은 properties)에서 CORS 설정을 열어줍니다. 로컬 개발 주소와 외부 공인 IP 주소를 모두 허용합니다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQ3gE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# application-dev.yml
cors:
  allowed-origins: http://localhost:5173, http://[공인IP]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Windows 포트 프록시 설정&lt;/h2&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;WSL2는 재부팅 할 때마다 내부 IP가 변경되는 특징이 있습니다. 따라서 Windows가 받은 요청을 &lt;b&gt;현재의 WSL IP&lt;/b&gt;로 전달해주는 스크립트가 필요합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;아래 스크립트를 wsl_portforward.ps1으로 저장하고, &lt;b&gt;관리자 권한&lt;/b&gt;으로 실행하거나 시작 프로그램에 등록하세요.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQ3wE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 기존 포트 프록시 규칙 삭제 (초기화)
netsh interface portproxy delete v4tov4 listenport=80 listenaddress=0.0.0.0

# 현재 WSL의 IP 주소 동적 획득
$wsl_ip = wsl hostname -I | Out-String
$wsl_ip = $wsl_ip.Trim()

# 포트 프록시 규칙 추가 (Windows 80 -&amp;gt; WSL IP 80)
netsh interface portproxy add v4tov4 listenport=80 listenaddress=0.0.0.0 connectport=80 connectaddress=$wsl_ip

# 결과 확인
Write-Host &quot;WSL IP: $wsl_ip connected to Port 80&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설정 확인 명령어:&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQ4AE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;netsh interface portproxy show all
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Windows 방화벽 설정&lt;/h2&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;Windows의 80번 포트로 외부 접근이 가능하도록 인바운드 규칙을 추가합니다. (PowerShell 관리자 권한 필요)&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiXpKD83Y-RAxUAAAAAHQAAAAAQ4QE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;New-NetFirewallRule -DisplayName &quot;Allow Port 80&quot; -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow -Profile Any
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 공유기 포트포워딩&lt;/h2&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;공유기 설정 페이지에서 포트포워딩을 설정합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;33&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;규칙 이름&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;외부 포트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;내부 IP 주소&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;내부 포트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;프로토콜&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;33,1,0,0&quot;&gt;dev-server&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;33,1,1,0&quot;&gt;80&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;33,1,2,0&quot;&gt;[내부IP]&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;33,1,3,0&quot;&gt;80&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;33,1,4,0&quot;&gt;TCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;34&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;외부 포트:&lt;/b&gt; 80 (기본 HTTP 포트)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부 IP:&lt;/b&gt; [내부IP] (PC의 고정 IP 권장)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부 포트:&lt;/b&gt; 80 (Windows 포트 프록시 포트)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;접속 테스트&lt;/h2&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;설정이 완료되면 아래 주소로 접속하여 테스트합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;37&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;외부 접속:&lt;/b&gt; http://[공인IP]&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로컬 접속:&lt;/b&gt; http://localhost:5173&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>CS/INFRA</category>
      <author>ryukyungwoo1220</author>
      <guid isPermaLink="true">https://ryukyungwoo1220.tistory.com/70</guid>
      <comments>https://ryukyungwoo1220.tistory.com/70#entry70comment</comments>
      <pubDate>Wed, 26 Nov 2025 20:54:38 +0900</pubDate>
    </item>
    <item>
      <title>grafana에 loki 등록</title>
      <link>https://ryukyungwoo1220.tistory.com/69</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아이디와 비밀번호 입력 후 들어갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;1133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNtTIk/btsQ2hj4H1r/fmXFIZ4aaKkRTOcHgtK9W1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNtTIk/btsQ2hj4H1r/fmXFIZ4aaKkRTOcHgtK9W1/img.png&quot; data-alt=&quot;메인화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNtTIk/btsQ2hj4H1r/fmXFIZ4aaKkRTOcHgtK9W1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNtTIk%2FbtsQ2hj4H1r%2FfmXFIZ4aaKkRTOcHgtK9W1%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;1277&quot; height=&quot;1133&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;1133&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메인화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QNDfg/btsQ3bcykMC/LywjObNSGs9czzBSlBMxZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QNDfg/btsQ3bcykMC/LywjObNSGs9czzBSlBMxZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QNDfg/btsQ3bcykMC/LywjObNSGs9czzBSlBMxZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQNDfg%2FbtsQ3bcykMC%2FLywjObNSGs9czzBSlBMxZK%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;1256&quot; height=&quot;480&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 소스 추가하기로 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s84AM/btsQ2TQFSGa/KXmIlkiUTHkBIT03m5rrlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s84AM/btsQ2TQFSGa/KXmIlkiUTHkBIT03m5rrlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s84AM/btsQ2TQFSGa/KXmIlkiUTHkBIT03m5rrlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs84AM%2FbtsQ2TQFSGa%2FKXmIlkiUTHkBIT03m5rrlK%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;953&quot; height=&quot;311&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로키를 찾습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zukFS/btsQ3cJlavq/NV4VfFeVRVy2MxNRXuvWo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zukFS/btsQ3cJlavq/NV4VfFeVRVy2MxNRXuvWo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zukFS/btsQ3cJlavq/NV4VfFeVRVy2MxNRXuvWo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzukFS%2FbtsQ3cJlavq%2FNV4VfFeVRVy2MxNRXuvWo0%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;940&quot; height=&quot;646&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 방식으로 해놨기 때문에 loki의 url을 입력해줍니다. 여기선 k8s 환경이기 때문에 serivce 이름 입니다.&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;958&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SJGrs/btsQ2xGY8qu/oT2bDX52ByUodqxEO3vRhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SJGrs/btsQ2xGY8qu/oT2bDX52ByUodqxEO3vRhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SJGrs/btsQ2xGY8qu/oT2bDX52ByUodqxEO3vRhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSJGrs%2FbtsQ2xGY8qu%2FoT2bDX52ByUodqxEO3vRhk%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;958&quot; height=&quot;222&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;save &amp;amp; test를 눌러주어 연결을 확인해줍니다.&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;291&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7xhuB/btsQ4VzKpwU/0ckrK1EUi0z8VGYxs7Q800/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7xhuB/btsQ4VzKpwU/0ckrK1EUi0z8VGYxs7Q800/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7xhuB/btsQ4VzKpwU/0ckrK1EUi0z8VGYxs7Q800/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7xhuB%2FbtsQ4VzKpwU%2F0ckrK1EUi0z8VGYxs7Q800%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;291&quot; height=&quot;415&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;415&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;익스플로러로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J88kr/btsQ57HlXrR/uNFgQ804XgQSHkok1tLnwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J88kr/btsQ57HlXrR/uNFgQ804XgQSHkok1tLnwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J88kr/btsQ57HlXrR/uNFgQ804XgQSHkok1tLnwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ88kr%2FbtsQ57HlXrR%2FuNFgQ804XgQSHkok1tLnwk%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;430&quot; height=&quot;92&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;92&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Eh9FN/btsQ58lUoWF/lJpMtJqm1KagEtPmZ1Ylhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Eh9FN/btsQ58lUoWF/lJpMtJqm1KagEtPmZ1Ylhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Eh9FN/btsQ58lUoWF/lJpMtJqm1KagEtPmZ1Ylhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEh9FN%2FbtsQ58lUoWF%2FlJpMtJqm1KagEtPmZ1Ylhk%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;232&quot; height=&quot;161&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 누른 후&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSc9z3/btsQ57AxKpt/CJV8ZGV5zKGsgJWkKnugPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSc9z3/btsQ57AxKpt/CJV8ZGV5zKGsgJWkKnugPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSc9z3/btsQ57AxKpt/CJV8ZGV5zKGsgJWkKnugPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSc9z3%2FbtsQ57AxKpt%2FCJV8ZGV5zKGsgJWkKnugPk%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;701&quot; height=&quot;273&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 입력하고 run query를 클릭하면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2232&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNkGdx/btsQ7uVSg3V/KG60ABxnIrDOVqrA0Z5yJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNkGdx/btsQ7uVSg3V/KG60ABxnIrDOVqrA0Z5yJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNkGdx/btsQ7uVSg3V/KG60ABxnIrDOVqrA0Z5yJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNkGdx%2FbtsQ7uVSg3V%2FKG60ABxnIrDOVqrA0Z5yJK%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;2232&quot; height=&quot;632&quot; data-origin-width=&quot;2232&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과물이 나오게 됩니다.&lt;/p&gt;</description>
      <category>CS/INFRA</category>
      <author>ryukyungwoo1220</author>
      <guid isPermaLink="true">https://ryukyungwoo1220.tistory.com/69</guid>
      <comments>https://ryukyungwoo1220.tistory.com/69#entry69comment</comments>
      <pubDate>Sat, 11 Oct 2025 15:58:36 +0900</pubDate>
    </item>
    <item>
      <title>Loki syntax error: unexpected IDENTIFIER 에러</title>
      <link>https://ryukyungwoo1220.tistory.com/68</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;549&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpySmr/btsQ3wt4rFS/ZfOcAOUNCK72qOJcrY1dY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpySmr/btsQ3wt4rFS/ZfOcAOUNCK72qOJcrY1dY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpySmr/btsQ3wt4rFS/ZfOcAOUNCK72qOJcrY1dY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpySmr%2FbtsQ3wt4rFS%2FZfOcAOUNCK72qOJcrY1dY0%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;549&quot; height=&quot;321&quot; data-origin-width=&quot;549&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바른 logql를 입력했는데도 에러가 발생합니다.&lt;/p&gt;
&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/grafana/grafana/issues/84144&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/grafana/grafana/issues/84144&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760014773544&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;Explore Loki: Failed to load log volume for this query &amp;middot; Issue #84144 &amp;middot; grafana/grafana&quot; data-og-description=&quot;What happened? When browsing to Explore for Loki the Log Volume displays Failed to load log volume for this query parse error at line 1, col 77: syntax error: unexpected IDENTIFIER What did you exp...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/grafana/grafana/issues/84144&quot; data-og-url=&quot;https://github.com/grafana/grafana/issues/84144&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bpFr8H/hyZKGNK01l/yHclPtvzLpEknewFGQtNu1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/vdgq3/hyZKDwJSpx/FCstjga4jECfJszsBsrD81/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/grafana/grafana/issues/84144&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/grafana/grafana/issues/84144&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bpFr8H/hyZKGNK01l/yHclPtvzLpEknewFGQtNu1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/vdgq3/hyZKDwJSpx/FCstjga4jECfJszsBsrD81/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Explore Loki: Failed to load log volume for this query &amp;middot; Issue #84144 &amp;middot; grafana/grafana&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;What happened? When browsing to Explore for Loki the Log Volume displays Failed to load log volume for this query parse error at line 1, col 77: syntax error: unexpected IDENTIFIER What did you exp...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://community.grafana.com/t/all-queries-result-in-syntax-error-unexpected-identifier/82228&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://community.grafana.com/t/all-queries-result-in-syntax-error-unexpected-identifier/82228&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760014776538&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;All queries result in &amp;quot;syntax error: unexpected IDENTIFIER&amp;quot;&quot; data-og-description=&quot;I have grafana+loki+promtail configured, and everything seems to be working fine. I am able to see the logs (in the example below it&amp;rsquo;s nginx&amp;rsquo; access.log): However, when I&amp;rsquo;m trying to run any query expression, even the simplest one, I&amp;rsquo;m getting erro&quot; data-og-host=&quot;community.grafana.com&quot; data-og-source-url=&quot;https://community.grafana.com/t/all-queries-result-in-syntax-error-unexpected-identifier/82228&quot; data-og-url=&quot;https://community.grafana.com/t/all-queries-result-in-syntax-error-unexpected-identifier/82228&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dWMhJ9/hyZKGfUroq/jPiFwV5sIU4FgSNA5d88X0/img.png?width=1024&amp;amp;height=488&amp;amp;face=0_0_1024_488,https://scrap.kakaocdn.net/dn/XcU6S/hyZKOZkEW0/KysyzCYCKr0GbeL4yfRI0K/img.png?width=1024&amp;amp;height=488&amp;amp;face=0_0_1024_488,https://scrap.kakaocdn.net/dn/oZe6T/hyZKNzldcR/KR7vNExtKfH3ECKrkFXcZ0/img.png?width=690&amp;amp;height=329&amp;amp;face=0_0_690_329&quot;&gt;&lt;a href=&quot;https://community.grafana.com/t/all-queries-result-in-syntax-error-unexpected-identifier/82228&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://community.grafana.com/t/all-queries-result-in-syntax-error-unexpected-identifier/82228&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dWMhJ9/hyZKGfUroq/jPiFwV5sIU4FgSNA5d88X0/img.png?width=1024&amp;amp;height=488&amp;amp;face=0_0_1024_488,https://scrap.kakaocdn.net/dn/XcU6S/hyZKOZkEW0/KysyzCYCKr0GbeL4yfRI0K/img.png?width=1024&amp;amp;height=488&amp;amp;face=0_0_1024_488,https://scrap.kakaocdn.net/dn/oZe6T/hyZKNzldcR/KR7vNExtKfH3ECKrkFXcZ0/img.png?width=690&amp;amp;height=329&amp;amp;face=0_0_690_329');&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;All queries result in &quot;syntax error: unexpected IDENTIFIER&quot;&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I have grafana+loki+promtail configured, and everything seems to be working fine. I am able to see the logs (in the example below it&amp;rsquo;s nginx&amp;rsquo; access.log): However, when I&amp;rsquo;m trying to run any query expression, even the simplest one, I&amp;rsquo;m getting erro&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;community.grafana.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;비슷한 증상이 많은데 grafana와 loki의 버전 호환성 버그인 것 같습니다.&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;784&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brRnqz/btsQ4NojZw9/IeMLgJocVSQg2RKBrquwe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brRnqz/btsQ4NojZw9/IeMLgJocVSQg2RKBrquwe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brRnqz/btsQ4NojZw9/IeMLgJocVSQg2RKBrquwe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrRnqz%2FbtsQ4NojZw9%2FIeMLgJocVSQg2RKBrquwe1%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;784&quot; height=&quot;378&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loki를 2.9.0 버전으로 바꾸고 실행하였더니 에러가 사라졌습니다.&lt;/p&gt;</description>
      <category>CS/INFRA</category>
      <author>ryukyungwoo1220</author>
      <guid isPermaLink="true">https://ryukyungwoo1220.tistory.com/68</guid>
      <comments>https://ryukyungwoo1220.tistory.com/68#entry68comment</comments>
      <pubDate>Thu, 9 Oct 2025 22:07:30 +0900</pubDate>
    </item>
    <item>
      <title>wsl 환경에서 vagrant 실행 시 확인해야 될 것</title>
      <link>https://ryukyungwoo1220.tistory.com/67</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cU5Klk/btsQ495HpVz/j8OZT4G0gTZDcuYvHlbQ71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cU5Klk/btsQ495HpVz/j8OZT4G0gTZDcuYvHlbQ71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cU5Klk/btsQ495HpVz/j8OZT4G0gTZDcuYvHlbQ71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcU5Klk%2FbtsQ495HpVz%2Fj8OZT4G0gTZDcuYvHlbQ71%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;440&quot; height=&quot;99&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;99&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 반복된다면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #e3e6e8; color: #0c0d0e; text-align: left;&quot;&gt;vagrant plugin install virtualbox_WSL2&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;wsl 용 플러그인을 설치해주시면 됩니다.&lt;/p&gt;
&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://stackoverflow.com/questions/65001570/connection-refused-in-vagrant-using-wsl-2&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/65001570/connection-refused-in-vagrant-using-wsl-2&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760015179086&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;Connection Refused in Vagrant using WSL 2&quot; data-og-description=&quot;I am trying WSL 2, also trying Vagrant on it. I used it in Ubuntu 20.04 and it is working properly, but now i am facing problem here in WSL 2 in Windows 10. My Problem might be about SSH problem on...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/65001570/connection-refused-in-vagrant-using-wsl-2&quot; data-og-url=&quot;https://stackoverflow.com/questions/65001570/connection-refused-in-vagrant-using-wsl-2&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/botQ67/hyZKNe1XDR/KmDpasvsOoeGbNmuKAzpO0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/65001570/connection-refused-in-vagrant-using-wsl-2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/65001570/connection-refused-in-vagrant-using-wsl-2&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/botQ67/hyZKNe1XDR/KmDpasvsOoeGbNmuKAzpO0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;Connection Refused in Vagrant using WSL 2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I am trying WSL 2, also trying Vagrant on it. I used it in Ubuntu 20.04 and it is working properly, but now i am facing problem here in WSL 2 in Windows 10. My Problem might be about SSH problem on...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&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://developer.hashicorp.com/vagrant/docs/other/wsl&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.hashicorp.com/vagrant/docs/other/wsl&lt;/a&gt;&lt;/p&gt;</description>
      <category>CS/INFRA</category>
      <author>ryukyungwoo1220</author>
      <guid isPermaLink="true">https://ryukyungwoo1220.tistory.com/67</guid>
      <comments>https://ryukyungwoo1220.tistory.com/67#entry67comment</comments>
      <pubDate>Thu, 9 Oct 2025 22:06:23 +0900</pubDate>
    </item>
    <item>
      <title>RAG(Retrieval-Augmented Generation) 공부 정리</title>
      <link>https://ryukyungwoo1220.tistory.com/66</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;RAG를 공부하게 된 계기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업 준비를 하면서 여러 회사 채용공고를 보니 &quot;RAG&quot;, &quot;LLM 활용&quot;, &quot;벡터 DB&quot; 같은 키워드가 자주 보였습니다. 특히 AI 관련 직무가 아닌 백엔드 개발자 포지션에서도 이런 기술 스택을 요구하는 경우가 많더라구요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT는 써봤지만 RAG가 정확히 뭔지 몰라서 이번 기회에 제대로 공부해보기로 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RAG란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG(Retrieval-Augmented Generation)는 검색(Retrieval)과 생성(Generation)을 결합한 기술입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 ChatGPT같은 LLM은 학습된 데이터만으로 답변하지만, RAG는 외부 문서를 실시간으로 검색해서 그 내용을 바탕으로 답변을 생성합니다. 쉽게 말해 AI에게 참고자료를 주고 답변하게 만드는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &quot;우리 회사 휴가 규정이 어떻게 되나요?&quot;라는 질문에 일반 LLM은 답할 수 없지만, RAG는 회사 규정 문서를 검색해서 정확한 답변을 할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RAG의 동작 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부하면서 정리한 RAG의 기본 프로세스는 크게 5단계로 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;문서 로드&lt;/b&gt; 단계에서 PDF, Word, HTML 등 다양한 형식의 문서를 시스템에 읽어들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 &lt;b&gt;청킹(Chunking)&lt;/b&gt; 단계에서 긴 문서를 적절한 크기의 조각으로 나눕니다. 한 번에 처리하기엔 너무 길고, 검색 정확도도 떨어지기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;임베딩(Embedding)&lt;/b&gt; 단계에서는 텍스트를 벡터(숫자 배열)로 변환합니다. 컴퓨터가 텍스트의 의미를 이해할 수 있게 만드는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검색&lt;/b&gt; 단계에서는 사용자의 질문과 가장 유사한 문서 조각들을 찾습니다. 벡터 간의 거리를 계산해서 의미적으로 가까운 내용을 찾아냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;b&gt;생성&lt;/b&gt; 단계에서 검색된 문서를 컨텍스트로 제공하여 LLM이 답변을 생성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 코드 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain 라이브러리를 사용해서 직접 구현해봤습니다. 생각보다 코드가 간단해서 놀랐습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from langchain.document_loaders import DirectoryLoader, PyPDFLoader, TextLoader, BSHTMLLoader, Docx2txtLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.vectorstores import Chroma

# 1) 로드
loaders = [
  DirectoryLoader(&quot;./docs&quot;, glob=&quot;**/*.pdf&quot;, loader_cls=PyPDFLoader),
  DirectoryLoader(&quot;./docs&quot;, glob=&quot;**/*.txt&quot;, loader_cls=TextLoader),
  DirectoryLoader(&quot;./docs&quot;, glob=&quot;**/*.html&quot;, loader_cls=BSHTMLLoader),
  DirectoryLoader(&quot;./docs&quot;, glob=&quot;**/*.docx&quot;, loader_cls=Docx2txtLoader),
]
docs = []
for L in loaders: 
    docs += L.load()

# 2) 청크
chunks = RecursiveCharacterTextSplitter(
    chunk_size=600, 
    chunk_overlap=80
).split_documents(docs)

# 3) 임베딩+인덱스(로컬 Chroma 폴더에 영구 저장)
emb = OpenAIEmbeddings()
vs = Chroma.from_documents(chunks, emb, persist_directory=&quot;./chroma_db&quot;)

# 4) 질의
query = &quot;우리 회사 휴가 규정 핵심만 요약&quot;
hits = vs.similarity_search(query, k=6)

# 5) 생성
ctx = &quot;\n\n&quot;.join([h.page_content[:1200] for h in hits])
prompt = f&quot;다음 근거만으로 답해. 모르면 모른다고 해. 출처도 간단히: \n\n[근거]\n{ctx}\n\n[질문]\n{query}&quot;
llm = ChatOpenAI(model=&quot;gpt-4o-mini&quot;, temperature=0)
print(llm.predict(prompt))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 구현하면서 배운 점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문서 로딩의 다양성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DirectoryLoader를 사용하면 폴더 내의 모든 문서를 재귀적으로 탐색할 수 있습니다. 각 파일 형식마다 적절한 로더가 있어서 편리했습니다. PDF는 PyPDFLoader, Word는 Docx2txtLoader 등을 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;청킹 전략의 중요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecursiveCharacterTextSplitter는 문서를 의미 있는 단위로 분할합니다. chunk_size=600은 한 조각의 최대 크기, chunk_overlap=80은 인접한 조각 간 겹치는 부분입니다. 겹치는 부분을 두는 이유는 문맥이 갑자기 끊기는 것을 방지하기 위함입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습하면서 청크 크기에 따라 검색 품질이 크게 달라진다는 것을 알게 되었습니다. 너무 작으면 문맥이 부족하고, 너무 크면 정확도가 떨어집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;벡터 검색의 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 텍스트를 숫자로 변환한다는 개념이 추상적으로 느껴졌는데, 실제로는 의미가 비슷한 텍스트는 벡터 공간에서 가까이 위치한다는 간단한 원리였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;similarity_search()는 코사인 유사도를 기반으로 가장 관련 있는 문서를 찾습니다. k=6은 상위 6개 결과를 반환하라는 의미입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프롬프트 엔지니어링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 명확한 지시를 주는 것이 중요했습니다. &quot;다음 근거만으로 답해&quot;라는 제약을 주지 않으면 LLM이 학습된 지식으로 답변하는 경우가 있었고, &quot;모르면 모른다고 해&quot;를 추가하지 않으면 그럴듯한 거짓 정보를 만들어내기도 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 활용 시나리오 조사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG가 실무에서 어떻게 활용되는지 조사해봤습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기업 내부 지식 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 규정, 기술 문서, 프로젝트 히스토리 등을 RAG 시스템에 통합하여 직원들이 쉽게 정보를 찾을 수 있게 합니다. 신입사원 온보딩에도 활용된다고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;고객 지원 자동화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제품 매뉴얼과 FAQ를 기반으로 고객 문의에 자동 응답하는 시스템을 구축합니다. 특히 기술 지원 분야에서 효과적이라고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;법률/의료 분야&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량의 판례나 의학 논문에서 필요한 정보를 빠르게 검색하고 분석하는 데 활용됩니다. 규정 준수 확인이나 진단 보조 도구로도 사용된다고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;엔터프라이즈 RAG 아키텍처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 복잡한 프로덕션 환경에서는 AWS 서비스를 조합한 아키텍처를 사용한다는 것을 알게 되었습니다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;[오프라인 처리]
S3 ─┐                 ┌─&amp;gt; [Kendra Index]
    ├─ 로더/정제/메타 ┤
RDS ─┘                 └─&amp;gt; [청크] &amp;rarr; [Bedrock Embeddings] &amp;rarr; [OpenSearch]

[온라인 처리]
사용자 질문
  ├─ Kendra Query &amp;rarr; Top-k 문서
  ├─ Bedrock Embedding &amp;rarr; OpenSearch kNN 검색
  └─ 결과 병합 &amp;rarr; Bedrock LLM &amp;rarr; 답변 생성
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 장점은 각 서비스의 강점을 활용할 수 있다는 것입니다. Kendra는 문서 수준의 의미 검색에 강하고, OpenSearch는 청크 수준의 정밀 검색에 유용합니다. 두 결과를 병합하면 더 정확한 답변을 얻을 수 있다고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앞으로 공부할 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG의 기본은 이해했지만, 아직 공부할 것이 많습니다.&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;Fine-tuning&lt;/b&gt;: 도메인 특화 임베딩 모델 만들기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GraphRAG&lt;/b&gt;: 지식 그래프를 활용한 고급 RAG&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Multi-modal RAG&lt;/b&gt;: 텍스트뿐만 아니라 이미지, 표도 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Production 배포&lt;/b&gt;: Docker, Kubernetes 환경에서 운영&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모니터링&lt;/b&gt;: 검색 품질과 답변 정확도 측정 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG를 공부하면서 LLM의 한계를 극복하는 실용적인 방법이라는 것을 알게 되었습니다. 단순히 AI를 사용하는 것이 아니라, 실제 데이터와 결합하여 가치를 만들어내는 기술입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 취준생이라 실무 경험은 없지만, 이런 기술 스택을 이해하고 있다는 것이 면접에서 좋은 어필 포인트가 될 것 같습니다. 포트폴리오 프로젝트로 간단한 RAG 시스템을 만들어볼 계획입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서는 실제로 작은 프로젝트를 만들면서 겪은 경험을 공유하겠습니다.&lt;/p&gt;</description>
      <category>CS/CS</category>
      <author>ryukyungwoo1220</author>
      <guid isPermaLink="true">https://ryukyungwoo1220.tistory.com/66</guid>
      <comments>https://ryukyungwoo1220.tistory.com/66#entry66comment</comments>
      <pubDate>Wed, 17 Sep 2025 16:53:26 +0900</pubDate>
    </item>
    <item>
      <title>Vagrant + Skaffold + MetalLB로 로컬 k8s 개발 환경 구축하기</title>
      <link>https://ryukyungwoo1220.tistory.com/65</link>
      <description>&lt;h1&gt;Vagrant로 진짜 쓸만한 로컬 K8s 개발환경 만들기&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 로컬에서 Kubernetes 개발환경을 제대로 구축해보려다가 예상보다 많은 시행착오를 겪었습니다. Docker Desktop의 K8s나 minikube로는 실제 프로덕션 환경을 충분히 시뮬레이션하기 어려웠기 때문에, Vagrant를 이용한 멀티노드 클러스터를 구축하기로 결정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이것만으로는 충분하지 않았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 굳이 Vagrant로 구축했나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop의 Kubernetes나 k3s, minikube 같은 경량 솔루션들은 분명 편리하지만 몇 가지 한계가 있었습니다.&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;: 노드 장애 시나리오나 파드 분산 배치 테스트가 불가능 (k3s도 로컬에서는 주로 단일 노드로 사용)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LoadBalancer 타입 서비스의 한계&lt;/b&gt;: Docker Desktop K8s는 항상 pending 상태, k3s는 ServiceLB(Klipper)를 제공하지만 실제 MetalLB와는 동작이 다름&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로덕션 환경과의 괴리&lt;/b&gt;: 경량화된 환경이라 실제 kubeadm으로 구축한 클러스터와 설정이나 동작이 미묘하게 다른 경우가 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CNI 플러그인 테스트 제약&lt;/b&gt;: k3s는 Flannel이 기본 내장되어 있어 Calico 등 다른 CNI 테스트가 제한적&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 구성 아키텍처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 시행착오 끝에 다음과 같은 구성으로 정착했습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1758077455318&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;호스트 PC (Windows/Mac/Linux)
  ├─ Docker Desktop (레지스트리 서버용 - 10.10.10.1:5000)
  ├─ Skaffold (빌드 및 배포 자동화)
  └─ VirtualBox
      ├─ master-node (10.10.10.10)
      └─ worker-node-1 (10.10.10.11)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop은 결국 컨테이너 레지스트리 용도로만 활용하게 되었습니다. Skaffold는 호스트 PC에서 실행되어 이미지를 빌드하고 레지스트리에 푸시한 후 K8s 클러스터에 배포합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;첫 번째 문제: 네트워크 대역 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 Vagrant의 기본 네트워크 설정을 사용했으나, NAT 인터페이스(10.0.2.15)로 인해 모든 VM이 동일한 IP를 가지는 문제가 발생했습니다. 이로 인해 노드 간 통신이 제대로 이루어지지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책으로 Host-Only 네트워크를 추가 구성했습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758077476597&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Vagrantfile
node.vm.network &quot;private_network&quot;, ip: &quot;10.10.10.10&quot;  # master
node.vm.network &quot;private_network&quot;, ip: &quot;10.10.10.11&quot;  # worker&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하지만 이것만으로는 충분하지 않았습니다.&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 번째 문제: kubelet의 잘못된 IP 바인딩&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host-Only 네트워크를 설정했음에도 불구하고, kubectl get nodes -o wide 명령 결과에서 INTERNAL-IP가 여전히 10.0.2.15로 표시되는 문제가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조사 결과, kubelet이 기본 라우트 인터페이스를 자동으로 선택한다는 것을 발견했습니다. 각 노드에서 명시적으로 IP를 지정해야 했습니다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# /etc/default/kubelet
KUBELET_EXTRA_ARGS=&quot;--node-ip=10.10.10.10&quot;  # 각 노드의 Host-Only IP
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제 해결에 상당한 시간이 소요되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세 번째 문제: 컨테이너 이미지 레지스트리 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 Docker Hub를 사용하려 했으나, 빈번한 푸시 작업으로 인한 네트워크 지연이 개발 효율성을 크게 저하시켰습니다. 그래서 Docker Desktop에 registry:2 컨테이너를 띄워서 로컬 레지스트리로 활용하기로 했습니다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 호스트 PC에서 실행
docker run -d -p 5000:5000 --restart=always --name registry registry:2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 K8s 노드의 containerd가 HTTP 레지스트리 접근을 거부하는 새로운 문제에 직면했습니다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 발생한 에러
failed to do request: Head &quot;&amp;lt;https://10.10.10.1:5000/v2/my-app/manifests/latest&amp;gt;&quot;: 
http: server gave HTTP response to HTTPS client
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS 인증서 설정 대신, containerd 설정을 수정하여 해결했습니다:&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;# /etc/containerd/config.toml
[plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.mirrors.&quot;10.10.10.1:5000&quot;]
  endpoint = [&quot;&amp;lt;http://10.10.10.1:5000&amp;gt;&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;네 번째 문제: Calico CNI 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에 Flannel을 사용했으나 안정성 문제로 Calico로 전환했습니다. 그러나 VirtualBox 환경에서는 기본 BGP 모드가 작동하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VXLAN 모드로 전환하여 해결했습니다:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# calico-custom.yaml 패치
- name: CALICO_IPV4POOL_VXLAN
  value: &quot;Always&quot;  # 기본값: &quot;Never&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정을 찾기까지 Pod 간 통신 문제로 많은 시간을 소비했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다섯 번째 문제: LoadBalancer 서비스 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 환경에서는 LoadBalancer 타입 서비스가 영원히 pending 상태로 남는 것이 정상이지만, 테스트를 위해 실제와 유사한 환경이 필요했습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get svc
NAME          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)
my-service    LoadBalancer   10.96.100.200   &amp;lt;pending&amp;gt;     80:30123/TCP
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MetalLB를 도입하여 해결했습니다. 주의할 점은 최신 버전에서는 ConfigMap 대신 Custom Resource를 사용한다는 것입니다:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default-pool
  namespace: metallb-system
spec:
  addresses:
  - 10.10.10.200-10.10.10.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools:
  - default-pool
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;여섯 번째 문제: Ingress 접속을 위한 hosts 파일 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingress를 통한 도메인 기반 라우팅을 테스트하기 위해 hosts 파일 수정이 필요했습니다. MetalLB가 할당한 IP(예: 10.10.10.200)와 테스트 도메인을 매핑해야 했습니다:&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;# C:\\Windows\\System32\\drivers\\etc\\hosts (Windows, 관리자 권한 필요)
# /etc/hosts (Mac/Linux)
10.10.10.200  local.test
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 ingress-nginx-controller Service를 LoadBalancer 타입으로 변경하여 MetalLB가 IP를 할당할 수 있도록 했습니다:&lt;/p&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;kubectl patch svc ingress-nginx-controller -n ingress-nginx \\
  -p '{&quot;spec&quot;: {&quot;type&quot;: &quot;LoadBalancer&quot;}}'
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일곱 번째 문제: 호스트에서 kubectl 접근 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호스트 PC에서 직접 kubectl 명령을 실행하기 위해 마스터 노드의 kubeconfig를 복사해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 직접적이고 확실한 방법은 수동으로 복사하는 것입니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계: Vagrant VM에 SSH 접속&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;vagrant ssh master-node&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계: admin.conf 내용 확인 및 복사&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo cat /etc/kubernetes/admin.conf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령을 실행하면 다음과 같은 YAML 형식의 설정이 출력됩니다:&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...
    server: https://10.10.10.10:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: kubernetes-admin
  name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
  user:
    client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...
    client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tL...&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3단계:&lt;/b&gt;&lt;b&gt;복사한 내용을 config 파일에 붙여넣기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758247246165&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 원하는 텍스트 에디터 사용
vi ~/.kube/config  # 또는 nano, code 등
# 2단계에서 복사한 전체 내용을 붙여넣고 저장&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 과정이 반드시 필요한 이유:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubectl이 Kubernetes API 서버와 통신하기 위해서는 단순히 IP 주소와 포트 번호만 아는 것으로는 충분하지 않습니다. Kubernetes는 보안을 위해 **상호 TLS 인증(Mutual TLS Authentication)**을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;admin.conf 파일에 포함된 핵심 정보들:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;certificate-authority-data&lt;/b&gt;: 클러스터 CA(Certificate Authority) 인증서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 서버가 제시하는 인증서가 올바른 CA에 의해 서명되었는지 확인&lt;/li&gt;
&lt;li&gt;중간자 공격(MITM) 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;client-certificate-data&lt;/b&gt;: 클라이언트(kubectl) 인증서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubectl이 자신의 신원을 API 서버에 증명&lt;/li&gt;
&lt;li&gt;이 인증서는 클러스터 CA에 의해 서명되어 있어 유효성이 보장됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;client-key-data&lt;/b&gt;: 클라이언트 개인 키
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 인증서와 쌍을 이루는 개인 키&lt;/li&gt;
&lt;li&gt;TLS 핸드셰이크 과정에서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;server&lt;/b&gt;: API 서버 엔드포인트 (&lt;a href=&quot;https://10.10.10.10:6443&quot;&gt;https://10.10.10.10:6443&lt;/a&gt;)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 접속할 서버 주소&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 가지 정보가 모두 올바르게 설정되어야만 kubectl과 API 서버 간의 TLS 핸드셰이크가 성공하고, 안전한 통신 채널이 수립됩니다. 하나라도 없거나 잘못되면 다음과 같은 에러가 발생합니다:&lt;/p&gt;
&lt;pre id=&quot;code_1758247270877&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# CA 인증서가 없거나 잘못된 경우
Unable to connect to the server: x509: certificate signed by unknown authority

# 클라이언트 인증서/키가 없는 경우  
error: You must be logged in to the server (Unauthorized)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;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;여덟 번째 문제: Next.js 환경변수 주입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;K8s Secret을 통해 환경변수를 주입했지만, NEXT_PUBLIC_ 접두사가 붙은 변수들이 undefined로 나타나는 문제가 발생했습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 이 방법은 클라이언트 사이드 변수에 작동하지 않음
envFrom:
  - secretRef:
      name: my-app-secrets
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js의 특성상 NEXT_PUBLIC_ 변수는 빌드 시점에 결정된다는 것을 깨달았습니다. Skaffold의 buildArgs를 통해 해결했습니다:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# skaffold.yaml
build:
  artifacts:
    - image: 10.10.10.1:5000/my-next-app
      docker:
        buildArgs:
          NEXT_PUBLIC_NCP_CLIENT_ID: &quot;실제_값_입력&quot;  # 빌드 시점 주입
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 사이드 변수는 Secret으로, 클라이언트 사이드 변수는 빌드 인자로 관리하는 것으로 정리했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아홉 번째 문제: 마스터 노드의 taint 제거&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes는 기본적으로 마스터(컨트롤 플레인) 노드에는 일반 워크로드가 스케줄링되지 않도록 taint를 설정합니다. 이는 컨트롤 플레인의 안정성을 보장하기 위한 정책입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 로컬 개발 환경에서는 리소스가 제한적이고, 워커 노드 하나만으로는 메모리가 부족한 경우가 많았습니다. Pod describe 결과를 확인해보니:&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;0/2 nodes are available: 
1 node(s) had taint {node-role.kubernetes.io/control-plane: }, that the pod didn't tolerate, 
1 Insufficient memory.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메시지는 워커 노드는 메모리 부족, 마스터 노드는 taint로 인해 Pod가 스케줄링될 수 없다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 개발 환경에서는 마스터 노드의 리소스도 활용해야 하므로 taint를 제거했습니다:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;kubectl taint nodes --all node-role.kubernetes.io/control-plane-
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 모든 노드에서 node-role.kubernetes.io/control-plane taint를 제거합니다. 프로덕션 환경에서는 절대 권장하지 않는 방법이지만, 제한된 리소스의 로컬 환경에서는 실용적인 선택입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 프로젝트 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 시행착오를 거쳐 다음과 같은 구조로 정착했습니다:&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;my-k8s-project/
├── Vagrantfile           # VM 정의
├── scripts/
│   ├── common.sh        # 공통 설정 (containerd, kubeadm 등)
│   ├── master.sh        # 마스터 노드 초기화
│   ├── worker.sh        # 워커 노드 조인
│   └── addons.sh        # MetalLB, Ingress-NGINX 설치
├── configs/             # kubeconfig 자동 복사 디렉토리
├── skaffold.yaml        # 빌드/배포 자동화 설정
├── nextjs-app.yaml      # K8s 매니페스트
├── Dockerfile          
└── (Next.js 애플리케이션 소스)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Skaffold를 활용한 개발 워크플로우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 구축된 개발 환경은 매우 효율적입니다:&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 개발 모드 실행
skaffold dev

# 코드 수정 시 자동으로:
# 1. Docker 이미지 빌드
# 2. 10.10.10.1:5000 레지스트리에 푸시
# 3. Kubernetes 클러스터에 배포
# 4. &amp;lt;http://local.test&amp;gt; 에서 변경사항 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;636&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ebjqHp/btsQzGK7BEN/P88GQGNPevBKFV8DtiCB40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ebjqHp/btsQzGK7BEN/P88GQGNPevBKFV8DtiCB40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ebjqHp/btsQzGK7BEN/P88GQGNPevBKFV8DtiCB40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FebjqHp%2FbtsQzGK7BEN%2FP88GQGNPevBKFV8DtiCB40%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;636&quot; height=&quot;546&quot; data-origin-width=&quot;636&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;45&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l4IGL/btsQBFkobDl/CwNfjKzqdcyHvdPfk8oOw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l4IGL/btsQBFkobDl/CwNfjKzqdcyHvdPfk8oOw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l4IGL/btsQBFkobDl/CwNfjKzqdcyHvdPfk8oOw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl4IGL%2FbtsQBFkobDl%2FCwNfjKzqdcyHvdPfk8oOw0%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;534&quot; height=&quot;45&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;45&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byqNXm/btsQBiiImnw/Oedjpate2TgcGvuNjmFci1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byqNXm/btsQBiiImnw/Oedjpate2TgcGvuNjmFci1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byqNXm/btsQBiiImnw/Oedjpate2TgcGvuNjmFci1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyqNXm%2FbtsQBiiImnw%2FOedjpate2TgcGvuNjmFci1%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;933&quot; height=&quot;896&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핫 리로드는 아니지만, 빌드와 배포가 자동화되어 충분히 빠른 개발 사이클을 유지할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 간단할 것으로 예상했던 로컬 K8s 환경 구축이 생각보다 많은 도전 과제를 안겨주었습니다. 특히 네트워크 관련 설정에서 많은 시행착오를 겪었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 한 번 구축해 놓으면 실제 프로덕션과 유사한 환경에서 테스트할 수 있어 매우 유용합니다. 멀티노드 시나리오, 네트워크 정책, 리소스 제한 등 다양한 K8s 기능을 로컬에서 충분히 검증할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계로는 Istio를 추가하여 서비스 메시 구성을 테스트해볼 예정입니다. 또한 ArgoCD를 통한 GitOps 파이프라인 구축도 계획하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트가 로컬 K8s 환경을 구축하려는 분들께 도움이 되기를 바랍니다. 질문이나 개선 사항이 있다면 언제든 공유 부탁드립니다.&lt;/p&gt;</description>
      <category>CS/INFRA</category>
      <author>ryukyungwoo1220</author>
      <guid isPermaLink="true">https://ryukyungwoo1220.tistory.com/65</guid>
      <comments>https://ryukyungwoo1220.tistory.com/65#entry65comment</comments>
      <pubDate>Wed, 17 Sep 2025 11:53:22 +0900</pubDate>
    </item>
    <item>
      <title>객체 지향 프로그래밍의 5대 원칙(SOLID)</title>
      <link>https://ryukyungwoo1220.tistory.com/64</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가기 전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 객체 지향 프로그래밍에 대해 배웠습니다. 그렇다면 객체 지향 프로그래밍을 위해선 어떤 방법론이 필요할까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OOP의 5대 원칙 (SOLID)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SRP - 단일 책임 원칙 (Single Responsibility Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 클래스는 하나의 책임만 가져야 합니다. 이는 클래스가 변경되어야 하는 이유가 한가지여야 한다는 의미입니다. 해당 클래스가 여러 대상 또는 기능들에 대해 책임을 가져서는 안되고, 오직 하나의 기능에 대해서만 책임을 져야 한다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 어떤 클래스가 여러 액터에 대해 기능을 가지고 있다면 여러 클래스들로 부터 변경에 대한 요구가 올 수 있으므로, 해당 클래스을 수정해야 하는 이유 역시 여러개가 될 수 있습니다. 반면에 어떤 클래스가 단 하나의 책임 만을 갖고 있다면, 특정 기능으로부터 변경을 특정할 수 있으므로 해당 클래스들을 변경해야 하는 이유와 시점이 명확해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 단일 책임 원칙의 목적은 프로그램의 유지보수성을 높이기 위한 설계기법 입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OCP - 개방/폐쇄 원칙 (Open/Closed Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스는 확장에는 열려 있어야 하지만, 변경에는 닫혀 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확장에 열려있단 말은 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 큰 힘을 들이지 않고 애플리케이션의 기능을 확장할 수 있단 말입니다. 변경에 닫혀있단 말은 새로운 변경 사항이 발생했을 때 객체를 직접적으로 수정을 제한한다는 말입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하자면 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계 되어야 된다는 원칙입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LSP - 리스코프 치환 원칙 (Liskov Substitution Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브타입은 언제나 그것의 부모타입으로 교체할 수 있어야 한다는 원칙입니다. 다형성의 특징을 이용하기 위해 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면, 업캐스팅 된 상태에서 부모의 메서드를 사용해도 동작이 의도대로 흘러가야 하는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 LSP 원칙을 잘 적용한 예제가 자바 컬렉션 프레임워크입니다. 만일 변수에 LinkedList 자료형을 담아 사용하다, 중간에 전혀 다른 HashSet 자료형으로 바꿔도 add() 메서드 동작을 보장받기 위해서는 Collection 이라는 인터페이스 타입으로 변수를 선언하여 할당하면 됩니다. 왜냐하면 인터페이스 Collection의 추상 메서드를 각기 하위 자료형 클래스에서 implements 하여 인터페이스 구현 규약을 잘 지키도록 미리 잘 설계되어 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 확장하는 부분에 다형성을 제공해 변화에 열려있는 프로그램을 만들 수 있도록 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ISP - 인터페이스 분리 원칙 (Interface Segregation Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISP 원리는 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원리 입니다. 즉, 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스만을 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SRP 원칙이 클래스의 단일 책임을 강조한다면, ISP는 인터페이스의 단일 책임을 강조하는 것으로 보면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISP 원칙은 인터페이스를 사용하는 클라이언트를 기준으로 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것이 목표입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DIP - 의존성 역전 원칙 (Dependency Inversion Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DIP 원칙이란 객체에서 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소인 추상 클래스 혹은 인터페이스로 참조 하라는 원칙입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체들이 서로 정보를 주고 받을 때는 의존 관계가 형성되는데, 이 때 객체들은 나름대로의 원칙을 갖고 정보를 주고 받아야 하는 약속이 있습니다. 여기서 DIP 원칙이란 추상성이 낮은 클래스보다 추상성이 높은 클래스와 통신 한다는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 DIP란 비즈니스와 관련된 부분이 세부 사항에는 의존하는 않는 설계원칙을 의미합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 SOLID 원칙들에 대해 알아보았습니다. 위 내용에서 시사하는 바는 결국 추상화와 다형성입니다. 구체 클래스에 의존하지 않고 추상클래스 또는 인터페이스에 의존함으로써 우리는 유지보수하기 쉽고, 유연하고, 확장이 쉬운 소프트웨어를 만들 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.nextree.co.kr/p6960/https://mangkyu.tistory.com/194[https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)](https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)&quot;&gt;https://www.nextree.co.kr/p6960/https://mangkyu.tistory.com/194[https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)](https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)&lt;/a&gt;)&lt;a href=&quot;https://inpa.tistory.com/entry/OOP-&quot;&gt;https://inpa.tistory.com/entry/OOP-&lt;/a&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99-SOLID&quot;&gt; -객체-지향-설계의-5가지-원칙-SOLID&lt;/a&gt;&lt;/p&gt;</description>
      <category>CS/CS</category>
      <author>ryukyungwoo1220</author>
      <guid isPermaLink="true">https://ryukyungwoo1220.tistory.com/64</guid>
      <comments>https://ryukyungwoo1220.tistory.com/64#entry64comment</comments>
      <pubDate>Wed, 17 Sep 2025 11:48:08 +0900</pubDate>
    </item>
    <item>
      <title>객체 지향 프로그래밍의 5대 원칙(SOLID)</title>
      <link>https://ryukyungwoo1220.tistory.com/63</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가기 전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 객체 지향 프로그래밍에 대해 배웠습니다. 그렇다면 객체 지향 프로그래밍을 위해선 어떤 방법론이 필요할까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OOP의 5대 원칙 (SOLID)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SRP - 단일 책임 원칙 (Single Responsibility Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 클래스는 하나의 책임만 가져야 합니다. 이는 클래스가 변경되어야 하는 이유가 한가지여야 한다는 의미입니다. 해당 클래스가 여러 대상 또는 기능들에 대해 책임을 가져서는 안되고, 오직 하나의 기능에 대해서만 책임을 져야 한다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 어떤 클래스가 여러 액터에 대해 기능을 가지고 있다면 여러 클래스들로 부터 변경에 대한 요구가 올 수 있으므로, 해당 클래스을 수정해야 하는 이유 역시 여러개가 될 수 있습니다. 반면에 어떤 클래스가 단 하나의 책임 만을 갖고 있다면, 특정 기능으로부터 변경을 특정할 수 있으므로 해당 클래스들을 변경해야 하는 이유와 시점이 명확해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 단일 책임 원칙의 목적은 프로그램의 유지보수성을 높이기 위한 설계기법 입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OCP - 개방/폐쇄 원칙 (Open/Closed Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스는 확장에는 열려 있어야 하지만, 변경에는 닫혀 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확장에 열려있단 말은 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 큰 힘을 들이지 않고 애플리케이션의 기능을 확장할 수 있단 말입니다. 변경에 닫혀있단 말은 새로운 변경 사항이 발생했을 때 객체를 직접적으로 수정을 제한한다는 말입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하자면 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계 되어야 된다는 원칙입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LSP - 리스코프 치환 원칙 (Liskov Substitution Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브타입은 언제나 그것의 부모타입으로 교체할 수 있어야 한다는 원칙입니다. 다형성의 특징을 이용하기 위해 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면, 업캐스팅 된 상태에서 부모의 메서드를 사용해도 동작이 의도대로 흘러가야 하는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 LSP 원칙을 잘 적용한 예제가 자바 컬렉션 프레임워크입니다. 만일 변수에 LinkedList 자료형을 담아 사용하다, 중간에 전혀 다른 HashSet 자료형으로 바꿔도 add() 메서드 동작을 보장받기 위해서는 Collection 이라는 인터페이스 타입으로 변수를 선언하여 할당하면 됩니다. 왜냐하면 인터페이스 Collection의 추상 메서드를 각기 하위 자료형 클래스에서 implements 하여 인터페이스 구현 규약을 잘 지키도록 미리 잘 설계되어 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 확장하는 부분에 다형성을 제공해 변화에 열려있는 프로그램을 만들 수 있도록 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ISP - 인터페이스 분리 원칙 (Interface Segregation Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISP 원리는 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원리 입니다. 즉, 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스만을 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SRP 원칙이 클래스의 단일 책임을 강조한다면, ISP는 인터페이스의 단일 책임을 강조하는 것으로 보면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISP 원칙은 인터페이스를 사용하는 클라이언트를 기준으로 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것이 목표입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DIP - 의존성 역전 원칙 (Dependency Inversion Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DIP 원칙이란 객체에서 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소인 추상 클래스 혹은 인터페이스로 참조 하라는 원칙입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체들이 서로 정보를 주고 받을 때는 의존 관계가 형성되는데, 이 때 객체들은 나름대로의 원칙을 갖고 정보를 주고 받아야 하는 약속이 있습니다. 여기서 DIP 원칙이란 추상성이 낮은 클래스보다 추상성이 높은 클래스와 통신 한다는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 DIP란 비즈니스와 관련된 부분이 세부 사항에는 의존하는 않는 설계원칙을 의미합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 SOLID 원칙들에 대해 알아보았습니다. 위 내용에서 시사하는 바는 결국 추상화와 다형성입니다. 구체 클래스에 의존하지 않고 추상클래스 또는 인터페이스에 의존함으로써 우리는 유지보수하기 쉽고, 유연하고, 확장이 쉬운 소프트웨어를 만들 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.nextree.co.kr/p6960/https://mangkyu.tistory.com/194[https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)](https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)&quot;&gt;https://www.nextree.co.kr/p6960/https://mangkyu.tistory.com/194[https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)](https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)&lt;/a&gt;)&lt;a href=&quot;https://inpa.tistory.com/entry/OOP-&quot;&gt;https://inpa.tistory.com/entry/OOP-&lt;/a&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99-SOLID&quot;&gt; -객체-지향-설계의-5가지-원칙-SOLID&lt;/a&gt;&lt;/p&gt;</description>
      <category>CS/CS</category>
      <author>ryukyungwoo1220</author>
      <guid isPermaLink="true">https://ryukyungwoo1220.tistory.com/63</guid>
      <comments>https://ryukyungwoo1220.tistory.com/63#entry63comment</comments>
      <pubDate>Tue, 16 Sep 2025 13:45:02 +0900</pubDate>
    </item>
  </channel>
</rss>