<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>k-haechan.log</title>
        <link>https://velog.io/</link>
        <description>성실히 열심히 즐겁게</description>
        <lastBuildDate>Tue, 10 Jun 2025 07:07:01 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. k-haechan.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/k-haechan" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[
☁️ SNS 사이드 프로젝트에서의 이미지 파일 저장 전략과 인프라 설계]]></title>
            <link>https://velog.io/@k-haechan/SNS-%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%8C%8C%EC%9D%BC-%EC%A0%80%EC%9E%A5-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@k-haechan/SNS-%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%8C%8C%EC%9D%BC-%EC%A0%80%EC%9E%A5-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Tue, 10 Jun 2025 07:07:01 GMT</pubDate>
            <description><![CDATA[<p>SNS 서비스를 사이드 프로젝트로 구현하면서 <strong>이미지 파일을 어떻게 저장할지</strong> 고민이 생겼습니다.</p>
<p>처음에는 Spring Boot 내부에서 파일을 직접 받아 서버에 저장하고 관리하는 구조를 고려했지만, 결국 <strong>AWS S3를 사용하기로 결정</strong>했습니다.</p>
<h2 id="✅-왜-s3인가"><strong>✅ 왜 S3인가?</strong></h2>
<ul>
<li><strong>성능 측면</strong>: WAS가 직접 파일을 핸들링하는 것보다 S3에 위임하는 것이 I/O 성능 부담을 줄입니다.</li>
<li><strong>확장성 측면</strong>: 트래픽 증가 시에도 S3는 서버리스 기반으로 자동 확장됩니다.</li>
<li><strong>유지보수성</strong>: 정적 자산은 정적 자산답게! 관리 책임을 분리할 수 있습니다.</li>
</ul>
<blockquote>
<p>온디맨드 수요에 맞춰 잘 설계된 클라우드 구조를 이해하면서, 확장성과 책임 분리를 자연스럽게 체득하게 됐습니다.</p>
</blockquote>
<hr>
<h2 id="🛠️-인프라-구성-terraform--github-actions"><strong>🛠️ 인프라 구성: Terraform + GitHub Actions</strong></h2>
<p>클라우드를 배우면서 자연스럽게 <strong>IAC(Infrastructure as Code)</strong>에 관심을 갖게 되었고,</p>
<p>Terraform을 통해 <strong>실습용 아키텍처를 코드로 구성</strong>해보았습니다.</p>
<p>또한 GitHub Actions를 연동하여 코드 푸시 시 자동으로 배포 및 이미지를 갱신하도록 만들었습니다.</p>
<pre><code>resource &quot;aws_s3_bucket&quot; &quot;image_bucket&quot; {
  bucket = &quot;my-sns-image-bucket&quot;
  acl    = &quot;public-read&quot;

  tags = {
    Name = &quot;SNS Image Bucket&quot;
  }
}</code></pre><pre><code># .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Build Docker Image
        run: |
          docker build -t $DOCKER_IMAGE_NAME .
      - name: Push to GHCR
        run: |
          docker push ghcr.io/username/$DOCKER_IMAGE_NAME:latest</code></pre><hr>
<h2 id="⚙️-앞으로"><strong>⚙️ 앞으로</strong></h2>
<ul>
<li>CloudFront와 S3를 연결하여 정적 파일의 CDN 최적화</li>
<li>API 서버(Spring Boot)는 EC2에서 관리하되, 이미지 정적 파일만 S3로 위임</li>
<li>보안 설정 및 버킷 정책을 통해 접근 제어</li>
</ul>
<hr>
<p>이번 실습을 통해 “어떤 파일은 서버에서 관리하지 않는 것이 더 좋은가?“라는 질문에 대해 스스로 답을 찾게 되었습니다.</p>
<p>향후 MSA나 CDN 도입 시에도 이 경험이 좋은 밑거름이 될 것이라 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS CloudFront Signed Cookie 적용하기 (Spring)]]></title>
            <link>https://velog.io/@k-haechan/AWS-CloudFront-Signed-Cookie-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-Spring</link>
            <guid>https://velog.io/@k-haechan/AWS-CloudFront-Signed-Cookie-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-Spring</guid>
            <pubDate>Wed, 04 Jun 2025 11:30:05 GMT</pubDate>
            <description><![CDATA[<h2 id="aws-cloudfront-signed-cookie란">AWS CloudFront Signed Cookie란?</h2>
<ul>
<li>사용 이유 : S3에 저장된 자원에 대해 접근을 제한해야 하기 위해 </li>
<li>장점 : CloudFront(CDN)를 이용하기 때문에 S3만 이용할 때 보다 더 최적화된 리소스 접근을 할 수 있다. Signed Cookie를 통해 사용자 접근에 대한 제어를 백엔드 프로그램에서 할 수 있다.</li>
</ul>
<h2 id="signed-url-vs-signed-cookie">Signed URL vs Signed Cookie</h2>
<ul>
<li>Signed Cookie를 사용하면 접근 경로를 *을 통해 표시할 수 있다. -&gt; 다중 자원에 대한 권한을 처리할 수 있음. 반면에 SignedUrl은 자원 각각에 따라 접근권한을 처리해주어야한다. SNS 서비스 특성상 한 게시물에 여러개의 이미지가 포함되어 있으므로 postId에 종속적으로 S3 패키지 구조를 생성하여 특정 게시물에 속한 이미지에 권한을 부여하여 한번에 처리할 수 있다.</li>
</ul>
<h2 id="signed-서명된">Signed? (서명된)</h2>
<ul>
<li>서명된은 무슨 의미일까? 공개키 암호 방식에서 개인키를 이용하여 암호화한 방식을 의미한다.<ul>
<li>검증 가능한 공개키를 공유함으로써 개인키의 주인이 암호화(서명)한 파일임을 확인할 수 있다.</li>
</ul>
</li>
<li>Signed Cookie / URL에서는 S3 자원에 대해 접근 권한이 명시된 정책의 진위성을 확인하기 위해 쓰인다.</li>
</ul>
<h2 id="정책polcy">정책(Polcy)</h2>
<p><a href="https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html#private-content-custom-policy-statement-signed-cookies-examples">https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html#private-content-custom-policy-statement-signed-cookies-examples</a></p>
<pre><code class="language-json">{
    &quot;Statement&quot;: [
        {
            &quot;Resource&quot;: &quot;https://*&quot;,
            &quot;Condition&quot;: {
                &quot;IpAddress&quot;: {
                    &quot;AWS:SourceIp&quot;: &quot;192.0.2.10/32&quot;
                },
                &quot;DateGreaterThan&quot;: {
                    &quot;AWS:EpochTime&quot;: 1357034400
                },
                &quot;DateLessThan&quot;: {
                    &quot;AWS:EpochTime&quot;: 1357120800
                }
            }
        }
    ]
}</code></pre>
<ul>
<li>위는 AWS 공식문서에 명시된 예제이다. Resource, Condition 등 접근 권한에 필요한 속성을 사용한다.</li>
<li>시간은 EpochTime을 사용하여 TimeZone에 독립적인 장점이 있다.</li>
<li>위 데이터를 Base64방식으로 Encoding하여 서명하면 Signed Policy를 구할 수 있다. 이를 공유하여 백엔드 서비스에서 주관하는 이미지 리소스 제어를 강제할 수 있다!</li>
</ul>
<h2 id="어떤-정책을-쓸까">어떤 정책을 쓸까?</h2>
<ul>
<li>정책은 Custom Policy와 Canned Policy가 있다. 쉽게 말해 사용자 설정 정책과 기본 정책인데 다중 리소스 명시를 위해 *를 사용하려면 Custom Policy를 사용해야한다.</li>
</ul>
<h2 id="키-발급을-받자">키 발급을 받자!</h2>
<ul>
<li>openssl 라이브러리를 이용한 shell 프로그래밍으로 생성할 수도 있지만 Root 계정의 보안자격증명에서 CloudFront의 키를 발급받아 사용할 수도 있다.</li>
<li>발급받은 키는 파일로 다운받아서 CloudFront에 등록하자(키관리&gt; 키그룹, 퍼블릭 키)</li>
</ul>
<h2 id="spring을-통해-구현해보자">Spring을 통해 구현해보자!</h2>
<p>공식문서에 어느정도 나와있다. 그렇지만 Custom Policy를 사용하는 것은 설명이 친절하지 않으며 AWS Jdk 버전에 따라서 그 방식도 상이하다. 그래도 몇가지 포인트만 확인하면 쉽게 잘 구현할 수 있을 것이다.</p>
<pre><code class="language-java">private final CloudFrontUtilities cloudFrontUtilities = CloudFrontUtilities.create();

Instant now = Instant.now();
        Instant expireDate = now.plusSeconds(2 * 60 * 60); // 2시간 동안 유효

        CustomSignerRequest request;
        try {
        request = CustomSignerRequest.builder()
            .resourceUrl(resourcePathPattern) // https://~~~~~~~~~~.cloudfront.net/images/post/* 와 같은 패턴
            .resourceUrlPattern(resourcePathPattern) // 이부분은 사실 필요없다... 코드 내부를 까보면 resourceUrl로 대체된다.
            .privateKey(Paths.get(privateKeyPath))   // private key 경로이다. .pem파일도 가능하며 절대경로로 지정해야한다.(배포 환경에 따른 환경변수 처리 필수)
            .keyPairId(keyPairId) // key-pair-id 아니다.. 공개키의 ID이다. AWS 콘솔에서 확인할 수 있다.
            .expirationDate(expireDate)
            .activeDate(now.minusSeconds(5 * 60)) // 5분 전부터 유효
            .ipRange(&quot;0.0.0.0/0&quot;) // ip 제한하려면 이 부분을 조절하면 된다.
            .build();
        } catch (Exception e) {
            throw new RuntimeException(&quot;Failed to create CustomSignerRequest: &quot; + e.getMessage(), e);
        }

CookiesForCustomPolicy cookiesForCustomPolicy = cloudFrontUtilities.getCookiesForCustomPolicy(request);

// 아래와 같은 방식으로 쿠키에 필요한 3가지 값을 확인할 수 있다.
// 쿠키에 바로 사용가능한 Key=Value 형식으로 작성되어있다.
cookiesForCustomPolicy.policyHeaderValue(); // 정책을 Base64로 인코딩한 값
cookiesForCustomPolicy.signatureHeaderValue(); // 서명값
cookiesForCustomPolicy.keyPairIdHeaderValue(); // 공개키 ID 값
</code></pre>
<h3 id="여기에서-화나는-포인트가-있다">여기에서 화나는 포인트가 있다..</h3>
<ol>
<li>keyPairId는 키 그룹 ID가 아니라 공개키의 ID를 의미하는 것이다.
-&gt; 저는 그것을 확인하지 못해서 계속 403에러를 받았습니다..</li>
<li>resourceUrlPattern에는 값을 안넣어도 된다.
-&gt; resourceUrl에 CloudFront의 루트 도메인 경로를 명시하고 resourcePathPattern에 path를 명시하면 된다고 생각했지만 resourcePathPattern은 signedCookie방식에만 쓰이고 실제로 pattern이 포함된 문자열을 resourceUrl에 넣어야 한다.</li>
</ol>
<h3 id="실습">실습</h3>
<p><img src="https://velog.velcdn.com/images/k-haechan/post/619aa1e9-ec86-45a8-a0de-032165e9f132/image.png" alt=""></p>
<ul>
<li>여기에서도 아쉬운 점이 있다. CloudFront와 Backend의 도메인이 달라서 쿠키를 발급해도 공유하지 못한다. Backend에서 발급한 쿠키를 CloudFront의 도메인으로 설정한 후 사용해야한다. 실제 서비스에서는 CloudFront에 도메인을 연결하여 사용하면 될 듯 하다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>