<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Beelog</title>
    <link>https://developerbee.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 3 Jul 2026 18:05:25 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>DevBee</managingEditor>
    <item>
      <title>[dev] Dev Container 적용하기</title>
      <link>https://developerbee.tistory.com/274</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 다 같이 devcontainer를 스터디하기로 해서 오늘은 혼자 간단한(?) 서버에 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;우선 환경 정보는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mac (Intel)&lt;/li&gt;
&lt;li&gt;Java 21&lt;/li&gt;
&lt;li&gt;SpingBoot 3.4.0&lt;/li&gt;
&lt;li&gt;Gradle&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;Dev Container 란?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://containers.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://containers.dev/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738239804186&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Development containers&quot; data-og-description=&quot;A development container (or dev container for short) allows you to use a container as a full-featured development environment. It can be used to run an application, to separate tools, libraries, or runtimes needed for working with a codebase, and to aid in&quot; data-og-host=&quot;containers.dev&quot; data-og-source-url=&quot;https://containers.dev/&quot; data-og-url=&quot;https://containers.dev/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/chExBp/hyX71zix7P/DLsrmoHvqJVk8vR6jQCJOk/img.png?width=2192&amp;amp;height=1911&amp;amp;face=0_0_2192_1911&quot;&gt;&lt;a href=&quot;https://containers.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://containers.dev/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/chExBp/hyX71zix7P/DLsrmoHvqJVk8vR6jQCJOk/img.png?width=2192&amp;amp;height=1911&amp;amp;face=0_0_2192_1911');&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;Development containers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A development container (or dev container for short) allows you to use a container as a full-featured development environment. It can be used to run an application, to separate tools, libraries, or runtimes needed for working with a codebase, and to aid in&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;containers.dev&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dev Container는 &lt;b&gt;Docker 컨테이너 안에 개발 환경을 구성&lt;/b&gt;하여 로컬 환경을 오염시키지 않고, 팀원 간 동일한 개발 환경을 유지할 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Docker Compose와 연결하면 DB, API 서버 등도 함께 컨테이너로 실행 가능&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp;Dev Container vs docker-compose.yml&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1️⃣ docker-compose.yml 개념 및 용도&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 개념&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;여러 Docker 컨테이너를 정의하고 동시에 실행하기 위한 도구&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;서비스 정의, 네트워크 구성, 볼륨 공유 등을 한 곳에서 설정 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 용도&lt;/b&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;멀티 컨테이너 애플리케이션 구성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &lt;b&gt;API 서버&lt;/b&gt;, &lt;b&gt;DB&lt;/b&gt;(MySQL, PostgreSQL 등), &lt;b&gt;Redis&lt;/b&gt;, &lt;b&gt;Nginx&lt;/b&gt; 등을 하나의 구성 파일에서 정의하고 관리.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로컬 개발 및 테스트 환경 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬에서 쉽게 &lt;b&gt;테스트 환경 구축&lt;/b&gt; 및 &lt;b&gt;로컬 네트워크 구성&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 전 동일한 환경 구축&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발, 테스트, 프로덕션 환경을 동일하게 맞추기 쉽게 해줌.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 주요 명령어&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;docker-compose up -d : 백그라운드에서 모든 서비스 실행&lt;/li&gt;
&lt;li&gt;docker-compose down : 모든 서비스 중단 및 네트워크 정리&lt;/li&gt;
&lt;li&gt;docker-compose build : Dockerfile 기반으로 이미지 빌드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2️⃣ devcontainer 개념 및 용도&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 개념&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개발 환경을 코드로 정의하는 도구&lt;/b&gt;. 주로 &lt;b&gt;VS Code&lt;/b&gt; 및 &lt;b&gt;IntelliJ&lt;/b&gt;와 같은 IDE에서 사용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개발 환경 통일성&lt;/b&gt;을 유지하고, &lt;b&gt;개발자가 일관된 환경에서 작업&lt;/b&gt;할 수 있게 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 용도&lt;/b&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;일관된 개발 환경 제공&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팀원들이 동일한 개발 환경에서 작업할 수 있게 해줌.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IDE와 통합된 개발 경험&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;VS Code&lt;/b&gt; 또는 &lt;b&gt;IntelliJ&lt;/b&gt;에서 &lt;b&gt;직접 Dev Container를 열고 개발&lt;/b&gt; 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;추가적인 개발 도구 설치&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디버거, Linter, 포맷터 등을 컨테이너 내부에 미리 설치 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3️⃣ docker-compose와 devcontainer의 차이점&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;docker-compose&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;devcontainer&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;주요 목적&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;여러 서비스 실행 및 관리&lt;/td&gt;
&lt;td&gt;개발 환경 설정 및 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;설정 파일&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;docker-compose.yml&lt;/td&gt;
&lt;td&gt;devcontainer.json + docker-compose.yml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;실행 방법&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CLI 명령어 사용&lt;/td&gt;
&lt;td&gt;IDE 통합 실행 (VS Code, IntelliJ)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;주요 사용 사례&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;멀티 컨테이너 배포 및 로컬 테스트&lt;/td&gt;
&lt;td&gt;일관된 개발 환경 제공 및 IDE 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;네트워크 구성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;독립적인 Docker 네트워크 생성&lt;/td&gt;
&lt;td&gt;docker-compose와 동일한 네트워크 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;개발 도구 통합&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;기본적으로 없음&lt;/td&gt;
&lt;td&gt;IDE 확장, 디버거 등 통합 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;배포 활용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;배포 및 여러 환경 지원&lt;/td&gt;
&lt;td&gt;주로 로컬 개발&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;현재 디렉토리 구조&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1738242289929&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;starter-server/
│── gradlew
│── gradlew.bat
│── gradle/          # Gradle 관련 폴더
│── starter-api/     # Spring Boot API 모듈
│   ├── .devcontainer/   # Dev Container 설정 폴더 (여기에 있음!)
│   │   ├── devcontainer.json
│   │   ├── docker-compose.yml
│   │   ├── Dockerfile
│── starter-common/  # 공통 모듈 (domain, entity, db 설정)
│── build.gradle.kts
│── settings.gradle.kts&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1) .devcontainer 디렉토리 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 starter-server 내에 다른 모듈이 들어올 수 있고 해당 모듈들을 각각 컨테이너로 생성할 가능성이 높기 때문에 우선 starter-api 내부에 devcontainer를 위한 디렉토리를 생성했다.&lt;/p&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: #000000;&quot;&gt;&lt;b&gt;2) devcontianer.json, docker-compose.yml, Dockerfile 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 해당 디렉토리 아래 다음과 같이 3가지 파일을 추가하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;devcontainer.json&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738287620441&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;API Dev Container&quot;,
  &quot;dockerComposeFile&quot;: &quot;docker-compose.yml&quot;,
  &quot;service&quot;: &quot;api&quot;,
  &quot;workspaceFolder&quot;: &quot;/workspace&quot;,
  &quot;forwardPorts&quot;: [8080, 3306],
  &quot;postCreateCommand&quot;: &quot;./gradlew build&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;span style=&quot;color: #1a5490;&quot;&gt;docker-compose.yml&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738287709646&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.8&quot;
services:
  api:
    build:
      context: ../..  # starter-server를 기준으로 빌드
      dockerfile: starter-api/.devcontainer/Dockerfile
    container_name: dev-container-api
    ports:
      - &quot;8080:8080&quot;
    environment:
      SPRING_PROFILES_ACTIVE: default,dev
      SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/mydb
      SPRING_DATASOURCE_USERNAME: user
      SPRING_DATASOURCE_PASSWORD: password
    depends_on:
      - db
    volumes:
      - ../../starter-api/src:/workspace/starter-api/src
      - ../../gradlew:/workspace/gradlew

  db:
    image: mysql:8.0
    container_name: dev-container-db
    restart: always
    ports:
      - &quot;3306:3306&quot;
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: mydb
      MYSQL_USER: user
      MYSQL_PASSWORD: password
    volumes:
      - mysql-data:/var/lib/mysql

volumes:
  mysql-data:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;Dockerfile&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738287739897&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ✅ JDK 21 사용 (Debian 기반)
FROM eclipse-temurin:21-jdk

WORKDIR /workspace

# ✅ `findutils` (xargs 포함) 설치 (Debian 기반)
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y findutils

# ✅ `xargs`가 설치되었는지 확인 (빌드 로그에 출력)
RUN xargs --version || (echo &quot;⚠️ xargs is NOT available!&quot; &amp;amp;&amp;amp; exit 1)

# ✅ starter-server 전체 복사 (starter-api + starter-common 포함)
COPY ../../ /workspace/

# ✅ 실행 권한 부여 (gradlew 실행 가능하도록)
RUN chmod +x /workspace/gradlew

# ✅ Gradle 빌드 실행 (빌드 중 에러 발생 시 로그 확인)
RUN /workspace/gradlew :starter-api:build --stacktrace

# ✅ JAR 파일 이동 및 존재 여부 확인
RUN ls -l /workspace/starter-api/build/libs/
RUN cp /workspace/starter-api/build/libs/starter-api-0.0.1-SNAPSHOT.jar /workspace/starter-api.jar

# ✅ JAR 파일 실행 권한 부여
RUN chmod +x /workspace/starter-api.jar

# ✅ 실행할 JAR 파일 지정
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/workspace/starter-api.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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3-1) 명령어로 실행하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 .devcontainer 디렉토리로 이동한 뒤 다음 명령어를 통해 docker container를 생성하고 실행할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1738287890708&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker-compose build --no-cache # 빌드 (이미지 생성)
$ docker-compose up -d            # 실행 (컨테이너 실행)
$ docker-compose down             # 종료 (컨테이너 종료 및 삭제)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3-2) IntelliJ 에서 실행하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;devcontainer 를 사용하는 것은 IDE를 통해 편리한 실행을 위한 것이므로 IntelliJ에서 조금 더 쉽게 실행이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 플러그인으로 docker를 설치하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Setting &amp;rarr; Build, Execution, Deployment &amp;rarr; Docker 에서 +로 기본 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 우측 상단 Run/Dedug Configuration 에서 edit 하고 docker 추가 후 위에서 작성한 docker-compose.yml 파일 위치를 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 추가한 Docker Configuration 을 선택하고 Run(초록색 화살표)을 클릭하여 build &amp;rarr; up 가능&lt;/p&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 실행에 대해서는 이것말고 View &amp;rarr; Tool Windows &amp;rarr; Docker 에서 할 수 있다고 하는데 Docker를 찾을 수가 없어서 위 방법으로 우선 하고 다시 찾게 되면 실행 방법을 업데이트 할 예정이다.  &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;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;이슈&lt;/b&gt;&lt;/span&gt;&lt;/h3&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. JDK 버전 및 xargs 이슈&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Dockerfile에서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323; background-color: #dddddd;&quot;&gt;FROM openjdk:21-jdk&amp;nbsp;&lt;/span&gt; 를 사용하려고 했다. 하지만 build 과정에서&amp;nbsp;&lt;span style=&quot;background-color: #dddddd; color: #ee2323;&quot;&gt; xargs is not available jdk 21&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 이라는 에러가 발생했다...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;찾아보니 그래들 7.5 버전부터&amp;nbsp;xargs&amp;nbsp;에 대한 명시적인 체크가 이루어진다고 한다. 그리고&amp;nbsp;xargs의 존재(설치) 여부는&amp;nbsp;POSIX standard의 일부이기 때문에 없는 것은 극히 드문 경우라고 한다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그렇지만&amp;nbsp;openjdk의 14버전 이상의 도커 이미지는&amp;nbsp;xargs를 포함하지 않는데 그 이유는 사용자제(deprecated)되었기 때문이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그래서 어쩔 수 없이 다른 21을 사용하고 xargs를 설치하기로 했다... &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그래서 아래와 같이 Dockerfile이 수정되었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738288983249&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Before]
# ✅ JDK 21 사용 (Debian 기반)
FROM openjdk:21-jdk
WORKDIR /workspace

[After]
# ✅ JDK 21 사용 (Debian 기반)
FROM eclipse-temurin:21-jdk
WORKDIR /workspace

# ✅ `findutils` (xargs 포함) 설치 (Debian 기반)
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y findutils

# ✅ `xargs`가 설치되었는지 확인 (빌드 로그에 출력)
RUN xargs --version || (echo &quot;⚠️ xargs is NOT available!&quot; &amp;amp;&amp;amp; exit 1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고: &lt;a href=&quot;https://stackoverflow.com/questions/73516116/got-error-xargs-is-not-available-when-trying-to-run-a-docker-image&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/73516116/got-error-xargs-is-not-available-when-trying-to-run-a-docker-image&lt;/a&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 style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. Error: Unable to access jarfile xxx.jar&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 빌드가 성공을 했는데 docker-compose up -d 로 컨테이너를 띄우니 db 컨테이너만 뜨고 api 컨테이너가 뜨지 않았다...ㅠㅠ 에러는 다음과 같았다.&amp;nbsp; &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Error: Unable to access jarfile xxx.jar&amp;nbsp;&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 style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다른 내용도 없이 위와 같은 에러가 떠서 어리둥절... 결론부터 말하면 &lt;u&gt;&lt;span style=&quot;color: #000000;&quot;&gt;volume 설정으로 인해 jar 파일이 복사되지 않는 이슈&lt;/span&gt;&lt;/u&gt;가 있었다...&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하나씩 확인하는 과정을 거쳤는데 살펴보면 다음과 같다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738289853347&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. 먼저 실행 중인 컨테이너 내리고 삭제
$ docker-compose down

# 2. 컨테이너 실행과 동시에 명령어로 디렉토리 확인
$ docker-compose run --rm api ls -lh /workspace
$ docker-compose run --rm api ls -lh /workspace/starter-api/build/libs
또는
$ docker-compose run --rm api sh -c &quot;ls -lh /workspace/starter-api/build/libs &amp;amp;&amp;amp; ls -lh /workspace&quot;

# 3. 결과 확인 - jar 파일은 생성되었지만 복사가 되지 않았음을 확인
Creating network &quot;devcontainer_default&quot; with the default driver
Creating dev-container-db ... done
Creating devcontainer_api_run ... done
total 63072
-rw-r--r-- 1 root root 64567552 Jan 30 13:28 starter-api-0.0.1-SNAPSHOT.jar
-rw-r--r-- 1 root root    12322 Jan 30 13:28 starter-api-0.0.1-SNAPSHOT-plain.jar
total 63116
drwxr-xr-x 1 root root     4096 Jan 30 08:46 build
-rw-r--r-- 1 root root     2697 Dec  2 11:55 build.gradle.kts
drwxr-xr-x 3 root root     4096 Jan 30 08:46 gradle
-rw-r--r-- 1 root root      509 Dec  2 11:55 gradle.properties
-rwxr-xr-x 1 root root     8762 Dec  2 11:55 gradlew
-rw-r--r-- 1 root root     2966 Dec  2 11:55 gradlew.bat
-rw-r--r-- 1 root root      375 Dec  2 11:55 README.md
-rw-r--r-- 1 root root      812 Dec  2 11:55 settings.gradle.kts
drwxr-xr-x 2 root root       64 Jan 30 15:08 src
drwxr-xr-x 1 root root     4096 Jan 30 08:46 starter-api
drwxr-xr-x 4 root root     4096 Jan 30 08:46 starter-common

# 4. 직접 복사해서 넣기
$ docker-compose down
$ docker-compose run --rm api sh -c &quot;cp /workspace/starter-api/build/libs/starter-api-0.0.1-SNAPSHOT.jar /workspace/starter-api.jar &amp;amp;&amp;amp; ls -lh /workspace&quot;

# 5. 실행 중인 컨테이너 종료 후 다시 실행
$ docker-compose down
$ 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;하지만 매번 빌드하고 파일 복사하고 다시 실행할 수 없기 때문에 원인을 알아보니 docker-compose.yml 에 설정한&amp;nbsp;&lt;span style=&quot;background-color: #dddddd; color: #ee2323;&quot;&gt; volumes: - ../..:/workspace&amp;nbsp;&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;&lt;span style=&quot;background-color: #dddddd; color: #ee2323;&quot;&gt;&amp;nbsp;volumes: - ../..:/workspace&amp;nbsp;&lt;/span&gt; 이런 식으로 설정되어 있다면, 도커 빌드 단계에서 만든 /workspace/starter-api/build/libs/...jar 파일이 로컬 측 디렉토리 구조로 인해 덮어쓰이거나, 로컬 디렉토리 구조가 우선 적용될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 빌드 단계에서 jar를 만들었더라도, 다음 레이어에서 동일 경로를 볼륨 마운트로 덮어버리면 컨테이너 안에서 jar가 없는 상태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 RUN cp가 실행될 시점에는 파일이 사라져 있는 경우가 많다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 해결책으로 volumes 를 아예 없애거나 필요한 디렉토리 위치만 설정하는 것이 좋다고 한다. 하지만 volumes를 아예 없애면 개발환경에서 변경한 소스가 반영되지 않을 수 있기 때문에 필요한 파일만 지정하는 방식으로 변경하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1738290482401&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Before]
version: &quot;3.8&quot;
services:
  api:
    ...
    volumes:
      - ../..:/workspace

[After]
version: &quot;3.8&quot;
services:
  api:
    ...
    volumes:
      - ../../starter-api/src:/workspace/starter-api/src
      - ../../gradlew:/workspace/gradlew&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. Error: Invalid or corrupt jarfile xxx.jar&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 문제들을 해결하던 중 volumes 설정을 바꾸는데 다음과 같이 적용했더니 jar 디렉토리가 생기면서 복사가 안되었고 그 상태에서 실행하려고 했더니 생긴 이슈였다. 위에서 volume 설정을 변경해주면서 해결이 되었다.&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;Dev Container 적용에 대해 알아보고 있었는데... 도커의 동작 방식이나 하나하나의 설정이 가지는 의미, 그리고 왜 이런 설정이 필요한지를 더 명확히 알고 있다면 큰 도움이 될 것 같았다. 도커의 volume과 빌드, 실행 과정 하나하나 어떻게 동작하는지 더 알아볼 예정이다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;To be continued... &lt;/p&gt;</description>
      <category>개발환경</category>
      <category>dev container</category>
      <category>Docker</category>
      <author>DevBee</author>
      <guid isPermaLink="true">https://developerbee.tistory.com/274</guid>
      <comments>https://developerbee.tistory.com/274#entry274comment</comments>
      <pubDate>Mon, 3 Feb 2025 21:46:50 +0900</pubDate>
    </item>
    <item>
      <title>[SpringBoot] 배포 환경 별 설정 파일 분리 (feat. gradle)</title>
      <link>https://developerbee.tistory.com/272</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;업무를 진행하면서 다양한 배포 환경에서 설정 파일을 다르게 적용해야 하는 경우가 생깁니다. 저희 프로젝트 같은 경우 스프링 실행 시점에 설정 파일을 환경별로 분리할 수 없다는 얘기를 들어서 빌드 파일을 생성할 때부터 프로파일을 지정하는 방법을 선택하였고 해당 과정을 기록하고자 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Spring Profile&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Profile 은 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;런타임 시&lt;/b&gt;&lt;/span&gt; 지정한 profile 값에 따라 설정 파일을 로드할 수 있습니다.&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;SpringBoot 에서는 기본적으로 실행 시 &lt;b&gt;&lt;i&gt;resources 폴더에서 application.properties(yml)&lt;/i&gt;&lt;/b&gt;을 찾아 로드합니다.&lt;/li&gt;
&lt;li&gt;그 다음으로 profile 값이 있다면 &lt;b&gt;&lt;i&gt;application-${profile}.properties(yml)&lt;/i&gt;&lt;/b&gt; 파일을 찾아 로드합니다.&lt;/li&gt;
&lt;li&gt;설정 파일 내부의 설정 값들 중 겹치는 것이 있으면 마지막에 로드한 값으로 오버라이드 됩니다.&lt;/li&gt;
&lt;li&gt;profile 값을 넘겨주지 않으면 기본 값은 default 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723425510746&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; └── resources
     ├── application.yml
     ├── application-dev.yml
     ├── application-prd.yml
     └── application-stg.yml&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;위와 같이 환경 별 설정 파일을 분리하고 빌드를 하게 되면 build 디렉토리 내 resources 폴더에 모든 설정 파일이 컴파일 된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 jar 파일 실행 시 지정한 프로파일에 따라 (&lt;i&gt;-Dspring.profiles.active&lt;/i&gt;) 설정 파일을 로드됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1723425643334&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 빌드
./gradlew bootJar

# 실행
java -jar build/libs/test-0.0.1-SNAPSHOT.jar

# 프로파일 지정 실행
java -jar -Dspring.profiles.active=dev build/libs/test-0.0.1-SNAPSHOT.jar&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Gradle Profile&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle Profile 설정을 하면 &lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;profile 값에 해당하는 설정 파일만 컴파일&lt;/span&gt;&lt;/b&gt; 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;build.gradle 에 설정을 추가합니다.&lt;/li&gt;
&lt;li&gt;기본적으로 컴파일 시 resources 디렉토리에서 리소스들을 찾지만 찾을 리소스 디렉토리를 별도로 추가해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723426107773&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

// profile의 기본값 local로 설정
ext.profile = (!project.hasProperty('profile') || !profile) ? 'local' : profile

// 리소스 디렉토리 추가
sourceSets {
    main {
        resources {
            srcDirs &quot;src/main/resources-env/${profile}&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723426067421&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; └── resources
 └── resources-env
     ├── dev
     │&amp;nbsp;   └── application.yml
     ├── local
     │&amp;nbsp;   └── application.yml
     ├── prd
     │&amp;nbsp;   └── application.yml
     └── stg
          └── application.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle 에 리소스 디렉토리를 새로 지정하고 위와 같이 설정 파일을 환경별 디렉토리에 추가해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 나서 profile을 지정하여 빌드를 진행하면 지정한 환경에 대한 설정 파일만 컴파일 되서 추가됩니다. 따라서 실행 시 별도의 profile을 지정하지 않아도 원하는 환경의 설정 파일이 지정되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1723426479105&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 프로파일 지정 빌드
./gradlew clean bootjar -Pprofile=dev 

# 실행
java -jar build/libs/test-0.0.1-SNAPSHOT.jar&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;추가로 환경에 구분되지 않는 공통 설정을 하고 싶다면 다음과 같이 resources 디렉토리 하위에 application-common.yml 같은 파일을 생성하고 각 환경별 프로파일에서 include를 해주면 됩니다. (include 된 파일이 나중에 로드되기 때문에 해당 파일의 값이 적용됩니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1723426517889&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  profiles:
    include: common
    active: dev(or local or prd)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@haerong22/Spring-%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD-%EB%B3%84%EB%A1%9C-%EC%84%A4%EC%A0%95%ED%8C%8C%EC%9D%BC-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0feat.-gradle&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@haerong22/Spring-%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD-%EB%B3%84%EB%A1%9C-%EC%84%A4%EC%A0%95%ED%8C%8C%EC%9D%BC-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0feat.-gradle&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <author>DevBee</author>
      <guid isPermaLink="true">https://developerbee.tistory.com/272</guid>
      <comments>https://developerbee.tistory.com/272#entry272comment</comments>
      <pubDate>Mon, 12 Aug 2024 10:39:56 +0900</pubDate>
    </item>
    <item>
      <title>[EventListener] 스프링에서 이벤트 발행과 구독</title>
      <link>https://developerbee.tistory.com/271</link>
      <description>&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 data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;이벤트 방식을 적용하면서 알게된 내용들을 간단히 정리하고자 합니다.  &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;1. 스프링 이벤트&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[스프링에서 이벤트 발행과 구독]&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 이벤트를 발행하고 구독하는 기능을 제공하고 있는데, 각 로직들을 느슨하게 결합하여 변경 및 추가를 용이하게 하고 재사용성을 높이기 위해 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 이벤트를 사용하는 방식은 이벤트를 ApplicationContext로 넘기고 Listener가 이를 구독하는 방식입니다. 따라서 애플리케이션 및 컨텍스트의 수명 주기에 연결되는 사용자 지정 작업을 수행할 수 있도록 ApplicationContextEvnet를 확장한 다양한 기본 제공 이벤트가 있습니다.&lt;/p&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: #f89009;&quot;&gt;&lt;b&gt;ApplicationEventPublisher&lt;/b&gt;&lt;/span&gt; 인터페이스를 사용하게 되는데 이는 Spring의 ApplicationContext가 구현한 인터페이스 중 하나이고&amp;nbsp;디자인 패턴 중 하나인 옵저버(Observer) 패턴의 구현체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[이벤트 장단점]&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스 간 의존성을 분리하여 느슨한 결합이 가능&lt;/li&gt;
&lt;li&gt;클래스가 독립적이므로 재사용성 증가 (추후 별도 서비스 분리 가능)&lt;/li&gt;
&lt;li&gt;이벤트 구독 모듈이 추가, 수정되어도 다른 모듈에 영향 적음&lt;/li&gt;
&lt;li&gt;단위 테스트 용이&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 클래스, 리스너 생성 등 작업량 증가&lt;/li&gt;
&lt;li&gt;코드가 분리되어 있어 흐름 파악이 어려움&lt;/li&gt;
&lt;li&gt;이벤트 구독 순서를 고려해야 하는 경우 복잡도 증가&lt;/li&gt;
&lt;li&gt;전체 이벤트 발행, 구독 테스트 어려움&lt;/li&gt;
&lt;li&gt;특정 프레임워크 API 의존성 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 사용했을 때 장점은 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;도메인 간의 의존성이 분리&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;된다는 것입니&lt;/span&gt;&lt;/span&gt;다. A라는 클래스는 B라는 클래스를 몰라도 되기 때문에 느슨하게 결합되고 재사용성이 증가하며 각각의 클래스 변경에도 용이합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코드가 분리되어 있기 때문에 전체적인 흐름을 파악하기 어렵다는 단점이 있습니다. 또한, 작업량이 증가할 수 있고 복잡도가 증가하기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 상황에 맞게 유연한 이벤트 적용이 필요합니다. 예를 들어 결재 승인이 된 경우 메일을 전송해야하는 비즈니스 로직이 있다고 했을 때 결재 도메인에서는 메일 발송에 대한 내용을 몰라도 된다고 생각했고 이벤트를 적용하여 느슨한 의존성을 가지도록 할 수 있습니다. 이렇게 하면 결재 도메인 외 다른 도메인에서도 메일 발송을 추가할 수 있고 메일 발송 대신 푸시 발송을 추가하는 것에도 용이합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 도메인에 속하며 명시적인 처리 흐름이 있는 경우는 직접 메서드를 호출하는 것이 좋고&lt;/li&gt;
&lt;li&gt;여러 도메인에서 공통으로 사용되며 다른 도메인과 연관 없이 분리되어 사용할 수 있고 순서 보장 없이 처리되어도 되는 경우 이벤트를 주체적으로 인지하고 처리하는 것이 더 좋다고 생각합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[구성 요소 및 구현 방법]&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 이벤트는 크게 Event Class와 이벤트를 발생시키는&amp;nbsp;Event Publisher 그리고 이벤트를 받아들이는&amp;nbsp;Event Listener 3가지 요소로 구성되어 있다고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Event Class&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event Class 는 이벤트를 처리하는데 필요한 데이터를 가지고 있으며 기존에는 ApplicationEvent 클래스를 상속 받아 사용하였지만 스프링 프레임워크 4.2 버전부터 ApplicationEvent를 확장할 필요가 없어졌습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710830316221&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 스프링 프레임워크 4.2 버전 이전
public class DomainEvent extends ApplicationEvent {

    private String key;

    public DomainEvent(Object source, String key) {
        super(source);
        this.key = key;
    }

    public String getKey() {
        return key;
    }
}

// 스프링 프레임워크 4.2 버전부터
public class DomainEvent {

    private String key;

    public DomainEvent(String key) {
        this.key = key;
    }

    public String getKey() {
        return key;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;EventPublisher&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EventPublisher는 ApplicationPublisher 빈을 주입하여 publishEvent() 메서드를 통해 생성된 이벤트 객체를 넣어주어 발행할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710830687156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Service
public class DomainEventPublisher {

    ApplicationEventPublisher publisher;

    public DomainEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    public void publish(String key) {
        // 로직 처리
        log.info(&quot;로직 처리 [key : {}]&quot;, key));
        publisher.publishEvent(new DomainEvent(key));
        
        // 4.2 버전 이전에서 event class가 ApplicationEvent를 구현하는 경우
        // publisher.publishEvent(new DomainEvent(this, key));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;EventListener&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EventListener는 발생한 이벤트를 캐치하여 이후 로직을 수행할 수 있는데 이 또한 스프링 프레임워크 4.2 버전을 전후로 구현 방법이 달라졌습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710831184178&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 스프링 프레임워크 4.2 버전 이전
@Slf4j
@Component
public class DomainEventListener implements ApplicationListener&amp;lt;DomainEvent&amp;gt; {

    @Override
    public void onApplicationEvent(DomainEvent event) {
        ...
    }
}

// 스프링 프레임워크 4.2 버전부터
@Slf4j
@Component
public class DomainEventListener {

    @EventListener
    public void sendPush(DomainEvent event) throws InterruptedException {
        log.info(&quot;푸시 메세지 발송 [key : {}]&quot;, event.getKey());
    }

    @EventListener
    public void sendMail(DomainEvent event) throws InterruptedException {
        log.info(&quot;메일 발송 [key : {}]&quot;, event.getKey());
    }
    
    @EventListener({DomainEvent.class})
    public void sample() throws InterruptedException {
        log.info(&quot;이벤트 객체 내부에 접근하지 않을 때는 이렇게 사용하는 것도 가능&quot;);
    }
}&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 style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[특징]&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티태스킹 (Multi-Tasking)&lt;/li&gt;
&lt;li&gt;동기방식 (Sync)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 이벤트 리스너는 &lt;span style=&quot;color: #f89009;&quot;&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;또한, 스프링 이벤트는 기본적으로 &lt;span style=&quot;color: #f89009;&quot;&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;비동기 방식으로 구현하거나 트랜잭션을 분리하는 방법은 아래에서 조금 더 살펴보겠습니다.&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;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;2. 실전 적용&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;[비동기 및 트랜잭션]&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 이벤트 리스너 적용은 다음과 같이 되어 있었습니다. 이렇게 적용을 하고 사용하다보니 추후에 API 응답이 느리다는 이슈가 있었고 확인해보니 그냥 @EventListener를 사용하는 경우는 동기 방식으로 하나의 트랜잭션에서 처리가 되고 있다는 것을 알 수 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710827624232&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Slf4j
public class OutboxEventListener {
    ...

    @Transactional(rollbackFor = Exception.class)
    @EventListener({ApplicationReadyEvent.class, OutboxCreated.class})
    public void deliveryOutboxEvents() {
        // 작업 내용
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 이 상태에서 리스너의 처리가 실패하는 경우 예외가 전파되면서 이전 트랜잭션의 처리도 실패하는 문제가 있을 수 있었습니다. (현재 코드 상 이전 트랜잭션에서 이후 트랜잭션에 대한 try-catch를 하지 않았기 때문이고 일반 @Transactional을 사용하는 경우 새로운 트랜잭션이 아닌 이전 트랜잭션이 있다면 해당 트랜잭션에 참여하기 때문입니다...)&lt;/p&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 응답 속도를 개선하고 트랜잭션 이슈도 해결하도록 수정하였습니다. 비동기를 구현하는 방식은 크게 @Async 애노테이션을 사용하거나 ApplicationEventMulticaster를 사용하여 구현할 수 있는데 저는 @Async 애노테이션을 붙여서 비동기를 구현하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710827567591&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Slf4j
public class OutboxEventListener {
    ...

    @Async
    @Transactional(rollbackFor = Exception.class)
    @EventListener({ApplicationReadyEvent.class, OutboxCreated.class})
    public void deliveryOutboxEvents() {
        // 작업 내용
    }
}&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;위와 같이 처리하는 경우 이벤트 리스너 부분이 비동기로 동작하기 때문에 이벤트 발행 주최는 발행한 이후 리스너 처리를 기다리지 않고 바로 종료되어 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;@Async 처리와 별개로 트랜잭션을 분리하고 싶거나 리스너 쪽 예외를 전파하고 싶지 않다면 @Transactional(REQUIRED_NEW)를 사용할 수 있습니다. (같은 트랜잭션을 사용하면서 예외 전파를 원하지 않는 경우 발행 쪽에서 try-catch를 사용해도 됩니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단순 @EventListner를 사용하는 경우 발행 쪽에서 예외가 발생한다고 해도 예외 발생 전 이벤트가 발행되었다면 이벤트 리스너가 실행됩니다. 트랜잭션을 조금 더 세부적으로 다루려면 @EventListener를 확장한 @TransactionalEventListener를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 @TransactionalEventListener를 사용하게 되면 phase가 AFTER_COMMIT 이기 때문에 이벤트 발행 쪽 트랜잭션이 커밋된 이후에 해당 이벤트 리스너가 이벤트를 받아 실행되므로 예외가 발생하는 경우는 실행되지 않습니다. 또한, 이렇게 되는 경우 이미 커밋이 된 다음 수행하는 것이므로 기존 트랜잭션과 분리가 되어 별도로 @Transactional(REQUIRED_NEW)를 하지 않아도 됩니다.&lt;/p&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시글-댓글 예시가 있을 때 게시글을 삭제하고 댓글 삭제를 이벤트로 처리한 경우 게시글 삭제는 성공했는데 댓글 삭제가 실패하는 경우 없는 게시글에 대한 댓글이 남아있을 수 있는 문제&lt;/li&gt;
&lt;li&gt;하지만 댓글의 경우 게시글이 없을 때 배치 등을 통해 참조하는 게시글이 없는 댓글은 따로 삭제할 수 있는 방법이 존재하여 큰 이슈는 아니라고 생각&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/event/EventListener.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/event/EventListener.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mangkyu.tistory.com/292&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mangkyu.tistory.com/292&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ktae23.tistory.com/258&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ktae23.tistory.com/258&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <author>DevBee</author>
      <guid isPermaLink="true">https://developerbee.tistory.com/271</guid>
      <comments>https://developerbee.tistory.com/271#entry271comment</comments>
      <pubDate>Wed, 20 Mar 2024 13:16:32 +0900</pubDate>
    </item>
    <item>
      <title>[AWS APIGateway] APIGateway WebSocket API 사용하기 2</title>
      <link>https://developerbee.tistory.com/270</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 AWS API Gateway를 WebSocket 형태로 생성하고 기본적인 connect, disconnect 하는 방법을 알아보았습니다. 이제 새로운 커스텀 경로를 생성하여 데이터를 전송하고 그 결과 다시 Spring Application에서 API Gateway 쪽으로 전송해 연결된 소켓 클라이언트에 데이터를 전달해보도록 하겠습니다.&lt;/p&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 API Gateway에 새로운 경로를 추가하고 통합 요청 설정을 해주겠습니다. 이는 기존에 추가한 connect, disconnect 경로와 유사합니다.&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;스크린샷 2024-03-04 오후 4.33.13.png&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;1070&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xPoDf/btsFxXoh1GQ/1hD7EhfgbIAw74jDdWEpXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xPoDf/btsFxXoh1GQ/1hD7EhfgbIAw74jDdWEpXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xPoDf/btsFxXoh1GQ/1hD7EhfgbIAw74jDdWEpXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxPoDf%2FbtsFxXoh1GQ%2F1hD7EhfgbIAw74jDdWEpXk%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;2092&quot; height=&quot;1070&quot; data-filename=&quot;스크린샷 2024-03-04 오후 4.33.13.png&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;1070&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 경로를 생성한 뒤, 요청 템플릿을 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1709537908103&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;connectionId&quot; : &quot;$context.connectionId&quot;,
    &quot;requestId&quot;: &quot;$input.path('requestId')&quot;,
    &quot;body&quot; : $input.json('$.body')
}

// 실제 POST 요청 시 다음과 같이 request body를 보내면 됩니다.
{
    &quot;action&quot;: &quot;echo&quot;,
    &quot;requestId&quot;: &quot;YOUR_UUID_아무거나&quot;,
    &quot;body&quot;: {
        &quot;key&quot;: &quot;12345&quot;,
        &quot;message&quot;: &quot;hello~~!!!&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 해당 경로를 호출하면 요청 데이터의 body를 그대로 다시 API Gateway 쪽으로 전송하는 방법을 알아보겠습니다. 데이터를 전송하기 위한 URL은 다음에서 확인할 수 있습니다.&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;스크린샷 2024-03-04 오후 4.39.26.png&quot; data-origin-width=&quot;3706&quot; data-origin-height=&quot;984&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lk9Hc/btsFxWiC2jF/Chw6EoxSRdjkYn1cGpcGRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lk9Hc/btsFxWiC2jF/Chw6EoxSRdjkYn1cGpcGRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lk9Hc/btsFxWiC2jF/Chw6EoxSRdjkYn1cGpcGRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flk9Hc%2FbtsFxWiC2jF%2FChw6EoxSRdjkYn1cGpcGRK%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;3706&quot; height=&quot;984&quot; data-filename=&quot;스크린샷 2024-03-04 오후 4.39.26.png&quot; data-origin-width=&quot;3706&quot; data-origin-height=&quot;984&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;해당 URL을 다음과 같이 application.yml에 설정합니다. 단, @connections 부분은 제거합니다. 그리고 나서 ApiGateway 설정을 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1709538633063&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cloud:
  aws:
    region: ap-northeast-2
    websocket-url: https://xxxxx.execute-api.ap-northeast-2.amazonaws.com/develop/&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1709538710701&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.apigatewaymanagementapi.AmazonApiGatewayManagementApi;
import com.amazonaws.services.apigatewaymanagementapi.AmazonApiGatewayManagementApiClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AwsApiGatewayConfig {

    @Value(&quot;${cloud.aws.region}&quot;)
    private String region;

    @Value(&quot;${cloud.aws.websocket-url}&quot;)
    private String webSocketUrl;

    @Bean
    public AmazonApiGatewayManagementApi awsApiGatewayManagementApi() {
        // WebSocket 백엔드 메세지 전송 URL 설정 정보 (* 연결 URL은 @Connections 경로는 제외)
        AwsClientBuilder.EndpointConfiguration config = new AwsClientBuilder
                .EndpointConfiguration(webSocketUrl, region);
                
        return AmazonApiGatewayManagementApiClientBuilder.standard()
                .withEndpointConfiguration(config)
                .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;이후 다음과 같이 AmazonApiGatewayManagementApi를 주입 받아 postToConnection 메서드를 호출하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1709538854558&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class AwsWsApiGateway {
    private final AmazonApiGatewayManagementApi client;

    public void send(String connectionId, PostToConnectionData&amp;lt;?&amp;gt; data) {
        PostToConnectionRequest wssRequest = new PostToConnectionRequest();
        wssRequest.setConnectionId(connectionId);

        // 전송 대상에게 보낼 메세지 생성
        Charset charset = StandardCharsets.UTF_8;
        CharsetEncoder encoder = charset.newEncoder();
        ByteBuffer buff = null;
        try {
            buff = encoder.encode(CharBuffer.wrap(ObjectMapperUtil.writeValueAsString(data)));
        } catch (CharacterCodingException e) {
            log.error(&quot;[AwsWsApiGateway.send] encode error={}&quot;, e.getMessage(), e);
        }

        log.debug(&quot;[AwsWsApiGateway.send] connectionId = {} | data = {}&quot;, connectionId, data);

        wssRequest.setData(buff);
        client.postToConnection(wssRequest);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1709539346981&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RestController
@RequiredArgsConstructor
public class SampleRestController {
    private final AwsWsApiGateway apiGateway;

    @PostMapping(&quot;/echo&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; echo(@RequestBody CommonWsRequest&amp;lt;?&amp;gt; request) {
        String connectionId = request.connectionId();
        apiGateway.send(connectionId, PostToConnectionData.builder().action(&quot;echo&quot;).body(request.body()).build());
        return createSuccessResponse(request.requestId(), request.body());
    }
}


// CommonWsRequest.java
public record CommonWsRequest&amp;lt;T&amp;gt;(
        String connectionId,
        String requestId,
        T body
) {
}

// PostToConnectionData.java
public record PostToConnectionData&amp;lt;T&amp;gt; (
        String action,
        T body
) {
    @Builder
    public PostToConnectionData {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 결과를 확인해보면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-03-04 오후 5.09.48.png&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;996&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPtGcq/btsFnY3MkWE/27vn3TA7CV5mhku0mxn0vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPtGcq/btsFnY3MkWE/27vn3TA7CV5mhku0mxn0vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPtGcq/btsFnY3MkWE/27vn3TA7CV5mhku0mxn0vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPtGcq%2FbtsFnY3MkWE%2F27vn3TA7CV5mhku0mxn0vk%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;1650&quot; height=&quot;996&quot; data-filename=&quot;스크린샷 2024-03-04 오후 5.09.48.png&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;996&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 ApiGateway 에서는 한번 연결된 소캣에 아무런 요청, 응답이 없는 경우 기본적으로 10분 뒤 연결이 종료됩니다. 따라서 이렇게 된 경우 연결이 종료된 connectionId에 postToConnection 메시지를 보내는 경우 GoneException 이 발생할 수 있습니다. 저는 try-catch 로 이 에러를 잡아서 redis에 저장했던 connectionId를 삭제하는 기능을 추가로 구현하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1709540223532&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class AwsWsApiGateway {
    private final AmazonApiGatewayManagementApi client;
    private final WebSocketService sessionService;

    public void send(String connectionId, PostToConnectionData&amp;lt;?&amp;gt; data) {
        ...
        
        try {
            client.postToConnection(wssRequest);
        } catch (GoneException e) {
            log.error(&quot;[AwsWsApiGateway.send] connectionId={} GoneException =&amp;gt; &quot;, connectionId, e);
            sessionService.disconnect(connectionId);  // 해당 메서드에서 redis session 삭제
        }
    }
}&lt;/code&gt;&lt;/pre&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;참고&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://realwater87.tistory.com/9&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://realwater87.tistory.com/9&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developerbee.tistory.com/266&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;&lt;/p&gt;</description>
      <category>AWS</category>
      <author>DevBee</author>
      <guid isPermaLink="true">https://developerbee.tistory.com/270</guid>
      <comments>https://developerbee.tistory.com/270#entry270comment</comments>
      <pubDate>Tue, 5 Mar 2024 08:50:11 +0900</pubDate>
    </item>
    <item>
      <title>[항해 99] 항해 플러스 3기 10주간의 솔직 후기  </title>
      <link>https://developerbee.tistory.com/269</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;23년 12월부터 시작한 항해 플러스 3기. 10주간의 여정이 모두 마무리되었다. 10주동안 정말 많은 일이 있었지만 조금 간단하게...? 회고해보려 한다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&amp;gt; &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;항해 플러스를 시작하기 전, 어떻게 시작했는지? 에 대한 내용은 이전 글에서 확인!&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://developerbee.tistory.com/265&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developerbee.tistory.com/265&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1주차: 남이 타준 커피가 젤 맛있다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 환경에 CI/CD 자동화 구축... 그동안 개인적으로 CI/CD 및 클라우드 환경에 대해 공부를 했었지만, 실무에서는 이미 만들어진 환경에서 개발만 진행하는 경우가 많아서 였을까... 과정을 하나하나 진행하는게 쉽지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Github Action을 통한 배포 자체가 처음이라 브랜치, 환경에 대한 전략을 어떻게 가져가는 것이 좋을지 많은 고민이 되었다. 일반적으로 회사에서 local, dev, stg, prd 환경을 만드니까 비슷하게 해봐야겠다 생각은 했고 개인 프로젝트라 local, dev 환경만 분리하는 것으로 마무리를 했다. 또 진행을 하면서 테스트-빌드만 필요한 경우와 배포까지 필요한 경우를 적절히 나눠 주는 것도 필요하다는 것을 알게 되었다. 가이드를 제공해주셔서 처음에는 많이 어려웠지만 그래도 어찌저찌 분리가 가능했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 클라우드 환경에 배포를 진행하면서도 많은 이슈가 있었다. 아무 생각 없이 ECS 서비스 생성 시 Public IP를 넣어줘야 하나 고민을 하다 뺐더니... ECR에서 이미지를 가져오거나 하는 등의 인터넷 통신이 필요한 부분이 전혀 실행되지 않았고 이 에러를 해결하는데도 시간이 한참이 걸렸다... 해결 방안은 Public IP 지정을 하거나 NAT 게이트웨이를 추가해 외부 통신이 가능하게 만들어주는 방법이 있었다... 또한, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;AWS 비용은 어떻게 할지 최소한의 비용을 서비스를 띄우는 방법은 뭐가 있을지 고민이 많았다. 비용을 아끼려고 별도 RDS 등을 사용하는 대신 ECS 서비스 생성 시 연관된 도커 컨테이너를 같이 띄우는 방법을 고민했는데... 멘토링을 통해 방법을 제안 받았지만 아직 시도는 못해보았다 ㅠㅠ&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이렇게 비용을 아끼려는 노력을 했음에도... ECS를 띄워두고 계속 두었더니 10만원 가까운 비용이 발생했다... 부랴부랴 블로그 글들을 검색하면서 환불 받을 수 있을 방안들을 찾았고 서비스 센터에 메일을 보내는 등의 방식으로 돈을 돌려 받을 수 있었다... 그러다보니 조금 더 쉽게 환경을 구축하고 없앨 수 있는 방안들을 고민하게 되었고 Terraform 적용을 검토중이다...! (사실... 전부터 공부해야지 했지만 아직도 안하고 있다니... 하... &amp;zwj;♀️)&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 style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2,3,4주차: 어서와, TDD는 처음이지?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주차부터는 3가지 주제 중 1개를 정해 TDD 방식으로 API를 구현하는 것이었다. 같은 주제로 팀원들과 각자 개인 프로젝트를 진행하다보니 내 속도에 맞춰 진행하면서도 각자 고민하는 내용을 공유하고 같이 해결할 수 있어서 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이커머스 관련된 주제를 선정하였고 API 5개? 정도를 구현하는 프로젝트였다. 사실 API 구현은 그동안 업무로 많이 해오던 일이라 크게 어렵지 않겠다 생각했는데... 오산이었다. 너무 당연하게 생각해오던 Layered Architecture 를 처음부터 도메인 구분으로 나누고 TDD 방식으로 기능을 구현하는 것은 여간 어려운 일이 아니었다...ㅠㅠ 평소 개발 시 service layer에 너무 많은 책임을 주고 있다고 생각해 도메인을 중심으로 기능을 잘 나눠보고자 했는데 처음 이렇게 하는거라 쉽지 않았다. 또한 TDD 방식은 생각보다 더 어려웠다. 기능을 만들지 않고 먼저 테스트를 짜면서 그때 그때 필요한 것들을 만들어 나가 테스트를 성공시키는 방식은 익숙하지 않아서인지 개발 속도를 너무 늦췄다... &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 3주동안 꾸준히 연습하고 멘토링하다보니 배우는 것들이 정말 많았다. 기본에 충실할 것!!! 이것이 첫번째다. 어떤 기능을 구현하기 위해 여러가지 방안이 있겠지만 최소한의 자원으로 기능을 구현해보고 거기서 안되면 다른 방안을 고민해야지 먼저부터 너무 다양한 방안을 시도하려고 하면 탈난다...ㅎㅎ 그리고 테스트의 중요성과 테스트를 짜면서 점점 기능 구현을 해나가는 방식은 매우 중요하고 나중에도 해당 기능을 이해하는데 많은 도움이 된다는 것을 알게 되었다. 테스트를 짜면서 더 좋은 코드란, 더 테스트 짜기 용이한 클래스란 무엇인지 고민하고 수정을 할 수 있어 좋았다. 또한, 도메인을 적절하게 잘 분리하고 각 도메인의 관계를 잘 이해하고 공유하는 것도 매우 중요하다는 것을 알게 되었다.&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 style=&quot;color: #000000;&quot;&gt;&lt;b&gt;5,6,7주차: 당신의 서버, 안전합니까?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주차부터는 로깅, 모니터링 그리고 장애 대응에 대한 내용을 배우게 되었다. 로그를 남기는 것이 항상 중요하고 이것을 모니터링하는 다양한 방법이 있는데 항해에서는 CloudWatch 를 통해 로그를 보고 에러 로그의 경우 슬랙으로 알림을 보내는 방식을 배울 수 있었다. 그리고 부하 테스트가 무엇인지, 어떤 식의 시나리오를 짤 수 있는지도 배우게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 파트 하나하나를 집중 분석하면서 해보지는 못했는데 꼭 알고 있어야 하고 연차가 쌓일수록 더 많이 필요한 부분이라고 생각했다. 코치님께서 멘토링 시간에 현업에서 어떻게 하고 계신지에 대한 내용들을 공유 주셔서 많은 도움이 되었다. 이 부분은 다시 한번 혼자 해보고 직접 만든 API를 대상으로 다양한 테스트를 진행하면서 로그를 보고 에러를 빨리 탐지하는 환경을 만들어보면 좋을 것 같다고 생각했다...!! (사실 이때 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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;8,9,10주차: 오픈소스? 나도 할 수 있다!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8주차부터는 팀 단위로 오픈소스 개발을 진행하게 되었다. 오픈 소스는 이름만 들었을 때 뭔가 유니콘처럼 너무 멀게 느껴졌다. 그동안 제공해주는 많은 오픈 소스들을 감사하게 쓰고 있었기 때문에 큰 불편함을 느끼지 않았고 '뭐가 있으면 좋겠다...' 해도 내가 못 찾아서 적용을 못하나보다 생각하곤 했다. 그래서 주제를 정하는 부분이 가장 어려웠던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 우리 팀은 인텔리제이 플러그인을 개발했다. (아이디어 주신 헌우 코치님 감사합니당  ) 평소에 깊게 생각하지 않았는데 스프링부트에서 관리해주는 dependencies의 경우 버전을 명시하지 않아도 된다고 알려준 뒤, 사용자가 확인하면 명시된 버전을 제거해주는 플러그인이다. (깃헙 참고해주세요  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Plugin 개발은 처음이라서 패키지 구조 및 어떤 식의 기능을 구현하고 테스트를 짜야할지 조금 막막한 부분도 있었지만, 전체 기능을 적어보고 순서대로 구현하면서 팀원들과 공유하고 멘토링도 받고 하면서 구조를 잡아갈 수 있었다. 또한, 어떤 방식이 더 좋을지 고민하면서 계속 소스를 수정하고 같이 논의하는 과정도 도움이 많이 되었다. 다른 사람의 생각을 듣고 기능을 발전시키는 일은 어렵지만 뿌듯하다. 역시 개발은 소통을 하는 시간이 중요하다는 것을 다시 느낄 수 있었고 누군가의 불편함을 해결하고 도움을 주는 오픈 소스를 제작한다는 점에서 의미있는 시간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 오픈 소스에 기여하는 방법도 알게 되었다. 이번에는 단순한 문서 수정이었지만 평소 사용하던 오픈 소스에서 불편한 점이나 이슈들을 찾고 해결 방안을 고민하고 소통하는 방법을 알게 되었다. 해당 오픈 소스에서 제공하는 가이드에 맞게 Issue와 PR을 생성해서 올리고 소통하면 된다. 이런 과정은 평소 멀게만 느껴지고 어렵게 여겨졌는데 한번 해보니 접근은 쉬울 수 있고 같이 소통해서 만들어나가는 뿌듯함을 경험할 수 있어서 좋았다. 앞으로 더 자주 시도해보면 좋겠다고 생각했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;결과물&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1; text-align: left;&quot; href=&quot;https://github.com/hanbee1005/hanghae-plus-3&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;개인 프로젝트 깃헙 - 이커머스 프로젝트&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/hhplus3-team4/dependencies-version-helper&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;팀 프로젝트 깃헙 - 인텔리제이 플러그인&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;소감 정리 및 앞으로 하고 싶은 것&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10주동안 정말 많은 일이 있었다. 시작 전 10주는 누구나 집중할 수 있고 나도 할 수 있는 짧은 코스라고 생각했는데 막상 진행하고 나니 생각보다 길었고 그 사이 많은 일들이 있어서 집중하기 힘든 하루들이었다. 그래도 10주간의 여정은 정말 의미있고 좋았다. 다양한 개발자들과 같이 고민하고 문제를 해결해가는 과정은 넓은 시야를 가질 수 있게 해주었고 좋은 동료들을 얻을 수 있게 해주었다. 또한, 코치님들의 진심이 가득 담긴 멘토링을 받으면서 현업에서 어떤 것을 중요시 하고 어떤 부분을 더 고민하고 계신지 들을 수 있어서 소중한 시간이었다. 앞으로도 배운 내용을 바탕으로 꾸준히 성장하며 동료들과 함께 서비스를 발전시키는 개발자가 되어야겠다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Keep&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매일 개발하기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;항해 플러스를 하면서 1일 1커밋을 실천해보았다. 매일 개인 프로젝트를 고민하고 테스트 코드를 짜고 수정하여 기능을 만들어가는 과정은 많은 도움이 되었고 퇴근 후 공부하는 습관을 만들어 주었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트 코드 짜기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀게만 느껴지던 TDD 방식을 배울 수 있었고 얼마나 중요한지 더 알게 되었다. 이를 통해 실무에서도 테스트 코드를 고민하며 클래스들을 수정해 나가고 안정적인 서비스를 만들어 가고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다른 사람 코드 보기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실무에서는 여러가지 이유로 코드 리뷰를 하지 못하고 있다. 하지만 항해를 통해 다양한 개발자들의 코드를 같이 보고 더 나은 방향을 고민할 수 있어서 좋았고 앞으로도 자주 다른 사람의 코드를 보면서 좋은 방향을 고민하고 내 코드에도 적용해보고 싶다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Problom&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인프라 구축
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라우드 환경을 처음부터 구축하고 전체 아키텍처를 만드는 일은 생각보다 쉽지 않았고 비용 문제도 있어서 더 고민할 것들이 많았던 것 같은데 이를 최종적으로 완벽하게 공부하고 해결하지 못해서 아쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;업무, 도메인 분리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;혼자 프로젝트를 할 때 처음해보는 도메인 설계를 어떻게 해야할지 고민이 많아 일을 나누고 일정을 산정하는데 어려움이 많았던 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Try&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드로 인프라 구축
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인프라 환경을 코드로 작성하여 더 쉽게 만들고 지울 수 있는 방법을 고민하면 좋을 것 같다. 또한, 클라우드 환경에 대한 이해를 높이기 위해 AWS 자격증도 공부하면 좋겠다고 생각했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BoostAPI 개발
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 프로젝트 기본 구조를 제공할 수 있는 프로젝트를 만들면 좋겠다고 생각했다. 이를 통해 다양한 방법론을 공부하면서 아키텍처 구조를 연습해보고 개인적으로 프로젝트를 진행할 때 베이스로 가져다가 사용하면 좋을 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;항해 플러스를 고민하는 분들께&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항해 플러스는 다른 것보다 열정 넘치는 코치님들 덕분에 더 좋은 코스라고 생각한다. 수강생들보다 열정적으로 본인의 시간을 투자하여 밤낮없이 멘토링을 해주시고 더 좋은 방향성을 알려주시기 때문에 다른 유료 강의들보다 가성비가 좋다고 생각한다!!!!!!&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지원 페이지 &amp;gt; 유입 경로 &amp;gt; 지인소개 &amp;gt; &amp;lsquo;3기 OOO&amp;rsquo;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발자사이드프로젝트</category>
      <category>주니어개발자</category>
      <category>주니어개발자멘토링</category>
      <category>주니어개발자역량강화</category>
      <category>코딩부트캠프</category>
      <category>코딩부트캠프후기</category>
      <category>항해99</category>
      <category>항해플러스</category>
      <author>DevBee</author>
      <guid isPermaLink="true">https://developerbee.tistory.com/269</guid>
      <comments>https://developerbee.tistory.com/269#entry269comment</comments>
      <pubDate>Sat, 17 Feb 2024 16:25:13 +0900</pubDate>
    </item>
    <item>
      <title>[AWS MSK] SpringBoot와 MSK Serverless 연동</title>
      <link>https://developerbee.tistory.com/268</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 MSK를 사용하게 되었습니다. MSK는 Serverless 로 생성되었는데 이와 SpringBoot 프로젝트를 연동하는 방식에 대해 살펴보고자 합니다.&lt;/p&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-kafka dependencies를 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1706565510123&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle
dependencies {
    ...
    // kafka
    implementation 'org.springframework.kafka:spring-kafka'
    ...
}&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;여기서 MSK 생성에 대해서는 다루지 않겠습니다. MSK를 Serverless로 생성하고 나서 클라이언트 정보 보기를 하면 아래와 같이 부트스트랩 서버의 엔드포인트를 확인할 수 있습니다. Serverless 의 경우 현재 인증 유형은 IAM 밖에 존재하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-01-30 오전 8.48.28.png&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;663&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crsRDm/btsEft2RzCz/aCnjQCOEksPFEKG96kWLn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crsRDm/btsEft2RzCz/aCnjQCOEksPFEKG96kWLn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crsRDm/btsEft2RzCz/aCnjQCOEksPFEKG96kWLn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrsRDm%2FbtsEft2RzCz%2FaCnjQCOEksPFEKG96kWLn0%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;3000&quot; height=&quot;663&quot; data-filename=&quot;edited_스크린샷 2024-01-30 오전 8.48.28.png&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;663&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-01-30 오전 8.48.58.png&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuLNMa/btsEcqMOvNL/THERkEcHdmiZeNFfiAkTyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuLNMa/btsEcqMOvNL/THERkEcHdmiZeNFfiAkTyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuLNMa/btsEcqMOvNL/THERkEcHdmiZeNFfiAkTyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuLNMa%2FbtsEcqMOvNL%2FTHERkEcHdmiZeNFfiAkTyK%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;3000&quot; height=&quot;741&quot; data-filename=&quot;edited_스크린샷 2024-01-30 오전 8.48.58.png&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;741&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;이를 SpringBoot 프로젝트의 application.yml 파일에 설정을 해줍니다. 그리고 별도로 작성한 Configuration 파일에서 가져다 쓸 수 있게 적어줍니다. 여기서는 Consumer 관련 설정만 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1706565801641&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// application.yml
spring:
  kafka:
    consumer:
      bootstrap-servers: BOOTSTRAP_SERVER_URL:PORT  // 예) boot-xxxxx.amazonaws.com:9098
      group-id: CONSUMER_GROUP_ID&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1706566009791&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// KafkaConsumerConfig.class

@EnableKafka
@Configuration
public class KafkaConsumerConfig {

    @Value(&quot;${spring.kafka.consumer.bootstrap-servers}&quot;)
    private String BOOTSTRAP_SERVER;

    @Value(&quot;${spring.kafka.consumer.group-id}&quot;)
    private String GROUP_ID;

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, EventPayload&amp;gt; iropsContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, EventPayload&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(getConfig(), new StringDeserializer(), new JsonDeserializer&amp;lt;&amp;gt;(EventPayload.class)));
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
        return factory;
    }

    private Map&amp;lt;String, Object&amp;gt; getConfig() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVER);
        props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
        props.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, StringDeserializer.class);
        props.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class);

        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;latest&quot;);
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

        return props;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❗️&lt;span style=&quot;color: #ef6f53;&quot;&gt;중요&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 MSK Serverless인 경우 추가 작업이 필요한데요. 먼저 build.gradle 파일에 iam 관련 dependencies를 추가해주어야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1706565569769&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle
dependencies {
    ...
    // kafka
    implementation 'org.springframework.kafka:spring-kafka'
    implementation 'software.amazon.msk:aws-msk-iam-auth:1.1.7'  // 여기 추가
    ...
}&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;그리고 나서 Configuration 파일에 설정을 추가해줍니다. application.yml 파일에 설정한 뒤, @Value 애노테이션으로 configuration 파일에서 가져다 사용할 수 있도록 처리를 해도 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1706566208464&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// KafkaConsumerConfig.class

@EnableKafka
@Configuration
public class KafkaConsumerConfig {
    private final List&amp;lt;String&amp;gt; LOCAL_PROFILES = List.of(&quot;local&quot;, &quot;default&quot;);

    @Value(&quot;${spring.kafka.consumer.bootstrap-servers}&quot;)
    private String BOOTSTRAP_SERVER;

    @Value(&quot;${spring.kafka.consumer.group-id}&quot;)
    private String GROUP_ID;

    @Value(&quot;${spring.config.activate.on-profile}&quot;)
    private String profile;

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, EventPayload&amp;gt; iropsContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, EventPayload&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(getConfig(), new StringDeserializer(), new JsonDeserializer&amp;lt;&amp;gt;(EventPayload.class)));
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
        return factory;
    }

    private Map&amp;lt;String, Object&amp;gt; getConfig() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVER);
        props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
        props.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, StringDeserializer.class);
        props.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class);

        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;latest&quot;);
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

        // 여기 추가 (local, default 프로파일이 아닌 경우에만 설정 추가)
        if (!LOCAL_PROFILES.contains(profile)) {
            props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, &quot;SASL_SSL&quot;);
            props.put(SaslConfigs.SASL_MECHANISM, &quot;AWS_MSK_IAM&quot;);
            props.put(SaslConfigs.SASL_JAAS_CONFIG, &quot;software.amazon.msk.auth.iam.IAMLoginModule required;&quot;);
            props.put(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS, &quot;software.amazon.msk.auth.iam.IAMClientCallbackHandler&quot;);
        }

        return props;
    }
}&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;이렇게 하고 만들어둔 SpringBoot 프로젝트를 MSK와 같은 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;이 설정을 하지 않고 접근을 시도(예. Producer로 send 하는 등) 하는 경우 &lt;b&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;java.lang.OutOfMemoryError: Java heap space&lt;/span&gt;&amp;nbsp;&lt;/b&gt;가 발생하면서 애플리케이션 서버가 다운되는 현상이 발생하기 때문에 꼭 올바른 인증유형을 사용한 접근이 필요합니다  &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;추가로 MSK에 토픽이 생성되어 있지 않은 경우 토픽을 찾을 수 없다는 DEBUG 레벨의 로그가 계속 남을 수 있습니다. 현재 기준으로 MSK의 토픽을 생성하는 방법은 MSK와 같은 VPC 내에 클라이언트 서버(아무 EC2)를 하나 생성하고 그곳에 접근하여 kafka 설치 후 명령으로 토픽을 생성하는 방법 뿐입니다... &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;https://docs.aws.amazon.com/ko_kr/msk/latest/developerguide/serverless-getting-started.html&quot; href=&quot;https://docs.aws.amazon.com/ko_kr/msk/latest/developerguide/serverless-getting-started.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;서버리스 가이드&lt;/a&gt;를 참고하여 클라이언트 서버를 생성한 뒤 다음 명령을 통해 토픽을 생성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1706652618900&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;path-to-your-kafka-installation&amp;gt;/bin/kafka-topics.sh --bootstrap-server $BS --command-config client.properties --create --topic &amp;lt;your-topic-name&amp;gt; --partitions 2&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;a href=&quot;https://docs.aws.amazon.com/ko_kr/msk/latest/developerguide/serverless-getting-started.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/ko_kr/msk/latest/developerguide/serverless-getting-started.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@dlgosla/aws-aws-mskkafka-serverless-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@dlgosla/aws-aws-mskkafka-serverless-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://repost.aws/knowledge-center/msk-cluster-connection-issues&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://repost.aws/knowledge-center/msk-cluster-connection-issues&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AWS</category>
      <category>AWS</category>
      <category>msk</category>
      <author>DevBee</author>
      <guid isPermaLink="true">https://developerbee.tistory.com/268</guid>
      <comments>https://developerbee.tistory.com/268#entry268comment</comments>
      <pubDate>Wed, 31 Jan 2024 07:14:37 +0900</pubDate>
    </item>
    <item>
      <title>[MyBatis Mapper] MyBatis Interceptor로 Auditing 구현</title>
      <link>https://developerbee.tistory.com/267</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 프로젝트 작업 중 갑자기 테이블들이 수정되면서 대부분의 테이블에 생성자, 생성일시, 수정자, 수정일시가 추가되었습니다. 로그인한 사용자의 Id를 기반으로 데이터를 insert, update, delete할 때 해당 내용들을 추가해줘야 했고 이를 위해 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;CommonAuditing 추상 클래스 생성 후 필요한 Model 들에서 이를 상속받아 추가하는 방법도 고민했지만 이미 어느정도 작업이 진행되어 있어서 전체 DTO, Domain, Model을 수정하는 것은 무리가 있다고 생각했고 그나마 나은 방법으로 Mapper와 xml 파일만 수정할 수 있는 방법을 고민하여 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를 구현해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705069943536&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = &quot;query&quot;, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }),
        @Signature(type = Executor.class, method = &quot;update&quot;, args = { MappedStatement.class, Object.class })
})
public class MybatisExecuteInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String method = invocation.getMethod().getName();

        Object[] args = invocation.getArgs();
        Object param = args[1];

        if (&quot;update&quot;.equals(method)) {
            setAuditing(param);
            return invocation.proceed();
        } else if (&quot;query&quot;.equals(method)) {
            // TODO 조회 시 할 일
        }

        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Interceptor.super.plugin(target);
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }

    private void setAuditing(Object param) {
        log.info(&quot;[MybatisExecuteInterceptor] update param = {}&quot;, param);

        SessionDto session = SessionScopeUtil.getContextSession(); // 로그인한 사용자 정보 조회

        if (param instanceof MapperMethod.ParamMap&amp;lt;?&amp;gt;) {
            MapperMethod.ParamMap&amp;lt;Object&amp;gt; newParam = (MapperMethod.ParamMap&amp;lt;Object&amp;gt;) param;

            newParam.put(&quot;createdBy&quot;, session.getStaffId());
            newParam.put(&quot;createAt&quot;, LocalDateTime.now());
            newParam.put(&quot;lastModifiedBy&quot;, session.getStaffId());
            newParam.put(&quot;lastModifiedAt&quot;, LocalDateTime.now());
        }
    }
}&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;org.apache.ibatis.plugin.Interceptor 인터페이스를 구현하면 interceptor 라는 메서드를 오버라이딩 해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드의 인자엔 Invocation 객체가 제공되는데 여기에 파라미터로 전달된 값과 호출된 xml 태그에 대한 메타 정보들을 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Signature 애노테이션을 좀 더 살펴보면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;type
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Executor 라는 인터페이스는 Mybatis의 xml 파일에 작성된 SQL을 실행합니다. 해당 인스턴스 내부를 보면 각 mybatis method 시그니처 정보를 볼 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;method
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;insert / update / delete 가 실행되면 Excutor의 update 라는 메서드를 호출하며 select 는 query 라는 메서드를 호출합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;args
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통적으로 MapperStatement라는 객체가 Object[] 타입의 인덱스 0번에 필수로 저장됩니다. 여기에 xml 메타 정보가 담겨 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 insert / update / delete 시 전달된 파라미터들을 받아 해당 파라미터들 뒤에 필요한 값들을 Map의 key, value 형태로 넣어줍니다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;(어떻게 할지 고민했는데 내용은 뒤에서...)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 사용하고 있던 mybatis-config.xml에 plugin으로 interceptor를 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705070001558&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE configuration PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&amp;gt;

&amp;lt;configuration&amp;gt;
    &amp;lt;settings&amp;gt;
        &amp;lt;setting name=&quot;mapUnderscoreToCamelCase&quot; value=&quot;true&quot;/&amp;gt;
        &amp;lt;setting name=&quot;callSettersOnNulls&quot; value=&quot;true&quot;/&amp;gt;
        &amp;lt;setting name=&quot;defaultFetchSize&quot; value=&quot;100&quot;/&amp;gt;
    &amp;lt;/settings&amp;gt;

    &amp;lt;plugins&amp;gt;
        &amp;lt;plugin interceptor=&quot;me.hanbee.test.interceptor.MybatisExecuteInterceptor&quot;/&amp;gt;
    &amp;lt;/plugins&amp;gt;

&amp;lt;/configuration&amp;gt;&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를 사용하는 방법은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1705071784365&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Mapper 클래스
@Mapper
public interface MemberMapper {
    ...
    int save(@Param(&quot;m&quot;) Member member);
    ...
}

// xml 파일
&amp;lt;insert id=&quot;save&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;m.id&quot;&amp;gt;
    INSERT INTO member(email, name, created_by, create_at)
    VALUES (#{m.email}, #{m.name}, #{createdBy}, #{createAt})
&amp;lt;/insert&amp;gt;&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;이렇게 하면 비즈니스 또는 프레젠테이션 레이어의 객체들 또는 Model 객체 자체에 데이터 저장, 수정을 위한 생성자/생성일시/수정자/수정일시에 대한 프로퍼티 값들을 세팅할 필요가 없어지고 Mapper와 xml 쿼리 부분만 수정을 통해 원하는 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  @Param(&quot;dto&quot;) 를 꼭 넣어줘야할지&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존에 insert 시 파라미터 하나만 넘기는 경우 별도의 @Param 애노테이션을 사용하지 않고 xml 파일에서도 #{dto.xxx} 대신 #{xxx}를 바로 사용했습니다.&lt;/li&gt;
&lt;li&gt;이 경우에는 interceptor에서 파라미터를 확인했을 때 바로 객체가 들어오게 되고 이 경우에 객체 내부에 동적으로 필드를 추가하는 것은 조금 어렵지 않을까 판단했습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;param = Member(email=xxx@gmail.com, ...)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;따라서 기본적으로 xml 로 전달되는 파라미터에 @Param 애노테이션을 붙여서 interceptor에서 확인했을 때 Map 형태로 받을 수 있도록 하였고 추가적인 파라미터를 Map에 추가하는 방식으로 개발하였습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;param = {m=Member(), param1=Member(), createdBy=&quot;xxx&quot;, ... }&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;@Param 을 사용하지 않고 객체 하나를 파라미터로 받는 경우 이를 Map으로 변환하고 해당 Map에 추가 파라미터를 넣는 방향으로 진행을 해볼까 생각했지만 직렬화/역직렬화 이슈가 있었고&lt;/li&gt;
&lt;li&gt;객체를 그대로 사용한다면 각 Model(DTO, Domain 등)에서 추가 컬럼에 대한 필드를 가지고 있거나 상속을 받는 코드를 추가해줘야 합니다. (코드 수정이 많아 선택하지 않았습니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗️이슈&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ObjectMapper 사용하여 객체를 Map으로 변경 시 LocalDateTime 변환 이슈 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&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;지금까지 MyBatis 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는 특정 필드를 DB에 저장/조회 시 암/복호화할 때도 사용할 수 있을 것 같고 별도 로그를 남기고 싶을 때도 사용할 수 있을 것 같습니다. (추후에 암복호화도 적용하면 글을 수정해보겠습니다.  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kim-jong-hyun.tistory.com/23&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kim-jong-hyun.tistory.com/23&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타</category>
      <author>DevBee</author>
      <guid isPermaLink="true">https://developerbee.tistory.com/267</guid>
      <comments>https://developerbee.tistory.com/267#entry267comment</comments>
      <pubDate>Sat, 13 Jan 2024 00:31:21 +0900</pubDate>
    </item>
    <item>
      <title>[AWS APIGateway] APIGateway WebSocket API 사용하기 1</title>
      <link>https://developerbee.tistory.com/266</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사 업무 중 채팅 서버를 구현 중이었는데요... 의도치 않은 방향성의 전환으로(?) AWS API Gateway 에서 제공하는 WebSocket 기능을 사용하게 되었습니다.&lt;/p&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 Gateway가 제공하는 WebSocket 기능을 사용하여 소캣 기능을 구현하도록 하였습니다. 이렇게 하면 별도 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;우선 API Gateway 의 웹 소켓 API를 그림으로 살펴보면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-05 오전 11.54.29.png&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BXMma/btsBqCPFyGO/f2yGc2W8iBRKyRFjMxjF2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BXMma/btsBqCPFyGO/f2yGc2W8iBRKyRFjMxjF2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BXMma/btsBqCPFyGO/f2yGc2W8iBRKyRFjMxjF2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBXMma%2FbtsBqCPFyGO%2Ff2yGc2W8iBRKyRFjMxjF2K%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;1066&quot; height=&quot;684&quot; data-filename=&quot;스크린샷 2023-12-05 오전 11.54.29.png&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 HTTP REST API 들이 늘어나는 환경에서도 테스트를 진행할 예정입니다. (우선 지금은 간단하게 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;먼저 SpringBoot 프로젝트를 띄우고 EC2에 배포하도록 하겠습니다. 포트는 8082로 열려있습니다. EC2 보안 그룹 인바운드 규칙도 우선 8082로 들어오는 모든 요청을 열어두도록 하겠습니다...! (보안상 안좋으니 테스트 후 다시 수정하겠습니다...!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 RestController 는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701748370941&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class CommonRestController {

    @PostMapping(&quot;/connect&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; connect(@RequestBody Object request) {
        log.info(&quot;connection success&quot;);
        log.info(request.toString());
        return ResponseEntity.ok(&quot;Connect OK&quot;);
    }

    @GetMapping(&quot;/&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; any() {
        log.info(&quot;call any&quot;);
        return ResponseEntity.ok(&quot;Any OK&quot;);
    }

    @PostMapping(&quot;/disconnect&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; disconnect(@RequestBody Object request) {
        log.info(&quot;disconnection success&quot;);
        log.info(request.toString());
        return ResponseEntity.ok(&quot;Disconnect OK&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 형태로 데이터가 올지 몰라 우선 Object 형태로 받았지만 Dto를 생성하는 편이 좋다고 생각합니다...!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 API Gateway를 WebSocket 형태로 생성해보도록 하겠습니다.&lt;/p&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: #000000;&quot;&gt;&lt;b&gt;1. WebSocket Api 를 생성합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_스크린샷 2023-12-05 오후 12.54.59.png&quot; data-origin-width=&quot;2752&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvXwvV/btsBl7bY4fI/NnK3PF3SRODKyEqfzo1yKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvXwvV/btsBl7bY4fI/NnK3PF3SRODKyEqfzo1yKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvXwvV/btsBl7bY4fI/NnK3PF3SRODKyEqfzo1yKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvXwvV%2FbtsBl7bY4fI%2FNnK3PF3SRODKyEqfzo1yKk%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;2752&quot; height=&quot;796&quot; data-filename=&quot;edited_edited_스크린샷 2023-12-05 오후 12.54.59.png&quot; data-origin-width=&quot;2752&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-05 오후 12.55.38.png&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;1542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l7oly/btsBqC94726/OasuXXk1x8KjRFLNyEjEH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l7oly/btsBqC94726/OasuXXk1x8KjRFLNyEjEH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l7oly/btsBqC94726/OasuXXk1x8KjRFLNyEjEH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl7oly%2FbtsBqC94726%2FOasuXXk1x8KjRFLNyEjEH0%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;1606&quot; height=&quot;1542&quot; data-filename=&quot;스크린샷 2023-12-05 오후 12.55.38.png&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;1542&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: #000000;&quot;&gt;&lt;b&gt;2. API 세부 정보를 지정합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-05 오후 12.56.11.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9ZmGy/btsBqeayVW8/oG1OFmJ9eASC2CF7A4k8E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9ZmGy/btsBqeayVW8/oG1OFmJ9eASC2CF7A4k8E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9ZmGy/btsBqeayVW8/oG1OFmJ9eASC2CF7A4k8E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9ZmGy%2FbtsBqeayVW8%2FoG1OFmJ9eASC2CF7A4k8E0%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;1980&quot; height=&quot;1120&quot; data-filename=&quot;스크린샷 2023-12-05 오후 12.56.11.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 라우팅 선택 표현식이 중요한데 클라이언트에서 소켓에 이벤트를 전송할 때 request.body.action 이라는 값으로 보내면 그 값을 가지고 라우팅 한다는 의미입니다.&lt;/p&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: #000000;&quot;&gt;&lt;b&gt;3. 경로 추가 및 통합 연결&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 우선 넘어가도 되고 기본적으로 $connect, $disconnect, $default 만 생성한 뒤 통합 연결을 우선 mock 으로 연결해도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-05 오후 12.56.43.png&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;1426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkcV2C/btsBlafBtuk/PCkTDoRzhPK3wkPSiAjFS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkcV2C/btsBlafBtuk/PCkTDoRzhPK3wkPSiAjFS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkcV2C/btsBlafBtuk/PCkTDoRzhPK3wkPSiAjFS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkcV2C%2FbtsBlafBtuk%2FPCkTDoRzhPK3wkPSiAjFS1%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;1986&quot; height=&quot;1426&quot; data-filename=&quot;스크린샷 2023-12-05 오후 12.56.43.png&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;1426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-05 오후 12.57.23.png&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;1424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7a6G1/btsBogNeOml/l3SMAph8lpjIZI2uC6Zimk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7a6G1/btsBogNeOml/l3SMAph8lpjIZI2uC6Zimk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7a6G1/btsBogNeOml/l3SMAph8lpjIZI2uC6Zimk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7a6G1%2FbtsBogNeOml%2Fl3SMAph8lpjIZI2uC6Zimk%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;1570&quot; height=&quot;1424&quot; data-filename=&quot;스크린샷 2023-12-05 오후 12.57.23.png&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;1424&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: #000000;&quot;&gt;&lt;b&gt;4. 스테이지 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 환경의 경로를 선택하는 것으로 dev 로 지정했습니다. dev, test, production 등 원하는 이름을 넣으시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-05 오후 12.57.51.png&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ysL2O/btsBreVpag8/UBdoLouioCb8OPxKkpytZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ysL2O/btsBreVpag8/UBdoLouioCb8OPxKkpytZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ysL2O/btsBreVpag8/UBdoLouioCb8OPxKkpytZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FysL2O%2FbtsBreVpag8%2FUBdoLouioCb8OPxKkpytZ1%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;1572&quot; height=&quot;720&quot; data-filename=&quot;스크린샷 2023-12-05 오후 12.57.51.png&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하고 나면 WebSocket 용 API Gateway가 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-12-05 오후 1.00.52.png&quot; data-origin-width=&quot;2750&quot; data-origin-height=&quot;1436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E24R2/btsBl7XoYUB/gYIZTjhz0AcCM04RF0ItK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E24R2/btsBl7XoYUB/gYIZTjhz0AcCM04RF0ItK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E24R2/btsBl7XoYUB/gYIZTjhz0AcCM04RF0ItK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE24R2%2FbtsBl7XoYUB%2FgYIZTjhz0AcCM04RF0ItK1%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;2750&quot; height=&quot;1436&quot; data-filename=&quot;edited_스크린샷 2023-12-05 오후 1.00.52.png&quot; data-origin-width=&quot;2750&quot; data-origin-height=&quot;1436&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: #333333;&quot;&gt;기본적으로 websocket에 필요한 라우트 키가 같이 생성됩니다. &lt;span style=&quot;color: #9d9d9d;&quot;&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 style=&quot;color: #333333;&quot;&gt;1. $connect - 기본적으로 클라이언트가 websocket API에 연결될때 발생하는 이벤트입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. $disconnect - 클라이언트가 websocket API와의 연결이 종료될때 발생하는 이벤트입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3. $default - 특정 라우트키 값이나 매칭되는 값이 없을때 기본적으로 호출되는 이벤트입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #666666; 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;먼저 모든 이벤트에 대해 Mock을 추가해서 연결을 확인해보겠습니다. 아래와 같이 통합 요청 탭에서 Mock을 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-12-05 오후 1.57.21.png&quot; data-origin-width=&quot;2776&quot; data-origin-height=&quot;1432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eeILBO/btsBmTSEsGQ/1ERXXyboFajlFqT99ycyg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eeILBO/btsBmTSEsGQ/1ERXXyboFajlFqT99ycyg0/img.png&quot; data-alt=&quot;통합 요청에서 Mock으로 설정합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eeILBO/btsBmTSEsGQ/1ERXXyboFajlFqT99ycyg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeeILBO%2FbtsBmTSEsGQ%2F1ERXXyboFajlFqT99ycyg0%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;2776&quot; height=&quot;1432&quot; data-filename=&quot;edited_스크린샷 2023-12-05 오후 1.57.21.png&quot; data-origin-width=&quot;2776&quot; data-origin-height=&quot;1432&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;통합 요청에서 Mock으로 설정합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-12-05 오후 1.59.37.png&quot; data-origin-width=&quot;1974&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjG7MB/btsBmTykS1n/KdT4NXhs63WWyKilC17HD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjG7MB/btsBmTykS1n/KdT4NXhs63WWyKilC17HD0/img.png&quot; data-alt=&quot;Mock을 선택합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjG7MB/btsBmTykS1n/KdT4NXhs63WWyKilC17HD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjG7MB%2FbtsBmTykS1n%2FKdT4NXhs63WWyKilC17HD0%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;1974&quot; height=&quot;1200&quot; data-filename=&quot;edited_스크린샷 2023-12-05 오후 1.59.37.png&quot; data-origin-width=&quot;1974&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Mock을 선택합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-05 오후 2.00.06.png&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n4e8B/btsBueOk7CH/CjaprycIknJ3BMNzenzds1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n4e8B/btsBueOk7CH/CjaprycIknJ3BMNzenzds1/img.png&quot; data-alt=&quot;요청 템플릿 편집을 눌러서 아래와 같이 템플릿 표현식을 추가합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n4e8B/btsBueOk7CH/CjaprycIknJ3BMNzenzds1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn4e8B%2FbtsBueOk7CH%2FCjaprycIknJ3BMNzenzds1%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;1538&quot; height=&quot;552&quot; data-filename=&quot;스크린샷 2023-12-05 오후 2.00.06.png&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;요청 템플릿 편집을 눌러서 아래와 같이 템플릿 표현식을 추가합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-05 오후 2.00.18.png&quot; data-origin-width=&quot;1976&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/esHayc/btsBlaUFC5G/6GjbOfrpLVfydZ4NldBSLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/esHayc/btsBlaUFC5G/6GjbOfrpLVfydZ4NldBSLk/img.png&quot; data-alt=&quot;\$default 템플릿 표현식 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/esHayc/btsBlaUFC5G/6GjbOfrpLVfydZ4NldBSLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FesHayc%2FbtsBlaUFC5G%2F6GjbOfrpLVfydZ4NldBSLk%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;1976&quot; height=&quot;694&quot; data-filename=&quot;스크린샷 2023-12-05 오후 2.00.18.png&quot; data-origin-width=&quot;1976&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;\$default 템플릿 표현식 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-05 오후 2.01.33.png&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;1102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOMdmm/btsBlbsxOQA/Zm10AjvIG2YewhsUmbnIBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOMdmm/btsBlbsxOQA/Zm10AjvIG2YewhsUmbnIBK/img.png&quot; data-alt=&quot;템플릿 생성을 눌러 위와 같이 템플릿을 생성합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOMdmm/btsBlbsxOQA/Zm10AjvIG2YewhsUmbnIBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOMdmm%2FbtsBlbsxOQA%2FZm10AjvIG2YewhsUmbnIBK%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;1518&quot; height=&quot;1102&quot; data-filename=&quot;스크린샷 2023-12-05 오후 2.01.33.png&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;1102&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;템플릿 생성을 눌러 위와 같이 템플릿을 생성합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 통합 요청 템플릿을 다음과 같이 작성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701757171167&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;statusCode&quot;: 200,
    &quot;id&quot; : &quot;$context.connectionId&quot;,
    &quot;domain&quot; : &quot;$context.domainName&quot;,
    &quot;stage&quot; : &quot;$context.stage&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;❗️Mock 연결 시 통합 요청에 statusCode 가 포함되어 있어야 합니다...!!! 그렇지 않으면 정상적인 응답이 보이지 않습니다...! 가능하다면 꼭 CloudWatch 로그 설정을 하여 직접 API Gateway의 로그를 확인하시기 바랍니다!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합 요청 템플릿을 적용하지 않는 경우 연결은 잘 되겠지만 응답에서 500 에러 등이 발생할 수 있습니다...!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 wscat을 통해 접속해보면 다음과 같이 API Gateway에 웹소켓 접속이 가능해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-12-05 오후 3.09.19.png&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lt3w6/btsBuoDlgTO/pzlYcI2uVkU6pcGMLTQZB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lt3w6/btsBuoDlgTO/pzlYcI2uVkU6pcGMLTQZB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lt3w6/btsBuoDlgTO/pzlYcI2uVkU6pcGMLTQZB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flt3w6%2FbtsBuoDlgTO%2FpzlYcI2uVkU6pcGMLTQZB1%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;1144&quot; height=&quot;142&quot; data-filename=&quot;edited_스크린샷 2023-12-05 오후 3.09.19.png&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접속 주소는 API Gateway console에서 스테이지 탭을 확인하시면 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제로 API 서버로 통신하는 것을 구현해보겠습니다. 아래와 같이 통합 요청 부분을 수정해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-12-06 오후 3.47.14.png&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m6MNv/btsBx7hqeYJ/hX3NbynoqH2D0kZHJiiZxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m6MNv/btsBx7hqeYJ/hX3NbynoqH2D0kZHJiiZxK/img.png&quot; data-alt=&quot;$connect 경로 통합 요청 수정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m6MNv/btsBx7hqeYJ/hX3NbynoqH2D0kZHJiiZxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm6MNv%2FbtsBx7hqeYJ%2FhX3NbynoqH2D0kZHJiiZxK%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;1734&quot; height=&quot;1054&quot; data-filename=&quot;edited_스크린샷 2023-12-06 오후 3.47.14.png&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1054&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;$connect 경로 통합 요청 수정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-12-06 오후 3.47.21.png&quot; data-origin-width=&quot;1738&quot; data-origin-height=&quot;986&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMnqYL/btsBuuLiHAV/gs7342ZjWGKa6yxzFDB54K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMnqYL/btsBuuLiHAV/gs7342ZjWGKa6yxzFDB54K/img.png&quot; data-alt=&quot;$disconnect 경로 통합 요청 수정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMnqYL/btsBuuLiHAV/gs7342ZjWGKa6yxzFDB54K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMnqYL%2FbtsBuuLiHAV%2Fgs7342ZjWGKa6yxzFDB54K%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;1738&quot; height=&quot;986&quot; data-filename=&quot;edited_스크린샷 2023-12-06 오후 3.47.21.png&quot; data-origin-width=&quot;1738&quot; data-origin-height=&quot;986&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;$disconnect 경로 통합 요청 수정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$connect와 $disconnect의 통합 요청을 변경해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합 유형을 HTTP POST 방식으로 변경하고 (spring 코드에서 PostMapping으로 받고 있기 때문에) 엔드포인트를 EC2 퍼블릭 ip (탄력적 ip)로 지정하고 HTTP 경로를 적어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합 요청을 사용하지 않는 경우 클라이언트의 요청이 바로 API 서버로 전달됩니다. (저는 그 전에 데이터를 가공하여 전달하기 위해 통합 요청을 사용하였습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 $connect, $disconnect를 하게 되면 API 서버에 로그가 찍히는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-12-05 오후 5.18.16.png&quot; data-origin-width=&quot;2064&quot; data-origin-height=&quot;1506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TQxPX/btsByQfejib/POSwlyiQ8ZVXHMyNd5ETHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TQxPX/btsByQfejib/POSwlyiQ8ZVXHMyNd5ETHK/img.png&quot; data-alt=&quot;포스트맨 실행 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TQxPX/btsByQfejib/POSwlyiQ8ZVXHMyNd5ETHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTQxPX%2FbtsByQfejib%2FPOSwlyiQ8ZVXHMyNd5ETHK%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;2064&quot; height=&quot;1506&quot; data-filename=&quot;edited_스크린샷 2023-12-05 오후 5.18.16.png&quot; data-origin-width=&quot;2064&quot; data-origin-height=&quot;1506&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;포스트맨 실행 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-05 오후 5.18.26.png&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9WUQR/btsBymSZipn/5IyKka7FqZv6Vd4CyeZfZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9WUQR/btsBymSZipn/5IyKka7FqZv6Vd4CyeZfZK/img.png&quot; data-alt=&quot;서버 실행 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9WUQR/btsBymSZipn/5IyKka7FqZv6Vd4CyeZfZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9WUQR%2FbtsBymSZipn%2F5IyKka7FqZv6Vd4CyeZfZK%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;2162&quot; height=&quot;836&quot; data-filename=&quot;스크린샷 2023-12-05 오후 5.18.26.png&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서버 실행 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&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;&lt;span style=&quot;color: #333333;&quot;&gt;지금까지 AWS API Gateway를 WebSocket 기능으로 생성하고 connect 하는 방법을 알아보았습니다. 다음 글에서는 실제 커스텀한 이벤트 경로로 요청을 보내고 AWS API Gateway를 통해 다시 같은 connectionId를 가진 소켓 클라이언트에게 메시지를 전달하는 방법에 대해 알아보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://realwater87.tistory.com/8&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://realwater87.tistory.com/8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://realwater87.tistory.com/9&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://realwater87.tistory.com/9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/Set-up-data-transformations-in-API-Gateway.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/Set-up-data-transformations-in-API-Gateway.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AWS</category>
      <author>DevBee</author>
      <guid isPermaLink="true">https://developerbee.tistory.com/266</guid>
      <comments>https://developerbee.tistory.com/266#entry266comment</comments>
      <pubDate>Wed, 6 Dec 2023 16:00:59 +0900</pubDate>
    </item>
    <item>
      <title>[항해플러스 3기] 시작하는 마음</title>
      <link>https://developerbee.tistory.com/265</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;항해플러스 3기를 신청하게 되었다. 오늘 첫날이라 시작하는 마음을 적어보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;개발자가 된 계기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 대한민국 고등학생들과 동일하게 나 또한 내가 좋아하는 것을 알지 못하고 그저 성적에 맞춰 대학을 진학했다. 대학에 가서 처음 배운 C 언어... 컴퓨터 관련 학과인지도 모르고 들어왔다가 컴퓨터 공학, OS 수업을 들으면서 이길은 내 길이 아니구나... 어떻게든 졸업만하자 생각했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 무난한 학교 생활을 하다가 우연히 사물 인터넷이라는 수업을 듣게 되었다. 그동안 알 수 없는 코드들을 작성하고 그것이 무슨 동작을 어떻게 하는지 어떻게 활용할 수 있을지 알 수 없었는데 사물 인터넷이라는 수업을 통해 내가 만든 코드가 일상 생활의 물건을 동작 시킬 수 있구나를 깨닫게 되었다. 생각보다 더 기분 좋은 날이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 그동안 배운 지식을 통해 일상의 물건을 작동 시키고 나아가 문제를 해결할 수 있을 것 같아 흥미가 생겼고 하나씩 만들어가는 코드에 뿌듯함을 느끼게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다보니 배운 지식으로 일을 하고 밥 벌어 먹을 수도 있겠다는 생각이 들었고 그렇게 나는 개발자가 되었다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;지금까지의 회고&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 개발자를 시작하고 어느덧 4년이라는 시간이 흘렀다. 처음 SI 회사에 들어가서 만들어 달라는 대로 요구사항과 설계서를 받아 개발을 했다. 그렇지만 얼마 못가 회사에 일이 없어졌고 본사에서 공부하는 시간이 늘어갔다. Java + Spring을 통한 백엔드 개발을 진행하고 싶었지만 SI 상황상 그때 그때 필요한 기술들을 바로바로 실무에 적용해서 만들어야 했다. 그래서 공부를 하고 개발을 하고 기능을 만들면서도 수박 겉핥기 식의 작업들을 하고 있다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 2년 6개월 SI 회사를 떠나 스타트업으로 이직을 했다. 해당 스타트업에서는 작지만 실 사용자가 있는 서비스를 개발하는 곳이었다. 그곳에서 Java + Spring 기반의 백엔드 작업들을 할 수 있었다. 이곳에서 비슷한 연차의 다른 개발자들과 사내 스터디도 진행하고 공부를 할 방향을 잡아가면서 꾸준히 소통하는 과정들을 거쳐나갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 1년 6개월 후 이 스타트업은 사업의 방향성을 변경하고 개발자들을 내보내기 시작했다. 내가 하던 서비스도 결국 종료를 맞이하고 나는 퇴사를 했다. 그리고 3개월 간의 휴식기를 가진 뒤, 기존에 일하던 SI 회사에서 1년 계약직으로 프로젝트를 수행하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;향후 5년 뒤 커리어 방향성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 길지 않은 짧은 경력들로 백엔드 개발을 하다보니 커리어의 방향성에 혼란이 오기도 했다. 학교에서는 Java 기반의 백엔드 공부를 주로 하다가 신입 때는 Angular 프레임워크를 사용한 프론트 개발을 하고 이후 지금은 다시 백엔드 개발을 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일을 하면서 회사에 맞는 다양한 업무를 하고 상황에 맞게 프론트, 백엔드 다 할 수도 있겠지만 하나를 고르자면 앞으로 백엔드를 중심으로 개발 커리어를 진행하고 싶다. 백엔드를 개발하면서 전반적인 인프라 환경도 이해하고 어떤 식의 통신이 일어나는지 구체적으로 파악할 수 있다는 점이 매력적으로 느끼기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 SI 보다는 금융이나 커머스쪽 서비스를 하고 있는 회사에서 근무하고 싶다. 어떤 환경이든 개발자가 공부를 하고 성장하는데 스스로의 의지가 중요한 것은 맞지만 그래도 금융이나 커머스 쪽 일을 해보고 싶은만큼 그런 업무를 하는 다른 개발자들에게 많이 배우면서 성장하고 싶다. 또한 가능하다면 내가 하고 있는 Java + Spring 뿐만 아니라 프론트까지 이해하고 소통이 가능한 개발자가 될 수 있게 꾸준히 공부하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;10주간의 목표&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10주간의 항해 플러스를 신청했다. 실무에서 다양한 문제를 겪을 수 있는데 그러면서 다른 사람들은 어떻게 할까... 이럴때 다른 서비스 회사에서는 어떻게 처리할까 궁금하던 부분들이 커리큘럼에 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 10주간의 항해 플러스 코스를 통해 부족하다고 생각했던 기반들을 다지고 4,5년차에 맞게 서비스, 프로그램의 전반적인 구조, 상황들을 파악하면서 에러 상황에서도 잘 대처하는 기술들을 배워나가고 싶다.&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;</description>
      <category>기타</category>
      <author>DevBee</author>
      <guid isPermaLink="true">https://developerbee.tistory.com/265</guid>
      <comments>https://developerbee.tistory.com/265#entry265comment</comments>
      <pubDate>Sat, 2 Dec 2023 15:04:50 +0900</pubDate>
    </item>
    <item>
      <title>[TypeORM] Node + Express 환경에서 TypeORM 사용하기</title>
      <link>https://developerbee.tistory.com/264</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;최근 회사에서 의도치 않게 Node + Express + TypeScript 환경에서 개발을 진행하고 있습니다...ㅠㅠ 매번 하던 것들이 아니라 여러가지 어려움도 많고 이해가 되지 않는 부분이 많지만... 저의 부족함을 매일 느끼며 그와중에 조금이나마 알게된 내용들을 정리해볼까 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;TypeORM이란?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;TypeORM은 TypeScript와 JavaScript에서 사용할 수 있는 데이터베이스 ORM(Object-Relational Mapping) 라이브러리입니다.&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 style=&quot;color: #333333;&quot;&gt;ORM은 객체 지향 프로그래밍 언어와 관계형 데이터베이스 간의 데이터를 변환하고 연결하는 기술을 의미합니다. TypeORM은 이러한 변환 및 연결 작업을 간편하게 수행하도록 도와주는 도구 중 하나입니다.&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 style=&quot;color: #333333; text-align: start;&quot;&gt;TypeORM은 다양한 데이터베이스 시스템을 지원하며, MySQL, PostgreSQL, SQLite, MSSQL 등과 같은 다양한 관계형 데이터베이스와 함께 사용할 수 있습니다. 이는 TypeORM이 강력하게 사용될 수 있도록 다양한 프로젝트에서 적용될 수 있는 유연성을 제공합니다.&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 style=&quot;color: #333333;&quot;&gt;TypeORM의 특징과 기능:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;객체 지향적인 데이터베이스 조작: TypeORM은 데이터베이스 테이블을 JavaScript 또는 TypeScript 클래스로 매핑하여 객체 지향적인 코드를 사용할 수 있도록 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;쿼리 언어의 대안: SQL 쿼리 대신 JavaScript나 TypeScript로 작성된 메소드를 사용하여 데이터베이스를 조작할 수 있습니다. 이는 개발자에게 더 친숙하고 읽기 쉬운 코드를 작성할 수 있도록 도와줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;자동 마이그레이션: TypeORM은 데이터베이스 스키마를 자동으로 생성하고 유지하며, 모델의 변경 사항을 데이터베이스에 적용하는 기능을 제공합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;트랜잭션 관리: 트랜잭션을 사용하여 여러 데이터베이스 작업을 원자적으로 실행하고 롤백할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;액티브 레코드 패턴 지원: TypeORM은 액티브 레코드(Active Record) 패턴을 지원하여 데이터베이스 레코드를 객체처럼 쉽게 다룰 수 있게 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;사용 방법&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1. npm install&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1700778121762&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i typeorm mysql2 reflect-metadata --save&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. DataSource cofiguration 추가&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1700810662341&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { DataSource } from 'typeorm';

export const AppDataSource = new DataSource({
  type: 'mysql',
  host: 'localhost',
  port: 3306,
  username: 'root',
  password: '1234',
  database: 'chat',
  synchronize: true,
  logging: true,
  entities: ['src/entity/*.ts'],
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위와 같이 DataSource 정보를 입력합니다. 보통은 env 파일을 활용하기 때문에 이 부분은 추후에 별도 설정 파일로 분리를 해야겠지만... 우선 이렇게 커넥션 연결 정보를 그대로 입력해주겠습니다.&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 style=&quot;color: #333333;&quot;&gt;synchronize 설정의 경우 만들어둔 entity가 있다면 해당 entity를 바탕으로 db에 자동으로 테이블 생성을 도와줍니다. JPA에서는 ddl-auto 설정을 create, update 등의 옵션값으로 세세한 설정이 가능하지만 여기서는 true, false 값만 줄 수 있고 기본적으로 update 방식으로 동작하는 것 같습니다.&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 style=&quot;color: #333333;&quot;&gt;logging은 말그대로 typeORM을 통해 실행되는 쿼리를 logging 할지 여부를 선택하는 것입니다. ORM을 사용하는 경우 의도치 않은 쿼리가 나가는지 확인이 필요하기 때문에 가급적 true 설정하는 것이 좋습니다.&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 style=&quot;color: #333333;&quot;&gt;entities의 경우 직접 생성한 entity들을 등록할 수 있습니다. 위 경우 경로를 지정하여 해당 경로에 있는 entity들을 등록할 수 있게 했습니다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&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 style=&quot;color: #333333;&quot;&gt;3. 초기화 진행&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1700810971167&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;AppDataSource.initialize().then(() =&amp;gt; {...});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위와 같이 main 파일에서 초기화를 진행한 뒤 사용할 연결이 잘 되는 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;간단한 예제 구성&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&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;스크린샷 2023-11-29 오전 12.35.47.png&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;882&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xpFCg/btsA9hdnfSe/8PKeUcbPWluajYZeEdEkyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xpFCg/btsA9hdnfSe/8PKeUcbPWluajYZeEdEkyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xpFCg/btsA9hdnfSe/8PKeUcbPWluajYZeEdEkyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxpFCg%2FbtsA9hdnfSe%2F8PKeUcbPWluajYZeEdEkyk%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;1382&quot; height=&quot;882&quot; data-filename=&quot;스크린샷 2023-11-29 오전 12.35.47.png&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;882&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: #333333;&quot;&gt;시나리오&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1. 사용자 등록을 할 수 있습니다. (POST /members)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;: 사용자 정보를 등록하는데 이때 사용자의 역할(role) 정보, 주소 정보를 같이 저장할 수 있도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사용자의 정보를 수정할 때 사용자의 역할(role) 및 주소도 수정할 수 있습니다. (PATCH /members/{memberId})&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;: 사용자의 정보를 수정하고 사용자의 기존 역할을 모두 삭제한 뒤 다시 모든 신규 권한을 추가, 기존 주소를 모두 삭제한 뒤 새로운 주소를 추가합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;우선 엔티티들을 설계해보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;role.entity.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1701002223167&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Timestamp} from &quot;typeorm&quot;;

@Entity('role')
export class Role {
    @PrimaryGeneratedColumn('increment')
    id: number;

    @Column('varchar', { length: 20 })
    name: 'ADMIN' | 'MEMBER';

    @CreateDateColumn()
    createdAt: Timestamp;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;member.entity.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1701002203728&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {Column, CreateDateColumn, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, Timestamp} from &quot;typeorm&quot;;
import { Role } from &quot;./role.entity&quot;;

@Entity('member')
export class Member {
    @PrimaryGeneratedColumn('increment')
    id: number;

    @Column('varchar', { length: 20 })
    name: string;
    
    @Column('varchar', { length: 4, nullable: true })
    mbti: string;

    @ManyToMany(() =&amp;gt; Role, {cascade: ['insert']})
    @JoinTable({
        name: 'member_role',
        joinColumn: {
            name: 'member_id',
            referencedColumnName: 'id'
        },
        inverseJoinColumn: {
            name: 'role_id',
            referencedColumnName: 'id'
        }
    })
    roles: Role[];

    @CreateDateColumn()
    createdAt: Timestamp;

    constructor(name: string) {
        this.name = name
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;JPA를 사용할 때 @ManyToMany를 사용한 경우는 거의 없었습니다. @ManyToMany를 사용하게 되면 자동으로 중간 테이블을 생성해주지만 각각의 키 하나씩만 추가되고 다른 컬럼을 추가할 수 없기 때문입니다. 실무에서는 보통 더 많은 컬럼들을 중간 테이블에 넣기 때문에 중간 테이블을 직접 생성하고 @OneToMany, @ManyToOne을 사용해 중간 테이블과 연결하여 사용하는 편입니다.&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 style=&quot;color: #333333;&quot;&gt;다음으로 각 repository에서 쿼리들을 작성하고 이를 사용하는 비즈니스 로직이 담긴 service를 개발해보겠습니다.&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 style=&quot;color: #333333;&quot;&gt;먼저 사용자의 name과 roles를 입력 받아 해당 정보를 저장하는 로직을 살펴보겠습니다. 먼저 전달 받은 정보를 가지고 member 객체를 생성합니다. 이후 생성된 Member Entity를 저장하면 중간 테이블인 member_role 테이블에도 데이터가 같이 저장됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1701091514600&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// member.repository.ts
export class MemberRepository {
    memberRepository = AppDataSource.getRepository(Member);

    constructor() {}
    
    ...

    save(member: Member) {
        return this.memberRepository.save(member);
    }

    ...
}

// member.service.ts
export class MemberService {
    private memberRepository = new MemberRepository();
    private roleRepository = new RoleRepository();

    constructor() {}

    ...

    async save(member: MemberCreateRepuest) {
        let findRole = await this.roleRepository.findByName(member.role); // 기존 role entity를 조회해서
        if (!findRole) findRole = new Role(member.role); // 없는 경우 새로운 role 객체를 생성한 뒤
        
        const newMember = new Member(member.name);
        newMember.roles = [findRole];
    
        return this.memberRepository.save(newMember); // member 만 장하면 같이 저장
    }
    
    ...
}&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;위와 같이 @ManyToMany로 연결되는 중간 테이블의 경우 기준 테이블에 정보를 저장할 때 해당 엔티티를 생성하여 넣어주는 경우 자동으로 해당 데이터도 같이 저장(insert)이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 자동 생성된 중간 테이블이 아니라고 하더라고 설정 중 cascade 설정 값을 통해 기준 테이블의 정보만 저장하면 자동으로 부가적인 정보를 같이 저장할 수 있게 됩니다. 다른 예시로 회원 저장 시 회원의 주소 정보를 저장할 수 있다고 가정해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701098277069&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// address.entity.ts
@Entity('address')
export class Address {
    @PrimaryGeneratedColumn('increment')
    id: number;

    @Column('varchar', { length: 6 })
    zipcode: string;

    @Column('varchar', { length: 20 })
    address: string;

    @ManyToOne(() =&amp;gt; Member, (member) =&amp;gt; member.addresses, { eager: false })
    @JoinColumn({ name: 'member_id', referencedColumnName: 'id' })
    member: Member

    @CreateDateColumn()
    createdAt: Timestamp;

    constructor(zipcode: string, address: string) {
        this.zipcode = zipcode;
        this.address = address;
    }
}

// member.entity.ts
@Entity('member')
export class Member {
    ...

    @OneToMany(() =&amp;gt; Address, (address) =&amp;gt; address.member, {cascade: ['insert']})
    addresses: Address[]

    ...
}

// member.service.ts
export class MemberService {
    ...

    async save(member: MemberCreateRepuest) {
        let findRole = await this.roleRepository.findByName(member.role);
        if (!findRole) findRole = new Role(member.role);
        
        const newMember = new Member(member.name);
        newMember.roles = [findRole];
        newMember.addresses = member.addresses.map(
          address =&amp;gt; new Address(address.zipcode, address.address),
        );
    
        return this.memberRepository.save(newMember);
    }

    ...
}&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;위와 같이 작성하는 경우 MemberRepository에서만 save를 해도 자동으로 address 테이블에 정보가 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서는 양방향 연관 관계 매핑이 되어 있는 경우 1. 연관 관계의 주인이 아닌 객체를 중심으로 한번에 save 하거나 2. 영속성 컨텍스트 내에서 순수 객체끼리 참조가 가능하도록 하기 위해 양쪽 모두에 참조 값을 추가해주는 작업이 필요합니다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;(JPA 양방향 연관 관계 저장, 연관 관계 편의 메서드 등으로 검색해보세요!)&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;하지만 TypeORM의 경우 양방향 연관 관계가 맺어져 있고 cascade 설정이 되어 있다면 연관 관계의 주인이 아닌 객체에 데이터를 저장하고 별도 객체 참조를 넣지 않은 상태로 저장할 때 양쪽 테이블에 모두 데이터가 잘 저장됩니다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;(이 부분이 혹시 잘못 알고 있는거라면 말씀주시면 감사하겠습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 회원의 정보를 수정할 때 회원 정보와 함께 member_role 테이블의 정보 및 address 테이블의 정보도 같이 수정(삭제 후 저장)하는 예제를 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701176090032&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class MemberService {
  private memberRepository = new MemberRepository();
  private roleRepository = new RoleRepository();
  private addressRepository = new AddressRepository();

  ...

  async updateMemberInfo(request: MemberUpdateRequest) {
    const member = await this.memberRepository.findById(request.memberId);
    if (!member) throw new Error('member does not exist.');

    if (request.mbti) member.mbti = request.mbti;

    if (request.roles) {
      const roles = await Promise.all(
        request.roles?.map(
          async role =&amp;gt; await this.roleRepository.findByName(role),
        ),
      );

      if (roles) {
        member.roles = roles.filter(role =&amp;gt; role !== null) as Role[];
      }
    }

    if (request.address) {
      const address = request.address?.map(
        adr =&amp;gt; new Address(adr.zipcode, adr.address),
      );
      if (address) member.addresses = address;
    }
    
    await this.addressRepository.deleteByMemberId(member.id); // 기존 멤버 주소 전체 삭제
    return this.memberRepository.save(member); // 멤버 (멤버, 롤, 주소 업데이트 및 삭제, 등록)
  }
}&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 처리하는 다양한 방법이 있겠지만...! 저는 일단 임시로 AppDataSource에서 runner를 가져와서 커넥션 연결 후 하나로 묶어야 하는 쿼리 작업 전 트랜잭션을 시작하고 성공인 경우만 커밋, 아닌 경우는 롤백할 수 있도록 하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701177146801&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class MemberService {
  private memberRepository = new MemberRepository();
  private roleRepository = new RoleRepository();
  private addressRepository = new AddressRepository();
  
  ...

  async updateMemberInfo(request: MemberUpdateRequest) {
    const member = await this.memberRepository.findById(request.memberId);
    if (!member) throw new Error('member does not exist.');

    if (request.mbti) member.mbti = request.mbti;

    if (request.roles) {
      const roles = await Promise.all(
        request.roles?.map(
          async role =&amp;gt; await this.roleRepository.findByName(role),
        ),
      );

      if (roles) {
        member.roles = roles.filter(role =&amp;gt; role !== null) as Role[];
      }
    }

    if (request.address) {
      const address = request.address?.map(
        adr =&amp;gt; new Address(adr.zipcode, adr.address),
      );
      if (address) member.addresses = address;
    }

    let response: Member;

    const runner = AppDataSource.createQueryRunner();
    try {
      await runner.connect();
      await runner.startTransaction();

      const addressRepo = runner.manager.getRepository(Address);
      const memberRepo = runner.manager.getRepository(Member);

      await addressRepo
        .createQueryBuilder()
        .delete()
        .from(Address)
        .where('member_id = :memberId', { memberId: member.id })
        .execute();

      response = await memberRepo.save(member);

      // throw new Error('강제 에러');

      await runner.commitTransaction();
    } catch (error) {
        console.log(error);
        await runner.rollbackTransaction();
        return Promise.reject(error);
    } finally {
      runner.release();
    }

    return 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;이렇게 하면 중간에 에러가 발생하는 경우 모두 rollback, 모두 성공하는 경우 commit 하게 되어 하나의 트랜잭션으로 처리가 가능합니다. 주석 처리 되어 있는 '강제 에러' 부분의 주석을 풀고 테스트하는 경우 주소 삭제, 멤버 저장 쿼리가 실행되었다가 다시 rollback 하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 보면 다른 곳에서도 트랜잭션이 있을 때 저렇게 다 try-catch 하고 써야하나라는 의문이 있는데요 이를 별도로 모듈화하여 작성하는 것이 좋을 것 같아 추후에 샘플 코드는 수정을 할 계획입니다...!! 우선 지금 글에서는 트랜잭션 처리의 필요와 방법에 대해 살펴보고자 하여 일회성 로직을 구현한 점 양해부탁드립니다.  &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;&lt;span style=&quot;color: #333333;&quot;&gt;지금까지 TypeORM 사용 방법과 트랜잭션 처리에 대해서 공부해보았습니다. 아직 제가 전체적인 구조를 잡고 이렇게 구성을 해야지 생각하기 보다 리딩하시는 분의 코드를 이해하고 따라가기 바쁜 상황이지만... 매일 조금씩 배워나간다는 마음으로 꾸준히 하면 어느새 다양한 코드를 이해하는 능력이 더 길러지겠죠...?ㅎㅎ&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 style=&quot;color: #333333;&quot;&gt;이번에 정리한 TypeORM은 기본 + 프로젝트에서 사용하면서 생겼던 이슈를 다루었습니다. 공부하면서 Spring의 JPA에 대해서도 조금 더 개념적인 이해를 할 수 있어 좋은 시간이었던 것 같습니다.  &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 style=&quot;color: #333333;&quot;&gt;TypeORM의 기본 사용법과 다양한 쿼리에 대해서는 공식 설명을 더 참고하시기 바랍니다. 감사합니다.  &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 style=&quot;color: #333333;&quot;&gt;해당 글에 사용된 샘플 코드는 &lt;a href=&quot;https://github.com/hanbee1005/chat-server&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://typeorm.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://typeorm.io/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://orkhan.gitbook.io/typeorm/docs&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://orkhan.gitbook.io/typeorm/docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타</category>
      <author>DevBee</author>
      <guid isPermaLink="true">https://developerbee.tistory.com/264</guid>
      <comments>https://developerbee.tistory.com/264#entry264comment</comments>
      <pubDate>Wed, 29 Nov 2023 00:43:30 +0900</pubDate>
    </item>
  </channel>
</rss>