<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코딩기록장</title>
    <link>https://baby-developer77.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 18 Jun 2026 11:21:47 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>코딩 기록하는 애기 개발자</managingEditor>
    <image>
      <title>코딩기록장</title>
      <url>https://tistory1.daumcdn.net/tistory/6969040/attach/ee129dd242d3414e9b0471d837eac37c</url>
      <link>https://baby-developer77.tistory.com</link>
    </image>
    <item>
      <title>[Infra/DevOps] 사이드 프로젝트를 위한 AWS 인프라 구축 가이드 (2026 ver.) #3- CICD, Github Actions, Docker</title>
      <link>https://baby-developer77.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 RDS를 생성하고 SSH 터널링을 이용해 데이터베이스에 안전하게 접근하는 환경을 구축했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 애플리케이션 서버와 데이터베이스까지 모두 준비되었으므로 서비스를 실제로 운영할 수 있는 기본적인 인프라가 완성되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서버와 데이터베이스를 구축했다고 해서 배포까지 끝난 것은 아니다. 서비스를 운영하다 보면 새로운 기능을 추가하거나 버그를 수정해야 하고, 수정된 코드를 서버에 반영하는 작업이 반복적으로 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 초기에는 서버에 직접 접속하여 코드를 업데이트하고 애플리케이션을 재시작하는 방식으로도 충분히 운영할 수 있다. 하지만 개발이 계속 진행될수록 배포 횟수가 증가하고, 매번 같은 작업을 반복하는 과정에서 실수가 발생할 가능성도 높아진다. 특히 사이드 프로젝트나 개인 프로젝트라고 하더라도 GitHub를 통해 소스 코드를 관리하고 있다면 코드 변경 사항을 자동으로 배포할 수 있는 환경을 구축해두는 것이 훨씬 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Docker와 GitHub Actions를 활용하여 간단한 자동 배포 환경을 구축해본다. 개발자가 GitHub 저장소에 코드를 Push하면 GitHub Actions가 자동으로 실행되고, EC2 서버에서 최신 코드를 반영한 뒤 Docker 컨테이너를 재시작하도록 구성할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;1. 배포 구조 설계&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 글에서 구축할 배포 환경은 Docker와 Github Actions를 기반으로 동작한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개발자가 Github 저장소에 코드를 Push 하면 Github Actions가 실행되고, EC2 서버에 접속하여 최신 코드를 반영한 뒤 Docker 이미지를 다시 빌드하고 컨테이너를 재시작한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, 배포 흐름은 다음과 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781485442464&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GitHub Push
    &amp;darr;
GitHub Actions 실행
    &amp;darr;
EC2 접속
    &amp;darr;
최신 코드 반영
    &amp;darr;
Docker Image Build
    &amp;darr;
Container 재시작
    &amp;darr;
배포 완료&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;이러한 배포 자동화 과정은 CI/CD 파이프라인의 일부이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 복잡한 테스트 자동화보다는 Github Actions를 이용한 CD 환경 구축을 해보는 것이 목표이다.&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. Docker를 이용한 배포 환경 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 자동화를 구성하기 전에 먼저 애플리케이션을 Docker 환경에서 실행할 수 있도록 설정해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker&lt;/b&gt;는 애플리케이션과 실행에 필요한 라이브러리, 설정 파일 등을 하나의 이미지로 패키징하여 어느 환경에서나 동일하게 실행할 수 있도록 도와주는 컨테이너 플랫폼이다. Docker를 사용하면 애플리케이션 실행 환경까지 함께 이미지로 관리할 수 있기 때문에 개발 환경과 운영 환경의 차이를 최소화할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Docker에서 더 궁금한 부분이 있으면, 아래 이전에 작성한 글을 참고하면 좋을 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1781486056254&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Docker] 01. 도커(Docker)란 ?&quot; data-og-description=&quot;도커 (Docker)의 등장 배경서버의 운영 구조도커의 구조를 살펴보기 전, 서버를 운영하는 4가지 구조에 대해서 살펴보자.Bare Metal : 하드웨어 위에 바로 운영체제를 설치하고, 그 위에로 바로 애플&quot; data-og-host=&quot;baby-developer77.tistory.com&quot; data-og-source-url=&quot;https://baby-developer77.tistory.com/14&quot; data-og-url=&quot;https://baby-developer77.tistory.com/14&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/0CKhg/dJMb9aKNHw6/8hKQx265LG8hwtf3i2HwZ0/img.png?width=800&amp;amp;height=312&amp;amp;face=0_0_800_312,https://scrap.kakaocdn.net/dn/cpO3CQ/dJMb9c9Gudw/Db9AwLSrf1ew4LQWfpCh01/img.png?width=800&amp;amp;height=312&amp;amp;face=0_0_800_312,https://scrap.kakaocdn.net/dn/XvZmT/dJMb8Z3z9tz/KlYfx81u1YZfpqOJcj1Iv0/img.png?width=2694&amp;amp;height=1844&amp;amp;face=0_0_2694_1844&quot;&gt;&lt;a href=&quot;https://baby-developer77.tistory.com/14&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://baby-developer77.tistory.com/14&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/0CKhg/dJMb9aKNHw6/8hKQx265LG8hwtf3i2HwZ0/img.png?width=800&amp;amp;height=312&amp;amp;face=0_0_800_312,https://scrap.kakaocdn.net/dn/cpO3CQ/dJMb9c9Gudw/Db9AwLSrf1ew4LQWfpCh01/img.png?width=800&amp;amp;height=312&amp;amp;face=0_0_800_312,https://scrap.kakaocdn.net/dn/XvZmT/dJMb8Z3z9tz/KlYfx81u1YZfpqOJcj1Iv0/img.png?width=2694&amp;amp;height=1844&amp;amp;face=0_0_2694_1844');&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;[Docker] 01. 도커(Docker)란 ?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;도커 (Docker)의 등장 배경서버의 운영 구조도커의 구조를 살펴보기 전, 서버를 운영하는 4가지 구조에 대해서 살펴보자.Bare Metal : 하드웨어 위에 바로 운영체제를 설치하고, 그 위에로 바로 애플&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;baby-developer77.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Dockerfile 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 이미지는 Dockerfile을 기반으로 생성되며, 애플리케이션 실행에 필요한 운영 환경, 의존성 패키지, 빌드 과정, 실행 명령 등을 정의할 수 있다.&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;프로젝트 루트 디렉터리에 Dockerfile을 생성한 뒤 사용하는 기술 스택에 맞게 내용을 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile은 프로젝트마다 조금씩 다르지만 일반적으로 다음과 같은 과정을 포함한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;베이스 이미지 선택&lt;/li&gt;
&lt;li&gt;작업 디렉터리 설정&lt;/li&gt;
&lt;li&gt;의존성 설치&lt;/li&gt;
&lt;li&gt;애플리케이션 빌드&lt;/li&gt;
&lt;li&gt;실행 명령 정의&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(Node.js 예시)&lt;/h4&gt;
&lt;pre id=&quot;code_1781486717009&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM node:22-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL

RUN npx prisma generate

EXPOSE 3000

CMD [&quot;npm&quot;, &quot;run&quot;, &quot;start&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 애플리케이션은 `package.json` 에 정의된 의존성을 설치한 뒤 애플리케이션을 빌드하고 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, ``prisma``를 사용하는 만큼 빌드 전, `npx prisma generate`를 해줘야 한다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;⚠️ Prisma를 사용하는 경우 `prisma generate` 과정에서 DATABASE_URL 환경 변수가 필요할 수 있다.&amp;nbsp;&lt;br /&gt;따라서 Docker Build 시 ARG와 ENV를 이용해 DATABASE_URL을 전달하도록 설정하였다.&amp;nbsp;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(Spring Boot 예시)&lt;/h4&gt;
&lt;pre id=&quot;code_1781486876809&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ===== 빌드 환경 =====
FROM gradle:8.7-jdk17 AS builder

WORKDIR /app

COPY build.gradle settings.gradle ./
COPY gradle gradle

RUN gradle dependencies --no-daemon || true

COPY src src

RUN gradle clean build -x test --no-daemon


# ===== 실행 환경 =====
FROM eclipse-temurin:17-jdk

WORKDIR /app

COPY --from=builder /app/build/libs/*.jar app.jar

EXPOSE 8080

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 명령어의 역할을 간단히 설명하자면 다음과 같다.&lt;/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;FROM&lt;/b&gt;&amp;nbsp;: Docker 이미지 생성 시 사용할 기본 이미지를 지정한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WORKDIR&lt;/b&gt;&amp;nbsp;: 컨테이너 내부에서 작업할 디렉터리를 설정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;COPY&lt;/b&gt;&amp;nbsp;: 애플리케이션 실행에 필요한 파일을 컨테이너 내부로 복사한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RUN&lt;/b&gt;&amp;nbsp;: 이미지 빌드 과정에서 필요한 명령어를 실행한다. ( 의존성 설치, 애플리케이션 빌드 등 )&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EXPOSE&lt;/b&gt;&amp;nbsp;: 컨테이너가 사용할 포트를 명시한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CMD&lt;/b&gt;&amp;nbsp;: 컨테이너 시작 시 기본으로 실행할 명령어를 지정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ENTRYPOINT&lt;/b&gt;&amp;nbsp;: 컨테이너가 시작될 때 반드시 실행할 명령어를 지정한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 예제에서는 `npm install`, `npm run build`와 같은 작업을 수행하기 위해 `RUN` 명령어를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Spring Boot 예제는 이미 빌드된 JAR 파일을 실행하는 방식이므로 별도의 빌드 과정 없이 `ENTRYPOINT`를 통해 애플리케이션을 실행한다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 .dockerignore 파일 생성&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 이미지를 빌드할 때는 현재 디렉터리의 파일들이 Docker Engine으로 전달된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 애플리케이션 실행에 필요하지 않은 파일까지 함께 포함되면 이미지 크기가 불필요하게 커지고 빌드 시간도 증가할 수 있다. 따라서 Docker 이미지에 포함하지 않은 파일은 ``.dockerignore``파일을 통해 제외하는 것이 좋다.&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;프로젝트 루트 디렉터리에 ``.dockerignore`` 파일을 생성한 뒤 다음과 같이 작성한다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;( Node.js 예시 )&lt;/h4&gt;
&lt;pre id=&quot;code_1781511974809&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;node_modules
dist
.git
.env&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;( Spring Boot 예시 )&lt;/h4&gt;
&lt;pre id=&quot;code_1781538094768&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Gradle
.gradle
build/

# IntelliJ / IDE
.idea/
*.iml

# OS
.DS_Store

# Git
.git
.gitignore

# Environment
.env

# Logs
logs/
*.log

# Test
test/&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;2.3 docker-compose.yml 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile 이 하나의 애플리케이션 이미지를 정의하는 역할이라면, `docker-compose.yml`은 실제로 컨테이너를 어떻게 실행할지 정의하는 역할을 한다. 현재 EC2 서버에서 컨테이너를 실행하는 용도로 Docker를 사용할 것이고, Github Actions를 통해 배포할 때도 동일한 설정을 이용해 컨테이너를 재생성하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;( Node.js 예시 )&lt;/h4&gt;
&lt;pre id=&quot;code_1781512836513&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  app:
    build:
      context: .
      args:
        DATABASE_URL: ${DATABASE_URL}

    env_file:
      - .env

    ports:
      - &quot;3000:3000&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;⚠️ Dockerfile에서 ARG DATABASE_URL을 사용하고 있으므로, Compose에서도 빌드 시점에 DATABASE_URL을 전달하도록 설정한다.&amp;nbsp;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;( Spring Boot 예시 )&lt;/h4&gt;
&lt;pre id=&quot;code_1781512856380&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.8&quot;

services:
  app:
    build: .
    container_name: spring-app

    ports:
      - &quot;8080:8080&quot;

    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_USER: ${DB_USERNAME}
      DB_PW: ${DB_PASSWORD}
      JWT_SECRET_KEY: ${JWT_SECRET}
      JWT_ACCESS_EXPIRATION: ${JWT_ACCESS_EXPIRATION}

    restart: always

    env_file:
      - .env&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영 환경에서는 데이터베이스, Redis, Nginx 등을 함께 Compose로 관리하기도 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번 글에서는 이미 RDS를 별도로 구축해두었으므로 애플리케이션 컨테이너만 실행하는 단순한 형태로 구성한다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  참고로, Dockerfile, .dockerignore, docker-compose.yml 파일은 각각 어떤 프레임워크를 사용했는지, 어떤 구조, 어떤 언어를 사용했는지에 따라 다르기 때문에 각자의 환경에 맞추어 설정하는 것을 권장한다. (단순한 복붙 금지 !!! 100% 오류 납니다 ! )&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 컨테이너 실행 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile과 Docker Compose 설정이 완료되었다면 실제로 컨테이너가 정상적으로 실행되는지 확인해보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 아래 명령어를 실행해 Docker와 Docker Compose가 설치되어 있는지 확인하자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781513304708&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker --version &amp;amp;&amp;amp; docker compose version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 정보가 출력된다면, 정상적으로 설치되어 있는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 설치되어 있지 않다면, 설치하고 오자 ........... (총총)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 버전 정보가 출력된다고 하더라도 Docker Engine이 실행중이라는 뜻은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 실행하여 Docker Engine이 정상적으로 실행되고 있는지 확인한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781513762952&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker info&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;정상적으로 실행 중이라면 Docker Client와 Server 정보가 출력된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop을 사용하는 경우, 실행되어 있지 않다면 Docker Desktop을 먼저 실행한 뒤 다시 시도하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker가 정상적으로 동작하는 것을 확인했다면 이제 애플리케이션 이미지를 빌드해보자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781513829904&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker compose build&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드가 완료되면 다음 명령어로 컨테이너를 실행한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781513884739&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker compose up -d&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;pre id=&quot;code_1781514007507&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker ps&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;pre id=&quot;code_1781513964340&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker compose logs -f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 많은 도커 명령어가 알고싶다면, 이전에 작성해둔 블로그를 참고하면 좋을 것 같다.&lt;/p&gt;
&lt;figure id=&quot;og_1781514100300&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Docker] 02. 도커 (Docker) 명령어 및 실습 해보기&quot; data-og-description=&quot;도커 (Docker)의 실행 흐름docker-cli (도커 클라이언트) : 사용자가 터미널에서 도커 명령어를 입력한다.dockerd (도커 데몬) : 도커 API 요청을 처리하는 백그라운드 프로세스containerd : 컨테이너 실행 및&quot; data-og-host=&quot;baby-developer77.tistory.com&quot; data-og-source-url=&quot;https://baby-developer77.tistory.com/15&quot; data-og-url=&quot;https://baby-developer77.tistory.com/15&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bf6p3t/dJMb83SrLf5/xApbmSmMUoO0rFdo6cE8b0/img.png?width=800&amp;amp;height=129&amp;amp;face=0_0_800_129,https://scrap.kakaocdn.net/dn/whgOC/dJMb9iIPBHs/flNG2j1xePCMr3T5sllzx1/img.png?width=800&amp;amp;height=129&amp;amp;face=0_0_800_129,https://scrap.kakaocdn.net/dn/eBh6Xk/dJMb89ymoBq/Aho6sEbcEET5ZjsuIc1yRK/img.png?width=1280&amp;amp;height=1356&amp;amp;face=0_0_1280_1356&quot;&gt;&lt;a href=&quot;https://baby-developer77.tistory.com/15&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://baby-developer77.tistory.com/15&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bf6p3t/dJMb83SrLf5/xApbmSmMUoO0rFdo6cE8b0/img.png?width=800&amp;amp;height=129&amp;amp;face=0_0_800_129,https://scrap.kakaocdn.net/dn/whgOC/dJMb9iIPBHs/flNG2j1xePCMr3T5sllzx1/img.png?width=800&amp;amp;height=129&amp;amp;face=0_0_800_129,https://scrap.kakaocdn.net/dn/eBh6Xk/dJMb89ymoBq/Aho6sEbcEET5ZjsuIc1yRK/img.png?width=1280&amp;amp;height=1356&amp;amp;face=0_0_1280_1356');&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;[Docker] 02. 도커 (Docker) 명령어 및 실습 해보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;도커 (Docker)의 실행 흐름docker-cli (도커 클라이언트) : 사용자가 터미널에서 도커 명령어를 입력한다.dockerd (도커 데몬) : 도커 API 요청을 처리하는 백그라운드 프로세스containerd : 컨테이너 실행 및&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;baby-developer77.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 실행되었다면 브라우저 또는 API 테스트 도구를 이용해 애플리케이션에 접속해보자.&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;434&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm6ES8/dJMcah5TX4c/zCQ0Knwfz64g0DmSo7OGIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm6ES8/dJMcah5TX4c/zCQ0Knwfz64g0DmSo7OGIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm6ES8/dJMcah5TX4c/zCQ0Knwfz64g0DmSo7OGIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm6ES8%2FdJMcah5TX4c%2FzCQ0Knwfz64g0DmSo7OGIk%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;434&quot; height=&quot;93&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Node.js 예제에서 `3000:3000`으로 포트를 매핑했다면,&lt;br /&gt;브라우저에서 `http://localhost:3000` 으로 접속하거나 Postman을 통해 API를 호출해 정상 응답이 반환되는지 확인해보자.&lt;br /&gt;&lt;br /&gt;정상적으로&amp;nbsp;응답이&amp;nbsp;반환된다면&amp;nbsp;Docker&amp;nbsp;환경&amp;nbsp;구성은&amp;nbsp;완료된&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;3. CI/CD 파이프라인 구성 (Github Actions)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 환경 구성이 완료되었다면 이제 Github Actions를 이용해 배포 자동화를 구성해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터는 Github 저장소에 코드를 push 하면 EC2 서버에 자동으로 최신 코드를 반영하도록 설정한다.&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;3.1 EC2 Docker 환경 사전 준비&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD를 구성하기 전에 반드시&lt;b&gt; EC2에서&lt;/b&gt; 아래 3가지는 먼저 확인해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 EC2 인스턴스에 접속하자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;01 Docker 설치 여부&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 통해 Docker 설치 여부를 확인한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781525881834&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker --version&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;정상적으로 출력되지 않는다면, 아래 명령어를 이용해 Docker를 설치하자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781525947833&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install -y docker.io&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;02 Docker 권한 설정&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에서 Docker 명령어는 기본적으로 ``sudo`` 권한이 필요하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 반드시 아래 명령어를 실행해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781526014682&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo usermod -aG docker ubuntu
newgrp docker&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;03 Docker Compose 설치 (v2 기준)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 Ubuntu 에서는 `docker-compose-plugin` 방식으로 설치해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 통해 Docker Compose 설치 여부를 확인해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1781526116667&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker compose version&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;마찬가지로, 정상 출력이 안된다면, 아래 명령어로 Docker Compose를 설치한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781526075017&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt install -y docker-compose-plugin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 EC2 프로젝트 초기 세팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD가 동작하려면 EC2에 최소 1회는 프로젝트가 존재해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어로 EC2 인스턴스 내부에 배포할 프로젝트를 클론 받도록 하자.&lt;/p&gt;
&lt;pre id=&quot;code_1781526222667&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mkdir -p /home/ubuntu/app
cd /home/ubuntu/app
git clone https://github.com/wldmsdl7/nodejs-deploy-test .&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;⚠️ 이때, Github 저장소 뒤에 &quot;.&quot; 을 빼먹으면 안된다 !!!!&amp;nbsp;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Frame 5.png&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eyS8TM/dJMcaicGep9/ANRtCieiyyXlxfLDjlkcYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eyS8TM/dJMcaicGep9/ANRtCieiyyXlxfLDjlkcYK/img.png&quot; data-alt=&quot;실제 클론 받았을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eyS8TM/dJMcaicGep9/ANRtCieiyyXlxfLDjlkcYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeyS8TM%2FdJMcaicGep9%2FANRtCieiyyXlxfLDjlkcYK%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;1610&quot; height=&quot;412&quot; data-filename=&quot;Frame 5.png&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 클론 받았을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에 프로젝트를 클론했다고 해서 모든 설정이 자동으로 완료되는 것은 아니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 데이터베이스 연결 정보, JWT Secret, API Key와 같은 민감한 정보는 보안상 Github 저장소에 포함하지 않기 때문에 `git clone`으로는 전달되지 않는다. 따라서 EC2 서버에서는 별도로 `.env` 파일을 직접 생성해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래&amp;nbsp;명령어로&amp;nbsp;`.env`&amp;nbsp;파일을&amp;nbsp;생성한&amp;nbsp;뒤,&amp;nbsp;애플리케이션&amp;nbsp;실행에&amp;nbsp;필요한&amp;nbsp;환경&amp;nbsp;변수를&amp;nbsp;직접&amp;nbsp;입력해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1781528053680&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd /home/ubuntu/app
nano .env&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;`.env` 파일은 Github 저장소에 업로드하지 않는 것이 일반적이며, 운영 서버 내부에서 별도로 관리한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때, ``DATABASE_URL``은 배포된 RDS의 주소를 작성해줘야 한다는 점 주의해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;⚠️ 운영 서버에서는 SSH 터널링을 사용하지 않는다.&lt;br /&gt;애플리케이션과 RDS가 같은 VPC 내부에 존재하므로, DATABASE_URL에는 RDS Endpoint를 직접 사용하면 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 SSH Key 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github Actions가 EC2 서버에 접속하려면 SSH 인증이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Github Actions 전용 SSH Key를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 서버가 아닌 로컬 PC에서 아래 명령어를 실행하자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781515675951&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh-keygen -t rsa -b 4096 -C &quot;github-actions&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에 다음과 같이 출력되면, 파일명을 입력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1781516290217&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Generating public/private rsa key pair.

Enter file in which to save the key
(/Users/user/.ssh/id_rsa):&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 비밀번호를 입력하는 란이 등장할텐데, 우리는 비밀번호를 별도로 설정하지 않을 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Enter`` 키를 2번 눌러 비밀번호를 설정하지 않고 SSH Key를 생성한다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❓ &lt;b&gt;SSH 키 중요한 거 아니야 ?? 왜 비밀번호를 설정하지 않지 ??&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;간단히 말하면,&amp;nbsp;&lt;b&gt;자동화 &lt;/b&gt;때문이다. 만약 SSH Key 생성 시 비밀번호를 설정하면, SSH 접속할 때 마다 비밀번호를 요구하게 된다.&amp;nbsp;&lt;br /&gt;사람이 직접 접속하는 경우에는 비밀번호를 입력할 수 있지만, Github Actions를 통해 자동으로 SSH 접속해 배포할 때에는 비밀번호를 입력할 사람이 없다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;따라서, CI/CD 용 배포 키는 일반적으로 &lt;b&gt;별도의 SSH Key를&amp;nbsp; 생성, Github Secret에 저장, 배포 전용 계정으로 배포&amp;nbsp;&lt;/b&gt;방식을 사용한다. 대신 보안을 위해 개인 PC에서 사용하던 SSH Key를 Github Secret에 올리는 것이 아니라,&amp;nbsp;&lt;b&gt;배포 전용 키를 새로 생성&lt;/b&gt;하는 것이 중요하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성이 완료되면, `github-actions` 와 `github-actions.pub` 두 파일이 생성된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 통해 생성된 공개 키 내용을 확인한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781516749132&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cat github-actions.pub&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력된 내용을 복사한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Frame 1.png&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brLXBV/dJMcahLycGM/S80n2kr4Zf7aSekCyRIjS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brLXBV/dJMcahLycGM/S80n2kr4Zf7aSekCyRIjS1/img.png&quot; data-alt=&quot;실제 SSH Key 생성 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brLXBV/dJMcahLycGM/S80n2kr4Zf7aSekCyRIjS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrLXBV%2FdJMcahLycGM%2FS80n2kr4Zf7aSekCyRIjS1%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;1544&quot; height=&quot;818&quot; data-filename=&quot;Frame 1.png&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 SSH Key 생성 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 배포한 EC2 서버에 접속한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 터미널을 이용하지 않고, AWS Console을 이용해 EC2 인스턴스에 직접 접속할 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접속했다면, 아래 명령어를 이용해 `~/.ssh/authorized_keys`에 복사한 SSH Key 를 맨 아래에 추가한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781517189701&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nano ~/.ssh/authorized_keys&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 사진처럼 기존에 존재하는 키 아래에 추가하고, 저장 후 종료한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1894&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCjzXQ/dJMcadCg0u3/aTkAc87wEGAjMND18nOqz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCjzXQ/dJMcadCg0u3/aTkAc87wEGAjMND18nOqz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCjzXQ/dJMcadCg0u3/aTkAc87wEGAjMND18nOqz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCjzXQ%2FdJMcadCg0u3%2FaTkAc87wEGAjMND18nOqz0%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;1894&quot; height=&quot;131&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1894&quot; data-origin-height=&quot;131&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;이제 아래 명령어를 이용해 로컬에서 해당 SSH 키로 EC2 인스턴스에 접속해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1781517403353&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -i github-actions ubuntu@EC2_PUBLIC_IP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Frame 3.png&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wZ55x/dJMcabLplj3/t0kBKjCQEjhRliRKfcdvV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wZ55x/dJMcabLplj3/t0kBKjCQEjhRliRKfcdvV0/img.png&quot; data-alt=&quot;성공 !&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wZ55x/dJMcabLplj3/t0kBKjCQEjhRliRKfcdvV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwZ55x%2FdJMcabLplj3%2Ft0kBKjCQEjhRliRKfcdvV0%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;643&quot; height=&quot;513&quot; data-filename=&quot;Frame 3.png&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성공 !&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 Github Secrets 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH Key 생성과 EC2 등록이 완료되었다면 이제 Github Actions 에서 해당 키를 사용할 수 있도록 Github Secrets에 등록해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Github Secrets&lt;/b&gt; 는 비밀번호, API Key, SSH Private Key와 같이 외부에 노출되면 안 되는 민감한 정보를 안전하게 저장할 수 있는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 조금 전에 생성한 SSH Key 중 Github Actions가 실제로 사용할 것은&lt;b&gt; 공개 키(.pub)가 아니라 개인 키&lt;/b&gt; 이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 로컬 PC에서 아래 명령어를 실행하여 개인 키 내용을 확인한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781518738806&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cat github-actions&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력된 내용을 전체 복사한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Github 저장소로 이동한 뒤 ``Repository &amp;gt; Settings &amp;gt; Secrets and variables &amp;gt; Actions &amp;gt; New repository secret`` 으로 이동해 아래와 같이 Secret 을 생성한다.&amp;nbsp;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 53.1395%; height: 36px;&quot; border=&quot;1&quot; width=&quot;100%&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 21.5116%;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #9b9b9b; color: #ffffff; text-align: start;&quot;&gt;Name&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 31.5116%;&quot;&gt;&lt;b&gt;Value&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 21.5116%;&quot;&gt;&lt;span&gt;EC2_SSH_KEY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 31.5116%;&quot;&gt;&lt;span&gt;github-actions 개인 키 전체 내용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Frame 4.png&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;917&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb1A4o/dJMcaftqbLQ/rUb6aRoO7DRtmgls5hLdUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb1A4o/dJMcaftqbLQ/rUb6aRoO7DRtmgls5hLdUk/img.png&quot; data-alt=&quot;Github Secrets 에 SSH KEY 넣는 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb1A4o/dJMcaftqbLQ/rUb6aRoO7DRtmgls5hLdUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb1A4o%2FdJMcaftqbLQ%2FrUb6aRoO7DRtmgls5hLdUk%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;852&quot; height=&quot;917&quot; data-filename=&quot;Frame 4.png&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;917&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Github Secrets 에 SSH KEY 넣는 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 EC2 접속에 필요한 서버 정보도 함께 등록한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 53.3721%;&quot; border=&quot;1&quot; width=&quot;100%&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.8605%;&quot;&gt;&lt;b&gt;Name&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.3954%;&quot;&gt;&lt;b&gt;Value&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.8605%;&quot;&gt;&lt;span&gt;EC2_HOST&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.3954%;&quot;&gt;&lt;span&gt;EC2 Public IP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.8605%;&quot;&gt;&lt;span&gt;EC2_USER&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.3954%;&quot;&gt;&lt;span&gt;ubuntu&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGDYl8/dJMb9905FJc/ePpovepE6ZmVj6JqtAePmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGDYl8/dJMb9905FJc/ePpovepE6ZmVj6JqtAePmK/img.png&quot; data-alt=&quot;3개의 변수 모두 등록한 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGDYl8/dJMb9905FJc/ePpovepE6ZmVj6JqtAePmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGDYl8%2FdJMb9905FJc%2FePpovepE6ZmVj6JqtAePmK%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;579&quot; height=&quot;394&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;3개의 변수 모두 등록한 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 등록한 값들은 Github Actions 실행 시 환경 변수처럼 사용할 수 있으며, Workflow 파일에서 `${{ secrets.SECRET_NAME }}` 형태로 참조할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;⚠️ Github Actions에 등록하는 것은 반드시 개인 SSH 키이다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`github-actions.pub` 파일의 내용을 등록하는 것이 아니라 `github-actions` 파일의 내용을 등록해야 정상적으로 SSH 접속이 가능하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Github Actions 가 사용할 SSH 인증 정보와 서버 접속 정보가 모두 준비되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 실제로 Github 저장소에 코드가 Push 되었을 때 자동으로 EC2 서버에 배포가 수행되도록 Workflow 를 작성해보자.&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;3.5 Github Actions Workflow 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제로 GitHub 저장소에 코드가 push 되었을 때 EC2 서버로 자동 배포가 수행되도록 Workflow를 작성해보자.&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;이 단계부터는 단순한 설정이 아니라, 앞에서 구성한 Docker 환경과 EC2 SSH 연결이 실제로 하나의 배포 파이프라인으로 연결되는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 Workflow를 작성해보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github Actions Workflow는 저장소의 `.github/workflows` 디렉터리에 YAML 파일 형태로 작성한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBk09O/dJMcafmHYSS/kRREAdebxLsLqL8VNhapV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBk09O/dJMcafmHYSS/kRREAdebxLsLqL8VNhapV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBk09O/dJMcafmHYSS/kRREAdebxLsLqL8VNhapV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBk09O%2FdJMcafmHYSS%2FkRREAdebxLsLqL8VNhapV0%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;420&quot; height=&quot;90&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 다음과 같이 `deploy.yml` 이라는 파일을 생성해주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아래 코드를 넣어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1781528372630&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Connect to EC2 via SSH
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            echo &quot;Start Deployment&quot;

            echo &quot;Move to project directory&quot;
            cd /home/ubuntu/app

            echo &quot;Pull latest code from GitHub&quot;
            git pull origin main

            echo &quot;Stop existing containers&quot;
            docker compose down

            echo &quot;Build Docker image&quot;
            docker compose build

            echo &quot;Start containers&quot;
            docker compose up -d

            echo &quot;Deployment Success&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions에서는 하나의 SSH Step으로 EC2에 접속한 뒤,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 내부에서 Docker 기반 배포 파이프라인을 순차적으로 실행하도록 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구성하면 Github push 만으로 EC2 서버에 자동 배포되는 환경이 완성된다.&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;04. 배포 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아래 사진처럼 Github에 push 해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Frame 6.png&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csIyCo/dJMcaccoWS6/IwSVWHgt8gyYELBuMBuua1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csIyCo/dJMcaccoWS6/IwSVWHgt8gyYELBuMBuua1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csIyCo/dJMcaccoWS6/IwSVWHgt8gyYELBuMBuua1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsIyCo%2FdJMcaccoWS6%2FIwSVWHgt8gyYELBuMBuua1%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;642&quot; height=&quot;225&quot; data-filename=&quot;Frame 6.png&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이렇게 아래처럼 배포를 성공한 것을 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWX269/dJMcai4G1IF/Gpcv8tfmb6jqNdmCKtIngk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWX269/dJMcai4G1IF/Gpcv8tfmb6jqNdmCKtIngk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWX269/dJMcai4G1IF/Gpcv8tfmb6jqNdmCKtIngk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWX269%2FdJMcai4G1IF%2FGpcv8tfmb6jqNdmCKtIngk%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;840&quot; height=&quot;314&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;314&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Frame 7.png&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WMN7R/dJMcadh6ZTB/CzWoM1sA4mur0KSJo96uuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WMN7R/dJMcadh6ZTB/CzWoM1sA4mur0KSJo96uuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WMN7R/dJMcadh6ZTB/CzWoM1sA4mur0KSJo96uuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWMN7R%2FdJMcadh6ZTB%2FCzWoM1sA4mur0KSJo96uuK%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;1786&quot; height=&quot;544&quot; data-filename=&quot;Frame 7.png&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공 !&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>AWS</category>
      <category>ci/cd</category>
      <category>DevOps</category>
      <category>docker</category>
      <category>Github Actions</category>
      <category>Node.js</category>
      <category>배포</category>
      <author>코딩 기록하는 애기 개발자</author>
      <guid isPermaLink="true">https://baby-developer77.tistory.com/34</guid>
      <comments>https://baby-developer77.tistory.com/34#entry34comment</comments>
      <pubDate>Tue, 16 Jun 2026 00:51:59 +0900</pubDate>
    </item>
    <item>
      <title>[Infra/DevOps] 사이드 프로젝트를 위한 AWS 인프라 구축 가이드 (2026 ver.) #2- RDS, SSH 터널링</title>
      <link>https://baby-developer77.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서는 AWS 인프라의 기본이 되는 네트워크 환경을 구성했다.&lt;br /&gt;VPC를 생성하고, Public / Private Subnet을 분리했으며, 인터넷 게이트웨이(IGW)와 라우팅 테이블을 통해 네트워크 흐름을 설계했다. 또한 보안 그룹을 이용해 외부 접근과 내부 통신을 제어하고, 실제 애플리케이션이 실행될 EC2 인스턴스까지 생성해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기본적인 서버 실행 환경은 갖춰진 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 서비스 구조를 생각해보면, 애플리케이션 서버만으로는 부족하다.&lt;br /&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;이번 글에서는 그 다음 단계로, AWS의 관리형 데이터베이스 서비스인 &lt;span&gt;&lt;b&gt;RDS (Relational Database Service)&lt;/b&gt;&lt;/span&gt; 를 활용해 MySQL 기반 데이터베이스를 구성해본다.&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;그리고 단순히 DB를 만드는 것에서 끝나는 것이 아니라,&lt;br /&gt;보안적으로 안전한 구조를 유지하면서도 로컬 환경이나 외부 툴(DataGrip, Workbench 등)에서 접근할 수 있도록 &lt;span&gt;&lt;b&gt;SSH 터널링을 이용한 안전한 접속 방식&lt;/b&gt;&lt;/span&gt;까지 함께 다뤄볼 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. RDS 구축&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon Web Services의 Amazon RDS(Amazon Relational Database Service, RDS)는 AWS에서 제공하는 &lt;span&gt;&lt;b&gt;관리형 관계형 데이터베이스 서비스&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 &lt;span&gt;MySQL&lt;/span&gt;, &lt;span&gt;PostgreSQL&lt;/span&gt;, &lt;span&gt;MariaDB&lt;/span&gt;, &lt;span&gt;Oracle Database&lt;/span&gt;, &lt;span&gt;Microsoft SQL Server&lt;/span&gt; 등 다양한 데이터베이스 엔진을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 데이터베이스의 &lt;span&gt;&lt;b&gt;설치, 패치 관리, 백업, 복제, 장애 복구, 스케일링&lt;/b&gt;&lt;/span&gt;과 같은 운영 작업을 AWS가 대신 처리해주기 때문에, 사용자는 인프라 관리 부담을 줄이고 애플리케이션 개발에 집중할 수 있다는 장점이 있다.&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;이처럼 Amazon RDS는 단순히 데이터베이스를 &amp;ldquo;하나 띄우는 서비스&amp;rdquo;가 아니라, 운영에 필요한 많은 부분을 AWS가 대신 관리해주는 형태의 서비스이다. 즉, 우리가 직접 EC2에 MySQL을 설치하고 설정하던 방식과 달리, &lt;span&gt;&lt;b&gt;DB 인스턴스를 생성하는 것만으로도 운영 가능한 데이터베이스 환경을 빠르게 구성할 수 있다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 RDS를 생성해보면서, 어떤 설정들이 필요한지 하나씩 살펴보자.&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;1.1 서브넷 그룹 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브넷 그룹을 생성하기 전에&amp;nbsp;&lt;b&gt;DB 서브넷 그룹&lt;/b&gt;이 무엇인지 이해할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon RDS는 기본적으로 특정 VPC 내부에서 동작하는데, 이때 데이터베이스 인스턴스가 어떤 서브넷에 위치할지를 지정해주는 설정이 바로&amp;nbsp;&lt;b&gt;DB 서브넷 그룹&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 말해,&amp;nbsp;&lt;b&gt;RDS가 배치될 서브넷들의 묶음&lt;/b&gt;이라고 이해하면 좋다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  자세히 알고 싶다면,&lt;br /&gt;RDS는 단일 서브넷이 아니라&amp;nbsp;&lt;b&gt;여러 가용 영역 (AZ)에 걸친 서브넷들을 묶어 구성&lt;/b&gt;하게 되는데, 이는 장애 상황을 대비한 고가용성 구조를 만들기 위함이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자, 이제 서브넷 그룹을 생성해보자.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;27.png&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDC95k/dJMcacpXpa4/p7AOU7v0yREP2KKgOXdIk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDC95k/dJMcacpXpa4/p7AOU7v0yREP2KKgOXdIk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDC95k/dJMcacpXpa4/p7AOU7v0yREP2KKgOXdIk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDC95k%2FdJMcacpXpa4%2Fp7AOU7v0yREP2KKgOXdIk0%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;1225&quot; height=&quot;428&quot; data-filename=&quot;27.png&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진과 같은 경로로 이동해 오른쪽 상단에 있는&amp;nbsp;&lt;b&gt;[DB 서브넷 그룹 생성]&amp;nbsp;&lt;/b&gt;버튼을 클릭한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;28.png&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2Fhsi/dJMcabLobbE/jDK5x9Tam3RKpNURK0f930/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2Fhsi/dJMcabLobbE/jDK5x9Tam3RKpNURK0f930/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2Fhsi/dJMcabLobbE/jDK5x9Tam3RKpNURK0f930/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2Fhsi%2FdJMcabLobbE%2FjDK5x9Tam3RKpNURK0f930%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;1225&quot; height=&quot;856&quot; data-filename=&quot;28.png&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 이름과 설명을 입력하고, 앞서 생성한 VPC를 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이전에 생성한 &lt;span&gt;Private Subnet이 위치한 가용 영역(AZ)&lt;/span&gt;을 기준으로, 해당 Subnet들을 선택해 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 설정을 완료한 뒤, 우측 하단의 &lt;span&gt;&lt;b&gt;[생성]&lt;/b&gt;&lt;/span&gt; 버튼을 클릭하면 DB 서브넷 그룹이 생성된다.&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;1.2 데이터베이스 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 데이터베이스를 생성해보자.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;29.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFvVOd/dJMcadh5Ks7/QdACvMmnkDIKUr3jmkcK41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFvVOd/dJMcadh5Ks7/QdACvMmnkDIKUr3jmkcK41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFvVOd/dJMcadh5Ks7/QdACvMmnkDIKUr3jmkcK41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFvVOd%2FdJMcadh5Ks7%2FQdACvMmnkDIKUr3jmkcK41%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;1236&quot; height=&quot;291&quot; data-filename=&quot;29.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Aurora and RDS] &amp;gt; [데이터베이스]&amp;nbsp;&lt;/b&gt;탭에서 우측 상단에 있는&amp;nbsp;&lt;b&gt;[데이터베이스 생성] &amp;gt; [전체 구성] &lt;/b&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-filename=&quot;30.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;999&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tYcBD/dJMcaiXUrfP/89KS3r8M0KgrXVQbjiaqDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tYcBD/dJMcaiXUrfP/89KS3r8M0KgrXVQbjiaqDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tYcBD/dJMcaiXUrfP/89KS3r8M0KgrXVQbjiaqDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtYcBD%2FdJMcaiXUrfP%2F89KS3r8M0KgrXVQbjiaqDK%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;1236&quot; height=&quot;999&quot; data-filename=&quot;30.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;999&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;선택한 기준은&amp;nbsp;&lt;b&gt;돈&amp;nbsp;&lt;/b&gt;이다 .... 학생은 돈이 없기 때문 .............&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;최대한 프리티어를 사용하려고 했기 때문에 위 사진과 같이 선택했다.&amp;nbsp;&lt;/span&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-filename=&quot;31.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;795&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjV3HA/dJMcafmGOvM/d8ZfYZhvINkYDolW7KWFVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjV3HA/dJMcafmGOvM/d8ZfYZhvINkYDolW7KWFVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjV3HA/dJMcafmGOvM/d8ZfYZhvINkYDolW7KWFVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjV3HA%2FdJMcafmGOvM%2Fd8ZfYZhvINkYDolW7KWFVk%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;1236&quot; height=&quot;795&quot; data-filename=&quot;31.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;795&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 data-ke-size=&quot;size16&quot;&gt;이 값들은 실제 데이터베이스 접속 시 사용되는 정보이므로, 원하는 값으로 자유롭게 설정하면 된다.&lt;br /&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-filename=&quot;32.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RJgXC/dJMcaaTbwzl/4GrwTKiFt401Ay9yyg4oH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RJgXC/dJMcaaTbwzl/4GrwTKiFt401Ay9yyg4oH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RJgXC/dJMcaaTbwzl/4GrwTKiFt401Ay9yyg4oH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRJgXC%2FdJMcaaTbwzl%2F4GrwTKiFt401Ay9yyg4oH1%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;1236&quot; height=&quot;319&quot; data-filename=&quot;32.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;319&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;이후 [스토리지] 설정으로 이동한 뒤, &lt;span&gt;[스토리지 자동 조정 활성화] 옵션은 선택 해제&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;이유는 ............. 돈을 아껴야 하기 때문 ...........&lt;/span&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-filename=&quot;33.png&quot; data-origin-width=&quot;2472&quot; data-origin-height=&quot;1270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kalvf/dJMcaicE1Z5/8SAXdkTkyFfNgrv83g6voK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kalvf/dJMcaicE1Z5/8SAXdkTkyFfNgrv83g6voK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kalvf/dJMcaicE1Z5/8SAXdkTkyFfNgrv83g6voK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKalvf%2FdJMcaicE1Z5%2F8SAXdkTkyFfNgrv83g6voK%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;2472&quot; height=&quot;1270&quot; data-filename=&quot;33.png&quot; data-origin-width=&quot;2472&quot; data-origin-height=&quot;1270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 앞서 사용했던 리소스들(VPC, 서브넷, 보안그룹)을 선택한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 [퍼블릭 엑세스] 옵션은 [아니오]로 설정해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon RDS를 외부 인터넷에서 직접 접근한 상태로 두지 않기 위함이다. RDS는 일반적으로 애플리케이션 서버 내부에서만 접근하도록 구성하며, 데이터베이스 자체는 Private Subnet 내부에서만 동작하는 구조로 설계하는 것이 안전하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 [퍼블릭 엑세스] 옵션을 [예]로 설정하면 인터넷을 통해 직접 DB 엔드포인트로 접근이 가능해지는데,&amp;nbsp; 이는 외부 공격에 노출될 가능성을 증가시키거나, DB 포트를 직접 노출하는 등의 위험을 야기할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;35.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NyWSp/dJMcag6UBVV/OsIkjaZOA2SCpRi0jkcbh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NyWSp/dJMcag6UBVV/OsIkjaZOA2SCpRi0jkcbh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NyWSp/dJMcag6UBVV/OsIkjaZOA2SCpRi0jkcbh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNyWSp%2FdJMcag6UBVV%2FOsIkjaZOA2SCpRi0jkcbh0%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;1236&quot; height=&quot;828&quot; data-filename=&quot;35.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크롤을 가장 아래까지 내려 &lt;span&gt;[추가 구성]&lt;/span&gt;&lt;span&gt; 토글을 연다.&lt;br /&gt;그 안의 &lt;/span&gt;&lt;span&gt;[백업] 섹션에서 [자동 백업 활성화] 옵션을 선택 해제&lt;/span&gt;&lt;span&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정 또한 비용과 직결되기 때문에 실습 환경에서는 비활성화해두는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 데이터베이스의 모든 설정을 완료했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[데이터베이스 생성]&amp;nbsp;&lt;/b&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. SSH 터널링을 이용한 데이터베이스 접속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일만적으로 로컬에서 개발할 때는 Datagrip이나 Workbench 같은 도구를 사용해 데이터베이스에 직접 접속하여 데이터를 확인하면서 개발을 진행한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 앞서 우리는 &lt;b&gt;RDS를 Private Subnet 내부에 배치하고, Public Access 도 비활성화&lt;/b&gt;했기 때문에 외부에서 직접 접근할 수 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Public IP가 할당되지 않은 구조이기 때문에, 기존처럼 DB 엔드포인트에 직접 접속하는 방식은 사용할 수 ㅇ벗다.&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;따라서 이번 단계에서는 보안을 유지하면서 로컬 환경에서 배포된 DB에 접근할 수 있는 방법인&amp;nbsp;&lt;b&gt;SSH 터널링 (SSH Tunneling)&lt;/b&gt;을 사용해 접속을 해볼 것이다. 이 방식은 중간에 EC2 인스턴스를&amp;nbsp;Bastion Host 처럼 활용하여 안전하게 내부 RDS로 연결하는 구조이다.&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;2.1 SSH 터널링의 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 RDS는 Private Subnet에 존재하고, EC2는 Public Subnet에 존재한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 SSH 터널링의 흐름은 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781368553751&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;내 PC
  &amp;darr; (SSH)
EC2 (Bastion Host)
  &amp;darr; (내부 네트워크)
RDS (MySQL)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, SSH 터널링은&amp;nbsp;&lt;b&gt;내 컴퓨터의 특정 포트를 EC2를 통해 RDS로 연결해주는 우회통로&lt;/b&gt;다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2&amp;nbsp; DB 접속&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제로 SSH 터널링을 이용해 로컬 환경에서 Amazon RDS에 접속해보자.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;01 SSH 터널링 실행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 로컬 터미널에서 아래 명령어를 실행한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;AWS 콘솔의 EC2 &lt;b&gt;[인스턴스 연결 (브라우저 접속)] &lt;/b&gt;로는 SSH 터널링을 사용할 수 없으며, 반드시 로컬 터미널에서 실행해야 한다. &amp;nbsp;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1781368884396&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -i &quot;key.pem&quot; \
-L 13306:your-rds-endpoint:3306 \
ec2-user@EC2_PUBLIC_IP&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;이 명령어를 실행하면, 내 로컬 PC의 ``13306``포트가 EC2를 거쳐 RDS의 ``3306`` 포트로 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&amp;nbsp;&lt;b&gt;외부에서는 접근할 수 없는 RDS 를 로컬에서 접속 가능한 것&lt;/b&gt;처럼&lt;b&gt; 우회 연결한 상태&lt;/b&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;1062&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgNbD4/dJMcai4FPeo/KIqlECM1RKwwElH1cU0Th0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgNbD4/dJMcai4FPeo/KIqlECM1RKwwElH1cU0Th0/img.png&quot; data-alt=&quot;위 명령어로 터미널에서 EC2 인스턴스에 접속 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgNbD4/dJMcai4FPeo/KIqlECM1RKwwElH1cU0Th0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgNbD4%2FdJMcai4FPeo%2FKIqlECM1RKwwElH1cU0Th0%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;1062&quot; height=&quot;792&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위 명령어로 터미널에서 EC2 인스턴스에 접속 성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;02 Datagrip 연결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 터널이 정상적으로 열려 있는 상태에서 Datagrip을 실행하고 아래와 같이 설정한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;36.png&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;711&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IdMjh/dJMb99UfWeE/8e07OAKt5zW6FzDbLQt9FK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IdMjh/dJMb99UfWeE/8e07OAKt5zW6FzDbLQt9FK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IdMjh/dJMb99UfWeE/8e07OAKt5zW6FzDbLQt9FK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIdMjh%2FdJMb99UfWeE%2F8e07OAKt5zW6FzDbLQt9FK%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;581&quot; height=&quot;711&quot; data-filename=&quot;36.png&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;711&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Host : ``127.0.0.1``&lt;/li&gt;
&lt;li&gt;Port : ``13306``&lt;/li&gt;
&lt;li&gt;User : RDS 생성 시 설정한 Master Username&lt;/li&gt;
&lt;li&gt;Password : RDS 생성 시 설정한 Master Password&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 중요한 점은 RDS 엔드포인트를 직접 사용하는 것이 아니라&amp;nbsp;&lt;b&gt;localhost로 접속한다&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SSH 터널이 로컬 포트를 RDS로 연결해주기 &lt;/b&gt;때문에 가능한 방식이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 좌측 하단에 &lt;b&gt;[연결 테스트]&amp;nbsp;&lt;/b&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;1372&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfKorD/dJMcabkivmN/z4LQJyrj9qBFyPbjKSZdkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfKorD/dJMcabkivmN/z4LQJyrj9qBFyPbjKSZdkk/img.png&quot; data-alt=&quot;Datagrip에서 Test Connection 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfKorD/dJMcabkivmN/z4LQJyrj9qBFyPbjKSZdkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfKorD%2FdJMcabkivmN%2Fz4LQJyrj9qBFyPbjKSZdkk%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;1372&quot; height=&quot;420&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Datagrip에서 Test Connection 성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 AWS에서 관리형 데이터베이스 서비스인 Amazon RDS를 생성하고,&lt;br /&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;단순히 RDS를 생성하는 것에서 끝나는 것이 아니라, Private Subnet 환경에서 외부 접근을 차단하고, EC2를 Bastion Host로 활용한 SSH 터널링을 통해 로컬 환경에서 안전하게 데이터베이스에 접근하는 방식까지 직접 구현해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이 구조 위에서 실제 서비스 운영 환경을 만들어보기 위해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CI/CD 파이프라인 구축&lt;/li&gt;
&lt;li&gt;GitHub Actions를 활용한 자동 배포&lt;/li&gt;
&lt;li&gt;Docker를 이용한 애플리케이션 컨테이너화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 다뤄볼 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 안농 !  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>AWS</category>
      <category>Bastion host</category>
      <category>MySQL</category>
      <category>RDS</category>
      <category>SSH 터널링</category>
      <category>데이터베이스</category>
      <category>배포</category>
      <category>서브넷 그룹</category>
      <author>코딩 기록하는 애기 개발자</author>
      <guid isPermaLink="true">https://baby-developer77.tistory.com/33</guid>
      <comments>https://baby-developer77.tistory.com/33#entry33comment</comments>
      <pubDate>Sun, 14 Jun 2026 02:13:01 +0900</pubDate>
    </item>
    <item>
      <title>[Infra/DevOps] 사이드 프로젝트를 위한 AWS 인프라 구축 가이드 (2026 ver.) #1- VPC, EC2, Security Group</title>
      <link>https://baby-developer77.tistory.com/32</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 시작하면 가장 먼저 고민하게 되는 부분 중 하나는 배포 환경이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트나 해커톤처럼 규모가 크지 않은 경우에는 복잡한 인프라를 처음부터 구축할 필요는 없다. 하지만 로컬 환경에만 의존하거나 수동 배포 방식으로 운영하게 되면, 협업이나 실제 서비스 단계에서 불편함이 생기기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 작은 규모의 프로젝트를 기준으로 AWS와 GitHub Actions를 활용해 기본적인 배포 환경을 구성하는 과정을 정리한다. VPC 기반의 네트워크 분리부터 EC2와 Docker를 이용한 애플리케이션 배포, RDS를 활용한 데이터베이스 구성, 그리고 CI/CD 파이프라인 구축까지 전체 흐름을 단계적으로 살펴볼 예정이다.&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;또한 AWS 콘솔은 업데이트에 따라 화면 구성이나 설정 위치가 달라지는 경우가 많기 때문에, 현재 구성 기준을 바탕으로 처음부터 끝까지 따라할 수 있도록 정리해보았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AWS ( Amzon Web Services ) 란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 인프라 구축에 앞서, 이번 글에서 사용할 AWS에 대해 간단히 알아보자.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AWS는 아마존에서 제공하는 클라우드 플랫폼&lt;/b&gt;으로, 서버와 데이터베이스, 스토리지, 네트워크 등 다양한 인프라 서비스를 인터넷을 통해 제공한다. 쉽게 말해, 직접 서버를 구매하고 관리하지 않아도 인터넷을 통해 서버, 데이터베이스, 스토리지, 네트워크 등의 인프라를 필요한 만큼 빌려 사용할 수 있는 서비스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 서비스를 운영하기 위해 물리 서버를 구매하고, 서버실을 구축하고, 네트워크를 직접 구성해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 AWS를 사용하면 몇 번의 클릭만으로 서버를 생성하고 데이터베이스를 구축할 수 있으며, 사용할 만큼만 비용을 지불하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;AWS는 개발자가 인프라 구축에 드는 시간과 비용을 줄이고, 서비스 개발과 운영에 집중할 수 있도록 도와주는 플랫폼&lt;/b&gt;이라고 볼 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리전(Region)과 가용영역 (AZ)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 리소스들을 생성하기 전에 AWS의 &lt;b&gt;리전(Region)과 가용 영역(Availability Zone)&lt;/b&gt;에 대해 먼저 이해할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;리전 (Region)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS는 전 세계 여러 지역에 데이터센터를 운영하고 있으며, 이러한 물리적 위치를 &lt;b&gt;리전(Region)&lt;/b&gt; 이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 서울 리전은&lt;span&gt; ``ap-northeast-2``&lt;/span&gt;,&lt;span&gt;&amp;nbsp; &lt;/span&gt;도쿄 리전은 ``&lt;span&gt;ap-northeast-1``&lt;/span&gt;, 그리고 버지니아 리전은 ``&lt;span&gt;us-east-1``&lt;/span&gt;이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 서비스 대상 사용자가 한국이라면 서울 리전을 선택하는 것이 지연 시간 (Latency) 측면에서 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;가용 영역 (Availability Zone)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 리전은 다시 여러 개의 &lt;b&gt;가용 영역 (AZ)&lt;/b&gt;으로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가용 영역은 물리적으로 분리된 데이터센터 그룹이라고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서울 리전의 경우, 다음과 같은 AZ를 제공한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ap-northeast-2a&lt;/li&gt;
&lt;li&gt;ap-northeast-2b&lt;/li&gt;
&lt;li&gt;ap-northeast-2c&lt;/li&gt;
&lt;li&gt;ap-northeast-2d&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 여러 AZ를 사용하는가 ?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모든 리소스를 하나의 AZ에만 배치했는데, 해당 데이터센터에 장애가 발생한다면 서비스 전체가 중단될 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 AWS에서는 여러 AZ에 리소스를 분산 배치하는 구조를 권장한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. VPC ( Virtual Private Cloud )&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 본격적으로 AWS 인프라를 구성해보자.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서 가장 먼저 생성할 리소스는 &lt;b&gt;VPC&lt;/b&gt;이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;VPC는 AWS 내에서 사용하는 독립적인 가상 네트워크 공간&lt;/b&gt;으로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 생성할 EC2, RDS와 같은 리소스들이 모두 이 네트워크 내부에 배치된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 VPC 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC를 생성할 때는 VPC 이외 다른 리소스들을 함께 생성해주는 것이 좋다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC는 단독으로 존재해도 의미가 없고, 반드시 Subnet, Route Table, IGW 구조와 함께 설계되어야 실제 네트워크로 동작한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Console에 로그인한 후, VPC 탭에 아래와 같은 경로로 접속한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rRgig/dJMcahx0LYz/TFX4FZhPTo1UKJbhYCK4OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rRgig/dJMcahx0LYz/TFX4FZhPTo1UKJbhYCK4OK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rRgig/dJMcahx0LYz/TFX4FZhPTo1UKJbhYCK4OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrRgig%2FdJMcahx0LYz%2FTFX4FZhPTo1UKJbhYCK4OK%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;579&quot; height=&quot;110&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;146&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;처음 VPC 생성 화면에 접속하면 아래와 같은 화면이 나타난다.&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;AWS에서는 VPC와 함께 여러 네트워크 리소스를 한 번에 생성할 수도 있지만, &lt;br /&gt;이번 글에서는 네트워크 구성을 직접 이해하며 진행하기 위해 &lt;b&gt;[생성할 리소스]&lt;/b&gt; 항목에서 &lt;b&gt;[VPC만]&lt;/b&gt; 을 선택한다.&lt;span&gt;&amp;nbsp;&lt;/span&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;2150&quot; data-origin-height=&quot;1530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oo018/dJMcahSfMJw/E7Z4exSWkGi096rFJ8kTtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oo018/dJMcahSfMJw/E7Z4exSWkGi096rFJ8kTtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oo018/dJMcahSfMJw/E7Z4exSWkGi096rFJ8kTtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foo018%2FdJMcahSfMJw%2FE7Z4exSWkGi096rFJ8kTtK%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;2150&quot; height=&quot;1530&quot; data-origin-width=&quot;2150&quot; data-origin-height=&quot;1530&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 data-ke-size=&quot;size16&quot;&gt;일반적으로 VPC는 ``&lt;span&gt;10.0.0.0/16``&lt;/span&gt; 과 같은 CIDR 대역으로 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 이 &lt;b&gt;CIDR은 이 VPC가 사용할 수 있는 전체 IP 범위를 의미&lt;/b&gt;한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;CIDR&lt;/b&gt; 이란 ?&lt;br /&gt;네트워크에서 사용할 IP 주소 범위를 정의하는 표기법이다.&lt;br /&gt;&lt;br /&gt;예를 들어, `10.0.0.0/16` 은 다음 범위를 의미한다.&lt;br /&gt;&lt;br /&gt;``10.0.0.0 ~ 10.0.255.255``&lt;br /&gt;&lt;br /&gt;즉, 약 65,536개의 IP 주소를 사용할 수 있는 네트워크이다.&lt;br /&gt;&lt;br /&gt;AWS에서 VPC를 생성할 때 CIDR 블록을 지정하고, 이후 해당 범위 내에서 Public Subnet, Private Subnet 등을 나누어 사용하게 된다.&lt;br /&gt;&lt;br /&gt;예를 들어 다음과 같이 환경별로 VPC를 분리할 수 있다.&lt;br /&gt;- Production VPC &amp;rarr; `10.0.0.0/16`&lt;br /&gt;- Staging VPC &amp;rarr; `10.1.0.0/16`&lt;br /&gt;- Development VPC &amp;rarr; `10.2.0.0/16`&lt;br /&gt;&lt;br /&gt;이처럼 환경을 분리하는 이유는 단순히 IP를 나누기 위해서가 아니라, 네트워크를 독립적으로 관리하고 보안을 분리하기 위함이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;왜 VPC CIDR을 보통 `/16` 으로 설정하는가 ?&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;AWS에서는 VPC CIDR을 `/8` 과 같이 매우 크게 설정할 수도 있지만, 일반적으로는 거의 사용하지 않는다.&lt;br /&gt;`10.0.0.0/8` 은 다음 범위를 의미한다.&lt;br /&gt;&lt;br /&gt;``10.0.0.0 ~ 10.255.255.255``&lt;br /&gt;&lt;br /&gt;즉, 약 1,677만 개의 IP를 사용할 수 있는 매우 큰 네트워크 공간이다.&lt;br /&gt;&lt;br /&gt;처음 보면 &lt;b&gt;&amp;ldquo;그럼 `/8`이 더 좋은 것 아닌가?&amp;rdquo;&lt;/b&gt;라고 생각할 수 있다.&lt;br /&gt;&lt;br /&gt;하지만 실제로는 IP 부족 때문이 아니라 네트워크 설계와 관리 문제 때문에 `/8`은 잘 사용하지 않는다.&lt;br /&gt;&lt;br /&gt;만약 첫 번째 VPC에서 `10.0.0.0/`8 을 사용해버리면, 이후 생성하는 다른 VPC들은 모두 같은` 10.x.x.x `대역을 사용할 수 없거나, &lt;b&gt;VPC Peering&lt;/b&gt; / &lt;b&gt;Transit Gateway&lt;/b&gt; 구성 시 CIDR 충돌 문제가 발생할 수 있다.&lt;br /&gt;&lt;br /&gt;AWS에서는 여러 VPC를 연결하는 경우가 많기 때문에, CIDR이 겹치면 네트워크 연결이 불가능하거나 복잡한 추가 설정이 필요해진다.&lt;br /&gt;&lt;br /&gt;또한 `/16` 단위로 VPC를 나누면 다음과 같이 서브넷 설계가 훨씬 명확해진다.&lt;br /&gt;- Public Subnet &amp;rarr; ALB, NAT Gateway &lt;br /&gt;- Private Application Subnet &amp;rarr; ECS &lt;br /&gt;- Private Database Subnet &amp;rarr; RDS, ElastiCache &lt;br /&gt;&lt;br /&gt;특히 고가용성을 위해 최소 2개의 AZ를 사용하는 경우, 각 계층별로 여러 서브넷이 필요해지기 때문에 /16 정도의 공간이 가장 균형이 좋다.&lt;br /&gt;&lt;br /&gt;즉, AWS에서는 IP를 많이 확보하기 위해 /8을 쓰는 것이 아니라, 확장성과 네트워크 분리, 충돌 방지, 운영 관리 편의성 때문에 /16을 기본으로 사용하는 경우가 많다.&lt;br /&gt;&lt;br /&gt;또한 VPC는 생성 이후 CIDR 변경이 어렵기 때문에, 처음 설계 단계에서 충분한 여유를 두고 구성하는 것이 중요하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 Subnet 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서브넷은 VPC 내부 IP 범위를 나누는 단위&lt;/b&gt;이며, 외부 노출 여부에 따라 구조가 완전히 달라진다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 서브넷을 생성했다고 해서 인터넷과 통신할 수 있는 것은 아니다.&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&gt;&lt;b&gt;인터넷 게이트웨이(IGW)&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 와 &lt;/span&gt;&lt;span&gt;&lt;b&gt;라우팅 테이블(Route Table)&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 이 함께 구성되어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인터넷 게이트웨이는 VPC와 인터넷을 연결하는 역할&lt;/b&gt;을 하고, &lt;b&gt;라우팅 테이블은 들어오고 나가는 트래픽을 어느 경로로 전달할지 결정&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서브넷에 인터넷 게이트웨이로 향하는 라우팅 경로를 연결하면 외부 인터넷과 통신할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 해당 경로가 없다면 외부 인터넷과 직접 통신할 수 없으며, 이를 일반적으로 Private Subnet이라고 부른다.&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;이제 Subnet 을 구성해보도록 하자.&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;Subnet은 Private Subnet과 Public Subnet을 각각 다른 가용 영역에 1개씩 구성할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, &lt;b&gt;하나의 Subnet은 반드시 하나의 AZ에만 속한다는 점&lt;/b&gt;을 주의해야 한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;343&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chhFtO/dJMb997M7Ue/wlzkuKMvHSK7sn6bEHteWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chhFtO/dJMb997M7Ue/wlzkuKMvHSK7sn6bEHteWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chhFtO/dJMb997M7Ue/wlzkuKMvHSK7sn6bEHteWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchhFtO%2FdJMb997M7Ue%2FwlzkuKMvHSK7sn6bEHteWk%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;1710&quot; height=&quot;343&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;343&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진에서 1번과 같은 경로로 이동하면, 다음과 같은 화면이 나타날 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 &lt;b&gt;[서브넷 생성]&lt;/b&gt; 버튼을 눌러 서브넷을 구성해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;833&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGmQYo/dJMcafAcb66/K5io1eH6cqvLkoVGRiaSX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGmQYo/dJMcafAcb66/K5io1eH6cqvLkoVGRiaSX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGmQYo/dJMcafAcb66/K5io1eH6cqvLkoVGRiaSX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGmQYo%2FdJMcafAcb66%2FK5io1eH6cqvLkoVGRiaSX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1255&quot; height=&quot;833&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;833&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서브넷을 생성할 VPC를 선택한다. &lt;br /&gt;이때 앞에서 생성한 VPC를 선택하면 된다.&lt;/li&gt;
&lt;li&gt;서브넷 이름을 작성한다.&lt;br /&gt;서브넷 이름은 어떤 용도의 서브넷인지 쉽게 파악할 수 있도록 VPC 이름과 어느 정도 통일하여 작성하는 것이 좋다. 위 이미지 하단에 있는 이름 태그를 사용해도 좋지만, 나는 잘 사용하지 않는 편이다.&lt;/li&gt;
&lt;li&gt;가용 영역(AZ)을 선택한다.&lt;br /&gt;이번 글에서는 ``ap-northeast-2a``와``ap-northeast-2c``를 사용할 예정이다. &lt;br /&gt;AWS에서는 고가용성을 위해 서로 다른 가용 영역에 리소스를 분산 배치하는 것을 권장한다.&lt;/li&gt;
&lt;li&gt;서브넷의 CIDR 대역을 설정한다. &lt;br /&gt;서브넷의 CIDR은 VPC 범위 내에서 설정해야 하며, 다른 서브넷과 겹치지 않도록 한다. 예를 들어, VPC를 `10.0.0.0/16`으로 생성했다면, 다음과 같이 서브넷을 나눌 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Public Subnet - 가용영역 A : `10.0.1.0/24`&lt;/li&gt;
&lt;li&gt;Private Subnet - 가용영역 A : `10.0.101.0/24`&lt;/li&gt;
&lt;li&gt;Public Subnet - 가용영역 C : `10.0.2.0/24`&lt;/li&gt;
&lt;li&gt;Private Subnet - 가용영역 C : `10.0.102.0/24`&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[서브넷 생성]&lt;/b&gt; 버튼을 눌러 서브넷을 생성해준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정을 4번 반복해 Public Subnet과 Private Subnet을 2개씩 생성해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuhN3m/dJMcahLw1LS/jDsXSggyCKKzaQHNcZMQe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuhN3m/dJMcahLw1LS/jDsXSggyCKKzaQHNcZMQe1/img.png&quot; data-alt=&quot;VPC &amp;amp;gt; VPC &amp;amp;gt; 하단 [리소스 맵]&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuhN3m/dJMcahLw1LS/jDsXSggyCKKzaQHNcZMQe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuhN3m%2FdJMcahLw1LS%2FjDsXSggyCKKzaQHNcZMQe1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;526&quot; height=&quot;708&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;VPC &amp;gt; VPC &amp;gt; 하단 [리소스 맵]&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;다 생성했으면, 위 사진처럼 리소스 맵에서 내가 생성한 VPC 내 서브넷들이 가용영역 별로 생성된 것을 확인할 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 인터넷 게이트웨이 (IGW) 생성&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1757&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BZvOx/dJMcaffVjDZ/VFtzfyflc4Gvqxay3P9nn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BZvOx/dJMcaffVjDZ/VFtzfyflc4Gvqxay3P9nn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BZvOx/dJMcaffVjDZ/VFtzfyflc4Gvqxay3P9nn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBZvOx%2FdJMcaffVjDZ%2FVFtzfyflc4Gvqxay3P9nn0%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;1757&quot; height=&quot;472&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1757&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 1번 경로로 이동해 &lt;b&gt;[인터넷 게이트웨이 생성]&lt;/b&gt; 버튼을 누른다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;1339&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZwAZc/dJMcagFUpzi/ajj6yQ7dytQHpdIlM8JjLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZwAZc/dJMcagFUpzi/ajj6yQ7dytQHpdIlM8JjLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZwAZc/dJMcagFUpzi/ajj6yQ7dytQHpdIlM8JjLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZwAZc%2FdJMcagFUpzi%2Fajj6yQ7dytQHpdIlM8JjLK%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;1339&quot; height=&quot;459&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;1339&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷 게이트웨이 이름을 입력해주고, &lt;b&gt;[인터넷 게이트웨이 생성]&lt;/b&gt; 버튼을 눌러 생성한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 상태는 인터넷 게이트웨이(IGW)만 생성된 상태이다.&lt;br /&gt;인터넷 게이트웨이는 VPC와 연결되어야 사용할 수 있으므로, 앞서 생성한 VPC에 연결해주도록 하자.&lt;br /&gt;다만, 이것만으로 외부 통신이 가능한 것은 아니며 이후 라우팅 테이블 설정도 함께 진행해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZjnIb/dJMcabdp8t3/29wec3yYk4Px0DD3xxw7g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZjnIb/dJMcabdp8t3/29wec3yYk4Px0DD3xxw7g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZjnIb/dJMcabdp8t3/29wec3yYk4Px0DD3xxw7g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZjnIb%2FdJMcabdp8t3%2F29wec3yYk4Px0DD3xxw7g1%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;1225&quot; height=&quot;415&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;415&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;6.png&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnHBEW/dJMcajbp0cz/qhhKAHRaLRKktE1ZCna1P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnHBEW/dJMcajbp0cz/qhhKAHRaLRKktE1ZCna1P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnHBEW/dJMcajbp0cz/qhhKAHRaLRKktE1ZCna1P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnHBEW%2FdJMcajbp0cz%2FqhhKAHRaLRKktE1ZCna1P1%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;1089&quot; height=&quot;286&quot; data-filename=&quot;6.png&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 이미지의 번호 순서대로 진행하여 인터넷 게이트웨이를 VPC에 연결한다.&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;1.4 라우팅 테이블 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명했듯이 Public Subnet은 인터넷과 직접 통신할 수 있는 서브넷이다.&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;따라서 인터넷 게이트웨이(IGW)로 향하는 기본 경로 `&lt;span&gt;0.0.0.0/0`&lt;/span&gt;가 설정된 라우팅 테이블을 생성한 뒤, 해당 라우팅 테이블을 서브넷에 연결하면 Public Subnet으로 동작하게 된다. 반면 Private Subnet은 인터넷 게이트웨이로 향하는 경로를 설정하지 않는다. 따라서 외부에서 직접 접근할 수 없으며, 내부 네트워크를 통해서만 통신할 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Public Subnet과 Private Subnet을 구분하는 핵심은 서브넷 자체가 아니라 어떤 라우팅 테이블이 연결되어 있는지에 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;만약 Private Subnet의 리소스가 패키지 설치나 AWS API 호출 등을 위해 인터넷에 접근해야 하는 경우에는 NAT Gateway를 통해 외부로 나가는 경로를 구성할 수 있지만, 이번 글에서는 다루지 않을 예정이다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅 테이블은 VPC를 만들면, 자동으로 생성된다. 따라서, 우리는 해당 라우팅 테이블을 수정해주면 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[VPC] &amp;gt; [라우팅 테이블] &amp;gt; [생성된 라우팅 테이블]&lt;/b&gt; 선택 하여 해당 탭으로 이동하는 방법도 있지만, &lt;br /&gt;&lt;b&gt;[VPC] &amp;gt; [내 VPC] &amp;gt; [리소스 맵] &amp;gt; [라우팅 테이블]&lt;/b&gt; 선택하여 이동하는 방법을 좀 더 추천한다.&lt;span&gt;&amp;nbsp;&lt;/span&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-filename=&quot;7.png&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6aKz7/dJMcadWEhcX/JwLm8l1W0GHuFh6TMVvJA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6aKz7/dJMcadWEhcX/JwLm8l1W0GHuFh6TMVvJA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6aKz7/dJMcadWEhcX/JwLm8l1W0GHuFh6TMVvJA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6aKz7%2FdJMcadWEhcX%2FJwLm8l1W0GHuFh6TMVvJA0%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;1630&quot; height=&quot;460&quot; data-filename=&quot;7.png&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;460&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;현재는 ``Destination``이 `10.0.0.0/16` 이고, ``Target``이 `local`인 &lt;b&gt;AWS가 자동으로 만들어주는 VPC 내부 통신용 라우트&lt;/b&gt;만 존재하는 것을 확인할 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  10.0.0.0/16 &amp;rarr;local&lt;br /&gt;의미 : 목적지가 VPC 내부 IP 라면 VPC 내부 네트워크를 통해 전달해라&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 우측에 하단에 있는 &lt;b&gt;[라우팅 편집] &lt;/b&gt;을 눌러 라우팅 테이블을 수정해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;8.png&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;335&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9R29p/dJMcadCfPex/NMzzb2VJlKG9zXNv6Z9qOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9R29p/dJMcadCfPex/NMzzb2VJlKG9zXNv6Z9qOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9R29p/dJMcadCfPex/NMzzb2VJlKG9zXNv6Z9qOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9R29p%2FdJMcadCfPex%2FNMzzb2VJlKG9zXNv6Z9qOK%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;1337&quot; height=&quot;335&quot; data-filename=&quot;8.png&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;335&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진처럼 `0.0.0.0/0`을 목적지(Destination)로 설정하고, 대상을 인터넷 게이트로 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 `0.0.0.0/0`은 모든 IP 주소를 의미한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 현재 VPC 내부 네트워크 (`10.0.0.0/16`)을 제외한 모든 외부 트래픽을 인터넷 게이트웨이로 전달하라는 뜻이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 EC2 인스턴스가 구글이나 깃허브와 같은 외부 서비스에 접근하려고 할 때, 해당 요청은 `0.0.0.0/0 &amp;rarr; 인터넷 게이트웨이` 라우팅 규칙을 통해 인터넷으로 전달된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 라우팅 규칙이 연결된 서브넷은 외부 인터넷과 통신할 수 있게 되며, 이를 &lt;b&gt;Public Subnet&lt;/b&gt;이라고 부른다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 `0.0.0.0/0 &amp;rarr; 인터넷 게이트웨이` 경로가 없는 서브넷은 외부 인터넷과 직접 통신할 수 없으며, 이를 &lt;b&gt;Private Subnet&lt;/b&gt; 이라고 부른다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅 테이블을 수정했으면, 이제 해당 라우팅 테이블을 Public Subnet과 연결해줘야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;9.png&quot; data-origin-width=&quot;997&quot; data-origin-height=&quot;335&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tmBlU/dJMcacpXitS/g8pMjk8mOzmybEhxKCkVo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tmBlU/dJMcacpXitS/g8pMjk8mOzmybEhxKCkVo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tmBlU/dJMcacpXitS/g8pMjk8mOzmybEhxKCkVo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtmBlU%2FdJMcacpXitS%2Fg8pMjk8mOzmybEhxKCkVo1%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;997&quot; height=&quot;335&quot; data-filename=&quot;9.png&quot; data-origin-width=&quot;997&quot; data-origin-height=&quot;335&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[작업] &amp;gt; [서브넷 연결 편집]&amp;nbsp;&lt;/b&gt;을 통해 연결할 서브넷을 수정한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;10.png&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bodGqF/dJMcaftoZl9/5yRcPlIt2ldUoYznV62P9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bodGqF/dJMcaftoZl9/5yRcPlIt2ldUoYznV62P9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bodGqF/dJMcaftoZl9/5yRcPlIt2ldUoYznV62P9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbodGqF%2FdJMcaftoZl9%2F5yRcPlIt2ldUoYznV62P9K%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;926&quot; height=&quot;311&quot; data-filename=&quot;10.png&quot; data-origin-width=&quot;926&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;위 사진처럼 Public Subnet 을 선택해주고, &lt;b&gt;[연결 저장]&lt;/b&gt; 버튼을 눌러 연결해준다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다시&amp;nbsp;&lt;b&gt;[VPC] &amp;gt; [내 VPC] 선택 &amp;gt; [리소스 맵]&amp;nbsp;&lt;/b&gt;화면을 확인해보면 한 가지 이상한 점을 발견할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Private Subnet들도 Public Subnet과 동일하게 인터넷 게이트웨이에 연결되어 있는 것처럼 보인다는 점이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 보면 설정이 잘못된 것처럼 보일 수 있지만, 실제로는 정상적인 상태이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모든 서브넷은 반드시 하나의 라우팅 테이블과 연결되어 있어야 한다.&lt;/b&gt; 현재 VPC에는 우리가 수정한 라우팅 테이블만 존재하기 때문에 Public Subnet뿐만 아니라 Private Subnet도 동일한 라우팅 테이블을 사용하고 있다. 문제는 현재 라우팅 테이블에 `0.0.0.0/0 &amp;rarr; 인터넷 게이트웨이` 규칙이 포함되어 있다는 점이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 현재 상태에서는 Private Subnet 역시 외부 인터넷과 통신할 수 있는 상태이며, Public Subnet과 동일하게 동작하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서는 Private Subnet 전용 라우팅 테이블을 별도로 생성해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;11.png&quot; data-origin-width=&quot;1852&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sIMFL/dJMcaiwUA9K/jAusoDkHQ7MJAZ6C9xrQJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sIMFL/dJMcaiwUA9K/jAusoDkHQ7MJAZ6C9xrQJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sIMFL/dJMcaiwUA9K/jAusoDkHQ7MJAZ6C9xrQJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsIMFL%2FdJMcaiwUA9K%2FjAusoDkHQ7MJAZ6C9xrQJK%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;1852&quot; height=&quot;622&quot; data-filename=&quot;11.png&quot; data-origin-width=&quot;1852&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[VPC] &amp;gt; [라우팅 테이블]&amp;nbsp;&lt;/b&gt;탭으로 이동한 뒤&amp;nbsp;우측 상단에 있는&amp;nbsp;&lt;b&gt;[라우팅 테이블 생성]&amp;nbsp;&lt;/b&gt;버튼을 클릭한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진처럼 이름을 입력하고, 앞서 생성한 VPC를 선택한 뒤 라우팅 테이블을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 라우팅 테이블은 별도의 라우팅 규칙을 추가하지 않고, 기본 라우팅 정보만 존재하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, VPC 내부 리소스들과는 통신할 수 있지만 인터넷 게이트웨이로 향하는 경로가 없기 때문에 외부 인터넷과는 직접 통신할 수 없다.&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;2476&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s35tG/dJMcahx0QDG/scxp5C4u5Al27kXsKwbrN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s35tG/dJMcahx0QDG/scxp5C4u5Al27kXsKwbrN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s35tG/dJMcahx0QDG/scxp5C4u5Al27kXsKwbrN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs35tG%2FdJMcahx0QDG%2Fscxp5C4u5Al27kXsKwbrN1%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;2476&quot; height=&quot;744&quot; data-origin-width=&quot;2476&quot; data-origin-height=&quot;744&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;이렇게 하면 Public Subnet은 인터넷과 통신할 수 있고, Private Subnet은 외부에서 직접 접근할 수 없는 구조가 완성된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3032&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kG8bI/dJMcacQXs4S/b8GqMkYoj4NRWHVRQF1sc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kG8bI/dJMcacQXs4S/b8GqMkYoj4NRWHVRQF1sc0/img.png&quot; data-alt=&quot;[VPC] &amp;amp;gt; [리소스 맵]&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kG8bI/dJMcacQXs4S/b8GqMkYoj4NRWHVRQF1sc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkG8bI%2FdJMcacQXs4S%2Fb8GqMkYoj4NRWHVRQF1sc0%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;3032&quot; height=&quot;764&quot; data-origin-width=&quot;3032&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[VPC] &amp;gt; [리소스 맵]&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  참고 )&lt;br /&gt;기존에 public subnet을 연결했던 라우팅 테이블의 이름을 `my-public-route-table`로 수정해 라우팅 테이블명을 통일해주었다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 보안그룹 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 VPC, Subnet, 인터넷 게이트웨이, 리우팅 테이블을 구성하여 네트워크가 어떻게 연결되는지 설정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 네트워크 경로가 열려있다고 해서 모든 통신이 허용되는 것은 아니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서는&amp;nbsp;보안 그룹 (Security Group)을 통해 어떤 트래픽을 허용할 것인지 제어할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;보안그룹은 AWS 리소스에 적용되는 가상 방화벽이라고 생각하면 된다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, Public Subnet에 EC2를 배치하고 인터넷 게이트웨이와 연결했다고 하더라도, 보안 그룹에서 HTTP(80) 또는 HTTPS(443) 포트를 허용하지 않으면 외부 사용자는 해당 서버에 접근할 수 없다. 반대로 라우팅 테이블을 통해 네트워크 경로가 존재하지 않는다면, 보안그룹에서 포트를 허용하더라도 통신은 이루어질 수 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 실제 통신이 가능하려면 다음 두 조건을 모두 만족해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 경로(Route)가 존재해야 한다.&lt;/li&gt;
&lt;li&gt;보안그룹(Security Group)에서 해당 트래픽을 허용한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 그룹은 크게 2가지 규칙으로 구성된다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인바운드 규칙 (Inbound Rule)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 리소스로 들어오는 트래픽을 제어한다. &lt;br /&gt;예를 들어, 사용자가 웹 브라우저를 통해 서버에 접속하는 경우, 인바운드 규칙이 적용된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;아웃바운드 규칙 (Outbound Rule)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스에서 외부로 나가는 트래픽을 제어한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로, EC2 인스턴스가 깃허브에서 코드를 내려받거나 외부 API 를 호출하는 경우에는 아웃바운드 규칙이 적용된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 보안그룹을 직접 생성해보자.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3410&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IrArG/dJMcafmGIbN/UJtU4pY8tqIqfCGeNQktmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IrArG/dJMcafmGIbN/UJtU4pY8tqIqfCGeNQktmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IrArG/dJMcafmGIbN/UJtU4pY8tqIqfCGeNQktmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIrArG%2FdJMcafmGIbN%2FUJtU4pY8tqIqfCGeNQktmk%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;3410&quot; height=&quot;582&quot; data-origin-width=&quot;3410&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[VPC] &amp;gt; [보안그룹]&amp;nbsp;&lt;/b&gt;탭으로 이동하면 위 사진처럼 VPC 생성 시 자동으로 생성되는 보안그룹이 존재할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 보안 그룹은 같은 보안 그룹에 속한 리소스끼리는 자유롭게 통신할 수 있도록 설정되어 있으며, 특별한 설정 없이도 동작한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 운영 환경에서는 이를 권장하지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 서비스별로 별도의 보안 그룹을 생성하여 관리하는 것이 좋다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 보안그룹을 다음과 같이 2개로 분리할 예정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MyApp-sg&lt;/li&gt;
&lt;li&gt;MyDB-sg&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Application 보안그룹 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진에서 우측 상단에 있는&amp;nbsp;&lt;b&gt;[보안 그룹 생성]&amp;nbsp;&lt;/b&gt;버튼을 클릭한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;13.png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb1opd/dJMcadoK6cr/OcIHlBgdovps3jzYEJmLJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb1opd/dJMcadoK6cr/OcIHlBgdovps3jzYEJmLJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb1opd/dJMcadoK6cr/OcIHlBgdovps3jzYEJmLJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb1opd%2FdJMcadoK6cr%2FOcIHlBgdovps3jzYEJmLJ1%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;1253&quot; height=&quot;656&quot; data-filename=&quot;13.png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진처럼 보안그룹 이름, 설명을 입력해주고, 이전에 생성한 VPC를 선택한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 인바운드 규칙을 설정해준다. 각 규칙에 대해 알아보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SSH (22)&amp;nbsp;&lt;/b&gt;: 원격으로 서버에 접속하기 위한 프로토콜
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;터미널을 통해 EC2 서버에 접속할 때 사용된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;서버 설정, 로그 확인, Docker 실행 등의 작업을 수행하기 위해 필요하다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;만약 실제 배포를 하게 된다면 `0.0.0.0/0`으로 열어두지 않고, 본인 IP 또는 팀원의 IP만 허용하는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP (80)&amp;nbsp;&lt;/b&gt;: 웹 브라우저가 웹 서버와 통신할 때 사용하는 기본 프로토콜이다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 ``http://app.com`` 과 같은 사이트에 접속하면 `80`번 포트를 통해 요청이 전달된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;최근에는 대부분 HTTPS를 사용하지만, HTTP 요청을 HTTPS로 리다이렉트하기 위해 여전히 사용되는 경우가 많다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTPS (443)&amp;nbsp;&lt;/b&gt;: HTTP에 SSL/TLS 암호화를 적용한 프로토콜이다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 ``https://app.com`` 과 같은 사이트에 접속하면 `443`번 포트를 통해 요청이 전달된다.&lt;/li&gt;
&lt;li&gt;로그인 정보나 개인정보가 암호화되어 전송되므로 실제 서비스에서는 반드시 사용해야 한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Custom TCP 3000&amp;nbsp;&lt;/b&gt;: Node.js 애플리케이션이 실행되는 포트이다. (Spring Boot로 백엔드를 구축할 경우 이는 생략해도 된다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Custom TCP 8080&amp;nbsp;&lt;/b&gt;: Spring Boot 애플리케이션이 실행되는 포트이다. (Node.js로 백엔드를 구축할 경우 이는 생략해도 된다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;14.png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0dIZu/dJMcagFUrtW/429DoBPtM424b8HPZzOHok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0dIZu/dJMcagFUrtW/429DoBPtM424b8HPZzOHok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0dIZu/dJMcagFUrtW/429DoBPtM424b8HPZzOHok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0dIZu%2FdJMcagFUrtW%2F429DoBPtM424b8HPZzOHok%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;1253&quot; height=&quot;380&quot; data-filename=&quot;14.png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아웃바운드 규칙은 별도로 설정하지 않고, 기본 설정을 유지한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버는 생각보다 다양한 외부 서비스와 통신한다. 깃허브에서 코드를 다운로드하고, npm 패키지를 설치하고, 외부 API를 호출하는 등이 모두 아웃바운드 통신이다. 만약 아웃바운드 규칙까지 엄격하게 제한하면 이러한 기능들이 정상적으로 동작하지 않을 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 금융권이나 공공기간과 같은 보안 요구사항이 높은 환경에서는 아웃바운드도 제한하는 경우가 있으니 참고하면 좋을 것 같다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 우측 하단에&amp;nbsp;&lt;b&gt;[보안 그룹 생성]&amp;nbsp;&lt;/b&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;2.2 Database 보안그룹 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 DB용 보안그룹을 생성해보자. DB용 보안그룹은 MySQL이 실행될 RDS에 연결할 보안그룹이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 외부에 직접 노출하지 않고, 서버에서만 접근할 수 있도록 최소한의 포트만 허용한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 방법으로 인바운드 규칙을 설정해주고, 보안그룹을 생성해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;15.png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgY34G/dJMcadWEjPk/dk3gWSElJ100Rz0FwSARP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgY34G/dJMcadWEjPk/dk3gWSElJ100Rz0FwSARP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgY34G/dJMcadWEjPk/dk3gWSElJ100Rz0FwSARP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgY34G%2FdJMcadWEjPk%2Fdk3gWSElJ100Rz0FwSARP1%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;1253&quot; height=&quot;794&quot; data-filename=&quot;15.png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;794&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application용 보안그룹을 생성했을 때와 마찬가지로, &lt;br /&gt;이름, 설명을 작성해주고, VPC를 선택한 뒤, 인바운드 규칙을 생성해 보안그룹을 생성해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database용 보안그룹은 데이터베이스에 접근할 수 있는 대상을 제한하는 것이 가장 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP(80), HTTPS(443)와 같은 웹 포트는 허용하지 않고, MySQL이 사용하는 `3306`포트만 허용한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 소스 (Source)는 앞서 생성한 Application 용 보안그룹으로 지정한다. 즉, Application 서버에 연결된 리소스만 Database에 접근할 수 있다는 의미이다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;우리는 흔히 배포된 DB 즉, RDS에 접근할 때 Datagrip이나 Workbench를 이용해 접근한다. 하지만, 위와 같이 소스를 Application 용 서버로 설정해두면, Datagrip이나 Workbench를 이용한 원격 접속이 불가능하다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;물론 개발이나 운영 과정에서 데이터 확인이 필요한 경우도 있다. 이럴 때는 SSH 터널링을 이용해 EC2를 경유하여 접속하거나, 특정 개발자의 공인 IP만 일시적으로 허용하여 접근하는 방법을 사용할 수 있다. &lt;br /&gt;&lt;br /&gt;이번 글에서는 데이터베이스 보안을 우선하여 Application 서버만 접근할 수 있도록 구성하겠다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Database용 보안그룹까지 생성했으면, 애플리케이션과 데이터베이스 간 통신을 위한 네트워크 환경은 어느 정도 준비된 상태이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. EC2 구축&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 애플리케이션을 실행할 서버를 생성해보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AWS에서 가상 서버를 생성하고 실행할 수 있는 서비스가 바로 EC2 (Elastic Compute Cloud) 이다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 AWS가 제공하는 가상 머신 서비스로, 사용자가 원하는 운영체제와 사양을 선택해 서버를 생성할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해, 애플리케이션을 배포하고 실행하기 위한 클라우드 서버라고 생각하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 우측 상단에 아래처럼 현재 리전이 서울 리전인지 확인하자.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C5Kei/dJMcad3nyRq/YmdDy3fn87EhsWVtUvLp0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C5Kei/dJMcad3nyRq/YmdDy3fn87EhsWVtUvLp0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C5Kei/dJMcad3nyRq/YmdDy3fn87EhsWVtUvLp0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC5Kei%2FdJMcad3nyRq%2FYmdDy3fn87EhsWVtUvLp0K%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;546&quot; height=&quot;72&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;72&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS의 모든 리소스는 특정 리전에 생성된다. 따라서 다른 리전이 선택된 상태에서 작업을 선택하면, 앞서 생성한 VPC, 서브넷, 보안그룹 등의 리소스가 보이지 않을 수 있다. 또한 EC2는 생성 시 동일한 리전 내의 VPC와 서브넷만 연결할 수 있으므로, 현재 작업 중인 인프라가 위치한 리전과 동일한 리전을 선택해야 한다.&amp;nbsp;&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;3.1 EC2 인스턴스 생성&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;16.png&quot; data-origin-width=&quot;1709&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAgH31/dJMcaci6Lrr/d968KMn2UkZE1KYCQUbEK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAgH31/dJMcaci6Lrr/d968KMn2UkZE1KYCQUbEK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAgH31/dJMcaci6Lrr/d968KMn2UkZE1KYCQUbEK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAgH31%2FdJMcaci6Lrr%2Fd968KMn2UkZE1KYCQUbEK0%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;1709&quot; height=&quot;341&quot; data-filename=&quot;16.png&quot; data-origin-width=&quot;1709&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 위 사진처럼&amp;nbsp;&lt;b&gt;[EC2] &amp;gt; [인스턴스]&amp;nbsp;&lt;/b&gt;탭으로 이동해&amp;nbsp;&lt;b&gt;[인스턴스 시작]&amp;nbsp;&lt;/b&gt;버튼을 클릭한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;17.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rXVK3/dJMcahrgQ0B/8PczeOUkK63gk1JHrysXfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rXVK3/dJMcahrgQ0B/8PczeOUkK63gk1JHrysXfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rXVK3/dJMcahrgQ0B/8PczeOUkK63gk1JHrysXfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrXVK3%2FdJMcahrgQ0B%2F8PczeOUkK63gk1JHrysXfk%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;1236&quot; height=&quot;694&quot; data-filename=&quot;17.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단, 이름을 작성해주고, `Ubuntu`를 선택해 프리티어를 제공하는 사양을 선택한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;학생은 돈을 아껴야 하기 때문에 ....... 최대한 프리티어를 사용하도록 구성한다 ....... !!!!&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;18.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9K8kB/dJMcagTpluV/zy79qpguosoCBSUJudTcE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9K8kB/dJMcagTpluV/zy79qpguosoCBSUJudTcE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9K8kB/dJMcagTpluV/zy79qpguosoCBSUJudTcE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9K8kB%2FdJMcagTpluV%2Fzy79qpguosoCBSUJudTcE1%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;1236&quot; height=&quot;307&quot; data-filename=&quot;18.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;307&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;EC2랑 통신할 때에는 반드시 키 페어로 로그인을 해야 보안 문제가 발생하지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키 페어를 이미 생성했으면 해당 키 페어를 선택하고, 그렇지 않을 경우에는 새 키 페어를 생성한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;19.png&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgh7pl/dJMcabR2vmT/Ora3lbr0V0WXnr1f6gIodk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgh7pl/dJMcabR2vmT/Ora3lbr0V0WXnr1f6gIodk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgh7pl/dJMcabR2vmT/Ora3lbr0V0WXnr1f6gIodk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgh7pl%2FdJMcabR2vmT%2FOra3lbr0V0WXnr1f6gIodk%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;820&quot; height=&quot;434&quot; data-filename=&quot;19.png&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키 페어 이름을 생성하고 키 페어 유형과 키 파일 형식을 선택한 뒤, &lt;b&gt;[키 페어 생성]&amp;nbsp;&lt;/b&gt;버튼을 클릭해 키 페어를 생성한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키 페어는 EC2에 SSH로 접속하기 위해 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 공개키와 개인 키로 구성되며, 생성 시 다운로드되는 ``.pem``파일이 개인키 역할을 한다.&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;한 번 생성한 키 페어는 여러 EC2 인스턴스에서 재사용할 수 있으므로, 이미 생성한 키 페어가 있다면 새로 만들 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, ``.pem``파일은 생성 시에만 다운로드할 수 있으므로 안전한 위치에 반드시 보관해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dU7fpi/dJMcaf7YmOW/7v05W87trD1bK0QPUpXegk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dU7fpi/dJMcaf7YmOW/7v05W87trD1bK0QPUpXegk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dU7fpi/dJMcaf7YmOW/7v05W87trD1bK0QPUpXegk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdU7fpi%2FdJMcaf7YmOW%2F7v05W87trD1bK0QPUpXegk%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;1236&quot; height=&quot;763&quot; data-filename=&quot;20.png&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 네트워크 설정 단계에서 앞서 생성한 VPC를 선택해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 서브넷은 반드시&amp;nbsp;&lt;b&gt;Public Subnet&lt;/b&gt;을 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 보안 그룹은 이전에 생성한&amp;nbsp;Application 보안 그룹을 선택해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토리지 설정에서는 프리티어 기준 최대 용량인&amp;nbsp;&lt;b&gt;30GiB&lt;/b&gt;를 입력한 뒤,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 우측에 있는&amp;nbsp;&lt;b&gt;[인스턴스 시작]&amp;nbsp;&lt;/b&gt;버튼을 클릭하면 EC2 생성이 완료된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3400&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3ClTs/dJMcahSfQzi/7Yy1XL7ZYkuFnOkwFOs790/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3ClTs/dJMcahSfQzi/7Yy1XL7ZYkuFnOkwFOs790/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3ClTs/dJMcahSfQzi/7Yy1XL7ZYkuFnOkwFOs790/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3ClTs%2FdJMcahSfQzi%2F7Yy1XL7ZYkuFnOkwFOs790%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;3400&quot; height=&quot;894&quot; data-origin-width=&quot;3400&quot; data-origin-height=&quot;894&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 인스턴스가 생성된 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;( NGINX 를&amp;nbsp; 사용하는 경우 ) 3.2 탄력적 IP 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스를 생성하면 기본적으로 Public IP가 자동으로 할당된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;(앞서 우리가 인스턴스를 생성할 때, 자동으로 할당되도록 설정해줬기 때문 !!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 IP는 이 인스턴스를 중지했다가 다시 시작하면 변경될 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버를 재시작할 때마다 IP가 바뀌기 때문에 프론트엔드 연결이나 도메인 설정이 필요한 경우에는 불편해진다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 사용하는 것이&amp;nbsp;&lt;b&gt;탄력적 IP (Elastic IP)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탄력적 IP는 AWS에서 제공하는 고정 Public IP로, EC2 인스턴스에 연결해두면 인스턴스를 재시작해도 동일한 IP를 유지할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;NGNIX를 사용할 때만 필요한 건가 ?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 헷갈릴 수 있는 부분이 있는데, 탄력적 IP는 NGINX를 사용할 때만 필요한 것이 아니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 기준은 NGINX 여부가 아니라 다음이다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서 EC2에 고정된 주소로 접근해야 하는지&lt;/li&gt;
&lt;li&gt;프론트엔드 / 도메인 / API 연동이 필요한지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NGINX를 사용하는 구조에서는 보통 EC2가 외부 요청의&amp;nbsp;진입점 역할을 하기 때문에 고정 IP가 필요해지는 경우가 많아 함께 언급되는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;21.png&quot; data-origin-width=&quot;1709&quot; data-origin-height=&quot;597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W0PHz/dJMcadCfSlb/nonnh1I5QKadfha2ZlawmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W0PHz/dJMcadCfSlb/nonnh1I5QKadfha2ZlawmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W0PHz/dJMcadCfSlb/nonnh1I5QKadfha2ZlawmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW0PHz%2FdJMcadCfSlb%2Fnonnh1I5QKadfha2ZlawmK%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;1709&quot; height=&quot;597&quot; data-filename=&quot;21.png&quot; data-origin-width=&quot;1709&quot; data-origin-height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[EC2] &amp;gt; [탄력적 IP] &lt;/b&gt;탭으로 이동해 우측 상단에 있는&amp;nbsp;&lt;b&gt;[탄력적 IP 주소 할당] &lt;/b&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-filename=&quot;21.png&quot; data-origin-width=&quot;1709&quot; data-origin-height=&quot;597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vYhUn/dJMcaip8dM6/dkVmFtNueWY7W4uc6yiek1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vYhUn/dJMcaip8dM6/dkVmFtNueWY7W4uc6yiek1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vYhUn/dJMcaip8dM6/dkVmFtNueWY7W4uc6yiek1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvYhUn%2FdJMcaip8dM6%2FdkVmFtNueWY7W4uc6yiek1%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;1709&quot; height=&quot;597&quot; data-filename=&quot;21.png&quot; data-origin-width=&quot;1709&quot; data-origin-height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진과 같이 탄력적 IP는 기본 설정을 건드리지 않고 바로 할당해주면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;23.png&quot; data-origin-width=&quot;1527&quot; data-origin-height=&quot;237&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZARM9/dJMcabdqcj0/R4YUd9Z2kDxX6kHmQNj6LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZARM9/dJMcabdqcj0/R4YUd9Z2kDxX6kHmQNj6LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZARM9/dJMcabdqcj0/R4YUd9Z2kDxX6kHmQNj6LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZARM9%2FdJMcabdqcj0%2FR4YUd9Z2kDxX6kHmQNj6LK%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;1527&quot; height=&quot;237&quot; data-filename=&quot;23.png&quot; data-origin-width=&quot;1527&quot; data-origin-height=&quot;237&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;24.png&quot; data-origin-width=&quot;1233&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KIYJS/dJMcabkiqqk/ew23dSJcSg6XuMvJvaw03K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KIYJS/dJMcabkiqqk/ew23dSJcSg6XuMvJvaw03K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KIYJS/dJMcabkiqqk/ew23dSJcSg6XuMvJvaw03K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKIYJS%2FdJMcabkiqqk%2Few23dSJcSg6XuMvJvaw03K%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;1233&quot; height=&quot;419&quot; data-filename=&quot;24.png&quot; data-origin-width=&quot;1233&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 탄력적 IP를 이전에 생성한 EC2 인스턴스에 연결하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 해당 EC2는 이제 고정된 Public IP를 가지게 되며, 외부에서 항상 동일한 주소로 접근할 수 있는 환경이 구성된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 EC2 인스턴스에 접속하여 NGINX를 설치하고, 브라우저에서 정상적으로 접근되는지 확인해보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;( NGINX 를&amp;nbsp; 사용하는 경우 ) 4. NGINX 설치 및 브라우저 접속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 EC2에 접속하는 방법은 여러 가지가 있다.&lt;br /&gt;하지만 나는 AWS 콘솔에서 직접 인스턴스에 접속하는 방식을 주로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 키 설정 문제를 피할 수 있고, 별도 설정 없이 바로 접속할 수 있어 가장 안정적인 방법으로 사용했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;25.png&quot; data-origin-width=&quot;1711&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFQxBJ/dJMcadWElQv/ji5hHW1rZvHZl1NmVoK1yK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFQxBJ/dJMcadWElQv/ji5hHW1rZvHZl1NmVoK1yK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFQxBJ/dJMcadWElQv/ji5hHW1rZvHZl1NmVoK1yK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFQxBJ%2FdJMcadWElQv%2Fji5hHW1rZvHZl1NmVoK1yK%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;1711&quot; height=&quot;215&quot; data-filename=&quot;25.png&quot; data-origin-width=&quot;1711&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;26.png&quot; data-origin-width=&quot;1237&quot; data-origin-height=&quot;549&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSkjwt/dJMcadoK9Ml/mMrbVDsjK0TrF3ky1xkgvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSkjwt/dJMcadoK9Ml/mMrbVDsjK0TrF3ky1xkgvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSkjwt/dJMcadoK9Ml/mMrbVDsjK0TrF3ky1xkgvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSkjwt%2FdJMcadoK9Ml%2FmMrbVDsjK0TrF3ky1xkgvk%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;1237&quot; height=&quot;549&quot; data-filename=&quot;26.png&quot; data-origin-width=&quot;1237&quot; data-origin-height=&quot;549&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[EC2 인스턴스 연결]&amp;nbsp;&lt;/b&gt;탭에서&amp;nbsp;&lt;b&gt;[연결]&lt;/b&gt;버튼을 클릭하면 콘솔 화면이 열린다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnDVVP/dJMcahkwnvz/ayQCdHMYsv3mRTDluxlKUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnDVVP/dJMcahkwnvz/ayQCdHMYsv3mRTDluxlKUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnDVVP/dJMcahkwnvz/ayQCdHMYsv3mRTDluxlKUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnDVVP%2FdJMcahkwnvz%2FayQCdHMYsv3mRTDluxlKUk%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;1414&quot; height=&quot;1034&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;1034&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진과 같이 콘솔이 나오는 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 해당 콘솔에 다음 명령어를 통해 NGINX를 설치한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781355001627&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update
sudo apt upgrade -y
sudo apt install nginx -y&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`apt update` : 패키지 목록 최신화&lt;/li&gt;
&lt;li&gt;`apt upgrade` : 기존 패키지 업데이트&lt;/li&gt;
&lt;li&gt;`apt install nginx` : NGINX 웹 서버 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 EC2 인스턴스의 퍼블릭 IP를 브라우저에 입력해 NGINX 기본 페이지가 정상적으로 출력되는지 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3406&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C22xG/dJMcafNMeja/DVIDNqxg4QfgEiXgChtkl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C22xG/dJMcafNMeja/DVIDNqxg4QfgEiXgChtkl1/img.png&quot; data-alt=&quot;성공 !&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C22xG/dJMcafNMeja/DVIDNqxg4QfgEiXgChtkl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC22xG%2FdJMcafNMeja%2FDVIDNqxg4QfgEiXgChtkl1%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;3406&quot; height=&quot;676&quot; data-origin-width=&quot;3406&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성공 !&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;(사실 단 한 번의 오류 없이 바로 성공해서 당황스럽다 ......  )&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 사이드 프로젝트 또는 장기 해커톤을 기준으로, AWS 기반의 기본 인프라 환경을 구성해보았다.&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;VPC를 통해 네트워크를 설계하고, Public / Private Subnet을 분리하여 기본적인 네트워크 구조를 구성했다.&lt;br /&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;이후 EC2 인스턴스를 생성하여 실제 애플리케이션을 실행할 수 있는 서버 환경을 구성하고, 탄력적 IP를 적용하여 고정된 접속 주소를 확보했다. 마지막으로 NGINX를 설치하여 웹 서버가 정상적으로 동작하는 것까지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 단순한 로컬 개발 환경을 넘어, 외부에서 접근 가능한 기본적인 클라우드 인프라를 갖춘 상태이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 현재 구조는 애플리케이션 서버 중심의 초기 구성 단계이며,&lt;br /&gt;실제 서비스 운영을 위해서는 데이터베이스 구성과 애플리케이션 간 안정적인 연결 구조가 추가로 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 &lt;b&gt;RDS를 활용한 데이터베이스 구축&lt;/b&gt;과&lt;b&gt; SSH 터널링&lt;/b&gt;을 통한 안전한 접근 방식에 대해 다뤄볼 예정이다.&lt;br /&gt;이를 통해 외부 노출 없이도 안전하게 데이터베이스를 관리하도록 구성해볼 예정이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 안농 .&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>2026</category>
      <category>AWS</category>
      <category>CIDR</category>
      <category>EC2</category>
      <category>nginx</category>
      <category>security group</category>
      <category>VPC</category>
      <category>배포</category>
      <category>보안그룹</category>
      <author>코딩 기록하는 애기 개발자</author>
      <guid isPermaLink="true">https://baby-developer77.tistory.com/32</guid>
      <comments>https://baby-developer77.tistory.com/32#entry32comment</comments>
      <pubDate>Sat, 13 Jun 2026 22:21:35 +0900</pubDate>
    </item>
    <item>
      <title>[BRIPONG] 프로젝트 시작 계기</title>
      <link>https://baby-developer77.tistory.com/30</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 다시 제대로 만들어보고 싶은 프로젝트가 생겨서 기록 겸 글을 남겨보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 BriPong 이라는 뉴스 요약 서비스 프로젝트입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작 계기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멋쟁이사자처럼 클라우드 엔지니어링 4기 당시, 최종 프로젝트로 뉴스 요약 서비스를 진행했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;&lt;b&gt;오늘의 뉴스를 간편하게 요약하고, 퀴즈로 재미있게 제공하는 서비스&lt;/b&gt;&amp;rdquo;를 주제로 진행했던 Newsugar라는 이름의 프로젝트였습니다.&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;br /&gt;&lt;b&gt;Docker 기반 MVP 환경 구성, AWS 인프라 설계, EKS 및 CI/CD 구성&lt;/b&gt;까지 프로젝트 범위에 포함되어 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사하게도 프로젝트는 최우수상을 수상할 수 있었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그런데 프로젝트가 끝난 뒤에도 아쉬움이 남았습니다 . .&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 결과와는 별개로, 개인적으로는 아쉬움이 정말 많이 남았던 프로젝트였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시에는 부트캠프를 따라가기에 급했던 시기이기도 했고,&lt;br /&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;br /&gt;Docker 기반 MVP 환경 구성이나 AWS 인프라 설계, EKS 및 CI/CD 구축 같은&lt;br /&gt;&amp;ldquo;인프라 중심 프로젝트&amp;rdquo;에 조금 더 초점이 맞춰져 있었습니다.&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;br /&gt;&amp;ldquo;&lt;b&gt;검색 품질&lt;/b&gt;&amp;rdquo;이나 &amp;ldquo;&lt;b&gt;최신 뉴스 반영&lt;/b&gt;&amp;rdquo; 같은 부분은 충분히 고려하지 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;분명 관련 뉴스가 존재하는데 검색 결과에 나오지 않거나,&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;최신 뉴스보다 오래된 뉴스가 우선적으로 노출되거나,&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;단순 키워드 기반 검색만으로는 원하는 정보를 제대로 찾기 어려운 경우&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시에는 기능 구현 자체에 집중하느라 이런 부분들을 깊게 고민하지 못했지만,&lt;br /&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;또한 당시 프로젝트에서는 EKS 기반으로 인프라를 구성했지만, 실제로는 팀 내에서 인프라를 깊게 다룰 수 있는 인원이 많지 않았고,&lt;br /&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;당시에는 Kubernetes나 EKS를 &amp;ldquo;사용해본 경험&amp;rdquo;에 가까웠다면,&lt;br /&gt;이번에는 왜 ECS를 사용하는지, 왜 이후에 EKS로 확장하는지까지 직접 경험해보고 싶다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 끝난 이후에도 계속 이런 생각이 들었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;지금 다시 만든다면 더 좋은 구조로 만들 수 있지 않을까?&amp;rdquo;&lt;br /&gt;&amp;ldquo;그때는 이해하지 못했던 것들을 지금은 제대로 구현할 수 있지 않을까?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 시간이 지나고 다시 공부를 하면서,&lt;br /&gt;예전에 만들었던 프로젝트를 처음부터 다시 구현해보고 싶다는 생각이 들었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 시작하게 된 BriPong&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BriPong은 기존 Newsugar를 단순히 리메이크하는 프로젝트는 아닙니다.&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;br /&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;br /&gt;뉴스를 수집하고 가공하고 검색하는 과정까지 직접 구성해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이번에는 &lt;b&gt;뉴스 크롤링&lt;/b&gt;부터 &lt;b&gt;RAG(Retrieval-Augmented Generation)&lt;/b&gt;,&lt;br /&gt;&lt;b&gt;LangChain&lt;/b&gt; 기반 검색 구조까지 직접 구성하면서,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뉴스 검색 정확도 개선&lt;/li&gt;
&lt;li&gt;최신 뉴스 기반 Retrieval&lt;/li&gt;
&lt;li&gt;뉴스 요약 품질 향상&lt;/li&gt;
&lt;li&gt;벡터 기반 검색 구조&lt;/li&gt;
&lt;li&gt;검색 결과 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 부분들도 함께 고민해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로젝트에서는 단순 키워드 기반 검색 위주였다면,&lt;br /&gt;이번에는 벡터 기반 검색과 문맥 기반 Retrieval을 통해 조금 더 정확한 뉴스 검색 경험을 만드는 것을 목표로 하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 이번 프로젝트에서는 인프라 구성 역시 처음부터 다시 직접 구축해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 ECS 기반으로 서비스를 직접 구축하고 운영해보면서 컨테이너 기반 배포와 AWS 인프라 구조를 충분히 이해한 뒤,&lt;br /&gt;이후에는 Kubernetes 및 EKS 환경으로 점진적으로 이전하는 과정까지 직접 진행해볼 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &amp;ldquo;EKS를 사용해봤다&amp;rdquo;에서 끝나는 것이 아니라,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ECS와 EKS의 차이점은 무엇인지&lt;/li&gt;
&lt;li&gt;운영 관점에서 어떤 장단점이 있는지&lt;/li&gt;
&lt;li&gt;실제 서비스에서는 어떤 구조가 더 적합한지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 부분들까지 직접 경험하고 이해해보는 것을 목표로 하고 있습니다.&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;또 기존처럼 단순히 기능만 구현하는 것이 아니라, Docker 환경 구성부터 AWS 인프라 설계, ECS/EKS 구축, CI/CD 자동화까지&lt;br /&gt;전체 서비스를 처음부터 끝까지 직접 설계하고 운영하는 경험을 목표로 진행해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 이름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 이름은 &lt;b&gt;BriPong &lt;/b&gt;으로 정하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뉴스를 짧고 간결하게 전달한다는 의미의 Brief 느낌을 담고 싶었고,&lt;br /&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;br /&gt;&amp;ldquo;예전에는 제대로 이해하지 못했던 것들을 다시 처음부터 구현해보는 과정&amp;rdquo;에 더 가까운 프로젝트가 될 것 같습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왜 이런 구조로 설계하는지&lt;/li&gt;
&lt;li&gt;왜 이런 인프라 구성을 사용하는지&lt;/li&gt;
&lt;li&gt;왜 이런 검색 방식을 선택하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 부분들까지 스스로 설명할 수 있을 정도로 이해하면서 프로젝트를 진행해보고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 잘 모르고 지나갔던 부분들도 이번에는 하나씩 직접 부딪혀보면서 공부해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 개발하면서 겪는 설계 과정이나 트러블 슈팅, RAG 및 검색 구조 개선 과정, AWS 및 인프라 구축 과정들도 블로그에 함께 기록해볼 예정입니다.&lt;/p&gt;</description>
      <category>Project/Bripong</category>
      <author>코딩 기록하는 애기 개발자</author>
      <guid isPermaLink="true">https://baby-developer77.tistory.com/30</guid>
      <comments>https://baby-developer77.tistory.com/30#entry30comment</comments>
      <pubDate>Mon, 8 Jun 2026 23:43:51 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] Spring의 전체 흐름 알아보기 - 5. DispatcherServlet 내부의 HandlerMapping 과 HandlerAdapter 동작 원리</title>
      <link>https://baby-developer77.tistory.com/29</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLjQJl/dJMcaiXlc9H/0rkHPvTsUaKjhfJnJVZFqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLjQJl/dJMcaiXlc9H/0rkHPvTsUaKjhfJnJVZFqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLjQJl/dJMcaiXlc9H/0rkHPvTsUaKjhfJnJVZFqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLjQJl%2FdJMcaiXlc9H%2F0rkHPvTsUaKjhfJnJVZFqK%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;1210&quot; height=&quot;818&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;818&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;앞선 글들에서 정리했다시피 DispatcherServlet에서 요청이 전달되는 흐름을 따라가다 보면 자연스럽게 ``HandlerMapping``과 ``HandlerAdapter``라는 개념을 만나게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 전체 요청 흐름을 중심으로 살펴보았기 때문에 DispatcherServlet 내 코드 역시 단순히 &quot;Handler를 실행하기 위한 Adapter를 찾는 과정&quot; 정도로 이해하고 넘어갔을 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실젤 ``DispatcherServlet`` 내부에서의 흐름을 살펴보면,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`doService` 메서드 내에서 `doDispatch()` 라는 메서드를 호출하게된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`doDisptach()` 메서드 내 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;nbsp;&lt;b&gt;실행할 Handler를 찾는다.&lt;/b&gt; &lt;br /&gt;클라이언트 요청이 들어오면 먼저 어떤 Controller가 요청을 처리할지 찾아야 한다. &lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring은 요청된 URL과 Controller의 매핑 정보를 비교해 등록되어 있는 Controller 목록을 탐색한다. 이후, 매칭되는 Controller를 찾아 DispatcherServlet에게 반환하는데, 이 과정을&amp;nbsp;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;HandlerMapping&lt;/b&gt;&lt;/span&gt;이라고 한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Interceptor 의 메서드 중 `preHandle()` 메서드를 실행한다.&lt;/b&gt;&lt;br /&gt;Controller 가 실행되기 전에 실행된다. 보통 로그인 체크, 권한 검사, 요청 로깅, 인증 여부 판단 등을 처리한다. `preHandle()` 메서드를 통해 ``boolean``값을 반환하는데, 이를 기반으로 Controller를 실행할지 말지 판단된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Handler를 실행할 HandlerAdapter 를 찾는다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;이전 단계에서 반환된 Controller를 실행하기 위한 HandlerAdapter를 찾는다.&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Controller를 실행하고, 응답을 생성한다.&amp;nbsp;&lt;/b&gt;&lt;br /&gt;위 단계에서 ``true``가 반환되면, 실제 비즈니스 로직이 실행되는 단게인 Controller가 실행된다. Controller가 성공적으로 실행되면, `ModelAndView` 형태로 응답이 생성된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Interceptor 의 메서드 중 `postHandle()` 메서드를 실행한다.&lt;/b&gt;&lt;br /&gt;Controller 실행 후 View가 렌더링되기 전에 호출되며,페이지 공통 정보를 세팅하거나 Model 데이터를 추가하는 등을 수행한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Controller가 반환한 View 이름을 통해 ViewResolver를 조회해 렌더링 하여 HTML을 생성한다.&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Interceptor 의 메서드 중 `afterCompletion()` 를 실행한다.&amp;nbsp;&lt;/b&gt;&lt;br /&gt;요청이 완전히 끝난 뒤, 리소스 정리, 예외 로깅 등을 처리하기 위해 사용된다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 흐름 중 눈에 띄는 부분은&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Handler를 찾은 뒤 곧바로 실행하지 않고, 반드시 HandlerAdapter를 찾는 과정이 존재한다는 점이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 Controller를 실행하는 것이라면 바로 호출해도 될 것 같지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC는 DispatcherServlet을 이용해 HandlerAdapter를 찾고, 이를 이용해 Handler를 실행한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에는 이 부분을 중심으로,&lt;b&gt; HandlerAdapter는 무엇인지, 또 왜 필요한지&lt;/b&gt; 정리해보려 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HandlerAdapter란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서 요청이 들어오면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet은 어떤 Handler가 실행되어야 하는지 먼저 찾고, 그 Handler를 실제로 실행해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 DispatcherServlet은 Handler를 직접 실행하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 &lt;b&gt;Spring MVC는 하나의 Controller 방식만 지원하지 않기 때문&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하나의 Controller 방식만 지원하지 않는다 ?&amp;nbsp; 그래서 HandleAdapter가 필요하다고 ?&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC를 사용하면서 대부분 `@Controller`, `@RestController`, `@GetMapping`과 같은 방식만 사용하다 보니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Contoroller 방식이 여러 개 존재한다&quot;는 말이 처음에는 낯설게 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 우리가 평소 개발을 할 때는 거의 하나의 방식만 사용하기 때문에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;' Controller 에도 여러 종류가 있었나 ? ' 라는 생각이 자연스럽게 들었던 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 과거에는 특정 인터페이스를 구현해 요청을 처리하는 방식도 존재했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 `handleRequest()` 메서드를 직접 구현하는 Controller 인터페이스 기반 구조나, 서블릿과 유사하게 HTTP 요청과 응답 객체를 직접 다루는 방식도 사용되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Spring MVC 내부에서 Handler는 단순히 우리가 흔히 사용하는 Controller 클래스만을 의미하지 않는다. &lt;b&gt;요청을 처리할 수 있는 객체라면 모두 Handler가 될 수 있으며, Spring은 이러한 다양한 구조를 동일한 요청 처리 흐름 안에서 동작할 수 있도록 지원한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 DispatcherServlet 은 Handler를 직접 실행하지 않고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 Handler 유형에 맞는 실행 방식을 연결해주는 HandlerAdapter 를 통해 요청을 처리&lt;/b&gt;하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 코드 (HandlerMapping)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 코드를 보면서 이해해보자 .&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 실제 `doDispatch()` 메서드 내부에서의 중요 로직부 중 &lt;b&gt;HandlerMapping&lt;/b&gt; 단계에 해당하는 로직부이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777189881630&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
    noHandlerFound(processedRequest, response);
    return;
}&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;위 코드를 자세히 살펴보면, 가장 먼저 실행되는 메서드는 `getHandler()`이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드는 현재 요청을 처리할 수 있는 Handler를 찾는 역할을 수행한다.&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;`getHandler()` 메서드&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DispatcherServlet 의 `getHandler()`&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777189918456&quot; class=&quot;aspectj&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;protected @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
    for (HandlerMapping mapping : this.handlerMappings) {
        HandlerExecutionChain handler = mapping.getHandler(request);
        if (handler != null) {
            return handler;
        }
    }
}
return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록되어있는 모든 HandlerMapping들을 조회해 해당 요청을 처리할 수 있는 Handler를 찾는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HandlerMapping의 `getHandler()`&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777189896434&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
public final @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    ApiVersionHolder versionHolder = initApiVersion(request);
    Object handler = getHandlerInternal(request);
    if (handler == null) {
        handler = getDefaultHandler();
    }
    if (handler == null) {
        return null;
    }

    if (versionHolder.hasError() &amp;amp;&amp;amp; !request.getDispatcherType().equals(DispatcherType.ERROR)) {
        throw versionHolder.getError();
    }

    // Bean name or resolved handler?
    if (handler instanceof String handlerName) {
        handler = obtainApplicationContext().getBean(handlerName);
    }

    // Ensure presence of cached lookupPath for interceptors and others
    if (!ServletRequestPathUtils.hasCachedPath(request)) {
        initLookupPath(request);
    }

    HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

    if (request.getAttribute(SUPPRESS_LOGGING_ATTRIBUTE) == null) {
        if (logger.isTraceEnabled()) {
            logger.trace(&quot;Mapped to &quot; + handler);
        }
        else if (logger.isDebugEnabled() &amp;amp;&amp;amp; !DispatcherType.ASYNC.equals(request.getDispatcherType())) {
            logger.debug(&quot;Mapped to &quot; + executionChain.getHandler());
        }
    }

    if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
        CorsConfiguration config = getCorsConfiguration(handler, request);
        if (getCorsConfigurationSource() != null) {
            CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
            config = (globalConfig != null ? globalConfig.combine(config) : config);
        }
        if (config != null) {
            config.validateAllowCredentials();
            config.validateAllowPrivateNetwork();
        }
        executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
    }

    return executionChain;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 메서드에서는 현재 요청을 처리할 Handler를 찾고 실행에 필요한 정보를 구성하는 역할을 한다.&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;내부적으로 `getHandlerInternal()`을 호출해 요청 URL과 매핑 정보를 기반으로 Handler를 탐색하며, 매칭된 Handler가 없으면 기본 Handler를 확인한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777191372336&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
protected @Nullable HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    String lookupPath = initLookupPath(request);
    this.mappingRegistry.acquireReadLock();
    try {
        HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    }
    finally {
        this.mappingRegistry.releaseReadLock();
    }
}&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;`getHandlerInternal()` 메서드 내부를 살펴보면, 실제로 요청 URL을 기반으로 Handler를 탐색하는 과정이 이루어진다.&lt;br /&gt;이 과정에서 현재 요청 경로를 추출하고, Spring이 애플리케이션을 시작하는 시점에 따라 미리 저장해둔 매핑 정보와 비교하여 일치하는 Handler를 찾는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탐색 과정은 다음과 같이 이루어진다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;`@RequestMapping`이 선언된 Controller와 메서드를 분석한다.&lt;/li&gt;
&lt;li&gt;애플리케이션 초기화 과정에서 URL, HTTP Method, 파라미터 조건 등의 매핑 정보를 생성한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;생성된 매핑 정보는 내부 저장소인 ``Mapping Registry``를 조회하여 현재 요청과 일치하는 Handler를 탐색한다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 요청이 들어올 때 마다 Controller를 새로 탐색하는 것이 아니라,&amp;nbsp;&lt;br /&gt;이미 등록된 매핑 정보를 기반으로 현재 요청 조건과 일치하는 Handler를 조회하는 방식이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777191511906&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protected String initLookupPath(HttpServletRequest request) {
    if (usesPathPatterns()) {
        request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);
        RequestPath requestPath = getRequestPath(request);
        String lookupPath = requestPath.pathWithinApplication().value();
        return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
    }
    else {
        return getUrlPathHelper().resolveAndCacheLookupPath(request);
    }
}&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;이렇게 Handler 탐색이 완료되면, 단순히 Controller 객체만 반환되는 것이 아니라,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 요청에 적용될 interceptor 정보까지 포함된 ``HandlerExecutionChain`` 이 생성되어 DispatcherServlet으로 전달된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 일치하는 Handler가 존재하지 않는 경웨는 `null`을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 DispatcherServlet은 해당 요청을 처리할 Handler가 없다고 판단하고, `notHandlerFound()`을 호출하여 `404(NOT FOUND)` 응답을 생성하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;실제 코드 (HandlerAdapter)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 interceptor의 `preHandle()` 메서드 호출 이후 요청에 맞는 HandlerAdapter를 찾는 과정이다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777194130661&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Determine handler adapter and invoke the handler.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
    return;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;`getHandlerAdapter()` 메서드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DispatcherServlet 의 `getHandlerAdapter()`&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777194700548&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    if (this.handlerAdapters != null) {
        for (HandlerAdapter adapter : this.handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
    }
    throw new ServletException(&quot;No adapter for handler [&quot; + handler +
            &quot;]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 등록된 ``HandlerAdapter`` 목록을 순회하면서 각 Adapter의 `supports()` 메서드를 호출해 현재 Handler를 처리할 수 잇는지 확인한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring MVC에는 다양한 형태의 Handler를 지원하기 위해 여러 종류의 HandlerAdapter 가 존재한다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;`RequestMappingHandlerAdapter` : `@&lt;/b&gt;RequestMapping` 어노테이션으로 구현한 Handler를 처리한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;`HandlerFunctionAdapter`&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;: 함수형 Handler를 처리한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;`HttpRequestHandlerAdapter`&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;: HttpRequestHandler를 상속한 Handler를 처리한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;`SimpleControllerHandlerAdapter`&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;: Controller를 상속한 Handler를 처리한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Spring MVC에서 가장 일반적으로 사용하는 방식은 `&lt;span&gt;@Controller`&lt;/span&gt;, `&lt;span&gt;@RestController`&lt;/span&gt;, `&lt;span&gt;@RequestMapping`&lt;/span&gt; 기반의 Annotation 방식이다. 따라서 대부분의 요청은 ``&lt;span&gt;RequestMappingHandlerAdapter``&lt;/span&gt;&amp;nbsp;를 통해 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Annotation 기반 Controller가 Handler로 선택되었다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``RequestMappingHandlerAdapter``의 `supports()` 메서드가 ``true``를 반환하게 되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet은 해당 Adapter를 선택하고, 실제 요청 처리를 위해 `handle()` 메서드를 호출한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, HandlerAdapter는 단순히 Adapter를 선택하는 역할에서 끝나는 것이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택된 Handler를 실제로 실행하는 진입 지점 역할까지 담당한다.&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;이때 만약 어떠한 Adapter도 현재 Handler를 지원하지 않는다면, 최종적으로 예외가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 실행할 Handler는 찾았지만, 이를 처리할 수 있는 Adapter가 존재하지 않는 상황을 의미한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 DispatcherServlet 내부 흐름을 따라가며 HandlerMapping과 HandlerAdatper가 어떤 방식으로 동작하는지 살폅왔다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 &quot;Controller를 찾고 실행하는 과정&quot; 정도로 보일 수 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 내부 구조를 따라가 보면 Spring MVC는 요청을 처리하기 위해 여러 역할을 명확하게 분리하고 있다는 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 HandlerMApping은 현재 요청을 처리할 Handler를 찾는 역할을 수행하고, HandlerAdapter는 해당 Handler를 실제로 실행할 수 있도록 연결해주는 역할을 담당한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조 덕분에 Spring MVC는 다양한 Handler 구현 방식을 동일한 흐름 안에서 처리할 수 있으며, 내부 확장성과 유연성을 확보할 수 있다.&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;그리고 실제 Handler가 실행되는 단계 내부로 들어가 보면, &lt;br /&gt;단순 메서드 호출이 아니라 파라미터 바인딩, Model 생성, Reflection 기반 메서드 호출, Proxy와 AOP까지 이어지는 흐름이 존재한다.&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;다음 글에서는 이러한 실행 과정의 내부 구조를 따라가며, Spring이 Bean과 AOP를 기반으로 실제 요청을 어떻게 처리하는지 조금 더 깊게 정리해보려 한다.&lt;/p&gt;</description>
      <category>Language &amp;amp; Framework/Spring Boot</category>
      <category>DispatcherServlet</category>
      <category>doDispatch</category>
      <category>getHandler</category>
      <category>getHandlerAdapter</category>
      <category>HandlerAdapter</category>
      <category>handlerMapping</category>
      <category>spring</category>
      <category>spring boot</category>
      <category>핸들러</category>
      <category>흐름</category>
      <author>코딩 기록하는 애기 개발자</author>
      <guid isPermaLink="true">https://baby-developer77.tistory.com/29</guid>
      <comments>https://baby-developer77.tistory.com/29#entry29comment</comments>
      <pubDate>Sun, 26 Apr 2026 18:58:24 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] Spring의 전체 흐름 알아보기 - 4. Servlet (HttpServlet, FrameworkServlet, DispatcherServlet)</title>
      <link>https://baby-developer77.tistory.com/28</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 Interceptor를 중심으로 요청 처리 흐름과 실행 구조를 살펴보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Spring MVC에서 요청이 어떻게 전&amp;middot;후 처리되는지 전반적인 흐름을 이해할 수 있었다.&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&gt;&lt;b&gt;Servlet&lt;/b&gt;&lt;/span&gt;에 대한 이해가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC는 결국 Servlet 위에서 동작하는 구조이며, DispatcherServlet을 중심으로 요청을 처리한다.&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;이번 글에서는 Servlet이 무엇인지 살펴보며 Spring MVC의 내부 동작 구조를 한 단계 더 깊이 이해해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Servlet 이란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청을 받아 처리하고, 그 결과를 응답으로 반환하는 자바 기반의 웹 컴포넌트이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿은 서버에서 대기하고 있다가 클라이언트 (브라우저)의 요청이 들어오면 해당 요청을 처리하고 그 결과를 응답으로 반환한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Servlet 의 동작과정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEtD0A/dJMb997aAXa/IftkHH33HfFTiaSFCNY50k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEtD0A/dJMb997aAXa/IftkHH33HfFTiaSFCNY50k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEtD0A/dJMb997aAXa/IftkHH33HfFTiaSFCNY50k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEtD0A%2FdJMb997aAXa%2FIftkHH33HfFTiaSFCNY50k%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;1180&quot; height=&quot;537&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청이 들어오면 WAS(Tomcat)가 이를 받아 적절한 Servlet을 실행하고, 요청을 처리한 뒤 그 결과를 응답으로 반환한다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 웹 서버에 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;웹 컨테이너 (Tomcat) 가 요청을 받아 내부에서 처리를 시작한다.&lt;/li&gt;
&lt;li&gt;Tomcat이 `HttpServletRequest`와 `HttpServletResponse` 객체를 생성한다.&lt;/li&gt;
&lt;li&gt;요청 URL에 맞는 Servlet을 찾는다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;최초 요청 시&lt;/b&gt; &lt;/span&gt;해당 Servlet이 생성되고 `init()`이 1회 호출된다.&lt;/li&gt;
&lt;li&gt;이후 요청이 들어올 때 마다 `service()`가 호출된다.&lt;/li&gt;
&lt;li&gt;`doGet()` 또는 `doPost()`에서 실제 비즈니스 로직을 수행한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;처리 결과를 `HttpServletResponse`에 담아 클라이언트에 응답한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;서버 종료 시 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`destroy()`를 실행해 자원을 정리한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 글들에서 DispatcherServlet이 요청 처리의 핵심이라는 점을 간접적으로 언급했지만,&amp;nbsp;&lt;br /&gt;이를 제대로 이해하기 위해서는 그 기반이 되는 구조를 먼저 짚고 넘어갈 필요가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 DispatcherServlet은 단순히 독립적으로 존재하는 것이 아니라,&amp;nbsp;&lt;br /&gt;Java의 표전 Servlet인 HttpServlet을 기반으로, Spring이 확장한 FrameworkServlet을 거쳐 구현된 구조이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이번에는 DispatcherServlet을 바로 살펴보기보단,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 상위 구조인 HttpServlet과 FrameworkServlet이 어떤 역할을 하는지 먼저 이해해보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HttpServlet&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 Servlet이 요청과&amp;nbsp; 응답을 처리하는 웹 컴포넌트라는 점을 살펴봉ㅆ다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 웹 애플리케이션에서는 단순한 요청 처리뿐 아니라, HTTP 프로토콜에 맞는 세부적인 데이터 처리도 필요하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 등장한 것이 바로 &lt;b&gt;HttpServlet&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpServlet은 Servlet을 HTTP 프로토콜 환경에 맞게 확장한 클래스이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 웹 브라우저와 서버 간의 HTTP 통신을 보다 쉽게 처리할 수 있도록 기능이 추가된 Servlet 구현체라고 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Servlet은 요청을 처리하는 구조만 정의되어 있지만,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpServlet은 HTTP 요청 방식에 따라 요청을 구분하여 처리할 수 있는 기능을 제공한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat과 같은 Servlet Container는 클라이언트 요청이 들어오면 `service()` 메서드를 호출하고, &lt;br /&gt;HttpServlet 내부에서는 요청 방식에 따라 적절한 메서드로 분기된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 HttpServlet의 핵심은 하나다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot; HTTP Method (Get, POST 등)에 따라 적절한 메서드로 분기하는 것 &quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 실제 코드에서 가장 중요한 코드는 `service()` 메서드다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #24292e; color: #d1d5da;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    String method = req.getMethod();

    switch (method) {
        case METHOD_GET -&amp;gt; {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // servlet doesn't support if-modified-since, no reason
                // to go through further expensive logic
                doGet(req, resp);
            } else {
                long ifModifiedSince;
                try {
                    ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                } catch (IllegalArgumentException iae) {
                    // Invalid date header - proceed as if none was set
                    ifModifiedSince = -1;
                }
                if (ifModifiedSince &amp;lt; (lastModified / 1000 * 1000)) {
                    // If the servlet mod time is later, call doGet()
                    // Round down to the nearest second for a proper compare
                    // A ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        }
        case METHOD_HEAD -&amp;gt; {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        }
        case METHOD_POST -&amp;gt; doPost(req, resp);
        case METHOD_PUT -&amp;gt; doPut(req, resp);
        case METHOD_DELETE -&amp;gt; doDelete(req, resp);
        case METHOD_OPTIONS -&amp;gt; doOptions(req, resp);
        case METHOD_TRACE -&amp;gt; doTrace(req, resp);
        case METHOD_PATCH -&amp;gt; doPatch(req, resp);
        default -&amp;gt; {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //

            String errMsg = lStrings.getString(&quot;http.method_not_implemented&quot;);
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);

            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 요청이 들어오면 Servlet Container (Tomcat)는 `service()`를 호출한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 HttpServlet은 요청의 HTTP Method를 확인하고, 해당 메서드에 맞는 메서드로 분기한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, GET 요청이 들어오면, `doGet()` 을 호출하고, POST요청이 들어오면, `doPost()`를 호출하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Http Servlet은 단순히 요청을 처리하는 것이 아니라,&lt;b&gt;&lt;br /&gt;HTTP 요청 종류에 따라 적절한 메서드를 연결해주는 Dispatcher 의 역할을 수행&lt;/b&gt;한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드 중 GET 요청을 처리할 때의 로직이 궁금해 조금 더 살펴보았다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #24292e; color: #d1d5da;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;case METHOD_GET -&amp;gt; {
    long lastModified = getLastModified(req);
    if (lastModified == -1) {
        // servlet doesn't support if-modified-since, no reason
        // to go through further expensive logic
        doGet(req, resp);
    } else {
        long ifModifiedSince;
        try {
            ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
        } catch (IllegalArgumentException iae) {
            // Invalid date header - proceed as if none was set
            ifModifiedSince = -1;
        }
        if (ifModifiedSince &amp;lt; (lastModified / 1000 * 1000)) {
            // If the servlet mod time is later, call doGet()
            // Round down to the nearest second for a proper compare
            // A ifModifiedSince of -1 will always be less
            maybeSetLastModified(resp, lastModified);
            doGet(req, resp);
        } else {
            resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단, `lastModified` 라는 변수에 현재 요청된 리소스가 마지막으로 수정된 시간을 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 이미지 파일, HTML, JSON 데이터와 같은 리소스들의 최종 수정&amp;nbsp; 시각을 저장하는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 수정 시간을 지원하지 않으면 바로 `doGet()`을 호출한다.&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;이후, 브라우저가 가진 캐시 시간을 `getDateHedader(HEDAER_IFMODSINCE)` 를 통해 읽어온다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 브라우저는 요청을 보낼 때 캐시 시간을 함께 보낸다는 점을 알아야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 데이터가 더 최신인지 비교한 후, &lt;br /&gt;최신 데이터면 다시 응답하고, 최신 데이터가 아니면 304를 응답한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하면,&amp;nbsp;&lt;b&gt;GET 요청에서 브라우저 캐시와 서버 수정 시간을 비교하여 불필요한 응답을 막는 HTTP Cache Validation 로직&lt;/b&gt;이다 !!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FrameworkServlet&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpServlet은 HTTP 요청을 분기하는 역할까지는 수행하지만,&amp;nbsp;&lt;br /&gt;Spring MVC가 동작하기 위해서는 단순히 HTTP 처리 이상의 기능이 필요하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FrameworkServlet은 Spring MVC 전용 Servlet 기반 클래스로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단순히 HTTP 요청 만 처리하는 것이 아니라, Spring 기능을 Servlet에 연결하는 역할&lt;/b&gt;을 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;Servlet과 Spring Container 를 연결해주는 역할&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1776934253060&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    processRequest(request, response);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 `FrameworkServlet`의 실제 코드 중 일부이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpServlet에는 `doGet()`, `doPost()`가 각각 처리되었지만,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FrameworkServlet은 이 모든 요청을 최종적으로 `processRequest()` 메서드를 통해 처리한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;processRequest ()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 요청이 이 메서드로 처리되기 때문에 `FrameworkServlet`의 핵심은 ``processRequest()`` 메서드라고 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #24292e; color: #d1d5da;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
       throws ServletException, IOException {

    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;

    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = buildLocaleContext(request);

    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

    initContextHolders(request, localeContext, requestAttributes);

    try {
       doService(request, response);
    }
    catch (ServletException | IOException ex) {
       failureCause = ex;
       throw ex;
    }
    catch (Throwable ex) {
       failureCause = ex;
       throw new ServletException(&quot;Request processing failed: &quot; + ex, ex);
    }

    finally {
       resetContextHolders(request, previousLocaleContext, previousAttributes);
       if (requestAttributes != null) {
          requestAttributes.requestCompleted();
       }
       logResult(request, response, failureCause, asyncManager);
       publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 `processRequest()` 메서드의 실제 코드이다. 중요한 부분만 살펴보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단, LocaleContext 를 생성하게 되는데, 이는 사용자가 어떤 언어와 어떤 국가 환경으로 요청했는지를 담고 있는 context라고 생각하면 된다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 현재 요청에 대한 정보를 Spring 내부에서 사용할 수 있도록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``HttpServletRequest``와 ``HttpServletResponse`` 객체를 만들어 저장한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 저장된 정보는 요청이 처리되는 동안 유지되며, Spring 내부에서는 필요할 때 현재 요청 정보를 꺼내 사용할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 `doService()`가 호출되며, 실제 요청 처리가 시작된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 중요한 점은 &lt;b&gt;`FrameworkServlet`이 직접 요청을 처리하는 것이 아니라, &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이를 상속한 DispatcherServlet이 실제 요청 흐름을 담당한다&lt;/b&gt;는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DispatcherServlet&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 것처럼 ``FrameworkServlet``은 요청을 준비하고 Spring Context를 연결하는 역할을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로 요청을 어디로 보낼지 결정하고, Controller를 호출하며, 응답까지 만들어내는 핵심 역할은 ``DispatcherServlet``이 담당한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 단순히 요청을 전달하는 것이 아니라, 요청을 분석하고 적절한 Controller를 찾아 실행한 뒤, 최종 응답까지 만들어내는 역할을 수행한다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 코드 기반의 내부 흐름&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 들어오면 DispatcherServletd은 내부적으로 `doDispatch()` 메서드를 실행한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776936665463&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;protected void doDispatch(HttpServletRequest request, HttpServletResponse response)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드가 실제 Spring MVC 요청 처리의 중심이라고 볼수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`doDispatch()` 메서드 코드 중 중요한 부분만 살펴보자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776936759068&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mappedHandler = getHandler(processedRequest);&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;일단, 현재 요청 URL에 맞는 Controller를 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Controller === Handler` 라고 생각하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776936803234&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mappedHandler.applyPreHandle(processedRequest, response)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller 실행 전에 interceptor가 먼저 실행된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 인증 처리, 권한 검사, 공통 로깅 등이 수행될 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776936971522&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mv = ha.handle(processedRequest, response, mappedHandler.getHandler());&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;실제 Controller 메서드가 실행되는 부분 즉, 비즈니스 로직이 수행되는 부분이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776937035026&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;applyDefaultViewName(processedRequest, mv);&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;Controller가 View 이름을 지정하지 않았다면, 기본 View를 설정한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 REST API처럼 JSON 응답을 반환하는 경우에는 View를 렌더링하지 않기 때문에 이 과정은 사실상 동작하지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776937065890&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mappedHandler.applyPostHandle(processedRequest, response, mv);&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;Controller 실행 후, interceptor의 `postHandle()`이 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서는 Controller 로직은 이미 끝난 상태이며, Veiw가 렌더링 되기 전에 추가 작업을 수행할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776937101155&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;processDispatchResult(...)&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;Controller 실행 결과를 기반으로 최종 응답을 생성한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewResolver를 통해 View를 찾아 화면을 렌더링하거나,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API의 경우 객체를 JSON 형태로 변환하여 응답하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776937154091&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;triggerAfterCompletion(...)&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;모든 요청처리가 끝난 뒤 interceptor의 `afterCompletion()`이 실행된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 발생 여부와 관계없이 마지막에 호출되며,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 정리나 로그 기록과 같은 후처리 작업을 수행한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, DispatcherServlet은 단순히 Controller를&amp;nbsp; 호출하는 역할만 하는 것이 아니라,&amp;nbsp;&lt;br /&gt;``요청 &amp;rarr; Handler 탐색 &amp;rarr; interceptor 실행 &amp;rarr; Controller 호출 &amp;rarr; View/Response 생성 &amp;rarr; 후처리 ``&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지의 전체 요청 흐름을 조율하는 핵심 Dispatcher 역할을 수행한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 Servlet부터 DispatcherServlet 내부 흐름까지 살펴보니 내용이 꽤 깊어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( 파도 파도 끝이 없는 Spring의 세계란 .................... )&lt;br /&gt;&lt;br /&gt;처음에는&amp;nbsp;Handler까지&amp;nbsp;함께&amp;nbsp;정리하려&amp;nbsp;했지만,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청&amp;nbsp;흐름을&amp;nbsp;제대로&amp;nbsp;이해하려면&amp;nbsp;먼저&amp;nbsp;Servlet&amp;nbsp;기반&amp;nbsp;구조를&amp;nbsp;충분히&amp;nbsp;이해하는&amp;nbsp;것이&amp;nbsp;중요하다고&amp;nbsp;느꼈다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이번&amp;nbsp;글에서는&amp;nbsp;Servlet&amp;nbsp;&amp;rarr;&amp;nbsp;HttpServlet&amp;nbsp;&amp;rarr;&amp;nbsp;FrameworkServlet&amp;nbsp;&amp;rarr;&amp;nbsp;DispatcherServlet으로&amp;nbsp;이어지는&amp;nbsp;구조와,&amp;nbsp;&amp;nbsp;&lt;br /&gt;Spring&amp;nbsp;MVC가&amp;nbsp;요청을&amp;nbsp;어떤&amp;nbsp;흐름으로&amp;nbsp;처리하는지&amp;nbsp;전체적인&amp;nbsp;기반을&amp;nbsp;정리해보았다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;다음&amp;nbsp;글에서는&amp;nbsp;이번&amp;nbsp;흐름&amp;nbsp;속에서&amp;nbsp;등장했던&amp;nbsp;Handler가&amp;nbsp;실제로&amp;nbsp;어떤&amp;nbsp;역할을&amp;nbsp;하는지,&amp;nbsp;&amp;nbsp;&lt;br /&gt;그리고&amp;nbsp;HandlerMapping,&amp;nbsp;HandlerAdapter와&amp;nbsp;어떤&amp;nbsp;관계로&amp;nbsp;동작하는지&amp;nbsp;이어서&amp;nbsp;정리해보려&amp;nbsp;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;그리고 ... 꼭 코드를 다 살펴본 뒤 흐름을 다시 !!!!! 정리해야 할 것 같ㄷㅏ.. 너무 어려워요 !!!!!&amp;nbsp;&lt;/blockquote&gt;</description>
      <category>Language &amp;amp; Framework/Spring Boot</category>
      <category>DispatcherServlet</category>
      <category>FrameworkServlet</category>
      <category>HttpServlet</category>
      <category>Servlet</category>
      <category>spring</category>
      <category>spring boot</category>
      <category>Spring MVC</category>
      <category>서블릿</category>
      <author>코딩 기록하는 애기 개발자</author>
      <guid isPermaLink="true">https://baby-developer77.tistory.com/28</guid>
      <comments>https://baby-developer77.tistory.com/28#entry28comment</comments>
      <pubDate>Thu, 23 Apr 2026 19:00:14 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] Spring의 전체 흐름 알아보기 - 3. Interceptor</title>
      <link>https://baby-developer77.tistory.com/27</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 Filter를 통해 요청이 DispatcherServlet에 도달하기 전에 어떻게 가로채지고 처리되는지 살펴보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 공부하다 보면 Filter와 함께 자주 등장하는 개념이 하나 더 있는데, 바로 &lt;b&gt;Interceptor &lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interceptor 역시 요청을 가로채 처리한다는 점에서 Filter와 비슷해 보이지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 동작 위치와 역할에서 분명한 차이를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Interceptor가 무엇인지, 어떻게 동작하는지, 그리고 Filter와는 어떤 차이가 있는지 중심으로 살펴보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Interceptor 란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션 내에서 특정한 URI 호출을 가로채는 역할을 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interceptor를 활용하면 기존 컨트롤러의 로직을 수정하지 않고도, 사전이나 사후에 제어가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해,&amp;nbsp;&lt;b&gt;요청과 으답을 가로채서 원하는 동작을 추가&lt;/b&gt;하는 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 컨테이너에서 동작하는 필터와 달리 인터셉터는 Spring이 제공해주는 기술로, Spring 영역에서 동작한다.&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;주요 메서드&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;01 ``preHandle()``&lt;/h4&gt;
&lt;pre id=&quot;code_1776250248880&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception {

    return true;
}&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;`preHandle()`은 Controller(Handler)가 실행되기 전에 호출되는 메서드로, interceptor의 가장 핵심적인 진입 지점이다.&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;이 시점은 HandlerMapping을 통해 어떤 컨트롤러가 실행될지 결정된 이후이며, HandlerAdapter가 실제로 해당 핸들러를 호출하기 직전에 위치한다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 메서드는 요청을 본격적으로 처리하기 전에 공통적인 전처리를 수행하기에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메서드는 ``boolean`` 타입으로 true와 false를 반환하며, 각각의 반환값에 따라 실행 흐름이 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;``true`` : 다음 interceptor 또는 controller로 요청이 정상적으로 전달된다.&lt;/li&gt;
&lt;li&gt;``false`` : 실행 체인이 중단되며, controller는 호출되지 않는다. 이 경우, interceptor에서 직접 응답을 생성해야 한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Execution Chain&lt;/b&gt; 이란 ?&lt;br /&gt;요청이 Controller에 도달하기까지 거치는 전체 흐름을 의미한다.&lt;br /&gt;특히 Spring에서는 하나의 요청이&amp;nbsp;&lt;b&gt;여러 Interceptor를 순서대로 통과&lt;/b&gt;하면서 처리되는데, 이 흐름 전체를 실행 체인이라고 한다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;02 ``postHandle()``&lt;/h4&gt;
&lt;pre id=&quot;code_1776250308131&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
        @Nullable ModelAndView modelAndView) throws Exception {
}&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;Controller가 정상적으로 실행된 이후, View가 렌더링되기 전에 호출되는 메서드이다.&amp;nbsp;&lt;br /&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;이 메서드는 Controller가 반환한 ModelAndView 객체를 인자로 받아, View에 전달된 데이터를 수정하거나 공통 데이터를 추가하는 작업을 수행할 수 있다.&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;다만, 예외가 발생하여 정상적인 흐름이 아닌 경우에는 호출되지 않는다. 또한 여러 개의 interceptor가 체인으로 구성되어 있을 경우, `postHandle()` 은 요청이 들어온 순서와 반대로, 즉 역순으로 실행되는 특징을 가진다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;03 ``afterCompletion()``&lt;/h4&gt;
&lt;pre id=&quot;code_1776250352029&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
        @Nullable Exception ex) throws Exception {
}&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;요청 처리의 모든 과정이 완료된 이후, 즉 View 렌더링까지 끝난 시점에 호출되는 메서드이다.&amp;nbsp;&lt;br /&gt;이 메서드는 요청 처리 결과와 관계없이 항상 호출되며, 예외가 발생한 경우에도 호출된다는&amp;nbsp; 특징이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댠, `preHandle()`이 ``true``를 반환하여 정상적으로 실행 체인에 포함된 경우에만 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서는 더 이상 응답을 수정하기보다는, 리소스 정리나 예외 로깅과 같은 마무리 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 여러 interceptor가 존재할 경우, `afterCompletion()` 역시 `postHandle()`과 동일하게 역순으로 실행되어 먼저 진입한 interceptor가 가장 마지막에 종료 작업을 수행하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;04 ``addInterceptors()``&lt;/h4&gt;
&lt;div style=&quot;background-color: #24292e; color: #d1d5da;&quot;&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;default void addInterceptors(InterceptorRegistry registry) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller 실행 전/후에 개입하는 interceptor를 등록할 수 있는 메서드이다.&amp;nbsp;&lt;br /&gt;전체 요청에 적용하거나 특정 URL에만 적용하도록 구현할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 코드를 실행하며 알아보기&amp;nbsp;&lt;/h3&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignLeft&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=mh5hNlMnFDw&amp;amp;list=PLlTylS8uB2fBOi6uzvMpojFrNe7sRmlzU&amp;amp;index=44&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/RVCZu/dJMb9kmegGO/baIkvxwbUwsKCxBhpTnrqK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/xaFXh/dJMb9c9znZv/fOu0dIedPbyJ9u0i2ELank/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;어라운드 허브 스튜디오 - Around Hub Studio&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/mh5hNlMnFDw&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 실습 강의에서 작성된 코드를 기반으로 실습해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;01 ``preHandle()``&lt;/h4&gt;
&lt;div style=&quot;background-color: #24292e; color: #d1d5da;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception {
  LOGGER.info(&quot;[preHandle] preHandle is performed&quot;);
  LOGGER.info(&quot;[preHandle] request : {}&quot;, request);
  LOGGER.info(&quot;[preHandle] request path info : {}&quot;, request.getPathInfo());
  LOGGER.info(&quot;[preHandle] request header names : {}&quot;, request.getHeaderNames());
  LOGGER.info(&quot;[preHandle] request request URL : {}&quot;, request.getRequestURL());
  LOGGER.info(&quot;[preHandle] request request URI: {}&quot;, request.getRequestURI());
  LOGGER.info(&quot;[preHandle] request Requested Session Id : {}&quot;, request.getRequestedSessionId());

  // TODO HttpServletRequestWrapper 구현하여 Body 값 확인할 수 있게 코드 추가

  return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 `preHandle()` 메서드를 오버라이딩한 후, request 정보들을 로그로 출려해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 과정을 성공하면, ``true``를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;02 ``postHandle()``&lt;/h4&gt;
&lt;div style=&quot;background-color: #24292e; color: #d1d5da;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
public void postHandle(
    HttpServletRequest request,
    HttpServletResponse response,
    Object handler,
    ModelAndView modelAndView)
    throws Exception {
  LOGGER.info(&quot;[postHandle] postHandle is performed&quot;);
  LOGGER.info(&quot;[postHandle] response : {}&quot;, response);
  LOGGER.info(&quot;[postHandle] response : {}&quot;, response.getHeaderNames());
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`postHandle()`도 `preHandle()`과 마찬가지로 기존 메서드를 오버라이딩한 후, response와 헤더 정보들을 출력해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;03 ``afterCompletion()``&lt;/h4&gt;
&lt;div style=&quot;background-color: #24292e; color: #d1d5da;&quot;&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Override
public void afterCompletion(
    HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
    throws Exception {
  LOGGER.info(&quot;[afterCompletion] afterCompletion is performed&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`afterCompletion()` 또한 기존 메서드를 오버라딩한 후, 해당 메서드가 동작했는지 여부만 판단하기 위한 로그를 출력해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;04 ``addInterceptors()``&lt;/h4&gt;
&lt;div style=&quot;background-color: #24292e; color: #d1d5da;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Override
public void addInterceptors(InterceptorRegistry registry) {
  registry
      .addInterceptor(new HttpInterceptor())
      .addPathPatterns(&quot;/**&quot;)
      .excludePathPatterns(&quot;/hello&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`addInterceptor()`를 오버라이딩한 후,&amp;nbsp;구현해둔 ``HttpInterceptor`` 를 추가해주고,&lt;br /&gt;`/hello` 를 제외한 모든 엔드포인트에 대하여 해당 interceptor를 실행하도록 구현해주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 결과&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;01 ``/hello``를 호출했을 경우&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2082&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DXDzl/dJMcaartH0r/m451JZS1SuZSixAcElfpkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DXDzl/dJMcaartH0r/m451JZS1SuZSixAcElfpkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DXDzl/dJMcaartH0r/m451JZS1SuZSixAcElfpkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDXDzl%2FdJMcaartH0r%2Fm451JZS1SuZSixAcElfpkK%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;2082&quot; height=&quot;220&quot; data-origin-width=&quot;2082&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DispatcherServlet 이 생성된다.&amp;nbsp;&lt;br /&gt;DispatcherServlet는 첫 요청을 받을 때만 생성된다. 이후, ``/hello``를 한 번 더 요청했을 경우에는 생성되지 않는다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;내부 구성 초기화가 완료된다.&lt;br /&gt;이 단계에서는 HandlerMapping이 생성되고, HandlerAdapter가 생성되며, Interceptor 목록을 로딩하여 Controller 매핑을 준비하는 단계이다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 실행에서는 interceptor 에 관련된 로그가 하나도 찍히지 않는 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 WebMvcConfig 파일을 보면 알 수 있는데, `addInterceptors()` 메서드에서 `/hello` 경로를 제외했기 때문에 해당 요청에서는 interceptor가 실행되지 않는다.&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;따라서, interceptor 체인을 거치지 않기 때문에 `preHandle`, `postHandle`, `afterCompletion`에 정의한 로그가 전혀 출력되지 않는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;02 ``/hello1``를 호출했을 경우&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2594&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sWm1Z/dJMcabw7DUp/6E4bTkMUa2GUtBZqNgOEKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sWm1Z/dJMcabw7DUp/6E4bTkMUa2GUtBZqNgOEKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sWm1Z/dJMcabw7DUp/6E4bTkMUa2GUtBZqNgOEKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsWm1Z%2FdJMcabw7DUp%2F6E4bTkMUa2GUtBZqNgOEKk%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;2594&quot; height=&quot;588&quot; data-origin-width=&quot;2594&quot; data-origin-height=&quot;588&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;반면 `/hello1`를 호출하면, 이는 excludePathPatterns에 포함되지 않은 경로이기 때문에 interceptor가 정상적으로 동작하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;`preHandle()` 이 호출되며, Controller 실행 전에 요청에 대한 정보를 확인하거나 전처리를 수행할 수 있다.&lt;/li&gt;
&lt;li&gt;Controller 로직이 실행된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Controller 로직 실행이 완료되면, `postHandle()` 이 호출된다.&amp;nbsp;&lt;br /&gt;이 단계에서는 응답이 생성되었지만 아직 클라이언트로 전달되기 전이기 때문에 추가적인 가공이 가능하다.&lt;/li&gt;
&lt;li&gt;응답이 완료된 이후 `afterCompletion()`이 호출되며, 전체 요청 처리 과정이 마무리된다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 로그를 보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``preHandle -&amp;gt; postHandle -&amp;gt; afterCompletion`` 순서로 출력되는 것을 확인할 수 있으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 interceptor가 요청의 전/후 처리 과정에 개입하고 있음을 알 수 잇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 `/hello` 요청은 interceptor가 제외된 흐름을 따르고,&lt;br /&gt;`/hello1` 요청은 interceptor 체인을 거치는 흐름을 따르게 되며, &lt;br /&gt;이 차이를 통해 interceptor의 동작 위치와 역할을 명확하게 이해할 수 있다.&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;interceptor의 전체 흐름&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVI8QD/dJMcaf0w9sz/Fqedzqs7Nw3csBQE3ldQK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVI8QD/dJMcaf0w9sz/Fqedzqs7Nw3csBQE3ldQK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVI8QD/dJMcaf0w9sz/Fqedzqs7Nw3csBQE3ldQK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVI8QD%2FdJMcaf0w9sz%2FFqedzqs7Nw3csBQE3ldQK1%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;601&quot; height=&quot;316&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;734&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;이제 코드 관점에서 Interceptor가 포함된 전체 요청 처리 흐름을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;애플리케이션이 시작되면 사전 준비 단계를 거치게 된다.&amp;nbsp;&lt;br /&gt;서버가 실행되면 Spring은 설정 파일을 먼저 읽어 필요한 객체들을 생성한다.&lt;br /&gt;`@Configuration` 클래스를 로딩하고, `WebConfigurer` 구현체를 확인한 후, `addInterceptors()` 메서드를 실행한다. 이 과정에서 interceptor가 어떤 경로에 적용될지 미리 등록된다.&lt;/li&gt;
&lt;li&gt;Client가 요청한다.&amp;nbsp;&lt;br /&gt;사용자가 브라우저 또는 클라이언트를 통해 특정 URL로 HTTP 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;Tomcat (WAS)이 실행된다.&lt;br /&gt;Apache Tomcat이 요청을 수신하고, HttpServletRequest와 HttpServletResponse 객체를 생성한다.&lt;/li&gt;
&lt;li&gt;DispatcherServlet이 실행된다.&lt;br /&gt;DispatcherServlet이 요청을 받아 전체 흐름을 제어하는 역할을 수행한다. 이는 프론트 컨트롤러의 역할이다.&lt;/li&gt;
&lt;li&gt;HandlerMapping을 통해 Controller를 찾는다.&lt;br /&gt;요청 URL에 매핑된 Controller를 탐색하며, 이 과정에서 Controller와 Interceptor가 함께 묶인 실행 체인 (HandlerExecutionChain)이 생성된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Interceptor의 `preHandle()`이 호출된다.&amp;nbsp;&lt;br /&gt;Controller 실행 전에 호출되며, 요청 로그 처리, 인증/인가, 요청 차단 등의 전처리를 수행할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Controller가 실행된다.&lt;br /&gt;실제 비즈니스 로직이 수행되는 단계로, 요청을 처리하고 View 이름 또는 데이터를 반환한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Interceptor의 `postHandle()`이 호출된다.&amp;nbsp;&lt;br /&gt;Controller가 실행된 이후 호출되며, 응답이 생성되었지만 아직 클라이언트로 전달되기 전 단계이다.&amp;nbsp;&lt;br /&gt;이 시점에서 Model 데이터 추가 및 View 변경 등의 후처리가 가능하다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;View or JSON으로 변환한다.&lt;br /&gt;반환 값에 따라 ViewResolver를 통해 HTML이 생성되거나, HttpMessageConverter를 통해 JSON으로 변환된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Interceptor의 `afterCompletion()`이 호출된다.&lt;br /&gt;응답까지 완료된 이후 실행되며, 리소스 정리, 예외처리, 최종로그 기록 등에 사용된다&lt;/li&gt;
&lt;li&gt;응답이 반환된다.&lt;br /&gt;최종적으로 생성된 응답이 Tomcat을 통해 클라이언트에게 전달된다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 intercptor의 개념, 주요 메서드, 그리고 실제 코드 실행 흐름까지 단계별로 살펴보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 Interceptor는 요청 처리 흐름의 전반에 개입하여 공통 로직을 효율적으로 분리할 수 있는 핵심 요소이다.&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;특히 Controller를 수정하지 않고도 전처리와 후처리를 수행할 수 있다는 점에서 실무에서 활용도가 매우 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC의 전체 흐름 속에서 Interceptor의 위치를 이해해두면, 보다 구조적인 설계가 가능해진다.&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;다음 글에서는 Servlet과 Handler에 대해 좀 더 자세히 살펴보도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Language &amp;amp; Framework/Spring Boot</category>
      <category>afterCompletion</category>
      <category>Interceptor</category>
      <category>mvc</category>
      <category>postHandle</category>
      <category>preHandle</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>springMVC</category>
      <category>인터셉터</category>
      <author>코딩 기록하는 애기 개발자</author>
      <guid isPermaLink="true">https://baby-developer77.tistory.com/27</guid>
      <comments>https://baby-developer77.tistory.com/27#entry27comment</comments>
      <pubDate>Mon, 20 Apr 2026 14:09:21 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] Spring의 전체 흐름 알아보기 - 02. Filter</title>
      <link>https://baby-developer77.tistory.com/26</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 Spring MVC의 전체적인 요청 처리 흐름을 중심으로, &lt;br /&gt;요청이 어떻게 Controller까지 전달되고, 어떤 과정을 거쳐 응답이 생성되는지에 대해 살펴보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 흐름을 따라가다 보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC와 함께 자주 듣게 되는 Filter에 대해서도 공부해봐야겠다는 생각이 들었다.&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;또한 이전에 Figma로 그려본 Spring MVC 구조를 기준으로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 요소가 실제로 어느 위치에서 동작하는지도 함께 정리해보고 싶었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Filter 란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet에 도달하기 전, 요청을 가로채 로직을 수행하는 객체를 말한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버로 요청이 들어오는 경우, `Dispatcher Servlet`을 거치기 전에 요청 사항에 대한 공통 관심사를 처리하기 위한 필터 로직을 수행한다. 대표적으로 `Spring Security`를 활용하여 요청에 대한 필터 기반 인증, 인가 처리를 수행하기 위해 주로 많이 사용된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776182865102&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Filter {&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter는 &lt;b&gt;Servlet 또는 정적 리소스에 대한 요청이나 응답을 중간에서 가로채서 필요한 처리를 수행하는 객체&lt;/b&gt;이다.&amp;nbsp;&lt;br /&gt;즉, 요청이 Controller까지 들어가기 전이나, 응답이 클라이언트로 나가기 전에 공통적인 작업을 수행할 수 있는 역할을 한다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이러한 실제 필터링 로직은 `doFilter()`메서드에서 이루어지며, 인증, 로깅 및 데이터 변환과 같은 공통 기능들이 주로 이곳에서 처리된다. 또한, ``FilterConfig``를&amp;nbsp; 통해 초기화 설정 값을 가져오거나 ``ServletContext``에 접근하여 필요한 리소스를 사용할 수도 있다.&lt;br /&gt;&lt;br /&gt;이처럼 Filter는 단순히 요청을 전달하는 것이 아니라, 웹 애플리케이션 전반에서 공통 관심사를 처리하기 위해 활용되는 중요한 컴포넌트이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;ServletContext&lt;/b&gt; 란?&amp;nbsp;&lt;br /&gt;웹 애플리케이션 전체를 대표하는 객체로, 쉽게 말하면&amp;nbsp;&lt;b&gt;서버 안에서 모든 요청이 함께 공유하는 공간&lt;/b&gt;이라고 생각하면 된다.&lt;br /&gt;Tomcat 같은 웹 컨테이너가 애플리케이션을 처음 실행할 때 딱 한 번 생성되며, 애플리케이션이 종료될 때까지 하나만 존재한다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이 객체를 통해 모든 서블릿이나 필터는 공통된 데이터를 저장하거나 가져다 사용할 수 있다.&amp;nbsp;&lt;br /&gt;&amp;gt;&amp;gt; React 의 Context API 와 굉장히 비슷한 것 같다 !!&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼 Filter는 어떻게 동작할까 ?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;726&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbU7jw/dJMcahD2dLi/hK9ODkR1TMtVUMZSflDS4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbU7jw/dJMcahD2dLi/hK9ODkR1TMtVUMZSflDS4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbU7jw/dJMcahD2dLi/hK9ODkR1TMtVUMZSflDS4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbU7jw%2FdJMcahD2dLi%2FhK9ODkR1TMtVUMZSflDS4K%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;511&quot; height=&quot;284&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;726&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;DispatcherServlet은 Spring의 가장 앞단에 존재하는&amp;nbsp;&lt;b&gt;Front Controller&lt;/b&gt;이므로 Filter는 Spring 밖에서 처리가 된다.&lt;br /&gt;즉, Spring Container가 아닌 Tomcat과 같은 Web Container 에 의해 관리가 되는 것이고, DispatcherServlet 전/후 에 처리하는 것이다.&amp;nbsp;&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 서버에 HTTP 요청을 보내면, 서버에 도착한 요청은 먼저 Filter를 거치게 된다.&lt;/li&gt;
&lt;li&gt;Filter는 요청을 가로채 여러가지 작업을 수행한다.&lt;br /&gt;이러한 Filter의 동작 흐름은 상황에 따라 다음과 같이 나눌 수 있다.
&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;: 작업을 수행한 후 DispatcherServlet으로 전달한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유효하지 않은 요청인 경우&amp;nbsp;&lt;/b&gt;: DispatcherServlet을 거치지 않고 Filter 내에서 유효하지 못한 요청에 대한 응답을 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DispatcherServlet은 전달받은 요청을 기반으로 이후의 과정을 수행한다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Filter가 여러 개인 경우에는 어떻게 동작할까 ?&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림을 보면 &lt;b&gt;Filter Chain&lt;/b&gt; 이라는 개념과 함께 여러 개의 Filter가 하나의 흐름으로 묶여있는 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter는 하나만 동작하는 것이 아니라, 여러 개가 순서대로 연결된 Chain 구조로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청이 들어오면 첫번째 Filter부터 순차적으로 실행되며, 각 Filter는 FilterChain을 통해 다음 Filter로 요청을 전달한다.&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;이러한 과정을 거쳐 마지막에는 DispatcherServlet까지 요청이 전달된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2852&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF0s6m/dJMcagd6bHo/N5400AAUefmdykaaPVh1HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF0s6m/dJMcagd6bHo/N5400AAUefmdykaaPVh1HK/img.png&quot; data-alt=&quot;실제 코드를 실행했을 때의 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF0s6m/dJMcagd6bHo/N5400AAUefmdykaaPVh1HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF0s6m%2FdJMcagd6bHo%2FN5400AAUefmdykaaPVh1HK%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;2852&quot; height=&quot;778&quot; data-origin-width=&quot;2852&quot; data-origin-height=&quot;778&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 코드를 실행했을 때의 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 코드를 디버깅 했을 때, 위와 같이 Filter들이 체인으로 구성된 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gq9I5/dJMcabRm3VZ/aK14tFElMKOOW8CrIJ8t8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gq9I5/dJMcabRm3VZ/aK14tFElMKOOW8CrIJ8t8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gq9I5/dJMcabRm3VZ/aK14tFElMKOOW8CrIJ8t8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGq9I5%2FdJMcabRm3VZ%2FaK14tFElMKOOW8CrIJ8t8k%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;1466&quot; height=&quot;560&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;560&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;실제로 직접 만든 Filter 코드 내부에서&amp;nbsp;`&lt;span style=&quot;color: #8a3db6;&quot;&gt;doFilter&lt;/span&gt;(&lt;span style=&quot;color: #f89009;&quot;&gt;request&lt;/span&gt;, &lt;span style=&quot;color: #f89009;&quot;&gt;response&lt;/span&gt;)` 메서드가 호출되는 것을 볼 수 있는데,&amp;nbsp;&lt;br /&gt;이 코드는 현재 Filter에서 처리를 마친 뒤, 다음 Filter로 요청과 응답을 넘기는 역할을 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Filter의 주요 메서드&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter는 위에서 설명했듯이 클라이언트의 요청을 가로채 다양한 작업을 수행한다. &lt;br /&gt;이러한 Filter의 동작은 정해진 생명주기와 주요 메서드를 통해 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;01 ``init()``&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필터 객체를 초기화하고 서비스에 등록하기 위한 메서드&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터가 생성될 때 한 번 번 호출되며, 초기화 이후 들어오는 모든 요청은 `doFilter()` 메서드를 통해 처리된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776182804036&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default void init(FilterConfig filterConfig) throws ServletException { }&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;Tomcat과 같은 서버가 Filter 객체를 생성한 뒤, 해당 Filter가 정상적으로 동작할수 있도록 준비시키기 위해 위 `init()` 메서드를 딱 한 번 호출한다. 이 과정은 Filter가 실제 요청을 처리하기 전에 반드시 먼저 완료되어야 하는 초기화 단계이다. &lt;br /&gt;&lt;br /&gt;만약 `init()` 메서드 실행 중에 ``ServletException``이 발생하거나,&lt;br /&gt;웹 컨테이너가 정해둔 시간 안에 메서드가 끝나지 않으면 해당 Filter는 정상적으로 서비스에 등록되지 못한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 `init()`은 아무 작업도 하지 않는 빈 구현 상태로 제공되며,&amp;nbsp;&lt;br /&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;또한, ``filterConfig``를 통해 해당 Filter의 설정 정보나 초기화 파라미터를 가져올 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;02 ``doFilter()``&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라이언트의 요청 및 응답을 처리할 때 실행되는 메서드&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 들어오면 이를 가로채 인증, 인가, 로깅 등을 수행할 수 있으며, &lt;br /&gt;`FilterChain`을 통해 다음 Filter 또는 DispatcherServlet으로 요청을 전달한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 조건에 따라 요청을 전달하지 않고 해당 메서드 내에서 바로 응답을 반환할 수도 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776182911117&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;  void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;&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;`doFilter()` 메서드는 클라이언트의 요청과 응답이 Filter를 통과할 때마다 웹 컨테이너에 의해 호출되는 핵심 메서드이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드 안에서는 Filter를 들어온 요청을 검사하거나, 필요에 따라 요청과 응답 객체를 감싸서 내용을 수정할 수 있다.&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;이후 ``FilterChain``을 통해 다음 Filter 또는 DispatcherServlet으로 요청을 전달할 수 있으며, &lt;br /&gt;반대로 `chain.doFilter()`를 호출하지 않으면 요청을 다음 단계로 넘기지 않고 여기서 차단할 수도 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, `doFilter()`는 요청을 전처리하고, 다음 단계로 전달하며, 후처리까지 하는 전체 흐름을 제어하는 역할을 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;03 ``destroy()``&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필터 종료 메서드로 서블릿 컨테이너가 종료될 때, 즉 필터 인스턴스가 제거될 때 호출되는 메서드&lt;/b&gt;이다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776153460942&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public default void destroy() {}&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;`destroy()` 메서드는 웹 컨테이너가 Filter를 더 이상 사용하지 않을 때, 즉 서비스에서 제거될 때 호출되는 메서드이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드는 Filter 내부에서 실행중이던 모든 `doFilter()` 작업이 끝난 이후에 단 한 번만 실행되며, 이후에는 해당 Filter 인스턴스의 `doFilter()`는 다시 호출되지 않는다.&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;주로 이 시점에는 Filter가 사용하는 자원들을 정리하거나, 필요한 상태를 저장하는 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 기본 구현은 `init()`과 마찬가지로 아무 동작도 하지 않는 빈 구현으로 제공되기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별히 정리할 자원이 없는 경우에는 별도로 구현하지 않아도 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드들을 보면 알다시피, 별도로 구현하지 않아도 되는 메서드인 `init()`과 `destroy()`에는 ``default`` 키워드가 붙어있다. 이는 해당 메서드에 이미 기본 구현이 포함되어 있음을 의미하며, 필요한 경우에만 오버라이드하여 사용할 수 있다. 따라서 개발자는 해당 메서드를 직접 구현하지 않더라도 컴파일이나 실행에는 문제가 없으며, 실제로는 필요한 경우에만 선택적으로 구현하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Filter에 대해서 알아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 Filter와 비슷한 역할을 하지만, Spring 내부에서 동작하는 ``Interceptor``에 대해 살펴보며&lt;br /&gt;두 개념의 차이와 각각 언제 사용하는 것이 적절한지에 대해 정리해보고자 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring ..............&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너 정말 파면 팔 수록 끝이 보이지 않는군아 ......  &lt;/p&gt;</description>
      <category>Language &amp;amp; Framework/Spring Boot</category>
      <category>Filter</category>
      <category>filterchain</category>
      <category>spring</category>
      <category>SpringBoot</category>
      <category>구조</category>
      <category>흐름</category>
      <author>코딩 기록하는 애기 개발자</author>
      <guid isPermaLink="true">https://baby-developer77.tistory.com/26</guid>
      <comments>https://baby-developer77.tistory.com/26#entry26comment</comments>
      <pubDate>Wed, 15 Apr 2026 01:55:07 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] Spring의 전체 흐름 알아보기 - 01. MVC 패턴과 Spring MVC 구조</title>
      <link>https://baby-developer77.tistory.com/25</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot로 웹 개발을 해보면, Controller를 만들고, API를 구현하는 과정은 생각보다 빠르게 익힐 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이면에서 &lt;b&gt;요청이 어떤 흐름을 거쳐처리되는지, DispatcherServlet이 어떤 역할을 하는지&lt;/b&gt; 는 정확히 이해하지 못한 채 사용하는 경우가 많다. (특히, 나 ...... )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;b&gt;&lt;i&gt;휴학도 했다 ! , Spring 워크북도 하고 있다 !&lt;/i&gt;&lt;/b&gt; &lt;/span&gt;이 참에 이 과정에 대해서 깊게 정리해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 가장 먼저 짚고 넘어가야 할 개념이 바로 &lt;b&gt;MVC 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC는 단순히 Spring MVC에서만 사용하는 개념이 아니라, Spring을 처음 공부할 때 반드시 이해하고 넘어가야 하는 핵심 개념이다.&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;이번 글에서는 Spring MVC의 흐름을 이해하기 위한 첫 단계로, &lt;b&gt;MVC 패턴의 개념과 역할&lt;/b&gt;에 대해 먼저 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MVC (Model-View-Controller) 란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC 패턴은 사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자의 인터페이스(UI)와 비즈니스 로직(도메인)을 분리&lt;/b&gt;하여 유지보수를 독립적으로 수행할 수 있게 하는 디자인 패턴이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 패턴이란,&lt;br /&gt;유지보수와 로직을 분리하고 코드를 재사용하는 등 개발에 있어 좀 더 쉽고 편리하게 사용할 수 있게 만든 특정한 방법들&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해, 한 파일에 모든 로직이 들어있는 코드를 부분 별로 나누는 패턴이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MVC 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC는 3가지 구성요소로 나눈다. 각각의 구성요소들 사이에는 다음과 같은 관계가 있다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`Controller` : Model과 View사이의 매개체로, View를 생성 및 관리하고, 사용자의 입력을 받아 Model의 상태를 직접 변경하기도 한다.&lt;/li&gt;
&lt;li&gt;`Model` : 도메인과 비즈니스 로직을 추상화하여 나타낸 객체로, 필요한 데이터들을 관리한다.&lt;/li&gt;
&lt;li&gt;`View` : 사용자에게 보여지는 것으로, 사용자의 인터페이스(UI)를 구현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 구조를 가진 시스템은 다음과 같은 과정으로 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자는 View를 통해 Model의 데이터를 확인한다.&lt;/li&gt;
&lt;li&gt;사용자는 Controller를 이용해 Model을 가공 및 변경한다.&lt;/li&gt;
&lt;li&gt;Model은 Controller에게서 받은 입력에 따라 값이나 상태를 변경한다.&lt;/li&gt;
&lt;li&gt;Model의 변경이 View에게 전달되어, View에서 보여지는 값이 업데이트된다.&lt;/li&gt;
&lt;li&gt;사용자는 변경된 데이터를 View를 통해 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 MVC 패턴이 무엇인지에 대해 간단하게 알아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 이 MVC 패턴은 실제 Spring 에서는 어떻게 적용되고 있는지 살펴보도록 하자. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;i&gt;(이게 진짜 주 목적이다 !!!)&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 은 MVC 패턴을 기반으로 웹 요청을 처리할 수 있도록 &lt;b&gt;Spring MVC&lt;/b&gt; 라는 구조를 제공하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 Spring MVC의 동작 과정을 중심으로 하나씩 정리해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring MVC 란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework에서 웹 애플리케이션을 위해 제공되는 Servlet API를 기반으로 구축된 최초의 웹 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC는 클라이언트의 요청을 편리하게 처리해주는 Servlet 기반 프레임워크라고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청을 처리하는 방식에 따라 &lt;b&gt;View 방식과 REST 방식&lt;/b&gt;으로 나눌 수 있는데,&lt;br /&gt;이 2가지 동작 방식을 살펴보며 좀 더 구체적으로 알아보도록 하자.&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;View 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View 방식은 서버가 비즈니스 로직을 처리한 후, &lt;b&gt;화면(View)까지 생성하여 클라이언트에게 반환하는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 클라이언트는 완성된 HTML을 전달받아 그대로 화면에 렌더링하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 작동 방식은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Spring MVC - Veiw 방식.jpg&quot; data-origin-width=&quot;2730&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO9fHg/dJMcacbEMMy/mjkBUm2ibW6xLhwkUiAqBk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO9fHg/dJMcacbEMMy/mjkBUm2ibW6xLhwkUiAqBk/img.jpg&quot; data-alt=&quot;출처 : 나 !!!! &amp;amp;gt;.&amp;amp;lt;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO9fHg/dJMcacbEMMy/mjkBUm2ibW6xLhwkUiAqBk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO9fHg%2FdJMcacbEMMy%2FmjkBUm2ibW6xLhwkUiAqBk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2730&quot; height=&quot;1174&quot; data-filename=&quot;Spring MVC - Veiw 방식.jpg&quot; data-origin-width=&quot;2730&quot; data-origin-height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 나 !!!! &amp;gt;.&amp;lt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Client가 특정 URL을 통해 HTTP Request가 들어오면, 이 요청은 DispatcherServlet으로 전달된다.&lt;br /&gt;이때, DispatcherServlet은 모든 요청을 가장 먼저 받는 &lt;i&gt;Front Controller&lt;/i&gt; 이다.&lt;/li&gt;
&lt;li&gt;DispatcherServlet은 요청을 처리할 수 있는 적합한 Controller를 찾기 위해 HandlerMapping에게 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;DispatcherServelt은 Handler를 실행할 수 있는 HandlerAdapter를 조회한 후, 이를 실행한다.&lt;/li&gt;
&lt;li&gt;HandlerAdapter는 Handler, 즉 Controller를 실행한다.&lt;/li&gt;
&lt;li&gt;Controller는 비즈니스 로직을 수행한다.&lt;/li&gt;
&lt;li&gt;비즈니스 로직을 수행한 후, 처리 결과를 반환한다.&lt;br /&gt;이때 처리 결과로 Model 데이터와 View 이름 등의 정보가 생성된다.&lt;/li&gt;
&lt;li&gt;Handler는 처리 결과를 HandlerAdapter로 전달한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;HandlerAdapter는 Handler가 반환한 결과를 ModelAndView 형태로 변환해 반환한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;i&gt;ModelAndView : Model과 View 정보를 함께 담는 객체&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;DispatcherServlet은 전달받은 View 정보를 다시 ViewResolver에게 전달해서 View 조회를 요청한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;ViewResolver는 해당 View를 찾아 DispatcherServlet에 다시 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;DispatcherServlet은 View 객체에 통해 Model 데이터를 전달하여 응답 생성을 요청한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;View는 응답 데이터를 생성하여 DispatcherServlet에게 전달한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;DispatcherServlet은 최종적으로 View로부터 전달받은 응답을 클라이언트에게 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REST 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 View 방식과 달리, REST 방식은 &lt;b&gt;서버가 화면을 생성하지 않고 데이터만 반환하는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 클라이언트가 요청을 보내면 서버는 비즈니스 로직을 수행한 후,&lt;br /&gt;View가 아닌 데이터 자체를 응답으로 전달하며, 클라이언트가 이를 기반으로 화면을 구성하게 된다.&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;위 흐름에서 Controller까지의 요청 처리 과정(7번까지의 과정)은 View 방식과 동일하며, 이후 응답을 처리하는 방식에서 차이가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Spring MVC - REST 방식.jpg&quot; data-origin-width=&quot;2758&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOP1J2/dJMcabRlgx6/Jop2mQ7D5hdnWFzs3qomH0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOP1J2/dJMcabRlgx6/Jop2mQ7D5hdnWFzs3qomH0/img.jpg&quot; data-alt=&quot;출처 : 나 !!!! &amp;amp;gt;.&amp;amp;lt;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOP1J2/dJMcabRlgx6/Jop2mQ7D5hdnWFzs3qomH0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOP1J2%2FdJMcabRlgx6%2FJop2mQ7D5hdnWFzs3qomH0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2758&quot; height=&quot;1174&quot; data-filename=&quot;Spring MVC - REST 방식.jpg&quot; data-origin-width=&quot;2758&quot; data-origin-height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 나 !!!! &amp;gt;.&amp;lt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Client가 특정 URL을 통해 HTTP Request가 들어오면, 이 요청은 DispatcherServlet으로 전달된다.&lt;br /&gt;이때, DispatcherServlet은 모든 요청을 가장 먼저 받는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;Front Controller&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이다.&lt;/li&gt;
&lt;li&gt;DispatcherServlet은 요청을 처리할 수 있는 적합한 Controller를 찾기 위해 HandlerMapping에게 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;DispatcherServelt은 Handler를 실행할 수 있는 HandlerAdapter를 조회한 후, 이를 실행한다.&lt;/li&gt;
&lt;li&gt;HandlerAdapter는 Handler, 즉 Controller를 실행한다.&lt;/li&gt;
&lt;li&gt;Controller는 비즈니스 로직을 수행한다.&lt;/li&gt;
&lt;li&gt;비즈니스 로직을 수행한 후, 처리 결과를 반환한다.&lt;br /&gt;이때 처리 결과는 View 이름이 아닌 객체 형태의 데이터이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Handler는 처리 결과를 HandlerAdapter로 전달한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;HandlerAdapter는 Handler가 반환한 객체를 HTTP 응답으로 변환하여 반환한다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;이 과정에서 MessageConverter가 객체를 JSON 형태로 변환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;JSON 형태로 변환된 데이터는 DispatcherServlet으로 전달된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;DispatcherServlet은 변환된 데이터를 그대로 클라이언트에게 응답으로 반환한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Spring MVC의 요청 처리 흐름을 중심으로 전체적인 구조를 살펴보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 요청은 DispatcherServlet에 도달하기 전과 이후에도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 계층에서 추가적인 처리가 이루어지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;b&gt;Filter&lt;/b&gt;, &lt;b&gt;Interceptor&lt;/b&gt;, &lt;b&gt;AOP&lt;/b&gt;는 각각 다른 위치에서 동작하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청과 응답 과정에 중요한 역할을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이 세 가지 개념이 어떤 차이를 가지는지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실제로 어떤 상황에서 사용되는지에 대해 정리해보려고 한다.&lt;/p&gt;</description>
      <category>Language &amp;amp; Framework/Spring Boot</category>
      <category>DispatcherServlet</category>
      <category>mvc</category>
      <category>REST방식</category>
      <category>spring</category>
      <category>springMVC</category>
      <category>View방식</category>
      <category>구조</category>
      <category>스프링</category>
      <category>흐름</category>
      <author>코딩 기록하는 애기 개발자</author>
      <guid isPermaLink="true">https://baby-developer77.tistory.com/25</guid>
      <comments>https://baby-developer77.tistory.com/25#entry25comment</comments>
      <pubDate>Sun, 12 Apr 2026 18:09:33 +0900</pubDate>
    </item>
    <item>
      <title>[Architecture] Hexagonal Architecture 알아보기</title>
      <link>https://baby-developer77.tistory.com/24</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 진행했던 대부분의 프로젝트는 Layered Architecture 기반으로 서버를 설계하고 구현해왔다. Controller, Service, Repository와 같이 역할에 따라 계층을 나누는 구조는 이해하기 쉽고 구현하기도 비교적 명확하기 때문에 초기 프로젝트를 진행할 때 자연스럽게 선택하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프로젝트를 경험하면서 계층 간 의존성이 복잡해지거나, 비즈니스 로직이 특정 계층에 집중되는 구조를 보며 &amp;ldquo;서버 구조를 더 잘 설계할 수 있는 방법은 없을까?&amp;rdquo;라는 고민이 생기기 시작했다.&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;이러한 고민을 하던 중 Hexagonal Architecture 라는 구조를 접하게 되었다. &lt;br /&gt;비즈니스 로직을 중심에 두고 외부 시스템과의 의존성을 분리한다는 점에서 기존의 Layered Architecture와는 다른 설계 철학을 가지고 있다는 점이 흥미롭게 느껴졌다.&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;이번 글에서는 Hexagonal Architecture가 무엇인지, 어떤 문제를 해결하기 위해 등장했는지, 그리고 기존 Layered Architecture와 어떤 차이가 있는지 정리해보려고 한다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hexagonal Architecture ( = Ports and Adapters Architecture) 란 ?&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hexagonal Architecture는 &lt;span&gt;&lt;b&gt;Ports and Adapters Architecture&lt;/b&gt;&lt;/span&gt;라고도 불리며, &lt;br /&gt;&lt;b&gt;도메인 중심 설계를 기반으로 한 아키텍처 패턴&lt;/b&gt;이다.&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 구조의 핵심 목적은 &lt;/span&gt;&lt;b&gt;비즈니스 로직을 외부 요소로부터 분리하는 것&lt;/b&gt;&lt;span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 애플리케이션에서는 UI, 데이터베이스, 외부 API와 같은 요소들이 비즈니스 로직과 강하게 결합되는 경우가 많다. 하지만 Hexagonal Architecture에서는 이러한 요소들을 &lt;b&gt;비즈니스 로직과 분리된 외부 요소(Adapters)&lt;/b&gt;로 취급한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, &lt;/span&gt;&lt;b&gt;UI나 데이터베이스는 언제든지 교체될 수 있는 외부 요소&lt;/b&gt;&lt;span&gt;로 보고, &lt;br /&gt;애플리케이션의 핵심인 &lt;/span&gt;&lt;b&gt;도메인 로직은 이러한 외부 시스템에 의존하지 않도록 설계&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조를 통해 &lt;span&gt;&lt;b&gt;비즈니스 로직은 외부 시스템에 직접 의존하지 않게 되고&lt;/b&gt;&lt;/span&gt;, 애플리케이션의 구조를 더 유연하게 유지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hexagonal Architecture 의 주요 구성 요소&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsnXah/dJMcaiJf5uV/ildnZY0dvD5XwXfw7saCjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsnXah/dJMcaiJf5uV/ildnZY0dvD5XwXfw7saCjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsnXah/dJMcaiJf5uV/ildnZY0dvD5XwXfw7saCjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsnXah%2FdJMcaiJf5uV%2FildnZY0dvD5XwXfw7saCjk%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;1280&quot; height=&quot;572&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;572&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;01 Application Core&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의&amp;nbsp;&lt;b&gt;핵심 비즈니스 로직이 위치하는 영역&lt;/b&gt;이며, 일반적으로&amp;nbsp;&lt;b&gt;Domain Entity&amp;nbsp;&lt;/b&gt;와&amp;nbsp;&lt;b&gt;Use Case &lt;/b&gt;로 구성된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Domain Entity&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Domain Entity 는&amp;nbsp;&lt;b&gt;비즈니스 도메인을 표현하는 객체&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서비스에서 다루는&amp;nbsp;&lt;b&gt;핵심 데이터와 그 데이터에 대한 규칙을 담고 있는 객체&lt;/b&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, `회원 서비스`를 만든다고 가정하면 `User`, `Payment`, `Product` 와 같은 객체들이 &lt;b&gt;Domain Entity&lt;/b&gt;가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이들이 단순한 데이터 객체라면 `User`라는 객체는 다음과 같이 표현된다.&lt;/p&gt;
&lt;pre id=&quot;code_1773215944562&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {
    String name;
    String email;
}&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;하지만 Domain Entity는 보통&amp;nbsp;&lt;b&gt;도메인 규칙을 함께 포함&lt;/b&gt;하기 때문에 다음과 같이 표현된다.&lt;/p&gt;
&lt;pre id=&quot;code_1773215989860&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {
    String name;
    String email;

    void changeEmail(String newEmail) {
        if(!newEmail.contains(&quot;@&quot;)) {
            throw new IllegalArgumentException(&quot;Invalid email&quot;);
        }
        this.email = newEmail;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Use Case (Application Service)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Use Case는&amp;nbsp;&lt;b&gt;애플리케이션이 수행해야 하는 실제 기능 (행위)&amp;nbsp;&lt;/b&gt;를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 요청을 처리하기 위한&amp;nbsp;&lt;b&gt;비즈니스 로직의 흐름을 정의하는 역할&lt;/b&gt;을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 `회원가입`, `주문 생성`, `상품 구매`, `결제 처리`와 같은 기능들이 Use Case가 될 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 보통 여러&amp;nbsp;Domain Entity를 사용해&amp;nbsp;&lt;b&gt;하나의 비즈니스 흐름을 완성&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 기능을 구현하면, 다음과 같은 흐름을 가질 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;회원 정보 검증&lt;/li&gt;
&lt;li&gt;회원 객체 생성&lt;/li&gt;
&lt;li&gt;데이터베이스 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 다음과 같이 표현될 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1773216347792&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class RegisterUserUseCase {

    private final UserRepository userRepository;

    public RegisterUserUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void register(String name, String email) {

        // 1. 회원 정보 검증
        if (!email.contains(&quot;@&quot;)) {
            throw new IllegalArgumentException(&quot;Invalid email&quot;);
        }

        // 2. 회원 객체 생성
        User user = new User(name, email);

        // 3. 데이터베이스 저장
        userRepository.save(user);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;02 Port&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Application Core&lt;/b&gt; &lt;b&gt;와 Adapter 사이의 통신을 담당하는 인터페이스&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application Core는 외부 시스템과 직접 통신하지 않고,&lt;b&gt; Port를 통해 간접적으로 상호작용&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Application Core는 외부 기술(DB, API 등)을 직접 알 필요 없이 Port&amp;nbsp;인터페이스만&amp;nbsp;의존하게&amp;nbsp;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`회원 정보를 저장해야 하는 경우`나 `외부 API에서 데이터를 가져와야 하는 경우`를 예시로 들어보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application Core는 직접 데이터베이스를 호출하는 대신,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;회원 정보를 저장해 주세요.&quot; &lt;/b&gt;라는 역할을 하는&lt;b&gt; Port 인터페이스를 정의&lt;/b&gt;한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 `회원정보를 검증`하고, `회원정보를 저장` 하는 Port 인터페이스의 예시다.&lt;/p&gt;
&lt;pre id=&quot;code_1773217046092&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface UserRepository {

    void save(User user);

    Optional&amp;lt;User&amp;gt; findByEmail(String email);

}&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;그리고 실제 데이터베이스와 연결되는 구현은 Adapter에서 담당하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;03 Adapter&lt;/h3&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;Hexagonal Architecture에서는 Adapter를 &lt;b&gt;Driving Adapter(Incoming Adapter)&lt;/b&gt;와 &lt;b&gt;Driven Adapter(Outgoing Adapter)&lt;/b&gt;, 크게 두 가지로 구분한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Driving Adapter (Incoming Adapter)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Driving Adapter&lt;/b&gt;는 &lt;b&gt;애플리케이션을 호출하는 역할&lt;/b&gt;을 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Web Controller, REST API 등과 같이 사용자의 요청이나 외부 이벤트를 받아&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Application Core의 Use Case를 호출하는 어댑터&lt;/b&gt;&lt;span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 사용자가 회원가입 요청을 보내면, Controller가 요청을 받아&amp;nbsp;&lt;b&gt;RegisterUserUseCase&lt;/b&gt;를 호출하게 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, Driving Adapter는 &lt;b&gt;외부 요청을 애플리케이션 내부로 전달&lt;/b&gt;하는 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773219362958&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/users&quot;)
public class UserController {

    private final RegisterUserUseCase registerUserUseCase;

    public UserController(RegisterUserUseCase registerUserUseCase) {
        this.registerUserUseCase = registerUserUseCase;
    }

    @PostMapping
    public ResponseEntity&amp;lt;Void&amp;gt; registerUser(@RequestBody RegisterUserRequest request) {

        registerUserUseCase.register(
            request.getName(),
            request.getEmail()
        );

        return ResponseEntity.ok().build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;Driven Adapter (Outgoing Adapter)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Driven Adapter&lt;/span&gt;&lt;/b&gt;&lt;span&gt;는&amp;nbsp;&lt;b&gt;Application Core 에 의해 호출되는 어댑터&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;데이터베이스 접근, 외부 API 호출, 메세지 큐 등과 같이&amp;nbsp;&lt;b&gt;애플리케이션이 외부 시스템과 상호작용 할 때 사용된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 정보를 저장해야 하는 경우, &lt;br /&gt;Application Core는 Port 인터페이스를 통해 요청을 전달하고, 실제 데이터베이스와의 통신은 Driven Adapter가 담당하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1773219393759&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public class UserRepositoryAdapter implements UserRepository {

    private final JpaUserRepository jpaUserRepository;

    public UserRepositoryAdapter(JpaUserRepository jpaUserRepository) {
        this.jpaUserRepository = jpaUserRepository;
    }

    @Override
    public void save(User user) {
        jpaUserRepository.save(user);
    }

    @Override
    public Optional&amp;lt;User&amp;gt; findByEmail(String email) {
        return jpaUserRepository.findByEmail(email);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;즉,&amp;nbsp;&lt;b&gt;Driving Adapter&lt;/b&gt;는 &lt;b&gt;애플리케이션을 구동시키는&lt;/b&gt; 쪽,&amp;nbsp;&lt;br /&gt;&lt;b&gt;Driven Adapter&lt;/b&gt;는 &lt;b&gt;애플리케이션에 의해 사용되는&lt;/b&gt; 쪽 이라고 이해하면 쉽다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hexagonal Architecture 의 동작 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 구조를 기준으로 회원가입 요청이 들어왔을 때의 동작 흐름을 예시로 살펴보면 다음과 같다.&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;사용자 요청 (HTTP Request)&lt;br /&gt;&lt;/b&gt;: 사용자가 회원가입을 요청하면 &lt;b&gt;HTTP 요청이 서버로 전달&lt;/b&gt;된다.&lt;br /&gt;이 요청은 먼저&amp;nbsp;Controller와 같은 Driving Adapter가 받게 된다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Driving Adapter (Controller)&lt;br /&gt;&lt;/b&gt;: User Controller는 &lt;b&gt;사용자의 요청을 받아 필요한 데이터를 추출&lt;/b&gt;한 뒤,&amp;nbsp;&lt;br /&gt;Application Core에 위치한 `RegisterUserUseCase`&lt;b&gt;를 호출&lt;/b&gt;한다.&lt;br /&gt;&lt;br /&gt;이 단계에서 Controller는&amp;nbsp;&lt;b&gt;요청을 전달하는 역할만 담당&lt;/b&gt;하며, 실제 비즈니스 로직은 Application Core에서 처리된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Application Core (Use Case)&lt;br /&gt;&lt;/b&gt;: `RegisterUserUseCase` 는 회원가입이라는 &lt;b&gt;비즈니스 로직의 흐름을 담당&lt;/b&gt;한다.&lt;br /&gt;&lt;br /&gt;이 단계에서는 `회원 정보 검증`, `회원 객체 생성`, `회원 정보 저장 요청` 과 같은 작업이 수행된다.&lt;br /&gt;여기서 중요한 점은&amp;nbsp;&lt;b&gt;Application Core가 데이터베이스에 직접 접근하지 않는다&lt;/b&gt;는 것이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Port&lt;br /&gt;&lt;/b&gt;: Application Core는 데이터베이스에 직접 접근하는 대신` UserRepository`라는 &lt;b&gt;Port 인터페이스를 통해 요청을 전달&lt;/b&gt;한다.&lt;br /&gt;&lt;br /&gt;즉, Application Core는 &lt;b&gt;회원 정보를 저장해 달라&lt;/b&gt;는 요청만 정의하고 실제 구현은 알 필요가 없다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Driven Adapter&lt;br /&gt;&lt;/b&gt;: `UserRepositoryAdapte`r는 `UserRepository` Port를 구현한 Adapter이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Application Core로부터 전달받은 요청을 실제 데이터베이스 작업으로 변환&lt;/b&gt;하여 회원 정보를 &lt;b&gt;데이터베이스에 저장&lt;/b&gt;한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Database&lt;br /&gt;&lt;/b&gt;: 마지막으로 Driven Adapter를 통해 실제 데이터베이스에 데이터가 저장된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Hexagonal Architecture의 개념과 주요 구성 요소, 그리고 실제 동작 흐름까지 간단하게 정리해보았다.&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;Hexagonal Architecture는 애플리케이션의 핵심인 &lt;span&gt;&lt;b&gt;비즈니스 로직을 중심에 두고 외부 시스템과의 의존성을 분리하는 구조&lt;/b&gt;&lt;/span&gt;라는 점에서 기존에 익숙하게 사용하던 Layered Architecture와는 다른 관점을 제시한다. 특히 데이터베이스나 웹 프레임워크와 같은 외부 기술에 직접 의존하지 않도록 설계할 수 있다는 점이 인상적으로 느껴졌다.&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&gt;&lt;b&gt;비즈니스 로직을 어떻게 보호하고 유지보수하기 좋은 구조를 만들 수 있을지&lt;/b&gt;&lt;/span&gt;에 대해 다시 생각해보게 된 것 같다.&lt;/p&gt;</description>
      <category>ETC ..</category>
      <category>ADAPTER</category>
      <category>Application Core</category>
      <category>Architecture</category>
      <category>Hexagonal Architecture</category>
      <category>port</category>
      <category>ports and adapters architecture</category>
      <category>아키텍처</category>
      <category>헥사고날 아키텍처</category>
      <author>코딩 기록하는 애기 개발자</author>
      <guid isPermaLink="true">https://baby-developer77.tistory.com/24</guid>
      <comments>https://baby-developer77.tistory.com/24#entry24comment</comments>
      <pubDate>Wed, 11 Mar 2026 18:15:40 +0900</pubDate>
    </item>
  </channel>
</rss>