From ecab5c10ec90f135fb5489ed71c29593a830f081 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:22:57 +0000 Subject: [PATCH] deploy: 838fa1bd90c7f04629347511c54676a2ab0ca416 --- 1.html | 4 ++-- 10.html | 4 ++-- 11.html | 4 ++-- 12.html | 4 ++-- 13.html | 4 ++-- 14.html | 4 ++-- 15.html | 4 ++-- 16.html | 4 ++-- 17.html | 4 ++-- 18.html | 4 ++-- 19.html | 4 ++-- 2.html | 4 ++-- 20.html | 4 ++-- 21.html | 4 ++-- 22.html | 4 ++-- 23.html | 4 ++-- 24.html | 4 ++-- 25.html | 4 ++-- 26.html | 4 ++-- 27.html | 4 ++-- 28.html | 4 ++-- 29.html | 4 ++-- 3.html | 4 ++-- 30.html | 4 ++-- 31.html | 4 ++-- 32.html | 4 ++-- 33.html | 4 ++-- 34.html | 4 ++-- 35.html | 4 ++-- 36.html | 4 ++-- 37.html | 4 ++-- 38.html | 4 ++-- 39.html | 4 ++-- 4.html | 4 ++-- 40.html | 4 ++-- 404.html | 4 ++-- 41.html | 4 ++-- 42.html | 4 ++-- 43.html | 7 ++++--- 44.html | 4 ++-- 5.html | 4 ++-- 6.html | 4 ++-- 7.html | 4 ++-- 8.html | 4 ++-- 9.html | 4 ++-- archive.html | 4 ++-- ...port-6-038d09fa5b07bf8b8814e5e2463c4ccd.jpg | Bin 0 -> 149632 bytes assets/js/06db3492.6ec62fef.js | 1 + assets/js/06db3492.853f24a9.js | 1 - assets/js/2e801cce.448d8484.js | 1 + assets/js/2e801cce.c1156dde.js | 1 - assets/js/c17ec8e8.4a256193.js | 1 + assets/js/c17ec8e8.b6a2f2da.js | 1 - ...in.a8937271.js => runtime~main.3ca96668.js} | 2 +- atom.xml | 3 ++- docs/category/tutorial---basics.html | 4 ++-- docs/category/tutorial---extras.html | 4 ++-- docs/intro.html | 4 ++-- docs/tutorial-basics/congratulations.html | 4 ++-- docs/tutorial-basics/create-a-blog-post.html | 4 ++-- docs/tutorial-basics/create-a-document.html | 4 ++-- docs/tutorial-basics/create-a-page.html | 4 ++-- docs/tutorial-basics/deploy-your-site.html | 4 ++-- docs/tutorial-basics/markdown-features.html | 4 ++-- docs/tutorial-extras/manage-docs-versions.html | 4 ++-- docs/tutorial-extras/translate-your-site.html | 4 ++-- index.html | 4 ++-- markdown-page.html | 4 ++-- page/10.html | 4 ++-- page/11.html | 4 ++-- page/12.html | 4 ++-- page/13.html | 4 ++-- page/14.html | 4 ++-- page/15.html | 4 ++-- page/16.html | 4 ++-- page/17.html | 4 ++-- page/18.html | 4 ++-- page/19.html | 4 ++-- page/2.html | 4 ++-- page/20.html | 4 ++-- page/21.html | 4 ++-- page/22.html | 4 ++-- page/23.html | 4 ++-- page/24.html | 4 ++-- page/25.html | 4 ++-- page/26.html | 4 ++-- page/27.html | 4 ++-- page/28.html | 4 ++-- page/29.html | 4 ++-- page/3.html | 7 ++++--- page/30.html | 4 ++-- page/31.html | 4 ++-- page/32.html | 4 ++-- page/33.html | 4 ++-- page/34.html | 4 ++-- page/35.html | 4 ++-- page/36.html | 4 ++-- page/37.html | 4 ++-- page/38.html | 4 ++-- page/39.html | 4 ++-- page/4.html | 4 ++-- page/40.html | 4 ++-- page/41.html | 4 ++-- page/42.html | 4 ++-- page/43.html | 4 ++-- page/44.html | 4 ++-- page/5.html | 4 ++-- page/6.html | 4 ++-- page/7.html | 4 ++-- page/8.html | 4 ++-- page/9.html | 4 ++-- rss.xml | 3 ++- tags.html | 4 ++-- tags/action.html | 4 ++-- tags/action/page/2.html | 4 ++-- tags/auto.html | 4 ++-- tags/aws.html | 4 ++-- tags/aws/page/2.html | 4 ++-- tags/aws/page/3.html | 4 ++-- tags/aws/page/4.html | 4 ++-- tags/blue-green.html | 4 ++-- tags/branch.html | 4 ++-- tags/cd.html | 4 ++-- tags/cd/page/2.html | 4 ++-- tags/cd/page/3.html | 4 ++-- tags/ci.html | 4 ++-- tags/ci/page/2.html | 4 ++-- tags/collaboration.html | 4 ++-- tags/commit.html | 4 ++-- tags/css-in-js.html | 4 ++-- tags/css.html | 4 ++-- tags/db.html | 4 ++-- tags/deadlock.html | 4 ++-- tags/dev.html | 4 ++-- tags/ec-2.html | 4 ++-- tags/ec-2/page/2.html | 4 ++-- tags/ec-2/page/3.html | 4 ++-- tags/ec-2/page/4.html | 4 ++-- tags/error.html | 4 ++-- tags/filter.html | 4 ++-- tags/ga-4.html | 4 ++-- tags/gc.html | 4 ++-- tags/git-branch.html | 4 ++-- tags/git-flow.html | 4 ++-- tags/git.html | 4 ++-- tags/git/page/2.html | 4 ++-- tags/github-flow.html | 4 ++-- tags/github.html | 4 ++-- tags/github/page/2.html | 4 ++-- tags/gitlab-flow.html | 4 ++-- tags/google-analytics-4.html | 4 ++-- tags/google-map.html | 4 ++-- tags/google-maps-api.html | 4 ++-- tags/google-maps-api/page/2.html | 4 ++-- tags/google-maps-api/page/3.html | 4 ++-- tags/google-maps.html | 4 ++-- tags/googlemaps-react-wrapper.html | 4 ++-- tags/hello.html | 4 ++-- tags/hello/page/2.html | 4 ++-- tags/hibernate.html | 4 ++-- tags/index.html | 4 ++-- tags/infra.html | 4 ++-- tags/infra/page/2.html | 4 ++-- tags/ip.html | 4 ++-- tags/issue.html | 4 ++-- tags/issue/page/2.html | 4 ++-- tags/jasypt.html | 4 ++-- tags/java-11.html | 4 ++-- tags/java-17.html | 4 ++-- tags/java-17/page/2.html | 4 ++-- tags/java.html | 4 ++-- tags/java/page/2.html | 4 ++-- tags/java/page/3.html | 4 ++-- tags/jpa.html | 4 ++-- tags/jpa/page/2.html | 4 ++-- tags/login.html | 4 ++-- tags/message.html | 4 ++-- tags/msw.html | 4 ++-- tags/mysql.html | 4 ++-- tags/mysql/page/2.html | 4 ++-- tags/mysql/page/3.html | 4 ++-- tags/oauth.html | 4 ++-- tags/oom.html | 4 ++-- tags/pr.html | 4 ++-- tags/pr/page/2.html | 4 ++-- tags/prod.html | 4 ++-- tags/react-state-management.html | 4 ++-- tags/react-wrapper.html | 4 ++-- tags/react.html | 4 ++-- tags/react/page/2.html | 4 ++-- tags/react/page/3.html | 4 ++-- tags/record.html | 4 ++-- tags/slack.html | 4 ++-- tags/spring.html | 4 ++-- tags/spring/page/2.html | 4 ++-- tags/spring/page/3.html | 4 ++-- tags/styled-components.html | 4 ++-- tags/subnet.html | 4 ++-- tags/tanstack-query.html | 4 ++-- tags/test.html | 4 ++-- tags/test/page/2.html | 4 ++-- tags/to-list.html | 4 ++-- tags/trouble-shooting.html | 4 ++-- tags/trouble-shooting/page/2.html | 4 ++-- tags/use-sync-external-state.html | 4 ++-- tags/use-sync-external-store.html | 4 ++-- tags/use-sync-external-store/page/2.html | 4 ++-- tags/vpc.html | 4 ++-- tags/webpack.html | 4 ++-- tags/world.html | 4 ++-- tags/world/page/2.html | 4 ++-- tags/zero-time.html | 4 ++-- ...\352\270\200-\354\247\200\353\217\204.html" | 4 ++-- ...\354\236\220-\353\266\204\354\204\235.html" | 4 ++-- "tags/\353\260\260\355\217\254.html" | 4 ++-- ...-\354\244\204\354\235\264\352\270\260.html" | 4 ++-- "tags/\354\204\234\353\262\204.html" | 4 ++-- "tags/\354\204\234\353\262\204/page/2.html" | 4 ++-- ...\354\212\244-\352\262\275\355\227\230.html" | 7 ++++--- .../page/2.html" | 4 ++-- .../page/3.html" | 4 ++-- ...4\355\202\244\355\205\215\354\262\230.html" | 4 ++-- ...4\355\201\254\354\275\224\354\212\244.html" | 4 ++-- .../page/2.html" | 4 ++-- .../\354\232\260\355\205\214\354\275\224.html" | 4 ++-- ...-\354\202\254\354\232\251\352\270\260.html" | 7 ++++--- .../page/2.html" | 4 ++-- .../page/3.html" | 4 ++-- ...\354\240\204\354\206\214-\354\225\261.html" | 4 ++-- .../page/2.html" | 7 ++++--- .../page/3.html" | 4 ++-- .../page/4.html" | 4 ++-- ...\355\203\234-\352\264\200\353\246\254.html" | 4 ++-- ...4\354\227\255\354\203\201\355\203\234.html" | 4 ++-- .../\354\271\264\355\216\230\354\235\270.html" | 4 ++-- .../page/2.html" | 7 ++++--- .../page/3.html" | 4 ++-- .../page/4.html" | 4 ++-- .../page/5.html" | 4 ++-- .../\355\205\214\354\212\244\355\212\270.html" | 4 ++-- .../\355\224\274\353\223\234\353\260\261.html" | 7 ++++--- .../page/2.html" | 4 ++-- .../page/3.html" | 4 ++-- "tags/\355\230\221\354\227\205.html" | 4 ++-- 244 files changed, 490 insertions(+), 481 deletions(-) create mode 100644 assets/images/report-6-038d09fa5b07bf8b8814e5e2463c4ccd.jpg create mode 100644 assets/js/06db3492.6ec62fef.js delete mode 100644 assets/js/06db3492.853f24a9.js create mode 100644 assets/js/2e801cce.448d8484.js delete mode 100644 assets/js/2e801cce.c1156dde.js create mode 100644 assets/js/c17ec8e8.4a256193.js delete mode 100644 assets/js/c17ec8e8.b6a2f2da.js rename assets/js/{runtime~main.a8937271.js => runtime~main.3ca96668.js} (99%) diff --git a/1.html b/1.html index 0c70cbf..54f076a 100644 --- a/1.html +++ b/1.html @@ -5,13 +5,13 @@ Hello World | CAR-FFEINE - +
본문으로 건너뛰기
- + \ No newline at end of file diff --git a/10.html b/10.html index 0056b4d..d91765f 100644 --- a/10.html +++ b/10.html @@ -5,13 +5,13 @@ webpack으로 msw 설정하기 | CAR-FFEINE - +
본문으로 건너뛰기

webpack으로 msw 설정하기

· 약 5분
센트

웹팩에서 msw 설정

이번 팀 프로젝트는 CRA와 같은 보일러 플레이트 코드를 사용하지 못하게 제한이 있다. 또한 요즘 많이 사용된다는 Vite의 사용도 제한이 있고, 웹팩으로 프로젝트를 시작하도록 강제하고 있다.

팀원 모두 한 번도 웹팩을 통해 프로젝트를 시작해본 경험이 없어 프론트엔드 팀원 각자 개인 레포에서 웹팩 공부를 진행한 후 어느정도 진척이 있을 때 팀 레포에 프로젝트를 시작하기로 했다.

다행히 웹팩으로 시작하는 프로젝트에 대한 많은 참고 자료들이 있어 첫 리액트 프로젝트 화면을 띄우는데 까지는 그리 오랜 시간이 걸리지 않았다. 그렇게 모든 팀원이 첫 웹팩 프로젝트를 성공시킨 후 모여 팀 프로젝트 초기 설정을 시작해보았다.

eslint, prettier, 웹팩 등등 여러 설정들을 하고 필요한 패키지를 설치하는데 문제가 발생했다. 큰 데이터를 다루는 백엔드의 개발 속도를 고려해 프론트엔드 개발을 진행하기 위해서 미션중에 배웠던 MSW 라이브러리를 사용하기로 결정했는데, 이 라이브러리가 우리 팀의 개발 환경에서 동작하지 않았다.

왜 동작하지 않는지 원인을 찾아보니 MSW service worker 파일을 찾을 수 없다는 오류 메세지가 나오는 것을 확인할 수 있었다. 원인을 더 자세히 알아보니 public 폴더에 있는 파일들은 웹팩이 번들링을 진행할 때 포함이 되지 않는다는 것을 알 수 있었고, 이를 어떻게 해결할 지 팀원들과 방법을 찾아보았다.

약 한시간쯤 지났을 무렵 copy-webpack-plugin 패키지를 통해 public 경로에 있는 파일들도 빌드 폴더에 포함시킬 수 있다는 것을 알게 되었다. 하지만 이 copy-webpack-plugin에 대한 사용법이 미숙해 public 폴더에 있는 mockServiceWorker.js 파일만 빌드 폴더로 옮겼어야 했는데 index.html과 같은 다른 파일들 까지 한꺼번에 빌드 폴더로 옮겨지게 되었다.

이런 저런 방법들을 시도해보다 webpack.config.js 파일의 plugins에 아래와 같은 설정을 추가 해주어 MSW를 프로젝트에 적용할 수 있게 되었다.

new CopyWebpackPlugin({
patterns: [
{ from: 'public/mockServiceWorker.js', to: '.' }, // msw service worker
],
}),

설정을 간단히 보면 public 경로에 있는 mockServiceWorker.js 파일을 빌드 후 폴더의 루트 디렉토리에 추가해준다는 설정이다.

문제 상황과 해결 방법을 간단하게 다시 정리해보면 다음과 같다.

  1. MSW를 적용해보려고 함.
  2. 웹팩에서 개발 서버를 열었을 때 MSW 실행을 위해 필요한 mockServiceWorker.js 파일을 찾을 수 없다는 오류가 발생함.
  3. 문제의 원인은 웹팩에서 번들링을 진행할 때 public 폴더 하위 경로에 있는 파일들을 무시하기 때문이었음.
  4. 문제를 해결하기 위해 public 경로에 있는 mockServiceWorker.js 파일을 번들링 후 폴더의 루트 디렉토리에 저장하도록 하는 설정을 추가해줌.
- + \ No newline at end of file diff --git a/11.html b/11.html index a518403..bd352b6 100644 --- a/11.html +++ b/11.html @@ -5,13 +5,13 @@ 카페인 팀에서 사용하는 지도 라이브러리를 소개합니다. | CAR-FFEINE - +
본문으로 건너뛰기

카페인 팀에서 사용하는 지도 라이브러리를 소개합니다.

· 약 9분
가브리엘

지도 api 벤더 선택 이유

국내 서비스 중인 지도 서비스로는 google, naver, kakao가 있습니다.

이 중에서도 google maps api는 css로 지도의 테마를 직접 스타일링할 수 있는 기능이 있어서 선택하게 됐습니다.

google maps api를 사용하기 위해서 별도의 라이브러리 사용이 필수는 아니지만

저희 팀에서 대중적인 라이브러리들과 기본 환경 설정법을 모두 테스트 했을 때, 반드시 사용하고 싶은 라이브러리가 존재하여 비교를 기록으로 남기게 됐습니다.

google maps api 관련 라이브러리

(선택한 라이브러리들은 ✅으로 표시했습니다.)

google maps API

https://github.com/tomchentw/react-google-maps

이 라이브러리는 구글에서 공식으로 제공하는 지도 api로, HTML DOM에 구글 지도를 부착하고, 사용(조작)할 수 있도록 도와줍니다. 이 라이브러리는 vanilla Javascript 기반으로 동작합니다.

@types/google.maps

https://www.npmjs.com/package/@types/google.maps

TypeScript에서 구글 지도를 사용할 때 타입을 제공해주는 역할을 합니다.

@googlemaps/js-api-loader

https://www.npmjs.com/package/@googlemaps/js-api-loader

이 라이브러리는 구글에서 공식으로 제공하는 지도 호출 api로, api key만 넘겨주더라도 구글 지도를 스크립트 형태로 불러와주는 역할을 하는 라이브러리입니다. 별도로 html 조작 없이 불러온 라이브러리에서 구글 지도를 꺼내서 동적으로 사용할 수 있습니다. vanilla Javascript 기반으로 동작하여 어디에서나 사용이 가능합니다.

대중적인 라이브러리 비교

react-google-maps@react-google-maps/api@googlemaps/react-wrapper
링크https://www.npmjs.com/package/react-google-mapshttps://www.npmjs.com/package/@react-google-maps/apihttps://www.npmjs.com/package/@googlemaps/react-wrapper
설명이 라이브러리는 개인이 만든 라이브러리로, google maps API를 react DOM 위에 올려서 사용하게 돕습니다.
구글 지도와 마커를 react component 처럼 사용하여 react스럽게 렌더링 하는 것을 지원합니다.
react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리였지만 2018년 이후로 업데이트가 끊겼습니다.
이 라이브러리도 개인이 만든 라이브러리로 앞서 소개한 react-google-maps를 개량하여 만든 라이브러리입니다.
이 라이브러리 역시 react에 지도나 마커 컴포넌트를 호출해서 사용이 가능합니다.
현재 react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리 입니다.
이 라이브러리는 구글에서 공식으로 제공하는 react용 라이브러리입니다.
이 라이브러리는 앞서 소개한 js-api-loader를 활용하여 만든 Wrapper 컴포넌트를 제공하는데, 구글 지도를 호출하는 과정에서 수신중, 실패, 성공에 따라 지도를 보여줄 지, 로딩중 컴포넌트를 보여줄 지, 에러 컴포넌트를 보여줄 지 결정하는 기능이 있습니다.
이외에는 기존의 js-api-loader의 기능과 완벽하게 동일합니다. (라이브러리를 열어서 직접 확인해봤습니다.)
선택여부

라이브러리 선택 이유

저희 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 다보니 지도 위에 띄워줘야 할 마커를 최적화 하는 과정이 굉장히 중요합니다.

  1. 전국 6만여 개의 마커를 전부 보여줄 수 없다.
  2. 현재 디스플레이 영역의 마커만을 호출해야한다.
  3. 그 마커들의 렌더링 과정을 저수준에서 다룰 수 있어야 한다.

이런 원칙을 가지고 있기에 대중적인 라이브러리들(react-google-maps, @react-google-maps/api)은 저희의 선택지에 없었습니다.

따라서 구글 지도는 오로지 vanilla로 제공되는 상태에서 직접 제어하기로 결정하였고, 마커를 관리하는 주체 또한 구글 지도에서 직접 컨트롤을 하려고 합니다.

따라서 구글 지도를 호출하는 작업은 @googlemaps/react-wrapper에 맡기고, 불러온 구글 지도는 vanilla로 통제하기로 했습니다.

지도의 조작, 지도에 마커를 찍는 과정을 모두 공식 문서에 나와있는 방법대로 통제하려고 합니다.

기존의 라이브러리들은 마커나 지도를 컴포넌트화 한 상태이기에 최적화 과정에서 저희가 제어할 수 없는 부분들이 있다고 생각합니다. 따라서 트러블슈팅 과정에서 마커의 호출 시점, 메모리에서 해제하는 시점, 렌더링하는 시점 등의 작업들을 훨씬 더 세밀하게 하려면 google maps api을 있는 그대로 사용할 수 있어야 합니다. 따라서 지도에 관련된 기능은 react DOM 위에서가 아닌 vanilla 환경에서 작업을 할 것입니다.

구글 지도 제어 전략

  1. 구글 지도와 마커는 항상 바닐라 환경(react DOM 바깥)에서 동작하게 한다.
  2. 바닐라 환경에서만 동작하게 하여 리액트 컴포넌트에서의 재 렌더링을 일절 방지한다.
  3. 마커나 지도의 동작 이벤트에 의해 UI를 조작해야하는 경우에는 react DOM 조작을 하도록 한다.
  4. 바닐라 환경인 google maps api와 react DOM 사이의 제어 과정에는 useSyncExternalStore 훅을 이용하여 리액트 UI를 강제로 동기화 시킬 수 있도록 한다.

구글 지도는 바닐라 환경에서, 각종 UI 통제는 리액트에서 통합하여 사용하는 환경을 구상하고 있습니다.

시중에 나와있는 대부분의 라이브러리들을 활용하여 비교하고 테스트한 결과 @googlemaps/react-wrapper를 선택하는 것이 최적화와 생산성, 앱 안정성을 모두 확보할 수 있는 선택이라고 생각했습니다.

현재 카페인 팀에서 사용중인 지도 제어에 관한 방법은 이후에 작성 될 글에서 상세하게 설명하겠습니다.

- + \ No newline at end of file diff --git a/12.html b/12.html index 9da79bc..6233c61 100644 --- a/12.html +++ b/12.html @@ -5,13 +5,13 @@ jasypt를 활용하여 프로퍼티를 암호화하자 | CAR-FFEINE - +
본문으로 건너뛰기

jasypt를 활용하여 프로퍼티를 암호화하자

· 약 6분
키아라

서론

안녕하세요 카페인팀 키아라입니다.

이번 프로젝트를 시작하면서 프로퍼티를 암호화하는 방법으로 jasypt를 알게되어

사용하는 방법을 익혀 저희 프로젝트에 적용해볼 계획입니다.

프로퍼티 암호화는 왜 필요할까?

spring:
datasource:
url: 데이터베이스 url
username: 계정
password: 비밀번호

프로젝트를 진행하면서 yml 파일에 DB 연결 URL이나 계정, 비밀번호 같이 노출되어선 안 되는 민감한 정보들이 많습니다.

git의 public repository와 CI/CD를 연동해 어플리케이션을 배포한다면 중요한 정보가 탈취될 가능성이 있죠.

Jasypt 라이브러리를 사용하면 평문으로 된 데이터베이스 접속 정보를 암호화 하여 방어막을 한 겹 쌓을 수 있게 됩니다.

간략하게 라이브러리를 소개하고 사용 방법을 알아볼까요?

jasypt는 뭐지?

Jasypt이란 쉽게 암호화 기능을 사용할 수 있도록 제공하는 Java 라이브러리입니다.

민감한 평문 정보를 암호화하고, 아래처럼 설정 값을 지정하면 어플리케이션이 실행될 때 자동으로 이를 복호화하여 사용합니다.

사용자가 편하게 암호화 기능을 사용할 수 있도록 제공하는 Java 라이브러리로

공식 홈페이지는 http://www.jasypt.org/ 에 가면 더 자세한 정보를 확인할 수 있습니다.

사용 방법

정말 간단하게 라이브러리 추가, key값 넘겨주기, 암호화 세 가지 단계로 프로퍼티를 암호화하여 관리할 수 있습니다.

1. 라이브러리 추가 (= 의존성 추가)

implementation "com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.3"

2. Jasypt 설정 및 Bean 등록

key를 사용해서 Bean을 등록하는 기본 설정입니다. 여기서 Bean의 이름을 jasyptEncryptor라고 설정했다면 프로퍼티 등록해야 합니다.

@Configuration
public class JasyptConfig {

private String ENCRYPT_KEY = "hello";

@Bean(name = "jasyptEncryptor")
public StringEncryptor stringEncryptor() {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();

SimpleStringPBEConfig config = new SimpleStringPBEConfig();

config.setPassword(ENCRYPT_KEY);
config.setAlgorithm("PBEWithMD5AndDES");
config.setKeyObtentionIterations(1000);
config.setPoolSize(1);
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor;
}
}
jasypt:
encryptor:
bean: jasyptEncryptor

3. 암호화

라이브러리를 사용할 준비는 거의 다 끝났습니다. 이제 암호화하여 프로퍼티에 작성합니다.

이때 암호화 하는 방법은, 아래 사이트에 접속해 평문과 키를 입력한 후 나온 암호문을 프로퍼티 파일에 'ENC(암호문)' 로 작성합니다.

암복호화 사이트

평문

  datasource:
url: 데이터베이스 url
username: 계정
password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)

나머지도 마저 암호화해줍시다.

  datasource:
url: ENC(j94r94hQbd1SfFHGCUeweg+GGDosfnxP8dL0FQxfXtE=)
username: ENC(vp3Gw8kLpwDZhmMMqf88/Q==)
password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)

실행

올바른 암호문을 입력했다면 정상적으로 실행이 됩니다.

그러나 이때 임의로 암호문을 수정한다면 다음과 같이 빌드를 실패합니다.

실행 실패

그런데 뭔가 이상하지 않나요?

프로퍼티는 분명 암호화 했는데 키가 코드에 그대로 노출되어 있습니다.

Git의 public Repository에 배포하면 다른 사람들도 볼 수 있습니다.

그럼 이 키를 어디에 숨길 수 있을까요?

저는 처음에 일반 file에 키를 넣어놓고 파일을 읽어오는 식으로 키를 관리하려고 했습니다. 당연히 해당 파일은 .gitignore로 커밋 대상에서 제외해야겠죠.

그런데 이것보다 더 쉽고 빠른 방법이 있습니다.

바로 환경변수를 설정하는 것이죠.

+ 환경변수 설정

private String ENCRYPT_KEY = "hello";

기존의 키를 관리하는 방식이었습니다.

우선 이 키를 프로퍼티에서 관리하도록 설정해볼까요?

// JasyptConfig.class
@Value("${jasypt.encryptor.password}")
private String ENCRYPT_KEY;
// application.yml
jasypt:
encryptor:
password: hello

이제 환경변수를 설정해봅시다.

Run > Edit Configurations... 경로로 들어가면

Run/Debug Configurations 창이 나오는데

Environment variables: 부분에 ENCRYPT_KEY=hello

라고 적어주세요.

그 후 다시 yml 파일로 돌아와 기존 hello로 되어있는 부분을 ${ENCRYPT_KEY}로 변경하고 실행한다면 정상적으로 작동됩니다.

jasypt:
encryptor:
password: ${ENCRYPT_KEY}

긴 글 읽어주셔서 감사합니다.

- + \ No newline at end of file diff --git a/13.html b/13.html index c258fea..f5fda5d 100644 --- a/13.html +++ b/13.html @@ -5,13 +5,13 @@ 충전소 리스트 클릭시 마커에 간단정보 모달을 띄우는 기능 추가에서 겪었던 트러블 슈팅 | CAR-FFEINE - +
본문으로 건너뛰기

충전소 리스트 클릭시 마커에 간단정보 모달을 띄우는 기능 추가에서 겪었던 트러블 슈팅

· 약 18분
센트

Untitled

위 이미지는 현재까지 구현한 지도의 모습이다. 구현된 기능은 다음과 같다.

  • 충전소 정보를 서버에 요청해 받아온 충전소 정보를 바탕으로 화면에 마커를 표시하는 기능
  • 화면이 이동하거나 줌인, 줌 아웃을 할 시 화면의 마커 정보가 최신화 되는 기능
  • 마커 정보를 최신화 할 때 화면에서 사라진 마커를 dom에서 제거하는 기능
  • 마커 정보를 최신화 할 때 이전 화면에서도 있었던 마커를 재생성 하지 않는 기능
  • 마커를 클릭했을 시 해당 마커에 대한 간단 정보를 모달로 띄워주는 기능
  • 화면에 표시된 마커들에 대한 충전소 정보를 리스트로 보여주는 기능

이번에 새로 추가하고자 한 기능은 다음과 같다.

  • 충전소 리스트에서 충전소를 선택하면 화면의 중심이 선택한 충전소 마커로 이동하고, 충전소의 간단 정보를 모달로 띄워주는 기능

위 기능을 구현하기 위해선 google maps api의 InfoWindow객체를 이용해야 한다. 사용 방식은 다음과 같다.

const infowindow = new google.maps.InfoWindow({
content: contentString,
ariaLabel: 'Uluru',
});

const marker = new google.maps.Marker({
position: uluru,
map,
title: 'Uluru (Ayers Rock)',
});

infowindow.open({
anchor: marker,
map,
});

간단하게 요약하자면 다음과 같다.

  • InfoWindow 생성자 함수를 통해 infoWindow 인스턴스를 생성한다.
    • 생성시 dom 요소 혹은 string을 전달해 infoWindow가 생성될 dom위치를 지정해준다.
  • marker 인스턴스를 infoWindow 인스턴스의 open 메서드에 인자로 전달한다.
  • infoWindow 생성 시 전달했던 dom요소의 위치가 marker의 위치로 고정되면서 화면에 그려진다.

Untitled

충전소 정보를 보여주는 위 StationList 컴포넌트는 충전소 정보에 접근할 때 react-query를 통해 서버 상태를 직접 내려 받아 컴포넌트 내부 리스트를 렌더링 한다.

또한, StationMarkersContainer에서도 충전소 정보를 react-query의 서버 상태에서 참조해 마커를 렌더링 하고 있다.

따라서 StationList 컴포넌트와 StationMarkersContainer는 각각 따로 서버 상태에 접근해 렌더링을 수행하고 있으므로 둘 사이에는 어떠한 연결 고리가 없다.

여기서 문제가 발생하게 되었다.


현재까지의 코드에서는 infoWindow인스턴스를 StationMarkersContainer컴포넌트에서 생성한다. 이를 하위 컴포넌트인 StationMarker에 내려주고, 이 컴포넌트 내부에서 marker인스턴스를 생성한다.

이번에 구현하기로 한 기능은 StationList의 항목 중 하나를 선택했을 시 선택된 충전소에 해당하는 마커에 간단 정보 모달이 뜨며 화면을 해당 마커가 중심으로 오도록 이동 시키는 것이었다.

하지만 지금의 코드 구조상 StationListStationMarkersContainer사이에는 어떠한 연결 고리도 없으므로 infoWindowmarkerStationList는 접근할 수 없는 상태가 된다.

이를 해결하기 위해서 다음과 같은 방법을 사용하기로 했다.

  • infoWindow인스턴스를 root 단에서 생성해 전역적으로 관리한다.
  • 생성될 marker 인스턴스들을 배열 형태의 전역 상태로 관리한다.

위 내용을 말로만 본다면 별로 어려울 것 없어 보이지만 실제 구현을 진행해보니 내부적으로 큰 문제가 두 가지 존재했다.

  1. 따로 모듈을 분리해 infoWindow를 생성할 수 없다.
  2. marker인스턴스를 생성하는 주체가 StationMarkersContainer가 되어서는 안된다.

각각의 문제점을 살펴보자.


1. 따로 모듈을 분리해 infoWindow를 생성할 수 없다.

infoWinodw를 전역 상태로 만들어 사용하기 위해 처음으로 했던 생각은 infoWindowStore.ts로 모듈을 분리하여 infoWindow를 생성해 store의 초기값으로 지정하는 것이었다.

위 생각을 가지고 그대로 구현해보았더니 google을 참조할 수 없다는 에러가 발생했다. InfoWindow생성자 함수는 google.maps.InfoWindow를 통해 접근할 수 있기 때문에 해당 에러는 infoWindow인스턴스를 생성할 수 없다는 것을 의미했다.

google을 참조할 수 없는지 이유를 분석해보니 이유는 다음과 같았다.

우리 팀이 구글 지도 로드를 위해 선택한 라이브러리는 @googlemaps/react-wrapper이다. 이 라이브러리의 동작을 살펴보면 다음과 같다.

  • Wrapper컴포넌트가 @googlemaps/js-loader라이브러리의 Loader생성자 함수를 호출한다.
  • 생성된 loader인스턴스의 load메서드를 실행시켜 지도의 로딩 작업을 시작한다.
    • load 메서드는 최종적으로 Promise<typeof google>을 반환하는데, 지도 로드에 성공하면 resolve(window.google) 을 실행시켜 google을 전역적으로 사용 가능하도록 만들어준다.
  • 지도의 로딩이 완료되면 Wrapperrender props를 통해 받은 콜백 함수를 실행시킨다.
    • render콜백 함수는 로딩 상태를 나타내는 Status를 파라미터로 넘겨 받아 호출된다.

최종적으로 render를 실행 시켰을 때 반환 되는 컴포넌트에서는 google 로딩 되어 전역적으로 접근이 가능함을 보장할 수 있으므로 이때부터 google에 접근이 가능해진다. → 따라서 Wrapper를 통해 반환되는 컴포넌트의 하위 컴포넌트에서 google.maps.Map생성자 함수를 사용해 지도를 생성할 수 있게 된다.

infoWindow를 생성하기 위해 만든 새로운 모듈은 첫 import시기에 평가될 것이기 때문에 Wrapper의 하위 컴포넌트에서 import를 수행한다면 로드가 완료된 이후 시점일 것이므로 window.google이 등록되어 google에 접근이 가능할 것으로 예상했다.

하지만 웹팩을 통한 번들링 과정에서 모듈이 뒤섞여 파일의 평가 시기를 보장할 수 없어져 새로 만든 모듈에서는 google에 대한 접근이 불가능해지게 되었다. 웹팩을 좀 더 공부해본다면 이 문제를 해결할 수 있을 것 같았지만, 너무 지엽적인 부분에서 많은 시간을 들이기 보단 기존에 개발하던 방식을 통해 문제를 해결해보기로 결정했다.

최종적으로 문제를 해결한 방식은 다음과 같다.

  • InfoWindow생성자 함수를 호출할 CarFfeineInfoWindowInitializer컴포넌트를 만든다.
  • Wrapper로 감싸진 컴포넌트 하위에 CarFfeineInfoWindowInitializer 컴포넌트를 추가한다.
  • google에 접근이 가능한 상태를 보장받은 CarFfeineInfoWindowInitializer내부에서 infoWindow인스턴스를 생성한다.
  • storeinfoWindow인스턴스를 set해주어 전역적으로 infoWindow를 사용 가능하도록 한다.

2. marker인스턴스를 생성하는 주체가 StationMarkersContainer가 되어서는 안된다.

이번 팀 프로젝트에서 지도를 구현하기 위해 google maps api를 사용하게 되었다. 뜬금없이 이 이야기를 한 이유는 다음과 같다.

  • google maps api는 바닐라 자바스크립트를 기반으로 동작한다.
  • 이번 팀 프로젝트는 리액트를 기반으로 개발을 진행할 것이다.
  • 지도를 그리기 위해서 바닐라 자바스크립트와 리액트의 적절한 조화가 필요하다.
  • 다소 혼란스러울 수 있는 지도의 조작 방식을 리액트와 조화롭게 사용하기 위해서 컴포넌트 설계시 컴포넌트의 책임을 확실하게 구분해야겠다는 생각을 하게 되었다.

이 컴포넌트의 책임에 대한 문제로 인해 marker 인스턴스를 생성하는 주체에 대해 많은 고민을 하게 되었다.

일단 원래 코드 구조에서 마커를 그리기 위해 컴포넌트를 다음과 같이 추상화 했다.

  • StationMarkersContainer 컴포넌트
    • 리액트 쿼리를 통해 받아온 서버 상태(충전소 정보 배열)로 StationMarker를 호출한다.
  • StationMarker 컴포넌트
    • 상위에서 내려받은 충전소 정보 props를 통해 marker 인스턴스를 생성한다. (google maps api에서는 인스턴스 생성이 곧 렌더링을 의미한다)
    • 생성한 marker 인스턴스에 infoWindow 인스턴스의 open 메서드를 트리거 하는 클릭 이벤트 리스너를 추가해준다.
    • useEffect의 클린업 함수를 이용해 충전소 정보가 최신화 되었을 때 마커가 더이상 화면에 보이지 않는다면 marker 인스턴스의 setMap(null) 메서드를 호출해 google maps api에서 마커를 지우도록 한다. (마커 렌더링 최적화)

간략히 설명하자면 StationMarkersContainer 컴포넌트는 충전소 정보를 서버에서 받아 StationMarker를 호출하는 역할만을 수행하고, 마커에 대한 모든 세부 로직은 StationMarker가 수행하도록 컴포넌트를 추상화 해보았다.

이름에서도 드러나듯 StationMarker 컴포넌트가 marker 인스턴스를 생성하는 주체가 되어야 바닐라 자바스크립트와 리액트의 혼종인 이 프로젝트의 코드를 추후 유지보수 할 때 문제가 없으리라 판단했다.

하지만 이렇게 추상화 된 컴포넌트들은 marker 인스턴스를 배열 형식의 전역 상태에 담아 관리하고자 할 때 문제가 되었다.


일단 먼저 서버에서 내려 받은 충전소 정보를 station이라고 하자, 우리는 이 station을 통해 marker 인스턴스를 생성하고자 한다.

이때 생각 할 수 있는 가장 간단한 방법은 station에서 map 메서드를 통해 marker 인스턴스를 생성하여 이 marker 인스턴스를 하위 컴포넌트인 StationMarker에 넘겨주는 방식일 것이다.

하지만 이 방식은 인스턴스를 생성하는 것이 곧 화면에 렌더링을 발생시키는 것을 의미하는 google maps api의 특성상 우리가 처음 설계한 컴포넌트의 책임을 반하는 구조를 만들어내게 된다.

자세히 설명해보자면 마커의 렌더링은 StationMarkersContainer가 수행하고 있는데 화면에 보이지 않는 마커를 지우는 역할은 StationMarker컴포넌트가 수행하고 있고, 이벤트 핸들러의 추가 역시 마커가 생성된 이후에 하위 컴포넌트에서 이를 수행하는 괴상한 코드가 만들어지게 된다.

추후 코드의 유지보수성을 위해선 피해야 할 방식임이 명확했다.

해결 방식을 고민해보다가 다음과 같은 해결 방안을 생각하게 되었다.

StationMarker 컴포넌트의 역할

  • marker 인스턴스를 생성한다.
  • marker 인스턴스의 이벤트 핸들러를 추가한다.
  • 생성된 marker 인스턴스를 배열 형식의 전역 상태에 추가한다.
  • 충전소 정보가 최신화 되었을 때 마커가 화면에 보이지 않는 상태가 되었다면 marker 인스턴스를 전역 상태에서 삭제한다.

위와 같이 StationMarker 의 역할을 잡게 되면 기존의 컴포넌트 설계 구조를 해치지 않으면서 전역 상태에 marker인스턴스를 잘 추가할 수 있게 된다. 하지만 이렇게 되면 StationMarker 컴포넌트는 다음의 큰 문제들을 가지게 된다.

  1. marker들을 가지는 전역 상태를 구독하고 있는 컴포넌트가 새로 생성되는 마커의 개수만큼 리렌더링 된다.
  2. 현재 사용하고 있는 전역 상태 관리 도구의 특성상 이전 상태를 참조해와야 marker를 추가할 수 있게 되는데, 이 때 이전 상태가 최신의 상태임을 보장하지 못할 수 있다.

이 두 문제를 해결할 방식을 고민해보았을 때 다음과 같은 결론에 도달하게 되었다.

  • 현재 사용하고 있는 전역 상태 관리 도구는 React 18에 새로 추가된 useSyncExternalState 훅을 기반으로 recoil과 비슷하게 사용할 수 있도록 계층을 분리하여 만든 도구이다.
  • 기존에 사용하던 전역 상태 관리 도구의 메서드 useExternalState, useExternalValue, useSetExternalState 이외에 store 인스턴스에 직접 접근하여 최신의 상태를 참조하는 getStoreSnapShot 메서드를 추가한다.
  • store에 직접 접근해 받아온 최신의 상태는 바닐라 자바스크립트 객체 이므로 리액트의 리렌더링을 발생 시키지 않는다.
  • 리렌더링으로 인한 문제점들을 getStoreSnapShot 메서드를 추가함으로써 해결할 수 있다.

새로운 기능 추가를 위해 마주했던 앞선 두 가지의 문제와 해결 방식을 살펴 보았다. 그래서 최종적으로 이전까지 계속해서 고민해왔던 문제를 해결한 과정을 간추려보자면 다음과 같다.

  • 충전소 정보를 서버에서 받아와 렌더링 하는 StationList 컴포넌트에서 marker 인스턴스 배열을 저장하고 있는 store인스턴스에 직접 접근해 최신의 marker인스턴스들을 가져온다.
  • 충전소 목록에서 사용자가 충전소를 클릭했을 때 전역으로 관리되는 infoWindow 인스턴스의 open메서드에 marker 인스턴스들 중 선택된 marker를 전달해 간단 정보 모달을 띄워준다.
- + \ No newline at end of file diff --git a/14.html b/14.html index 6eca346..092c0c0 100644 --- a/14.html +++ b/14.html @@ -5,13 +5,13 @@ 카페인팀 서버 아키텍처를 설명해드리겠습니다 | CAR-FFEINE - +
본문으로 건너뛰기

카페인팀 서버 아키텍처를 설명해드리겠습니다

· 약 11분
누누

안녕하세요 우아한테크코스 카페인팀 누누입니다

이번에 카페인 팀에서 배포 아키텍처를 결정하게 되었던 과정에 대해서 정리를 해보고 싶어서 글을 쓰게 되었습니다.

아키텍처와 서버가 배포되는 과정을 보여드리면서 시작하도록 하겠습니다

배포 아키텍처

서버가 배포되는 과정은 다음과 같습니다.

server image

우아한테크코스 인스턴스에 대한 소개

우테코에서 선택할 수 있는 인스턴스는 총 2가지 종류입니다.

  1. 퍼블릭 서브넷에 있는 인스턴스
    • 캠퍼스에서만 SSH 접근이 가능한 인스턴스입니다.
    • 미리 열려있는 포트들만 허용이 되어 있습니다.
    • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다
  2. 프라이빗 서브넷에 있는 인스턴스
    • 퍼블릭 서브넷에 있는 인스턴스를 통해서만 접근이 가능합니다.
    • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다.

1번 인스턴스를 2개 사용 가능하고, 2번 인스턴스를 1개 사용 가능합니다.

권장되는 환경에서 1개는 db 서버로 사용하고, 나머지 2개는 자유롭게 사용이 가능했습니다.

그전에 알면 좋아요

여기서는 Self Hosted Runner를 사용했는데요.

Self Hosted Runner에 대한 내용은 여기 에 잘 나와있습니다.

외부 IP로부터 SSH 접근이 불가능하기에, Self Hosted Runner 나, Jenkins 같은 방법을 사용할 수 있었는데, 러닝 커브를 고려해서 Self Hosted Runner를 선택하게 되었습니다.

배포 아키텍처에 대한 고민

저희 팀이 이번 아키텍처를 만들기 위해서 고민했던 점들은 다음과 같습니다.

  1. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
  2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
  3. 2차 데모데이까지의 과제인 개발 서버를 어떻게 구성할 수 있을까?

여기서 1번을 가장 먼저 생각한 아키텍처를 구성하게 되었는데, 다음과 같습니다.

선택의 기준이 되었던 것은 총 3가지였습니다.

  1. DB는 프라이빗 서브넷에 위치시키고, 우리 인스턴스를 거쳐서만 접근이 가능하게 한다.
    • 이 부분은 보안을 위해서 어쩔 수 없이 선택하게 된 부분입니다.이 부분을 고려하다 보니, 최소한으로 구성할 수 있는 구조가 db 용 private 인스턴스 1개, 그리고 우리가 사용할 public 인스턴스 1개가 됩니다
  2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
    • 개발용 인스턴스에 CD 툴이나, 모니터링 툴을 설치하게 되면, 운영 서버에도 동일하게 작업을 해야 합니다.
    • 이 부분을 최소화하기 위해서, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 되었습니다.
  3. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
    • 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 않는다면 개발용 인스턴스에서 장애가 발생했을 때, CD 툴과 모니터링 툴에도 영향을 미치게 됩니다. 이 부분을 생각했을 때도, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리해야 한다고 결정하게 되었습니다
    • 한 부분의 장애가 다른 툴까지 사용할 수 없게 만들게 되어서, 롤백이나, 상황 파악을 하기 힘들게 만들게 됩니다.

이런 과정들을 생각했을 때, 인스턴스 1개를 개발 서버용으로, 인스턴스 1개를 CD 툴과 모니터링 툴을 설치한 인스턴스로 사용하게 되었습니다.

실제 내부 구성은 어떻게 될까요?

개발 서버

이 인스턴스에는 총 2가지 기능이 들어가 있습니다.

  1. 프론트 서버
    • react로 되어있는 프론트엔드 코드를 사용자에게 전달해 주는 역할을 합니다.
  2. 백엔드 서버
    • spring으로 되어있는 api 서버입니다.

물론, 이렇게 하면 두 곳 중 한 곳에 장애가 발생했을 때, 프론트 서버와 백엔드 서버가 모두 영향을 받게 됩니다.

같이 관리하게 된 첫 번째 이유로 비용이 들기 때문에 비용의 문제를 고려하게 되었습니다. 개발 서버에서 프론트 서버와 백엔드 서버를 관리하게 되었습니다.

두 번째 이유로는, 아직 프로젝트 초창기 이기 때문에, 백엔드에서 장애가 났을 때, 프론트에서 일정 이상의 에러 처리가 불가능했습니다.

프로젝트가 많이 진행되었다면, 프론트엔드만으로 혹은 장애가 나지 않은 서버를 활용해 에러 처리를 할 수 있지만, 아직은 그런 기능을 구현하지 못했습니다.

이와는 별개로 실행 시 편의를 위해서 도커를 사용해 개발 서버를 관리하고 있습니다.

CD 툴과 모니터링 툴

이 인스턴스에는 총 3가지 기능이 들어가 있습니다.

  1. CD 툴
    • 위에서 설명드린 것처럼, self hosted runner 가 동작하게 되어있습니다
  2. 보안을 위한 리버스 프록시
    • 저희 프로젝트에서 구글 지도를 사용하게 되는데, 이때 API 키를 사용하게 됩니다. 이렇게 하면, API 키를 노출시키지 않고, 사용할 수 있습니다.
    • 이 API 키를 노출시키지 않기 위해서, 리버스 프록시를 하나 두고, 여기서 API 키를 추가해 요청을 보내는 방식으로 구성하게 되었습니다.
  3. 모니터링 툴
    • 저희 프로젝트에서 아직 도입하지 않았지만, 현재 이슈로는 올라가 있는 상태입니다.
    • Actuator, 프로메테우스, 그라파나 이 3가지를 활용해서 모니터링 툴을 구성하게 될 예정입니다

위 기능들이 한 인스턴스에 모여있기에, 위의 기능들은 추후에 운영 서버가 추가되었을 때, 중복으로 관리하지 않아도 됩니다.

배포 과정 더 자세히 알아보기

아래에 사진에서 보이는 과정을 통해서 배포를 진행하고 있는데요

server image

  1. 사용자가 push를 하면, github actions에서 도커 빌드를 진행하고, 도커 허브에 이미지를 올립니다.
  2. 도커 허브에 이미지가 올라간 이후에, self hosted runner 가 작동을 시작합니다.
  3. 개발용 인스턴스에 접근해서, 이미지를 받고, 컨테이너를 실행합니다.

이런 과정을 통해서, 개발용 인스턴스에 배포를 진행하고 있습니다.

느낀 점

좋은 아키텍처를 설계하기 위해서는 고려해야 할 점들이 정말 많다는 것을 다시 한번 느꼈습니다.

운영 서버가 추가된다던가, 인스턴스가 늘어나고, 줄어드는 상황에 유연하게 대처할 수 있도록 설계를 해야 한다는 것을 다시 한번 느꼈습니다.

중복으로 관리될 포인트를 줄여야 한다는 것도 다시 한번 느낄 수 있었고요

긴 글을 읽어주셔서 감사합니다

- + \ No newline at end of file diff --git a/15.html b/15.html index 581c537..70de328 100644 --- a/15.html +++ b/15.html @@ -5,7 +5,7 @@ 주기적인 데이터 요청으로 받은 데이터를 효율적으로 업데이트 및 삽입하기 (with. 박스터) | CAR-FFEINE - + @@ -26,7 +26,7 @@ 이를 통해서 Ver2에 비해서 1초정도 줄었습니다.

Ver4. 이전 방식 + Fetch Join 사용하기 (약 6초)

마지막 방법은 조회 과정의 시간 단축입니다.

처음에 Stations를 findAll()하는 쿼리를 확인해보니 N+1 문제가 발생하고 있었습니다. 그 이유는 Station에서 Chargers를 지연로딩으로 설정 했는데, 이를 그대로 get 메서드를 통해 조회해서 해당 문제가 발생했습니다.

List<ChargeStation> findAll(); // 기존

@Query("SELECT DISTINCT c FROM ChargeStation c JOIN FETCH c.chargers"); // Fetch Join 적용
List<ChargeStation> findAll();

따라서 위에 코드와 같이 Fetch Join을 이용해서 처음에 데이터를 가져왔습니다. 이렇게 효율적인 조회로 변경하면서 시간을 많이 줄일 수 있었습니다.

지금까지의 방법을 정리를 하자면

Ver1 과 같은 방식에서는 업데이트 과정에서 JPA의 식별자에 따른 처리 방식으로 인해 [SELECT + UPDATE] or [SELECT + INSERT] 와 같이 쿼리가 두 번씩 나갔습니다.

그래서 Ver3까지 개선을 하기 위해서 저장과 업데이트를 한 번에 JDBC를 이용해서 Batch로 처리해주는 방식을 선택했고,

변경 감지 + 배치 데이터를 모으기 위해서 자료구조를 이용해서 시간을 조금씩 단축 했습니다.

마지막으로 Ver4에서는 findAll()에서 발생하는 N+1의 문제를 해결하면서 시간을 단축했습니다.

이런 과정을 통해서 동일 작업을 14초에서 6초 정도로 줄일 수 있었습니다!

- + \ No newline at end of file diff --git a/16.html b/16.html index 93adb69..059fc47 100644 --- a/16.html +++ b/16.html @@ -5,7 +5,7 @@ JPA에서 ID가 있는 Entity에 대해 save 시에 select 쿼리가 나가는 이유 | CAR-FFEINE - + @@ -22,7 +22,7 @@ 아주 간단하게 entity의 isNew()를 호출한다고 적혀있습니다. 하지만 Persistable 인터페이스를 구현한 Entity의 isNew() 를 호출하는 것 입니다.

그럼 남은 하나의 클래스를 확인하겠습니다.

info-support

위 사진처럼 이 클래스가 Entity 마다 Persistable 구현 유무에 따라 동적으로 구현체를 변경해주고 있었습니다.

그럼 답이 나온 것 같습니다. ID를 직접 할당하는 Entity에 Persistable을 구현해주면 됩니다.

Persistable 구현하기

@Entity
public class ChargeStation implements Pesistable{

@Id
private String stationId;

private String stationName;

@CreatedDate
private LocalDateTime createdTime;

...

@Override
public Object getId() {
return getStationId();
}

@Override
public boolean isNew() {
return createdTime == null;
}
}

간단히 만들어봤습니다. @CreatedDate는 Entity가 처음 영속화될 때 동작하기 때문에 이 Entity의 CreateTime 필드가 null 이면 새로운 Entity라고 확신할 수 있습니다. 그럼 이렇게 인터페이스를 구현하고 아까 실행했던 테스트를 다시 실행해보겠습니다.

solved

깔끔하게 구현된 것을 확인할 수 있었습니다. 원하던대로 쿼리가 2번 발생합니다. 이런 Persistable@MappedSuperClass를 통해 더 깔끔하게 구현할 수 있습니다. 하지만 따로 설명드리지는 않겠습니다.

결론

JPA는 많은 편의 기능을 제공해주는 것 같아보입니다. 쫄지맙시다.

- + \ No newline at end of file diff --git a/17.html b/17.html index d35708b..a08f651 100644 --- a/17.html +++ b/17.html @@ -5,7 +5,7 @@ 카페인 팀의 CI/CD | CAR-FFEINE - + @@ -20,7 +20,7 @@ 위에 사진과 같이 설정을 해주시면 됩니다.

그리고 이를 yml에서 사용하기 위해선 secrets.Key이름으로 사용해주시면 됩니다.


이제 마지막으로 Dockerfile을 만들어줍니다.

저희는 /backend/ 경로에 만들어주었습니다.

FROM amazoncorretto:17-alpine-jdk
ARG JAR_FILE=./backend/build/libs/carffeine-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-jar","/app.jar"]

저희는 위처럼 절대 경로를 기준으로 JAR_FILE 위치를 지정하고, profiles는 dev로 설정해서 만들어주었습니다.


3. 배포하기

트리거를 작동시켜서 저희가 yml 파일에서 지정해준 것들이 잘 작동하는지 확인합니다.

jobSuccess 위에 사진처럼 모든 Job이 성공적으로 통과하는 것을 보실 수 있습니다.

dockerPs 이렇게 인프라 서버에서 배포 서버로 들어가서 성공적으로 서버를 도커로 띄운 것을 보실 수 있습니다.

EC2 배포 서버에서 docker ps를 입력했을 때에도 잘 실행이 되네요!


CD 배포 과정 요약

지속적 배포 과정을 요약 하자면 다음과 같습니다.

  1. Self Hosted Runner를 EC2 인프라 서버에 등록해준다.
  2. yml 파일과 Dockerfile을 만들어준다.
  3. 트리거를 작동시켜서 Github Actions의 태스크가 모두 잘 되는지 확인한다.
  4. 잘 됐다면 EC2 배포 서버에 Docker image가 성공적으로 띄워진다.
- + \ No newline at end of file diff --git a/18.html b/18.html index c613e1f..8cc6d4b 100644 --- a/18.html +++ b/18.html @@ -5,13 +5,13 @@ private 서브넷에 인스턴스를 외부와 연결할 때, public ip? private ip? | CAR-FFEINE - +
본문으로 건너뛰기

private 서브넷에 인스턴스를 외부와 연결할 때, public ip? private ip?

· 약 11분
누누

어떤 문제가 있었나요?

우아한테크코스에서 private 서브넷에 db 인스턴스를 두고, 보안을 위해 외부에서 접속을 차단하려고 했습니다.

이 과정에서 총 2가지의 문제점이 있었습니다.

  1. private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었습니다.
  2. public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안되었습니다.

이 부분을 어떻게 해결했는지 알아보도록 하겠습니다.

아래의 모든 설명은 AWS 를 기준으로 합니다.

private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었다.

해결 방법

public ip 자동할당을 해주지 않아서, 인터넷에 연결이 안 되었습니다.

이를 해결하기 위해 public ip 자동할당을 해주었습니다.

왜 public ip를 할당했더니 문제가 해결되었을까요?

private 서브넷이란?

정말 간단하게 설명했을 때

private 서브넷은 인터넷에 연결되지 않은 서브넷입니다.

조금 자세하게 들어가 보도록 하겠습니다

private 서브넷은 인터넷 게이트웨이가 연결되지 않은 서브넷입니다.

aws 공식문서에서 사진을 통해 보면 아래와 같이 되어있습니다

private subnet

public 서브넷에만 인터넷 게이트웨이가 연결되어 있고, private 서브넷에는 인터넷 게이트웨이가 연결되어있지 않습니다.

private 서브넷에 인터넷 게이트웨이가 연결되어 있지 않다고 했을 때, 기본적으로 인터넷에 접속이 안됩니다.

mysql을 설치할 때도, 인터넷에 접속을 해야하는데, 인터넷에 접속이 안되니 설치가 안되는 것입니다.

어? 인터넷 자체가 접근이 안되면 어떻게 설치하나요?

정말 원시적으로 해결하기 위해서는 public 서브넷에 인스턴스를 하나 더 만들어서, mysql 을 압축해서 scp를 통해 private 서브넷에 있는 인스턴스에 전송하고, 압축을 풀어서 설치하는 방법이 있습니다.

하지만 이 방법은 너무 원시적이고, 비효율적입니다.

그래서 인터넷으로 요청을 보낼 수 있도록 만드는 과정이 필요합니다.

인터넷으로 요청을 보낼 수 있도록 만드는 과정

인터넷으로 요청을 보낼 수 있도록 만드는 과정은 크게 2가지가 있습니다.

private 서브넷을 public 서브넷으로 바꾸기

보안을 위해서 private 서브넷에 두려고 했던 것을 public 서브넷으로 바꾼다는 부분은 매우 위험합니다.

그래서 이 방법은 보통 사용하지 않습니다.

NAT 인스턴스(Gateway) 만들기

NAT 인스턴스는 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들어주는 인스턴스입니다.

인터넷에 접속을 하기 위해서는 public ip 가 필요합니다.

따라서 NAT 인스턴스, NAT 게이트웨이는 public 서브넷에 존재해야 합니다.

어? NAT 인스턴스를 통해서 바로 통신이 가능하면 왜 private 서브넷이 필요한가요? 그냥 다 public 서브넷에 두면 되지 않나요?

NAT 인스턴스, NAT Gateway는 내부에서 출발한 트래픽만 통과할 수 있도록 설정이 되어있습니다.

예를 들면 private 서브넷에 인스턴스에 접속해서 직접 mysql download 요청을 했을 때만 허용이 됩니다.

외부에서 바로 private 인스턴스로 접근할 수는 없습니다.

NAT 인스턴스만 설정을 하면 바로 연결이 되나요?

public ip도 자동 할당을 해줘야 합니다

public ip 가 필요한 이유

NAT 인스턴스를 통해서 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들었는데, 왜 public ip 가 필요할까요?

외부 인터넷과 통신을 할 때 public ip 가 필요합니다.

NAT 인스턴스 혹은 NAT 게이트웨이가 인터넷과 통신할 때, NAT 인스턴스의 public ip + private ip를 통해서 통신을 하지 않습니다.

내부 인스턴스의 public ip 를 통해서 통신을 하게 되어있습니다.

따라서 NAT 인스턴스와 내부 인스턴스 모두 public ip 가 필요합니다.

이 과정을 통해서 1번 문제를 해결할 수 있었습니다.

이제 2번째 문제를 해결해 보도록 하겠습니다.

public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안 되는 문제

public 서브넷에 있는 서버가 private 서브넷에 있는 서버에 접속을 하려고 했는데, 접속이 안 되는 문제가 있었습니다.

해결 방법

해결 방법에는 2가지 과정이 있습니다.

public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해 주기

기본적으로 public 서브넷에 있는 인스턴스의 보안 그룹에는 private 서브넷에 있는 인스턴스의 보안 그룹이 추가되어있지 않습니다.

따라서 public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.

private ip를 통해서 접속하기

public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속할 때, public ip 를 통해서 접속을 하면 안 됩니다.

public ip를 통해서 접속하는 과정을 자세하게 알아보겠습니다.

  1. public 서브넷에 있는 인스턴스가 public ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
  2. 라우팅 테이블에서 public ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
  3. 라우터를 통해서 외부 인터넷으로 나가게 됩니다.
  4. 트래픽이 NAT 인스턴스에 도착합니다.
  5. NAT 인스턴스는 내부에서 출발한 트래픽이 아니기 때문에, 트래픽을 거부합니다.

이 과정이 일어나기에, public ip 를 통해서 접속을 하면 안 됩니다.

private ip를 통해서 접근하면 어떻게 되는지 알아보겠습니다

  1. public 서브넷에 있는 인스턴스가 private ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
  2. 라우팅 테이블에서 private ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
  3. 라우터를 거쳐서 private 서브넷의 라우터로 이동합니다.
  4. private 서브넷의 라우터는 private 서브넷에 있는 인스턴스에게 트래픽을 전달합니다.
  5. private 서브넷에 있는 인스턴스는 트래픽을 받아서 처리합니다.

이 과정을 통해서 2번 문제를 해결할 수 있었습니다.

요약

  1. private 서브넷에 있는 인스턴스가 인터넷에 접속을 하려면 NAT 인스턴스 혹은 NAT 게이트웨이가 필요합니다.
  2. private 서브넷에 있는 인스턴스도 public ip 가 필요합니다.
  3. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 하려면 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.
  4. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 할 때, private ip 를 통해서 접속을 해야 합니다.
- + \ No newline at end of file diff --git a/19.html b/19.html index 04734f1..906853e 100644 --- a/19.html +++ b/19.html @@ -5,7 +5,7 @@ OAuth 2.0의 흐름과 설정 해보기 | CAR-FFEINE - + @@ -24,7 +24,7 @@ Map<String, Object>로 지정해준 이유는, 플랫폼마다 반환되는 JSON 타입이 다르기 때문에 그런 부분에 대해 중복을 제거하기 위해 이러한 형태로 만들었습니다.

그리고 아까 yml에 작성했던 정보들을 가져와야합니다. @Value 어노테이션으로도 가져올 수 있습니다.

        @Value("oauth.provider.google.id")
private String id;
@Value("oauth.provider.google.secret")
private String secret;

...

하지만 이렇게 계속 binding을 해줘야한다는 점이 아주 귀찮고 보기도 안좋습니다.

build.gradle
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"

하지만 위의 의존성을 추가해준다면 아주 편하게 property를 가져올 수 있습니다.

OAuthProviderProperties.java
@Component
@ConfigurationProperties(prefix = "oauth2")
public class OAuthProviderProperties {
// prefix oauth2 기준으로 알아서 google이 이름인 Provider Enum을 찾아서 Key로 바인딩
private final Map<Provider, OAuthProviderProperty> provider = new EnumMap<>(Provider.class);

public OAuthProviderProperty getProviderProperties(Provider provider) {
return this.provider.get(provider);
}

@Getter
@Setter
public static class OAuthProviderProperty {
// 그리고 provider 하위 정보들은 아래의 필드에 바인딩
private String id;
private String secret;
private String redirectUrl;
private String tokenUrl;
private String infoUrl;
}
}

이렇게 되면 구조적인 준비는 끝났습니다.

이제는 해당 플랫폼에 정보를 요청하는 작업만 하면 됩니다. 그럼 아까 말씀드렸던 순서로 요청을 해보겠습니다.

RestTemplateOAuthRequester.java
public class RestTemplateOAuthRequester implements OAuthRequester {

@Override
public OAuthMember login(OAuthLoginRequest request) {
// frontend에서 받아온 로그인 platform
Provider provider = Provider.from(request.provider());
// 해당 Platform에 맞는 정보 찾음
OAuthProviderProperty property = oAuthProviderProperties.getProviderProperties(provider);
// frontend에서 받아온 code와 등록해놓은 property로 Access Token 요청
OAuthTokenResponse token = requestAccessToken(property, requet.getCode());
// 받아온 Token으로 해당 Resource Owner의 정보 요청
Map<String, Object> userAttributes = getUserAttributes(property, token);
return provider.getOAuthProvider(userAttributes);
}

private OAuthTokenResponse requestAccessToken(OAuthProviderProperty property, String code) {
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(property.getId(), property.getSecret());
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers);
URI tokenUri = getTokenUri(property, code);
return restTemplate.postForEntity(tokenUri, request, OAuthTokenResponse.class).getBody();
}

private URI getTokenUri(OAuthProviderProperty property, String code) {
return UriComponentsBuilder.fromUriString(property.getTokenUrl())
.queryParam(CODE, URLDecoder.decode(code, StandardCharsets.UTF_8))
.queryParam(GRANT_TYPE, AUTHORIZATION_CODE)
.queryParam(REDIRECT_URI, property.getRedirectUrl())
.build()
.toUri();
}

private Map<String, Object> getUserAttributes(OAuthProviderProperty property, OAuthTokenResponse tokenResponse) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(tokenResponse.accessToken());
headers.setContentType(MediaType.APPLICATION_JSON);
URI uri = URI.create(property.getInfoUrl());
RequestEntity<?> requestEntity = new RequestEntity<>(headers, HttpMethod.GET, uri);
ResponseEntity<Map<String, Object>> responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<>() {
});
return responseEntity.getBody();
}
}

이렇게만 한다면 그 어려워 보이던 OAuth 인증도 간단하게 해결할 수 있습니다. (물론 제 코드가 정답이 아닙니다)

Reference

https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-16

https://developers.google.com/identity/protocols/oauth2?hl=ko

- + \ No newline at end of file diff --git a/2.html b/2.html index d1e83f5..9506ba7 100644 --- a/2.html +++ b/2.html @@ -5,13 +5,13 @@ git branch 전략 작성해보기 | CAR-FFEINE - +
본문으로 건너뛰기

git branch 전략 작성해보기

· 약 11분
누누

현재 상황은 어떤데?

현재 우아한테크코스에서는 프론트 코드와 백엔드 코드가 같은 레포지토리를 사용하고 있습니다.

프론트와 백엔드가 같이 작업하기에, 의도치 않은 충돌이 자주 생길 수 있는 구조이기에, 이를 git branch 전략으로 충돌을 줄이고자 합니다

Git Branch 전략이란?

git을 사용해서 소프트웨어 개발을 관리하는 방법입니다.

여러 개발자가 동시에 작업하고 코드를 통합할 때 생기는 충돌을 효율적으로 조정하기 위한 방법입니다.

왜 git branch 전략이 중요한데?

아래에 있는 4가지를 제외하고도 훨씬 많은 장점이 있을 수 있습니다.

1. 동시 작업이 편하다

여러 사람이 독립적으로 작업하고, 커밋을 할 때, 자신의 브랜치에서 변경 사항을 커밋하게 됩니다.

브랜치가 병합될 때만 충돌을 해결하면 되니, 아무 규칙이 없는 것보다 충돌 시점이 명확해지기에 생산성을 높일 수 있습니다.

2. 목적이 명확한 브랜치

애플리케이션의 상태에 몇 가지가 있는데, 안정된 프로덕션, 테스트 환경, 기능 추가 환경... 등이 있습니다

여러 기능별 브랜치(안정된 버전의 코드만이 관리되는 브랜치, 테스트 환경을 위한 브랜치, 기능 추가를 위한 브랜치)를

네이밍을 통해 구분하면 각각의 브랜치에 대해서 추가적인 설명을 할 필요 없이 명확하게 관리할 수 있습니다.

3. 배포 파이프라인 관리가 편함

브랜치가 네이밍으로 명확하게 구분이 되어있다면, 조건을 설정하기 쉽습니다.

특정 타입의 브랜치에 push 되었을 때, pull request를 만들었을 때 같은 조건에 따른 스크립트를 만들어둔다면 CI/CD를 구축하기 쉽습니다.

4. 버전 관리가 편리하다

서버에 문제가 생겼을 때, 어떤 브랜치로 돌아가서 롤백을 해야 하는지에 대한 것들이 명확합니다.

안정된 브랜치가 어떤 것인지 명확하기에, 롤백 과정에 대한 의사결정을 줄일 수 있습니다.

그러면 어떤 종류가 있는지 더 자세하게 알아보도록 하겠습니다.

Git Branch 전략의 종류는?

총 3가지의 전략이 있습니다.

1. Github Flow

2. Gitlab Flow

3. Git Flow

git을 사용하기에, Git Flow라는 네이밍이 가장 직관적이고 유명한데요. 

3가지 전략 중에서 가장 복잡하기에, 쉬운 순서대로 진행해 보도록 하겠습니다.

1. Github Flow

그림으로 flow 간단하게 보고 가도록 하겠습니다.

img

img2

브랜치는 총 2가지 종류가 존재합니다

1. master 브랜치

여기에 머지가 되면 배포가 되도록 CD를 연결해 놓은 경우가 많습니다.

안정된 버전의 코드가 관리되는 브랜치입니다.

2. feature 브랜치

기능 추가, 버그 수정 등 모든 작업은 feature 브랜치에서 일어납니다.

master 브랜치에서 새로운 브랜치를 만들어서, 마스터로 머지되는 단순한 사이클을 가지고 있습니다.

장점

위에서 볼 수 있는 것처럼 2종류의 브랜치만 있기에, 정말 간단합니다.

학습 과정까지의 러닝 커브가 거의 없다시피 하기에, 간단한 프로젝트에 적용하기 정말 좋습니다.

릴리즈 되지 않은 코드가 최소화됩니다. 최신 버전의 코드와 최대한 빠르게 동기화를 계속해서 시킬 수 있습니다

단점

모든 코드는 다 master 브랜치에 머지가 되어야 한다는 점이 개발 서버와, 운영서버를 나누기 애매할 수 있습니다.

개발 서버에 배포를 하고 싶은 상황이라면, master에 머지가 되어야 합니다.

머지가 된 이후에 cd 파이프라인을 통해서 개발 서버와 운영 서버 모두에 배포가 됩니다.

여러 환경을 나누고 관리를 하고 싶으시다면 다음에 소개해드릴 전략을 사용해 보셔도 좋을 것 같습니다

2. Gitlab Flow

그림으로 flow 간단하게 보고 가도록 하겠습니다.

img2

밑에 환경은 총 2개의 서버가 존재할 때를 가정하고 있습니다.

1. pre-production 서버

2. production 서버

편의를 위해 main에 머지되는 과정은 간단하게 표현했습니다.

img3

브랜치 종류

총 3가지 브랜치가 필요하고, 추가에 따라서 더 추가할 수 있습니다.

1. main(or develop) 브랜치

기능에 대한 개발이 완료되었지만, 여기에 머지되어도 바로 배포되지는 않습니다.

2. feature브랜치

기능을 개발하는 브랜치입니다. Github Flow 와도 유사합니다.

3. production 브랜치

실제 배포가 일어나는 브랜치입니다. 

여기에 머지가 되는 순간 배포가 일어납니다.

위 사진에 있는 것처럼, 필요에 따라서 pre-production이나, staging 같은 환경에 따른 브랜치를 추가할 수 있습니다.

특징

1. 무조건 단방향으로 머지가 일어납니다.

긴급하게 라이브 서버에 수정을 해야 할 때, production 부터 시작하는 것이 아닌, main 부터 차근차근 올라가야 합니다

2. 환경에 따라 브랜치 종류가 늘어날 수 있습니다.

위 사진에서는 pre-production 이 그 예시가 되겠네요.

장점

1. Github Flow에서 환경별 브랜치를 통해서 개발 서버나 pre-production 서버에 버전을 깔끔하게 관리할 수 있습니다.

3. Git Flow

브랜치 전략 중 가장 처음으로 유명해진 브랜치 전략입니다.

배포가 특정 주기를 가지고 있는 애플리케이션일 때, 가장 적합합니다.

가장 복잡한 전략을 가지고 있어서, 모두가 브랜치 전략에 대해서 이해하고 있다면 역할에 따른 깔끔한 분리가 가능합니다

그림으로 보고 가도록 하겠습니다

img4

가장 유명한 브랜치 전략이지만, 가장 어려운 전략이기도 합니다.

특징

1. 브랜치에 대해서 양방향으로 머지가 일어납니다

release 브랜치에서 버그 수정이 일어나면, develop 브랜치에도 머지해줘야 합니다.

hotfix 브랜치를 main 브랜치뿐만 아니라, develop 브랜치에도 머지해줘야 합니다

브랜치의 종류가 5가지나 됩니다

1. main

production 이 배포되었을 때, 이 브랜치에 머지되는 것이 기준이 됩니다.

2. develop 

위에서 설명드렸던 브랜치들과 큰 차이가 없이 배포 전 브랜치입니다.

3. feature

기능을 개발할 때 사용하는 브랜치입니다. 이것도 위와 큰 차이가 없습니다

4. release

Gitlab Flow에서 pre-production에 해당한다고 봐도 무방합니다.

여기서 버그 수정이 일어났을 경우에,  develop에 머지하는 것을 까먹으면 안 됩니다.

5. hotfix

main 브랜치에서 생성된 브랜치로, 긴급한 변경사항을 처리합니다.

이때, develop에 머지하는 것을 깜빡하면 안 됩니다.

더 자세하게 알아보실 분은 아래 링크들을 확인해 보세요

우리 프로젝트에는 어떤 것이 적절할까?

나중에 개발 서버 혹은 스테이징 서버를 두고 싶기에, 이 부분에 대한 처리가 부족한 Github Flow는 적절하지 않습니다.

Git Flow는 깔끔하게 처리할 수 있지만, 러닝 커브가 Gitlab Flow 보다 약간 더 있어서, 빠르게 개발하는 취지에 맞지 않아 보였습니다.

이런 과정을 통해서 Gitlab Flow를 사용하려고 합니다 

참고

https://techblog.woowahan.com/2553/

https://docs.gitlab.com/ee/topics/gitlab_flow.html

- + \ No newline at end of file diff --git a/20.html b/20.html index 93a3591..7b1af3a 100644 --- a/20.html +++ b/20.html @@ -5,13 +5,13 @@ 카페인 팀이 styled-components를 선택한 이유 | CAR-FFEINE - +
본문으로 건너뛰기

카페인 팀이 styled-components를 선택한 이유

· 약 2분
야미

왜 styled-components인가?


여러 CSS-in-JS 중 styled-components를 선택한 이유는 다음과 같다.

  1. 컴포넌트 안에 관련 CSS를 작성할 수 있어 컴포넌트별 디자인 코드 확인 및 수정이 용이하다.

  2. 혹자는 코드 가독성이 안 좋아진다고도 하지만, 개인적으로는 태그를 더 시맨틱 하게 작성할 수 있어서 좋다고 느꼈다.

  3. 팀원들 모두 styled-components가 익숙하다.

  4. 지금까지 사용하면서 불편한 점을 못 느꼈다.


styled-components와 emotion은 기능도, 작성법도 상당히 유사하다.

그래서 이번에는 styled-components 대신 emotion을 써볼까도 생각했었다.

하지만 emotion에서만 사용 가능하던 *CSS Props라는 편리한 기능을

styled-components(v5.2.0 이상)에서 쓸 수 있게 되기도 했고,

'새로운 기술 공부를 해보면 좋을 것 같다'는 이유를 제외하고는

딱히 emotion을 사용할 필요성을 못 느껴 styled-components를 채택했다.

// *CSS Props 예시

const buttonStyle = css`
font-size: 18px;
color: white;
background: black;
`;

const ClickButton = styled.button<{ css: CSSProp }>`
width: 100px;

${({ css }) => css}
`;

<ClickButton css={buttonStyle}>Click me!</ClickButton>;
- + \ No newline at end of file diff --git a/21.html b/21.html index 49be5cc..9fb6775 100644 --- a/21.html +++ b/21.html @@ -5,13 +5,13 @@ 카페인 팀의 상태관리 전략 (왜 Tanstack Query여야 하는가?) | CAR-FFEINE - +
본문으로 건너뛰기

카페인 팀의 상태관리 전략 (왜 Tanstack Query여야 하는가?)

· 약 9분
가브리엘

안녕하세요? 카페인 팀 FE에서 상태관리 라이브러리를 어떻게 해야할 지 고민 끝에 서드파티 라이브러리가 필요하게 되어 글을 작성하게됐습니다.

서버 상태와 클라이언트 상태의 구분

서버상태와 UI상태를 이해하는 것은 굉장히 중요했습니다. 데이터를 송수신하는 작업과 상태를 관리하는 작업은 유기적으로 동작해야했습니다. 기존에는 상태와 데이터 송수신 과정을 분리해서 생각했다면, 현대의 react 프로젝트들은 서버와 동기화를 해야할 상태그렇지 않은 상태로 분리해서 생각해야 합니다.

React에서 어떤 데이터를 상태로 다뤄야 하는가에 대해서는 여러 의견이 나올 수 있다고 생각하지만 상태가 특성을 가지고 있는가에 대해서는 대부분 특성이 있다고 동의할 것입니다. 이 글에서는 React의 상태란 무엇인가?에 대해서 다루지 않고 React의 상태의 특성에 대해서만 언급을 하려고 합니다.

상태의 특성으로는 크게 두 가지가 있습니다.

클라이언트 상태

클라이언트 상태는 컴포넌트들 간에 어떤 값을 공유해야하면서 오로지 React DOM 내부에서만 CRUD가 일어나는 상태를 의미합니다. 이 상태들은 React DOM 외부 세계와 크게 관련이 없으며 동기적으로 반영됩니다. 대표적으로는 UI를 조작하는 상태들이 될 것입니다. 클라이언트 상태들은 대부분 장기적으로 유지될 필요가 없기에 화면을 벗어나거나 세션이 끊기는 경우 사라져도 괜찮은 경우가 많습니다.

서버 상태

서버 상태는 React의 바깥 세상(서버)에 존재하는 데이터가 React의 상태 관리와 비동기적으로 동기화 된 것을 의미합니다. 어떤 상태가 외부에서 관리되는 데이터와 반드시 연동되어야 한다면 이는 곧 서버 상태임을 의미합니다. React의 상태를 CRUD 하는 것 뿐만 아닌, 서버에서도 항상 같은 일이 일어나야 합니다. 서버 상태는 장기적으로 유지되어야 하며, 세션에서 벗어나더라도 서버로 부터 복구를 해야 합니다.

기존의 상태 관리 라이브러리들은 리액트의 전역에서 상태를 조작하는 것에 특화되어있고, 비동기적인 상태 관리도 지원하여 서버와의 통신이 가능합니다. 하지만 대부분의 라이브러리들은 클라이언트 상태를 조작하는 것에 초점이 맞춰져있습니다.

더군다나 클라이언트 상태와 서버 상태가 하는 일이 명확하게 다른 상황에서 이 둘을 한 곳에서 관리하는 것 보다는 완벽하게 분리하는 것이 더 나을 것입니다. 따라서 서버 상태를 관리하는 것에 중점을 둔 라이브러리들이 등장하였습니다. 대표적인 라이브러리로는 RTK Query, Tanstack Query, SWR 등이 있습니다.

왜 Tanstack Query였나?

vs RTK Query

RTK Query는 RTK를 반드시 사용해야 하는 것은 아니지만 RTK를 타겟으로 나온 서버 상태 관리 라이브러리입니다. 카페인 팀에서는 클라이언트 상태를 관리하기 위해 라이브러리를 사용하지 않습니다. 더욱이 Redux의 복잡한 코드 구성과 방대한 보일러 플레이트는 매력적이지 않았습니다. tanstack query에서는 무한 데이터 페칭을 지원하기 위해 Infinite Queries가 있지만 RTK Query는 그렇지 않았습니다.

vs SWR

SWR도 하나의 좋은 선택지였지만, 전역 상태 관리 라이브러리들이 범용적으로 지원하는 셀렉터 기능을 지원하지 않았습니다. 또, 가비지 컬렉터의 부재도 아쉬웠습니다. 재요청을 하기 위한 stale time 설정이나 쿼리 취소 기능이 없는 점도 매력적이지 않았습니다.

카페인 팀에서 하려는 일은요…

저희 카페인 팀의 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 로 지도 기반의 프로젝트입니다. 서버 상태를 적극적으로 다뤄야 하는 상황에서 Tanstack Query를 서버 상태 관리 라이브러리로 선정하게 됐습니다.

메인 기능 중 Tanstack Query가 핵심으로 사용될 것 같은 기능은 다음과 같습니다.

  • 지도에서 충전소 조회
    • 현재 접속한 클라이언트에 렌더링 된 지도 화면(디스플레이)의 크기에 따른 GPS좌표를 알아내어 서버로 부터 충전소 정보를 수신 받습니다. 즉, 화면이 이동하게 되면 사용자가 바라보고 있는 영역이 변하므로 새로운 요청을 보내게 됩니다.
    • 서버에서 수신한 충전소 정보는 실시간 사용 현황도 반영되어있으므로 주기적인 업데이트도 필요합니다.
    • 빈번한 데이터의 변화가 필요하며 그만큼 통신 실패 등 에러가 발생할 가능성도 많아지게 됩니다.
    • 사용자의 빠른 지도 이동이 발생하는 경우를 대응할 수 있어야 합니다.
  • 전국 충전소 검색기
    • 원하는 충전소 검색을 하는 기능을 지원합니다. 전국 단위로 검색 결과를 수신하는 기능입니다.
    • 네이버와 구글 검색창 처럼 사용자가 input 창에 검색어를 입력할 때 마다 검색 결과가 동적으로 표시되어야 합니다.
    • 빈번한 데이터의 변화가 필요하고, 사용자의 빠른 타이핑으로 인해 잦은 검색이 발생하는 경우를 대응할 수 있어야 합니다.
    • 이를 위해 데이터를 캐싱할 필요도 있다고 생각합니다.

프로젝트에서 클라이언트와 서버와의 통신이 어쩌다 한번 일어난다면 굳이 라이브러리가 필요가 없겠지만, 서버의 데이터 전적으로 의존해야 하는 저희 프로젝트 특성상 Tanstack Query의 여러 기능이 생산성에 많은 도움이 될 것으로 기대합니다.

- + \ No newline at end of file diff --git a/22.html b/22.html index 1e69d81..37535ab 100644 --- a/22.html +++ b/22.html @@ -5,7 +5,7 @@ 필터링 기능 구현과 인덱스 이용한 조회 속도 개선하기 | CAR-FFEINE - + @@ -18,7 +18,7 @@ 평균적으로 0.63초가 나왔습니다. 약 25 ~ 30%의 조회 속도가 개선되었습니다.

아직 이 부분은 개선이 더 필요해보입니다.

그래도 개선이 됐고, 삽입과 갱신에는 큰 지장이 없어서 일단 이정도로 마무리 하고, 추후에 개선을 해보도록 하겠습니다.

이미지 추가적으로 충전기 조회는 굉장히 빨라졌습니다!

배우는 단계이다보니 미숙하고 틀린 부분이 있을 수 있습니다.

긴 글 읽어주셔서 감사합니다 :)

- + \ No newline at end of file diff --git a/23.html b/23.html index 60dade8..c833366 100644 --- a/23.html +++ b/23.html @@ -5,7 +5,7 @@ Deadlock trouble shooting | CAR-FFEINE - + @@ -26,7 +26,7 @@ 트랜잭션을 오래 가지고 있으면 Lock을 가지고 있는 시간이 오래걸립니다. 그래서 트랜잭션을 작게 분리할 수 있습니다. 페이징을 통해 트랜잭션을 작게 분리하다보면 쿼리가 여러번 나가 성능상 문제가 생길 수 있을 것 같습니다.
  • INSERT ~~ ON DUPLICATE KEY UPDATE ~~ 사용하지 않기 해당 sql이 아닌 INSERT IGNORE을 사용하여 추가된 정보만 넣고, update는 다른 작업으로 분리하기
  • 이런 방법들을 사용하면 될 것 같았습니다. 그 중 저는 현재는 간단하게 2번째 방법이 제일 나을 것 같다는 생각에 쿼리를 수정했습니다.

    그리고 문제를 해결했습니다. 해당 문제가 발생하게 되어 좀 더 재밌는 것들을 고민하고 공부할 수 있는 저희 팀에게 감사하고 모르는 키워드를 많이 알려준 누누에게 감사합니다.

    아직 배우는 단계라 정확한 정보가 아닐 수 있습니다. 부족한 부분에 대해 많은 지적 부탁드립니다.

    - + \ No newline at end of file diff --git a/24.html b/24.html index b14878f..758933a 100644 --- a/24.html +++ b/24.html @@ -5,7 +5,7 @@ Out of memory trouble shooting | CAR-FFEINE - + @@ -31,7 +31,7 @@ 하지만 직접 확인해보기 전까지는 확신할 수 없으니 간단히 Runtime 클래스에서 제공해주는 totalMemory(), freeMemory() 메서드를 통해 알아보겠습니다.

        @Test
    void 페이징을_사용한_조회() {
    List<Station> stations = stationRepository.findAllByOrder(Pageable.ofSize(1000));

    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();
    System.out.println("paging 사용 중인 메모리: " + ((total - free) / 1024 / 1024) + "MB");
    }

    @Test
    void 페이징을_사용하지_않고_조회() {
    List<Station> stations = stationRepository.findAllFetch();

    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();

    System.out.println("findAll() 사용 중인 메모리: " + ((total - free) / 1024 / 1024) + "MB");
    }

    findAll paging 확연히 차이가 나는 것을 확인할 수 있습니다.

    물론 테스트코드에서는 23만건의 API 요청은 같은 조건이니 배제하고 확인했습니다.

    이로써 하나의 문제가 또 해결된 것 같습니다.

    아직 배우는 단계라 혹시 틀린 점이 있다면 지적 부탁드리겠습니다.

    Reference

    - + \ No newline at end of file diff --git a/25.html b/25.html index 260f004..260a718 100644 --- a/25.html +++ b/25.html @@ -5,7 +5,7 @@ flyway를 이제서야 적용하는 이유 | CAR-FFEINE - + @@ -21,7 +21,7 @@ 거기에 file을 만듭니다. 파일 이름이 중요한데요 V1__init.sql 이러한 방식으로 V{version 숫자}__{어떠한 파일인지에 대한 이름}.sql 언더스코어 2개는 필수로 작성해야합니다.

    create table member(
    id bigint auto_increment primary key,
    name varchar(255) null,
    );

    이렇게 V1__init.sql에 대한 파일을 작성했습니다. 이제는 email을 추가한다는 요구사항을 반영해보겠습니다.

    ALTER TABLE member
    ADD COLUMN email varchar(255);

    이렇게 새로운 파일을 만들어서 해당 스크립트를 작성했습니다. 파일명이 중요한데요, 이전 파일의 숫자보다 +1 이 되는 숫자를 V 뒤에 붙입니다.

    따라서 이번 파일은 V2__add_column_email.sql 이라고 만들었습니다.

    그럼 이제 또 시간이 지나 회원이 많아졌습니다. 하지만 email이 없는 사용자도 많습니다. 이 상황에서 email을 not null로 변경해야한다는 요구사항이 생겼습니다.

    그러면 아래와 같이 반영할 수 있습니다.

    ALTER TABLE member
    MODIFY email VARCHAR(20) NOT NULL default 'default'

    이렇게 V3__add_constraints.sql 파일을 만들었습니다. 그러면 null이 있던 row들은 email이 default가 되고 not null 제약조건이 활성화 된 것을 볼 수 있습니다.

    그러면 주어진 요구사항은 모두 만족할 수 있습니다. 거기에다 v1, v2, v3 가 나뉘어져있어서 어느 커밋부터 해당 sql이 추가되었는지도 확인할 수 있습니다.

    그리고 ddl-auto update를 사용하면 반영되지 않았던 제약조건의 추가도 확인할 수 있습니다. 그러면 ddl-auto의 속성을 validate로 변경하여, db schema와 entity의 필드가 다르면 어플리케이션이 실행되지 않도록 해서 좀 더 안전한 개발을 할 수 있습니다.

    결론

    flyway는 roll back을 하는 것이 유료라서, production 서버에서 혹은 롤백을 해야하는 일이 있는 서버에서는 사용하는 것이 좋지 않지만, 이와 같이 데이터를 drop 할 수 없는 상황이라면, 사용하지 않을 이유가 없어보이는 좋은 도구입니다.

    짧은 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git a/26.html b/26.html index 7396516..c23867f 100644 --- a/26.html +++ b/26.html @@ -5,7 +5,7 @@ 카페인 팀 클라이언트의 테스트 자동화 | CAR-FFEINE - + @@ -22,7 +22,7 @@ 하지만 Storybook을 이용하면 특정 컴포넌트를 Storybook 위에 올려놓고 테스트를 할 수 있어 빠르게 작업이 가능합니다. 인터렉션이나 웹접근성을 확인해주는 플러그인도 존재하여 프론트엔드 개발에서 굉장히 중요한 역할로 부상했습니다.

    저희 팀은 이외에 Cypress를 사용하는 것도 고려하였으나, 지도와 결합된 애플리케이션을 테스트하기에 다소 어려움이 있어 위 라이브러리들을 개발에 활용했습니다.

    저희는 위 테스팅 라이브러리들을 원활히 활용하기 위해 테스트 자동화를 구축했습니다.

    Jest와 React Testing Library 테스트 자동화

    name: frontend-test

    on:
    pull_request:
    branches:
    - main
    - develop
    paths:
    - frontend/**
    - .github/**

    permissions:
    contents: read

    jobs:
    test:
    name: test-when-pull-request
    runs-on: ubuntu-latest
    environment: test
    defaults:
    run:
    working-directory: ./frontend
    steps:
    - name: Checkout PR
    uses: actions/checkout@v2
    - name: Install dependencies
    run: npm install
    - name: Test
    run: npm run test

    이벤트 트리거 설정

    pull_request 이벤트가 발생하였을 때, 해당 이벤트가 main 브랜치와 develop 브랜치에서만 동작합니다.

    변경 사항 경로 제한

    테스트를 실행할 때는 frontend 디렉토리와 .github 디렉토리 내의 파일들을 고려하도록 했습니다. 백엔드와의 환경 분리를 위해 이러한 접근 제한을 했습니다.

    권한 설정

    permissions은 읽기 권한만 설정되어 있어 코드나 파일을 변경을 방지합니다.

    작업(Job) 설정

    test라는 이름의 작업을 정의하였고, 이 작업에서는 Ubuntu 환경에서 테스트를 실행합니다. test라는 이름의 환경 변수를 사용합니다. 테스트는 (카페인 팀 레포지토리의) frontend 디렉토리에서 작업하도록 하였습니다.

    스텝(Step) 설정

    코드를 체크아웃하고, 의존성을 설치하며, 테스트를 실행하는 세 가지 단계로 구성되어 있습니다.

    이러한 설정을 통해 PR에 코드가 올라올 때 자동으로 프론트엔드 테스트가 실행됩니다.

    이러한 테스트 자동화 전략은 프론트엔드 애플리케이션을 안정적이게 개발하고 유지할 수 있도록 도와줍니다.

    Storybook의 빌드 자동화

    name: storybook-deploy

    on:
    pull_request:
    branches:
    - develop
    paths:
    - frontend/**
    - .github/**

    jobs:
    build:
    runs-on: ubuntu-22.04
    defaults:
    run:
    working-directory: ./frontend
    steps:
    - name: Setup Repository
    uses: actions/checkout@v3

    - name: Set up Node
    uses: actions/setup-node@v3
    with:
    node-version: 18.16.0

    - name: Install dependencies
    run: npm install

    - name: Cache node_modules
    id: cache
    uses: actions/cache@v3
    with:
    path: '**/node_modules'
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
    ${{ runner.os }}-node-

    - name: storybook build
    run: npm run build-storybook

    - name: Upload storybook build files to temp artifact
    uses: actions/upload-artifact@v3
    with:
    name: Storybook
    path: frontend/storybook-static
    deploy:
    needs: build
    runs-on: self-hosted
    steps:
    - name: Remove previous version app
    working-directory: .
    run: rm -rf dist

    - name: Download the built file to AWS
    uses: actions/download-artifact@v3
    with:
    name: Storybook
    path: frontend/dev/dist

    - name: Move folder
    working-directory: frontend/dev/
    run: |
    rm -rf /home/ubuntu/dist/*
    cp -r ./dist /home/ubuntu

    - name: comment PR
    uses: thollander/actions-comment-pull-request@v1
    env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    with:
    message: '🚀storybook: https://storybook.carffe.in/'

    비슷한 코드이지만, 매번 PR이 열릴 때 마다 스토리북이 자동으로 빌드 및 배포됩니다. 배포가 완료되면 배포된 URL을 알려 코드 리뷰할 때 참고할 수 있도록 돕습니다.

    이상 카페인 팀에서 사용하고 있는 테스팅 라이브러리와 테스트 자동화 방법을 알아봤습니다.

    - + \ No newline at end of file diff --git a/27.html b/27.html index ec61a38..e58f73a 100644 --- a/27.html +++ b/27.html @@ -5,7 +5,7 @@ EC2 서버 추가와 동시에 Dev, Prod 환경 분리하기 | CAR-FFEINE - + @@ -17,7 +17,7 @@ 기존 dev 서버는 총 4개의 서버를 배포하고 있었고 배포하는 서버는 다음과 같습니다. prod-BE, prod-FE, dev-BE, dev-FE

    그리고, 기존 dev 서버에서는 환경을 분리해주기 위해서 Nginx를 통해서 포트 포워딩은 다음과 같이 해주었습니다.

    카페인 팀에서는 dev, prod 환경이 분리되지 않아서 인스턴스의 사용량이 높았고, 이에 따라 추가적인 EC2 인스턴스가 필요했습니다.


    문제 해결

    다행히도 카페인 팀에서 추가적인 EC2 인스턴스를 받았고, 저희는 배포 환경을 분리할 수 있었습니다.

    dev-prod-server

    이와 같이 기존 dev 서버 한 개가 infra 서버와 연결되어 있었는데, 두 갈래로 나뉜 것을 확인하실 수 있습니다.

    먼저 배포는 다음과 같이 진행됩니다.

    release branch에 push가 일어나면 dev서버에 배포 작업이 이뤄집니다. prod branch에 push가 일어나면 prod서버에 배포 작업이 이뤄집니다.

    또한 기존 dev 서버에서 4개의 포트포워딩 또한 굳이 그럴 필요가 없어졌습니다. 새로운 서버가 추가됨에 따라 dev, prod 서버 각각 Nginx에서 포트포워딩을 동일하게 FE:3000, BE:8080 으로 변경하였습니다.

    이렇게 카페인 팀에서는 dev, prod 환경을 분리했습니다.

    감사합니다!

    - + \ No newline at end of file diff --git a/28.html b/28.html index ea8e256..bda80c8 100644 --- a/28.html +++ b/28.html @@ -5,13 +5,13 @@ 카페인 팀에서 사용한 지도 시스템에 관하여 | CAR-FFEINE - +
    본문으로 건너뛰기

    카페인 팀에서 사용한 지도 시스템에 관하여

    · 약 18분
    가브리엘

    안녕하세요? 카페인 팀에서 사용한 지도 시스템에 대해서 소개하려고 합니다.

    지도 기능에서 가장 핵심인 기능 두 가지를 뽑자면, 지도 그 자체와 지도 위에 그려지는 마커를 뽑을 수 있을 것입니다. 지도 위에 마커를 그리는 일은 그다지 어렵지 않고, documents 에 있는 예제들을 잘 따라하면 누구나 충분히 구현할 수 있을 것입니다.

    no offset

    하지만 마커의 갯수가 과도하게 많다면 어떤 전략을 세울 수 있을까요?

    카페인 팀에서는요 ...

    카페인 서비스에서 지도는 굉장히 중요한 요소 중 하나였습니다. 사용자들이 궁금한 장소의 주변에 있는 충전소를 시각적으로 제공해주기 위해서는 지도를 잘 제어할 수 있어야 했습니다. 특히 전국에 이미 수만 대의 충전소가 보급이 된 상황에서 충전소 마커를 모두 그려주기 위해서는 많은 제약이 있었고, 마커를 적당한 수준으로 렌더링 하려면 클라이언트와 서버 간에 특별한 작업이 필요했습니다.

    어떤 전략을 펼쳤는지 소개하기에 앞서 미리 말씀드리지만, 저희 팀에서 취한 지도 관리 전략은 모든 프로젝트에 유효하지 않을 것입니다. 지도 위에 한번에 표현할 마커의 갯수가 수백 개 이하라면, 서버에 데이터가 과도하게 많은 것이 아니라면 오히려 이러한 전략이 사용자 경험을 해칠 수 있을 것입니다. (환경이 원활하다면 데이터를 가능한 많이 보여주는 것이 좋을테니깐요.)

    또, 이 글에서는 Google Maps API를 기준으로 설명하고 있지만, 지원하는 기능이 일부 다르더라도 대부분의 지도 API에서 사용이 가능한 전략일 것입니다. 참고로 개인적으로 사용 해본 여러 벤더 사의 지도 API들은 모두 이와 유사한 기능을 제공했습니다.

    좌표란 무엇일까?

    아마 어린 시절부터 우리나라에는 특별히 38선이라는 것이 존재한다는 사실을 교육받기에 좌표계라는 것이 있다는 사실은 누구나 알 것입니다. 하지만 당장 위도와 경도를 구분지으라고 하면 어떤 선이 위선이고 경선인지 헷갈리기에 찍어야 할 것입니다. 따라서 이 선이 어떤 선인지, 어떤 값을 얘기하려는 것인지 사진과 함께 간단히 설명하겠습니다.

    no offset

    사진을 보시면 아시겠지만 위도란, 남북의 위치를 나타내는 데 사용됩니다. 경도는 동서의 위치를 나타내는 데 사용됩니다. 대부분의 공식 문서가 영어로 작성되어있고, 코드에서도 이를 나타내는 것이 중요하기에 영문 표기법까지 소개를 하자면 위도는 Latitude, 경도는 Longitude로 표기합니다. 이유는 모르겠지만 제공되는 변수나 메서드 명으로 lat, lng라고 줄여서 표기하기도 합니다.

    no offset

    위도와 경도만 알면, 지구 위의 어떤 위치를 나타낼 수 있습니다.

    따라서, 어떤 마커를 어떤 위치에 찍을 것인지는 위도와 경도 값으로 결정할 수 있게 되겠죠?

    사용자가 어딜 보고 있을까?

    지도 api에서 제공해주는 메서드를 활용하면 사용자의 디바이스가 어느 위치를 보고 있는지 알 수 있습니다.

    let map = /* 어디선가 생성된 구글 맵 객체 */
    const center = map.getCenter();
    console.log(center.lng()); // 디바이스 중심의 longitude
    console.log(center.lat()); // 디바이스 중심의 latitude

    지도 객체로 부터 중심점을 알게되면 해당 디바이스의 중심의 좌표를 알아낼 수 있게 됩니다.

    no offset

    사용자의 디바이스는 얼마나 넓게 보고 있을까?

    지도 api에서 제공해주는 메서드를 활용하면 사용자의 디바이스가 어떤 영역을 보고 있는지도 알게 됩니다. 지도 api 마다 제공하는 스펙이 다르지만, 대부분은 어떤 식으로든 알려줍니다.

    google maps API에서는 디스플레이의 북동쪽 끝 점의 좌표와, 남서쪽 끝 점의 좌표를 제공해줍니다.

    const map = /* 어디선가 생성된 구글 맵 객체 */
    const bounds = map.getBounds();
    console.log(bounds.getNorthEast().lng(), bounds.getNorthEast().lat()); // 디바이스 1사분면 끝 점의 longitude와 latitude
    console.log(bounds.getSouthWest().lng(), bounds.getSouthWest().lat()); // 디바이스 3사분면 끝 점의 longitude와 latitude

    no offset

    편의상 좌표를 다음과 같이 정의해보겠습니다.

    • 중심 점 p0: (x0, y0)
    • 디바이스의 제 1사분면 끝점 p2: (x2, y2)
    • 디바이스의 제 3사분면 끝점 p1: (x1, y1)
    위 정의는 아래에서도 계속 설명 될 점과 좌표 입니다.

    이렇게 알아낸 값으로 사용자 디바이스의 영역을 알게 됐습니다.

    저희 카페인 팀에서는 이 값을 좀 더 효율적으로 다루기 위해 delta 개념을 도입했습니다.

    화면에서 보고 있는 영역을 확대/축소 하면 어떤 특징을 보일까?

    delta 설명을 앞서, 사용자의 디바이스 영역과 확대 수준에 따른 실제 좌표에 대해 알아보려고 합니다.

    사용자가 화면을 얼마나 넓게 보고 있는지를 쉽게 알기 위해서는 끝점들의 수치를 계산해줄 필요가 있었습니다.

    사진은 사용자가 디바이스를 통해 바라 보고 있는 중심 좌표와 그 끝 점을 의미합니다.

    no offset

    예를 들어 사용자가 지도를 많이 축소한 경우에는 중심 점 p0은 그대로지만 양 끝점 p1, p2의 위치가 점점 중심 점 p0으로 부터 멀어질 것입니다.

    반면에 사용자가 지도를 많이 확대한 경우에는 중심 점 p0은 그대로지만 양 끝점 p1, p2의 위치가 점점 중심점과 가까워질 것입니다.

    no offset

    양 사진 모두 중심 점 p0는 그대로지만, 디바이스의 확대 수준으로 인해 양 끝점인 p1과 p2가 달라진 모습을 보인 것입니다.

    즉, 이런 결론을 내릴 수 있습니다.

    1. 양 끝점 p1, p2가 중심 점 p0으로 부터 멀어질 수록 지도를 축소한 것이다.
    2. 양 끝점 p1, p2가 중심 점 p0으로 부터 가까워 수록 지도를 확대한 것이다.

    이 때 디바이스의 디스플레이가 위도 경도 상으로 얼마나 멀어져있는지를 수치화하면 편하게 다룰 수 있습니다.

    확대 수준을 수치화 할 수 없을까?

    사용자의 디스플레이의 중심 점 p0을 기준으로 하여 양 끝점 p1, p2이 얼마나 멀어져있는지에 따라 지도의 영역 뿐만 아니라 얼마나 많이 확대 되었는지 여부를 알게 됐습니다.

    그렇다면 이를 좀 더 효율적인 방법으로 나타내려면 어떤 전략을 취할 수 있을까요?

    사용자 디스플레이를 조금 더 자세히 살펴보겠습니다.

    no offset

    중학교 시절 배웠던 좌표 평면계를 떠올려보면 화면에서 얻을 수 있는 좌표들은 위와 같습니다. 여기에서 각 점의 수직/수평의 변화량인 delta를 알아보면 어떨까요?

    경도 델타 (longitudeDelta)

    p2와 p0의 경도 거리, 그리고 p1과 p0의 경도 거리는 같습니다.

    즉, x2 - x0 === x0 - x1 이라는 결론을 얻을 수 있습니다.

    이를 longitudeDelta로 정의하겠습니다.

    위도 델타 (latitudeDelta)

    p2와 p0의 위도 거리, 그리고 p1과 p0의 위도 거리는 같습니다.

    즉, y2 - y0 === y0 - y1 이라는 결론을 얻을 수 있습니다.

    이를 latitudeDelta로 정의하겠습니다.

    no offset

    코드로 알아보면 다음과 같습니다.

    const map = /* 어디선가 생성된 구글 맵 객체 */
    const bounds = map.getBounds();
    const longitudeDelta = (bounds.getNorthEast().lng() - bounds.getSouthWest().lng()) / 2; // 경도 변화량
    const latitudeDelta = (bounds.getNorthEast().lat() - bounds.getSouthWest().lat()) / 2; // 위도 변화량

    드디어 클라이언트에서 델타 값을 생성할 수 있게 되었습니다.

    그렇다면 왜 이렇게 굳이 델타 값을 생성한 것일까요?

    delta의 유용한 점 1: 원래 의도한 값을 복원하기 쉽다.

    서버의 입장에서는 중심 좌표와 델타 값만 알면 정확한 영역만큼 데이터를 호출할 수 있게 됩니다.

    예를 들어 클라이언트에서 서버로 다음과 같은 파라미터를 넘겨줬다고 가정해보겠습니다.

    {
    "longitude": 127,
    "latitude": 37,
    "longitudeDelta": 0.1,
    "longitudeDelta": 0.2,
    }

    그렇다면 서버에서는 다음과 같이 해석할 수 있게 됩니다.

    const maxLongitude = longitude + longitudeDelta;
    const minLongitude = longitude - longitudeDelta;
    const maxLatitude = latitude + latitudeDelta;
    const minLatitude = latitude - latitudeDelta;

    (javascript 기준으로 작성했습니다.)

    이렇게 알아낸 경계 값을 가지고 다음과 같은 sql문을 작성할 수 있게 될 것입니다.

    SELECT * FROM stations WHERE latitude >= :minLatitude AND latitude <= :maxLatitude AND longitude >= :minLongitude AND longitude <= :maxLongitude;

    no offset

    즉, 위 그림처럼, 원하는 영역만큼만 정확하게 데이터를 호출할 수 있게 됩니다.

    delta의 유용한 점 2: 델타가 무분별하게 커지는 것을 막기 쉽다.

    예를 들어 사용자가 지도를 축소하여 한반도를 디스플레이에 가득 채운다면 서버가 어떻게 될까요?

    이러한 행위를 막는 가장 쉬운 방법은 지도 api에서 지원하는 줌 레벨을 제한 하는 것입니다. 후술하겠지만 줌 레벨은 디스플레이의 해상도를 고려하지 못합니다.

    따라서 근본적으로 델타가 일정 값 이상 요청되지 못하도록, 혹은 연산되지 못하도록 막게 할 수 있습니다.

    물론 델타가 없더라도 델타 값을 추정하여 연산할 수 있겠지만, 이를 수치화 해서 관리한다면 클라이언트와 서버 모두 지도를 손쉽게 통제하는 것이 가능하게 됩니다.

    예를 들어 다음과 같이 델타 값을 고정하여 요청 영역을 제한할(요청을 보내지 않거나 고정된 사이즈로만 요청을 보낼) 수 있습니다.

    {
    longitude,
    latitude,
    longitudeDelta: longitudeDelta < 0.008 ? longitudeDelta : 0.008,
    latitudeDelta: latitudeDelta < 0.004 ? latitudeDelta : 0.004,
    }

    특정 수치를 넘기지 못하게 처리할 때 눈에 보이는 변수로 취급하기 쉽습니다. (즉, 매번 계산하지 않아도 됩니다.)

    디바이스 크기 관련 문제도 있습니다.

    분명히 같은 줌 레벨이지만, 디바이스의 크기나 해상도에 따라 지도가 보여지는 정도가 다릅니다.

    no offset

    위 사진은 구글에서 제공하는 zoom 레벨을 동일하게 맞춘 후, 여러 디바이스에서 호출한 것입니다.

    줌 레벨을 통해서 요청을 제한하다보면 여러 해상도를 제어하기 어렵습니다.

    no offset

    실제로 카페인 팀에서는 고해상도 모니터를 대응하기 위해 델타 값이 너무 크게 되면 요청의 제한을 하고 있습니다. 사진에서 보시다시피 고해상도 모니터의 경우, 너무 넓은 범위를 요청한다 싶으면 중심점으로 부터 일정 거리만 보여주도록 하고 있습니다.

    (참고로 줌 레벨에 따른 요청도 덤으로 제한하고 있어서 멀리서 호출하는 행위도 금지하고 있습니다.)

    delta의 유용한 점 3: 적당한 범위를 정해주기 편하다

    위 예제에서는 정확한 범위만큼 요청하는 것을 예제로 하지만, 프로젝트에 따라서 조금 더 넓은 영역을 호출하고 싶을 때가 있을 것입니다.

    no offset

    예를 들어 현재 사용자의 디바이스 크기보다 살짝 큰 범위의 데이터를 미리 로드해 놓으면 사용자가 좁은 움직임을 보일 때 불필요한 재 렌더링을 줄여서 더 빠른 렌더링이 가능하게 됩니다.

    사실 이 기법은 프로젝트마다 다르겠지만, 카페인 팀에서는 한번 불러온 마커를 매번 해제 하지 않고 이전 요청 데이터와 다음 요청 데이터를 비교하여 달라진 마커만을 정확하게 탈부착하는 작업을 진행하고 있습니다.

    이런 기법을 활용하면 사용자가 좁은 범위에서 움직임을 보였을 때, 기존에 불러온 마커를 메모리에서 탈락시키지 않으므로 사용자 경험을 개선할 수도 있을 것입니다.

    마커를 상태에 연동하여 정확하게 메모리에서 탈부착 시키는 전략에 대한 글은 이후에 작성할 예정입니다.

    긴 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git a/29.html b/29.html index cdeb838..7f565d0 100644 --- a/29.html +++ b/29.html @@ -5,7 +5,7 @@ useSyncExternalStore로 만들어보는 전역상태관리 도구 | CAR-FFEINE - + @@ -24,7 +24,7 @@ 빨간색은 개발자가 직접 건들지 못하지만 간접적으로 사용할 수 있는 영역 노란색은 React 18 엔진의 영역입니다.

    이외에 제공되는 다른 커스텀 훅들도 거의 비슷한 구조를 띄고 있습니다.

    // 추가로 구현할 수 있는 함수들

    export const useSetExternalState = <T>(store: DataObserver<T>) => {
    const { setState } = store;

    return setState;
    };

    export const useExternalValue = <T>(store: DataObserver<T>) => {
    const { subscribe, getState } = store;
    const state = useSyncExternalStore(subscribe, getState);

    return state;
    };

    // 바닐라JS 영역에서 자연스러운 읽기를 지원하는 함수

    export const getStoreSnapshot = <T>(store: DataObserver<T>) => {
    return store.getState();
    };

    더 다양한 예제는 여기에서 확인할 수 있고 작성한 라이브러리 코드 전문은 여기에서 확인할 수 있습니다.

    겨우 파일 수십 줄로 만든 초경량 상태관리 라이브러리였습니다

    - + \ No newline at end of file diff --git a/3.html b/3.html index bbe9b26..51484c9 100644 --- a/3.html +++ b/3.html @@ -5,13 +5,13 @@ Java 17 을 도입한 이유 | CAR-FFEINE - +
    본문으로 건너뛰기

    Java 17 을 도입한 이유

    · 약 6분
    누누

    우아한테크코스에서 자바 11을 사용하는 것이 너무 익숙해진 상황이어서, java 11 대신 java 17을 쓰려면 쓰는 대신, 왜 java 17을 쓰면 좋은지에 대해서 설득을 하는 시간이 있어야 하는데요

    처음에는 단순히 record 클래스가 좋아요, collect(Collectors.toList()); 대신 toList() 만으로 해결할 수 있어서 좋아요

    까지밖에 설명할 수 없었습니다.

    이것만으로 동의를 해줘서 일단 java 17 을 사용하기로 했지만, 이번 기회에 조금 더 자세하게 알아보려고 합니다

    Java 17 과 Java 11의 중요한 차이들

    기능적인 부분과, 숨겨진 부분을 나누어볼 수 있을 것 같습니다.

    기능적인 차이점

    언제나 직접 차이를 보면 더 직관적이기 때문에, 직접 코드를 보면서 설명을 해보려고 합니다

    record 클래스

    간단한 dto 클래스를 만들었을 때 코드가 정말 간단해지는 것을 확인할 수 있습니다

    Java 11

    public class Dto {
    private final int data;

    public Dto(int data) {
    this.data = data;
    }

    public int getData() {
    return data;
    }
    }

    lombok 을 사용했을 때


    @Getter
    @AllArgsConstructor
    public class Dto {
    private final int data;
    }

    Java17

    public record Record(int data) {
    }

    이렇게 보면 훨씬 간단해진 것을 볼 수 있습니다

    예상되는 문제점

    objectMapper를 사용하면 어떻게 되나요? noArgsConstructor 가 필요하지 않나요?

    class RecordTest {

    @Test
    void objectMapper_로_변환() throws JsonProcessingException {
    // given
    ObjectMapper objectMapper = new ObjectMapper();
    Record record = new Record(1);

    // when
    String json = objectMapper.writeValueAsString(record);

    // then
    assertEquals("{\"data\":1}", json);
    }

    @Test
    void string_에서_객체로_변환() throws JsonProcessingException {
    // given
    String json = "{\"data\":1}";
    ObjectMapper objectMapper = new ObjectMapper();

    // when
    Record record = objectMapper.readValue(json, Record.class);

    // then
    assertEquals(1, record.data());
    }
    }

    이 테스트에서 볼 수 있는 것처럼 성공적으로 deserialize, serialize 가 가능합니다

    toList() method

    Java 11

    이 부분도 정말 편의성이 높다고 생각하는 부분 중 하나인데요

    Collectors.toList() 대신 toList() 만으로도 사용이 가능합니다

    public class ToListWith11 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .collect(Collectors.toList());
    System.out.println(result);
    }
    }

    Java 17

    public class ToListWith17 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .toList();
    System.out.println(result);
    }
    }

    switch expression

    Java 11

    우테코에서는 switch, case 를 싫어하기에 볼 수는 없겠지만

    switch 문에도 정말 편하게 바뀌었는데요

    public class SwitchWith11 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = 0;
    switch (day) {
    case "Monday":
    result = 1;
    break;
    case "Tuesday":
    result = 2;
    break;
    case "Wednesday":
    result = 3;
    break;
    case "Thursday":
    result = 4;
    break;
    case "Friday":
    result = 5;
    break;
    case "Saturday":
    result = 6;
    break;
    case "Sunday":
    result = 7;
    break;
    }
    System.out.println(result);
    }
    }

    Java 17

    public class SwitchWith17 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = switch (day) {
    case "Monday" -> 1;
    case "Tuesday" -> 2;
    case "Wednesday" -> 3;
    case "Thursday" -> 4;
    case "Friday" -> 5;
    case "Saturday" -> 6;
    case "Sunday" -> 7;
    default -> 0;
    };
    System.out.println(result);
    }
    }

    코드 량이 엄청 줄어든 것을 확인하실 수 있습니다

    instanceof pattern matching

    물론 instanceof 를 사용할 경우가 많은가? 하면 많지는 않겠지만

    아래와 같이 변경되었습니다

    Java 11

    public class InstanceOfWith11 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.toUpperCase());
    }
    }
    }

    Java 17

    public class InstanceOfWith17 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String str) {
    System.out.println(str.toUpperCase());
    }
    }
    }

    number format

    이 기능은 12에 나왔는데요

    언어별로 숫자를 표현하는 방식이 다르지만, 쉽게 표현할 수 있도록 도와주는 기능입니다

    Java 17

    public class NumberFormatterWith11 {
    public static void main(String[] args) {
    int number = 1_000_000;

    String result = NumberFormat.getCompactNumberInstance(Locale.KOREA, NumberFormat.Style.LONG).format(number);

    System.out.println(result.equals("100만"));
    }
    }

    나머지 부분은 사실 그렇게 큰 역할을 할 것 같지는 않아서 생략하겠습니다

    숨겨진 부분들

    gc throughput

    위의 사진은 gc 의 버전별 처리량입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 15% 정도 향상되었고, java 11과는 10% 정도 향상되었습니다.

    gc latency

    위의 사진은 gc의 버전별 지연시간입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 30% 정도 향상되었고, java 11과는 25% 정도 향상되었습니다.

    이와 같이, 단순하게 새로운 기능만 추가되는 것이 아니라 꾸준히 성능도 향상되고 있습니다.

    이런 부분을 고려했을 때, Java 17을 사용하는 것이 좋을 것 같습니다.

    참고

    - + \ No newline at end of file diff --git a/30.html b/30.html index d1155e8..ea32109 100644 --- a/30.html +++ b/30.html @@ -5,7 +5,7 @@ 카페인 팀의 협업 일화 | CAR-FFEINE - + @@ -15,7 +15,7 @@ 예를 들면 충전소 회사 명에서 광주시라는 이름이 있었는데, 이 필터는 실제로 두 가지가 존재했습니다.

    하나는 경기도 광주, 하나는 전라도 광주였습니다.

    이런 부분에서 불필요한 지역의 필터까지 걸리게 되는 문제가 있었습니다. 협업하는 과정에서 이를 발견했고, 즉각 조치를 취했습니다.

    조치를 취할 때 서로에게 각자 편한 방법이 있었지만, 단순히 서로에게 편한 작업을 하지 않았고, 팀원과 상의하면서 추후 진행에 문제 없는 방향을 찾고 진행할 수 있었습니다.

    지금 생각해보면 만약 각자에게 편한 방식으로 문제를 수정했다면, 다른 팀원이 다른 작업을 할 때 지장이 갔을 수도 있고 불필요한 작업을 했을 수도 있었을 것 같습니다.

    이 시점을 계기로 저희 팀끼리 예상하지 못한 문제를 작업 중에 발견하더라도 다른 팀원에게 공유하고 서로 짧은 회의를 통해 문제 해결 방안을 같이 찾는 것이 자연스럽게 팀문화로 자리 잡게 되었습니다.

    - + \ No newline at end of file diff --git a/31.html b/31.html index 6271ffa..e8fe9f1 100644 --- a/31.html +++ b/31.html @@ -5,7 +5,7 @@ 조회 성능 개선하기 | CAR-FFEINE - + @@ -14,7 +14,7 @@ cpu

    조회 성능 개선하기

    먼저 제가 개선하기 위해 사용했던 방법들에 대해 적어보겠습니다.

    DTO 이용하기

    현재 구조는 아래의 JPA를 이용해 아래와 같은 쿼리로 entity로 데이터를 조회합니다.

     select distinct station.station_id,
    charger.charger_id,
    charger.station_id,
    chargerStatus.charger_id,
    chargerStatus.station_id,
    station.created_at,
    station.updated_at,
    station.address,
    station.company_name,
    station.contact,
    station.detail_location,
    station.is_parking_free,
    station.is_private,
    station.latitude,
    station.longitude,
    station.operating_time,
    station.private_reason,
    station.station_name,
    station.station_state,
    charger.created_at,
    charger.updated_at,
    charger.capacity,
    charger.method,
    charger.price,
    charger.type,
    charger.station_id,
    charger.charger_id,
    chargerStatus.created_at,
    chargerStatus.updated_at,
    chargerStatus.charger_condition,
    chargerStatus.latest_update_time
    from charge_station station
    inner join
    charger charger on station.station_id = charger.station_id
    inner join
    charger_status chargerStatus on charger.charger_id = chargerStatus.charger_id
    and charger.station_id = chargerStatus.station_id
    where station.latitude >= 37.5019194727953082567
    and station.latitude <= 37.5092305272047217433
    and station.longitude >= 127.044542269049714936
    and station.longitude <= 127.058071330950285064

    JPA를 통해 이러한 방식으로 조회한다면 아주 편하게 값을 가져오고, fetch join을 통해 하위의 entity들의 정보도 깔끔하게 가져옵니다.

    가져온 값으로 필요한 정보들을 매핑하고 가공하여 응답을 내려줬습니다.

    하지만 조회만을 위해 JPA의 entity를 조회한다는 것은 여러 단점이 존재합니다.

    제일 먼저 응답을 내려줄 때 불필요한 데이터까지 모두 조회를 한다는 부분입니다. 이렇게 많은 필드들이 있습니다. 하지만 응답에서는 대부분의 경우 모든 정보가 필요하지 않습니다. 그리고 모든 정보를 다 보내주는 것도 좋지 않습니다. 대량의 데이터를 조회할 때의 성능이 아주 나빠집니다.

    그래서 필요한 칼럼만 조회하는 것이 좋습니다.

    그리고 또 다른 단점으로는 JPA로 entity를 조회할 때 Hibernate 캐시에 저장한다던가, One To One 에서 N+1 쿼리가 발생하기 때문에 성능적인 이슈가 여러가지 있습니다.

    그래서 조회만 하는 api라면 DTO Projection으로 하는 것이 좋을 것 같습니다. 그리고 아래와 같이 변경하였습니다.

    SELECT s.station_id,
    s.station_name,
    s.latitude,
    s.longitude,
    s.is_parking_free,
    s.is_private,
    sum(case
    when cs.charger_condition = 'STANDBY' then 1
    else 0
    end),
    sum(case
    when c.capacity >= 50 then 1
    else 0
    end)
    FROM charge_station s
    inner join charger c on (c.station_id = s.station_id)
    inner join charger_status cs on (c.charger_id = cs.charger_id and c.station_id = cs.station_id)
    where s.station_id in (?, ?)
    group by s.station_id;

    이렇게 필요한 칼럼만 조회하는 방식으로 변경하여, 선릉역 근처를 조회하는 기준으로 약 450ms -> 350ms로 개선되었습니다.

    하지만 아직도 너무 느린 것을 확인할 수 있습니다. 그래서 실행 계획을 확인했습니다.

    실행 계획 확인하기

    sql의 실행 계획은 아주 중요하고 성능을 개선할 때 아주 유용합니다.

    실행 계획에는 여러가지 정보들이 있습니다.

    1. ID: 실행 계획 내에서 각 작업 또는 단계를 식별하는 일련번호입니다. 실행 계획은 여러 단계로 나뉘며, ID를 통해 이러한 단계를 식별할 수 있습니다.

    2. Select Type: 쿼리의 각 단계(예: SIMPLE, PRIMARY, SUBQUERY)에 대한 실행 유형을 나타냅니다. 이는 MySQL이 데이터를 선택하고 처리하는 방식을 나타냅니다.

    3. Table: 실행 계획에 포함된 테이블의 이름 또는 별칭입니다. 어떤 테이블이 사용되는지를 확인할 수 있습니다.

    4. Type: 테이블 접근 방식을 나타냅니다. 이 값은 인덱스 스캔, 풀 테이블 스캔 등과 같은 값일 수 있으며, 성능에 큰 영향을 미칩니다.

    5. Possible Keys: 사용 가능한 인덱스를 나타냅니다. MySQL이 어떤 인덱스를 사용할 수 있는지 알려줍니다.

    6. Key: 실제로 선택된 인덱스입니다. 이 값은 가능한 인덱스 중에서 실제로 사용되는 인덱스를 나타냅니다.

    7. Key Len: 사용된 인덱스의 길이를 나타냅니다.

    8. Ref: 인덱스를 사용하여 테이블 간의 연결을 나타내는 열입니다.

    9. Rows: 각 단계에서 예상되는 행의 수입니다. 이 값은 성능 평가에 중요한 역할을 합니다.

    10. Extra: 기타 정보를 제공합니다. 이 칼럼에는 추가 정보 및 힌트가 포함될 수 있습니다.

    이렇게 여러 칼럼이 있습니다. 그 중 성능에 큰 영향을 미치는 칼럼 두 가지만 자세히 알아보겠습니다.

    Type

    1. const : 쿼리에 Primary key 혹은 unique key 칼럼을 이용하는 where 조건절을 가지고 있고, 반드시 하나의 데이터를 반환하는 방식이다. (옵티마이저가 해당 부분은 상수로 처리하기 때문에 const라고 한다.)
    2. eq_ref : 조인에서 Primary key 혹은 unique key 칼럼을 이용하는 where 조건절을 가지고 있고, 반드시 하나의 데이터를 반환하는 방식이다. (const와 다른 점은 eq_ref는 조인에서 사용된다는 점이다.)
    3. ref : eq_ref와 다르게 join의 순서와 관계없이 사용된다. 그리고 primary key, unique key도 관계없다. 그냥 인덱스의 종류와 관계없이 = 조건으로 검색할 때 사용된다
    4. fulltext: mysql 전문 검색 인덱스를 사용해서 레코드에 접근하는 방법, 전문 검색할 컬럼에 인덱스가 있어야 한다. "MATCH ... AGAINST ..." 구문을 사용해서 실행된다
    5. range: 인덱스를 이용해서 검색하는데, 검색 조건이 >, >=, <, <=, BETWEEN, IN() 등의 연산자를 사용하는 경우이다. 보통의 인덱스 스캔이라고 하면 range, const, ref를 칭한다
    6. index: 인덱스 풀 스캔이다. 인덱스를 이용해서 테이블의 모든 레코드를 읽는다. 인덱스를 이용해서 테이블을 읽는 것이기 때문에 all보다는 빠르다.
    7. all: 테이블 풀 스캔이다. 테이블의 모든 레코드를 읽는다. 가장 느린 방법이다.

    실행 계획에서 자주 보이는 type들만 성능이 좋은 순으로 정리해봤습니다.

    Extra

    1. using filesort: 정렬을 위해 별도의 파일 정렬을 수행한다. 이는 인덱스를 사용하지 않고 정렬을 수행한다는 의미이다. 이는 성능에 좋지 않다.
    2. using index: 인덱스만으로 쿼리를 처리한다. 이는 인덱스만으로 쿼리를 처리하기 때문에 성능이 좋다.
    3. using join buffer: join이 되는 칼럼은 인덱스를 생성한다. 하지만 driven table에 적절한 인덱스가 없다면 driving table에 있는 모든 레코드를 읽어서 join을 수행한다. 그래서 이걸 보완하기 위해 driving table에 읽은 레코드를 임시 공간에 저장하는데 그 곳이 join buffer이다.
    4. using temporary: 쿼리를 처리하기 위해 임시 테이블을 생성한다. 인덱스를 사용하지 못하는 group by 쿼리가 대표적인 예이다.
    5. using where: mysql 엔진이 별도의 가공, 필터링 작업을 처리한 경우일 때만 나타난다. 범위 조건은 스토리지 엔진에서 처리되어 레코드를 리턴해주지만, 체크 조건은 mysql 엔진에서 처리된다.

    type뿐만 아니라 extra도 쿼리의 문제를 파악하는데 아주 큰 도움을 줍니다. 그 중 자주 보이는 것들에 대해서만 정리해봤습니다.

    그럼 아까 생성한 쿼리의 실행 계획을 확인해봅시다.

    +----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+
    | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    +----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+
    | 1 | SIMPLE | station | NULL | range | PRIMARY,idx_station_coordination | PRIMARY | 1022 | NULL | 2 | 100.00 | Using where; Using temporary |
    | 1 | SIMPLE | charger | NULL | ALL | PRIMARY | NULL | NULL | NULL | 240340 | 10.00 | Using where; Using join buffer (hash join) |
    | 1 | SIMPLE | chargersta | NULL | eq_ref | PRIMARY | PRIMARY | 2044 | charge.charger1_.charger_id,charge.station0_.station_id | 1 | 100.00 | NULL |
    +----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+

    station 테이블에 대해서는 range 스캔, 임시 테이블을 생성하고 있습니다, 그리고 charger에서는 테이블 풀 스캔, join buffer까지 생성하고 있습니다. 다행히도 chargersta 테이블에서는 적당한 조건을 생성한 것 같습니다.

    다시 한번 쿼리를 보고 실행 계획이 이렇게 나온 이유를 알아보겠습니다.

    SELECT
    ...
    FROM charge_station s
    inner join charger c on (c.station_id = s.station_id)
    inner join charger_status cs on (c.charger_id = cs.charger_id and c.station_id = cs.station_id)
    where s.station_id in (?, ?)
    group by s.station_id;

    아까 얘기했던, using temporary와 using join buffer가 발생하는 이유의 공통점을 찾아보면, 인덱스가 문제인 것을 유추할 수 있습니다.

    station과 charger를 join할 때, driven table 즉, charger 테이블에 적절한 인덱스가 없어 성능이 나빠진 것이라 의심하여, 인덱스를 생성하고 다시 한번 실행 계획을 확인했습니다.

    +----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+
    | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    +----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+
    | 1 | SIMPLE | station | NULL | range | PRIMARY,idx_station_coordination | PRIMARY | 1022 | NULL | 2 | 100.00 | Using where |
    | 1 | SIMPLE | charger | NULL | ref | PRIMARY,idx_station_id | idx_station_id | 1022 | charge.s.station_id | 3 | 100.00 | NULL |
    | 1 | SIMPLE | chargersta | NULL | eq_ref | PRIMARY | PRIMARY | 2044 | charge.charger1_.charger_id,charge.station0_.station_id | 1 | 100.00 | NULL |
    +----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+

    이렇게 charger 테이블에 인덱스를 생성한 것만으로도 실행 계획을 깔끔하게 개선했습니다.

    결과

    아래는 인덱스를 생성하기 전 실행 속도입니다.

    개선_전

    아래는 인덱스를 생성한 후 실행 속도입니다.

    개선_후

    315ms -> 24ms 로 약 13배 빨라진 것을 확인할 수 있습니다.

    결론

    실행 계획 확인은 필수입니다!

    참고

    real mysql 책

    - + \ No newline at end of file diff --git a/32.html b/32.html index a0688ef..38f8ab9 100644 --- a/32.html +++ b/32.html @@ -5,7 +5,7 @@ 데이터베이스 레플리케이션으로 조회 성능 개선하기 | CAR-FFEINE - + @@ -22,7 +22,7 @@ 소스 서버에서 커밋된 트랜잭션은 바이너리 로그에 기록되고, 레플리카 서버에서는 주기적으로 새로운 트랜잭션에 대한 바이너리 로그를 요청합니다. 이러한 방식은 소스 서버는 레플리카 서버가 제대로 변경 되었는지 알 수 없습니다. 즉 데이터 정합성에 문제가 생긴다는 단점이 있습니다. 하지만 이러한 방식은 소스 서버가 각 트랜잭션에 대해 레플리카 서버로 전송되는 부분을 고려하지 않는다는 점이 속도 측면에서 빠르고, 또 여러 대의 레플리카 서버를 구성하더라도 큰 성능 저하가 없다는 점이서 장점이 있습니다.

    반동기 복제

    반동기 복제는 비동기 복제보다 좀 더 데이터 정합성이 올라갑니다. 소스 서버는 변경된 트랜잭션이 있을 때 레플리카 서버가 다 전송이 되었다는 ACK 신호를 받기 때문에 확실히 알 수 있습니다. 하지만 전송여부만 확인하기 때문에 트랜잭션이 반영이 되었다는 보장은 없습니다. 반동기 복제 방식은 2가지가 있습니다.

    1. After sync: After Sync 방식은 소스 서버에서 트랜잭션을 바이너리 로그에 기록 후 Storage Engine에 바로 커밋하지 않습니다. 먼저 바이너리 로그에 기록 후 레플리카 서버의 ACK 응답을 기다립니다. 그리고 ACK 응답이 도착하면 그제서야 스토리지 엔진을 커밋하여 트랜잭션을 처리하고 결과를 반환합니다.
    2. After commit: After commit은 이름 그대로 커밋을 먼저 하는 것입니다. 트랜잭션이 생기면 먼저 바이너리 로그에 기록 후 소스 서버 스토리지 엔진에 커밋합니다. 그리고 레플리카 서버의 ACK 응답이 내려오면 클라이언트는 처리 결과를 얻고 다음 쿼리를 수행할 수 있습니다.

    먼저 after commit 방식은 소스 서버에 장애가 발생했을 때 팬텀 리드가 발생하게 됩니다. 트랜잭션이 스토리지 엔진 커밋까지된 후 레플리카 서버의 응답을 기다립니다. 이처럼 스토리지 엔진 커밋까지 완료된 데이터는 다른 세션에서도 조회가 가능합니다. 트랜잭션이 커밋되었고, 레플리카 서버로 아직 응답을 기다릴 때, 소스 서버에 장애가 발생한다면 새로운 소스 서버로 승격된 레플리카 서버에서 데이터를 조회할 때 자신이 이전 소스 서버에서 조회했던 데이터를 보지 못할 수도 있습니다.

    그리고 이처럼 레플리카 서버가 승격된 상황에 소스 서버의 장애가 복구되어 재사용할 경우 이미 커밋된 그 트랜잭션을 수동으로 롤백 시켜야만 데이터가 맞는 상황이 생깁니다.

    저희 팀의 복제 동기화 방식

    이러한 장단점으로 저희 팀은 데이터 무결성이 중요하다 판단되어 반동기 복제 방식을 사용하고, After Sync 방식을 적용하였습니다.

    복제 토폴리지

    복제 토폴리지는 여러가지 방식 중 자신의 상황과 가장 맞는 방식을 사용하면 될 것 같습니다. 저희 팀이 고려해야할 문제는 먼저 성능을 올려야 했고, 단일 장애포인트를 개선해야했습니다. 하지만 사용할 수 있는 서버는 2대 뿐이였습니다. 이러한 상황에서 어떤 방식을 택할 수 있을까요?

    싱글 레플리카

    가장 기본적이며 가장 많이 쓰이는 형태입니다. 어플리케이션에서 레플리카 서버에 읽기 요청을 전달하면, 레플리카 서버에 문제가 생겼을 때, 서비스 장애 상황이 발생할 수 있습니다. 그러므로 소스 서버에서 Read, Write를 둘 다 하고, 레플리카 서버는 failover를 위해 대기하는 예비용 서버로 구성합니다. 소스 서버에 장애가 발생했을 때 소스 서버를 대체하거나 데이터를 백업하는 용도로 사용합니다.

    멀티 레플리카

    싱글 레플리카와 비슷한 구성이지만 레플리카 서버가 한 대 더 추가된 구성입니다. 해당 방식은 SPOF 문제가 없기 때문에 레플리카 서버 하나를 읽기 전용 서버로 둘 수 있습니다. 읽기 작업을 분산함으로 어플리케이션의 성능을 향상 시킬 수 있습니다. 아까 말했던 장애 상황이 발생하면 예비용 서버인 Replica2 서버를 Source 서버 혹은 Replica1(읽기 전용) 서버로 대체할 수 있습니다.

    체인 복제

    레플리카 서버가 많아져 소스 서버의 바이너리 로그를 읽는 부하가 많아질 때 할 수 있는 구성입니다. 좀 전에 설명드렸던 멀티 레플리카 방식에서 똑같은 구성을 추가한 방식으로 볼 수 있습니다. Source 1 의 정보를 복제한 Replica 1-1, 1-2 서버는 빠르게 데이터가 반영되지만, Source1의 이벤트를 복제한 Source2를 복제한 Replica 2-1, 2-2 서버는 당연히 늦게 반영되기 때문에 해당 그룹은 예비용으로 사용합니다.

    듀얼 소스 복제

    데이터베이스 둘 다 소스 서버이면서 레플리카 서버인 경우입니다. 이 경우는 Active-Active구성과 Active-Passive 구성으로 나뉩니다

    Active-Active는 서버 둘 다 읽기와 쓰기가 가능한 형태입니다. 즉 부하를 분산시키기 위해 서버 모두 읽고 쓰는 작업을 하는 것입니다. 하지만 이러한 방식은 뻔한 단점이 있습니다. 서로의 이벤트가 동기화 되기 전에는 정합성이 깨질 수 있습니다. 또 동시에 같은 데이터에 대해 쓰기 작업을 수행할 때, 하나의 서버에서 쓰기가 완료되었더라도, 다른 하나의 서버에 늦게 끝난 쓰기가 있다면 마지막 트랜잭션인 늦게 끝난 쓰기 작업이 반영되어 예상하지 못한 결과가 나올 수 있습니다.

    또 다른 문제로는 Auto Increment를 사용할 때입니다. 새로운 데이터가 동시에 생성될 때 Auto Increment가 중복되는 에러가 발생할 수 있기 때문에 해당 토폴로지에서는 ID를 DB에 의존하지 않는 것이 좋습니다.

    Active-Passive 방식은 하나의 서버만 읽기와 쓰기 요청이 되지만, 나머지 서버는 대기하고 있습니다. 두 서버 모두 언제나 쓰기 작업이 가능한 형태이기 때문에 장애 발생 시 빠르게 Faliover할 수 있다는 점이 있습니다.

    멀티 소스 복제

    하나의 레플리카 서버가 다수의 소스 서버를 갖는 구성입니다. 데이터베이스 샤딩을 해뒀는데, 다시 하나의 서버로 통합하고 싶을 때 사용할 수 있습니다. 혹은 서로 다른 데이터를 한 곳에 백업을 할 때도 사용할 수 있습니다.

    저희 팀의 토폴로지 방식

    그럼 이렇게나 많은 구성 중에 저희 팀에서 택할 수 있는 토폴로지 방식은 싱글 레플리카 방식과 듀얼 소스 복제 방식 밖에 없습니다. 왜냐하면 주어진 서버가 2대뿐이기 때문입니다. 하지만 듀얼 소스 방식은 적용하는데 무리가 있는 부분이 있습니다. 일단 저희가 레플리케이션을 적용하려는 가장 큰 이유는 성능 이기 때문에 성능이 변하지 않는 듀얼 소스의 Active-Passive 방식은 제외하겠습니다. 그리고 Active-Active 방식은 부하를 분산시킬 수 있다는 장점이 있지만, 단점으로는 Auto Increment를 사용하는데에 위험이 있다는 점과, 데이터의 정합성 문제가 생길 수 있다는 점에서 듀얼 소스 방식은 제외하도록 했습니다.

    그럼 싱글 레플리카 방식을 적용할 수 밖에 없는데요. 싱글 레플리카의 방식은 가용성 문제를 해결하기 위해 만들어진 방식입니다. 하지만 저희 서비스는 현재 가용성보다 성능을 더 신경써야하는 상황이기때문에 싱글 레플리카 토폴로지를 구성하지만 레플리카 서버를 예비용이 아닌 읽기 전용 방식으로 사용하도록 하고, 가용성 부분을 포기하기로 정했습니다.

    코드에 적용하기

    replication-datasource Github 소스 코드를 참고하시거나, DB 복제, @Transactional에 따라 요청 분리해보기 글을 참고하여 따라하면 금방하실 수 있습니다!

    결론

    데이터베이스 레플리케이션 생각보다 어렵지 않습니다.

    데이터베이스 재밌습니다. 인프라도 재밌습니다.

    참고

    Real Mysql 8.0

    - + \ No newline at end of file diff --git a/33.html b/33.html index ce13d8a..36534cf 100644 --- a/33.html +++ b/33.html @@ -5,7 +5,7 @@ 혼잡도 조회 속도를 파티셔닝과 인덱스로 개선해보기 | CAR-FFEINE - + @@ -30,7 +30,7 @@ 위와 같은 조회 쿼리가 나왔으므로 인덱스를 아래와 같이 station_id, day_of_week에 걸어주었습니다.

    img 위 실행 속도에서 execution time을 확인해보면 인덱스를 걸고 134ms -> 5ms로 성능이 많이 개선 되었음을 확인할 수 있습니다.

    img 실행 계획도 의도한대로 잘 나오는 것을 보실 수 있습니다.


    정리

    1. DB Partitioning - (day_of_week : 요일)을 기준으로 파티셔닝
    2. 조회 쿼리에 맞게 인덱스 설정
    3. API 수정 (모든 요일의 혼잡도 조회 -> 해당 요일의 혼잡도 조회)

    결과적으로 기존 혼잡도 조회시 511ms가 나왔으나, 요일 별 조회 및 파티셔닝 & 인덱스를 적용하고 execution time = 5ms로 개선

    - + \ No newline at end of file diff --git a/34.html b/34.html index 58fe897..afbaeb8 100644 --- a/34.html +++ b/34.html @@ -5,7 +5,7 @@ 캐시와 이분 탐색으로 조회 성능 개선하기 | CAR-FFEINE - + @@ -16,7 +16,7 @@ 아래와 같이 간단히 조건을 stream()의 filter()를 사용해서 구현했습니다.

    public class StationCacheRepository {

    private final List<StationInfo> cachedStations;

    public List<StationInfo> findByCoordinate(
    BigDecimal minLatitude,
    BigDecimal maxLatitude,
    BigDecimal minLongitude,
    BigDecimal maxLongitude
    ) {
    return cachedStations.stream()
    .filter(it -> it.latitude().compareTo(minLatitude) >= 0 && it.latitude().compareTo(maxLatitude) <= 0)
    .filter(it -> it.longitude().compareTo(minLongitude) >= 0 && it.longitude().compareTo(maxLongitude) <= 0)
    .toList();
    }
    }

    하지만 해당 방법으로 로컬에서 조회를 테스트 했을 때 캐시를 적용한 것보다 더 느려진 결과가 나왔습니다. 캐싱을 해서 데이터베이스까지 요청을 보내지 않는데 왜 더 느려진 것일까요?

    답은 인덱스 였습니다. Mysql 에서 인덱스는 B Tree로 구성되어 있습니다. 데이터베이스에서는 위도, 경도로 복합 인덱스가 설정되어 있었지만, 현재 어플리케이션 로직에는 해당 부분이 없습니다.

    그래서 filter로 순회하는 시간복잡도가 O(n)이고, 데이터베이스에서는 O(log n)이기 때문에 더 느려진 것입니다. 그렇다고 제가 직접 B tree 자료구조를 직접 구현해야할까요?

    현재 해당 조회 API는 위도 경도로 범위 탐색을 하고 있습니다. 결국엔 station의 정보들이 위도, 경도로 정렬만 되어 있다면 B tree를 직접 구현하지 않더라도 같은 시간복잡도 O(log n)으로 탐색할 수 있습니다. 물론 B tree와 다른 부분은 해당 충전소의 정확한 위도, 경도로 단일 칼럼을 조회할 때는 O(n)이기 때문에 이런 방법이 문제가 될 수 있지만, 해당 캐시 데이터로는 무조건 범위 탐색을 하기 때문에, B tree를 구현하지 않고 이분 탐색으로 조회하는 방식으로 변경해보겠습니다.

        public void initialize(List<StationInfo> stations) {
    cachedStations.addAll(stations);
    cachedStations.sort((o1, o2) -> {
    int latitudeCompare = o1.latitude().compareTo(o2.latitude());
    if (latitudeCompare == 0) {
    return o1.longitude().compareTo(o2.longitude());
    }
    return latitudeCompare;
    });
    }

    private List<StationInfo> findStations(BigDecimal minLatitude, BigDecimal maxLatitude, BigDecimal minLongitude, BigDecimal maxLongitude) {
    int lowerBound = binarySearch(minLatitude, START_INDEX);
    int upperBound = binarySearch(maxLatitude, lowerBound);
    if (lowerBound == -1 || upperBound == -1) {
    return Collections.emptyList();
    }
    return cachedStations.stream()
    .skip(lowerBound)
    .limit(upperBound - lowerBound)
    .filter(station -> station.longitude().compareTo(minLongitude) >= 0 && station.longitude().compareTo(maxLongitude) <= 0)
    .toList();
    }

    private int binarySearch(BigDecimal latitude, int startIndex) {
    int left = startIndex;
    int right = cachedStations.size() - 1;
    int result = -1;
    while (left <= right) {
    int middle = left + (right - left) / 2;
    StationInfo middleStation = cachedStations.get(middle);
    if (middleStation.latitude().compareTo(latitude) >= 0) {
    result = middle;
    right = middle - 1;
    } else {
    left = middle + 1;
    }
    }
    return result;
    }

    먼저 어플리케이션이 실행될 때 cache 데이터를 찾아 저장하는 것 뿐만 아니라, 위도(Latitude)를 기준으로 정렬하도록 만들었습니다. 그리고 위도의 최소, 최대값의 인덱스를 가장 효율적으로 찾아올 수 있도록 binary search를 하는 메서드를 만들었습니다. 이렇게 한다면 O(log n) 으로 위도의 최대 최소 조건에 포함되는 모든 station의 값을 조회할 수 있습니다. 그리고 조회한 데이터들의 개수만큼 filter를 통해 경도(longitude) 가 포함되는지 확인합니다. 해당 방식의 구현은 B tree가 작동하는 방식과 유사할 것입니다.

    이분 탐색을 적용한 결과 로컬에서 응답 속도가 120 ms -> 50 ~ 70 ms로 약 2배 빨라진 것을 확인할 수 있습니다.

    실시간이 중요한 데이터는?

    앞서 말씀드렸다시피 지도로 충전소를 조회할 때, 충전소의 정보들에는 바뀌지 않는 정보뿐만 아니라, 최신화해야하는 충전기의 현재 상태 정보가 있습니다. 이러한 정보들은 캐싱해둘 수 없습니다. 하더라도, 관리 포인트가 늘어나기 때문에 데이터베이스에서 캐싱해둔 충전기 id로 충전기의 상태를 찾아와서 정보를 합쳐 반환하는 식으로 만들 수 있습니다.

        select cs.station_id,
    sum(case
    when cs.charger_condition = 'STANDBY' then 1
    else 0
    end)
    from charger_status cs
    where cs.station_id in (?, ?, ?, ?, ?, ?, ?)
    group by cs.station_id

    위와 같은 쿼리로 해당 충전소의 최신화된 충전기 상태를 가져올 수 있습니다.

    캐싱을 하기전에 데이터베이스를 이용해 데이터를 가져올 때의 쿼리는 아래와 같습니다.

     select
    distinct s.station_id
    from
    charge_station s
    inner join
    charger c
    on (
    c.station_id=s.station_id
    )
    where
    s.latitude>=?
    and s.latitude<=?
    and s.longitude>=?
    and s.longitude<=?
    -------------------------------------------------
    select
    s.station_id,
    s.station_name,
    s.latitude,
    s.longitude,
    s.is_parking_free,
    s.is_private,
    sum(case
    when cs.charger_condition='STANDBY' then 1
    else 0
    end),
    sum(case
    when c.capacity>=50 then 1
    else 0
    end)
    from
    charge_station s
    inner join
    charger c
    on (
    c.station_id=s.station_id
    )
    inner join
    charger_status cs
    on (
    c.station_id=cs.station_id
    and c.charger_id=cs.charger_id
    )
    where
    s.station_id in (
    ?,?,?,?
    )
    group by
    s.station_id

    원래는 위와 같이 여러번의 Join을 하고, 2번의 쿼리가 나갔던 반면 지금은 join을 하지않는 한번의 깔끔한 쿼리로 개선되었습니다.

    그리고 station 테이블의 위도, 경도로 범위 탐색을 위해 생성했던 index도 제거할 수 있게 되었습니다!

    결론

    1. 캐싱할 수 있는 부분은 하는 것도 좋을 것 같습니다
    2. 시간 복잡도를 계산해봅시다.
    3. 성능 개선 재밌습니다.
    - + \ No newline at end of file diff --git a/35.html b/35.html index be358ed..528471e 100644 --- a/35.html +++ b/35.html @@ -5,7 +5,7 @@ Scale-out 시 Scheduling 중복 실행 막기 | CAR-FFEINE - + @@ -23,7 +23,7 @@ 따라서 Schedule Thread Pool Size를 늘리도록 하겠습니다.

    @Configuration
    public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
    taskScheduler.setPoolSize(10);
    taskScheduler.setThreadNamePrefix("schedule-task-");
    taskScheduler.initialize();
    taskRegistrar.setTaskScheduler(taskScheduler);
    }
    }

    SchedulingConfigurer 를 구현하여 Thread Pool size를 일단 10개로 정의했습니다.

    success 스레드 풀을 늘렸더니 위와 같이 2의 배수의 시간에 정확히 작동이 되는 것을 확인할 수 있습니다.

    하지만 이렇게 여러 작업을 동시에 실행된다면 데이터베이스에 병목현상이 발생되어 오히려 작업이 더 느리게 끝날 수도 있다고 생각했습니다.

    그래서 해당 부분의 실행을 관리하는 클래스를 생성하여 해당 클래스에서 Schedule의 작업을 관리하도록 구현했습니다.

    @Service
    public class BusinessLogic {

    private final ApplicationEventPublisher applicationEventPublisher;

    @Scheduled(cron = "0/2 * * * * *")
    public void complexJobSchedule() {
    applicationEventPublisher.publishEvent(new SchedulingEvent(this::complexJob, "complexJob", LocalDateTime.now()));
    }

    @Scheduled(cron = "0/4 * * * * *")
    public void moreComplexJobSchedule() {
    applicationEventPublisher.publishEvent(new SchedulingEvent(this::moreComplexJob, "moreComplexJob", LocalDateTime.now()));
    }
    }

    로직이 있는 BusinessLogic 서비스에서 스케줄의 시간마다 실행해야할 메서드를 Event로 발행합니다.

    @Component
    public class ScheduleService {

    private final ExecutorService executorService = Executors.newFixedThreadPool(1);
    private final Queue<SchedulingEvent> scheduleTasks = new ConcurrentLinkedQueue<>();
    private final AtomicBoolean isRunning = new AtomicBoolean(false);

    @EventListener
    public void addTask(SchedulingEvent schedulingEvent) {
    scheduleTasks.add(schedulingEvent);
    }

    @Scheduled(cron = "0/1 * * * * *")
    public void polling() {
    if (!scheduleTasks.isEmpty() || isRunning.compareAndSet(false, true)) {
    SchedulingEvent schedulingEvent = scheduleTasks.poll();
    executorService.execute(() -> execute(schedulingEvent));
    }
    }
    }

    그리고 위와 같은 스케줄을 관리하는 서비스에서는 Schedule Event를 받아 실행하도록 만들었습니다. 해당 클래스에서는 ThreadPool을 새로 생성하여, schedule의 스레드에 영향을 받지 않도록 구현했습니다.

    그리고 1초마다 실행되는 스케줄을 만들어 queue에 작업이 있는지, 현재 작업 중인지 확인하여 그렇지 않다면 queue에서 작업을 꺼내 실행하도록 만들었습니다.

    거의 구현이 끝나갑니다. 이제는 해당 Schedule의 데이터를 저장하고, 작업이 실패했을 시에 다시 작업을 하기 위한 기능만 구현하면 될 것 같습니다.

    @Component
    public class ScheduleService {

    ...

    private void execute(SchedulingEvent schedulingEvent) {
    String jobId = schedulingEvent.jobId();
    LocalDateTime executionTime = schedulingEvent.executionTime();

    if (isJobInProgressOrDone(jobId)) {
    log.info("작업이 실행중입니다. {} {}", executionTime, jobId);
    return;
    }
    ScheduleTask entity = new ScheduleTask(jobId, executionTime, JobStatus.RUNNING);
    scheduleTaskJdbcRepository.save(entity);

    try {
    schedulingEvent.runnable().run();
    scheduleTaskJdbcRepository.updateById(entity.getId(), JobStatus.DONE);
    } catch (Exception e) {
    log.error("{} 작업 실행 중 에러가 발생했습니다.", jobId);
    scheduleTaskJdbcRepository.updateById(entity.getId(), JobStatus.ERROR);
    tasks.add(schedulingEvent);
    }
    }

    private boolean isJobInProgressOrDone(String jobId) {
    Optional<ScheduleTask> taskOptional = scheduleTaskRepository.findById(jobId);
    if (taskOptional.isPresent()) {
    ScheduleTask scheduleTask = taskOptional.get();
    return scheduleTask.getStatus() == JobStatus.RUNNING || scheduleTask.getStatus() == JobStatus.DONE;
    }
    return false;
    }
    }

    이 부분은 간단하게 구현할 수 있습니다. 위와 같이 작업의 실행 시간과, job의 이름으로 데이터베이스에서 조회하고, 없다면 작업을 실행하고 있다면 작업이 ERROR 인지 확인하여 작업을 실행해주면 될 것 같습니다.

    complete

    위와 같이 두 개의 서버를 동시에 띄웠을 때에도 스케줄이 잘 작동하는 것을 확인할 수 있습니다.

    결론

    스케줄을 이렇게 구현할 수도 있지만 환경이 된다면 Message Queue를 사용하는 것이 어떨까요?

    혹시 틀린 부분이 있다면 지적 부탁드립니다.

    - + \ No newline at end of file diff --git a/36.html b/36.html index 7ab67b9..c903624 100644 --- a/36.html +++ b/36.html @@ -5,13 +5,13 @@ 마커 렌더링 최적화 | CAR-FFEINE - +
    본문으로 건너뛰기

    마커 렌더링 최적화

    · 약 13분
    센트

    1. 개요

    기존의 구조에서는 마커 하나를 렌더링하기 위해 다음과 같은 과정을 거쳤다.

    1. StationMarkersContainer 컴포넌트에서 충전소 정보 요청
    2. 충전소 정보를 props로 넘겨 Marker 컴포넌트 호출
    3. 지도에 부착될 DOM요소 생성
    4. createRoot를 통해 리액트 root 생성
    5. 2번에서 생성한 DOM 요소를 전달해 구글 지도 api의 Marker 생성자 함수 호출
    6. 3번에서 생성했던 root의 render 메서드 호출
    7. 마커 인스턴스 전역 상태에 새로 생성한 마커 추가

    위 과정을 거쳤을 때의 마커 렌더링 모습을 보면 다음과 같다.

    before

    마커들이 한번에 렌더링 되는 것이 아니라 산발적으로 렌더링 되는 모습을 확인할 수 있다.

    2. 문제 원인 분석

    마커를 렌더링 하기 위해 거치는 과정을 분석해 보았다.

    1 ~ 3 과정에서는 성능에 크게 영향을 끼칠 요소가 없지만 4번 과정은 일반적인 리액트 프로젝트를 개발할 때 겪는 과정이 아니다. 따라서 createRoot를 통해 많은 개수의 루트를 생성했을 때의 영향에 대해 알아보았다.

    image

    리액트 공식 문서를 보니 페이지의 일부에 리액트를 뿌려서 사용하는 경우에는 루트를 필요한 만큼 생성해도 된다는 이야기가 포함되어 있었다. 따라서 4번 과정 또한 문제의 원인이라고 볼 수 없었다.

    5번 과정은 구글 지도에 마커를 특정 위도 경도에 위치시키기 위해서 어쩔 수 없이 거쳐야 하는 과정이므로 이 과정은 문제가 있더라도 개선이 불가능해 일단 고려하지 않았다.

    6번 과정은 4번 과정에서 생성했던 리액트 루트의 render 메서드를 호출해 실제로 화면에 리액트 컴포넌트를 그리도록 하는 과정이다. 이 과정 또한 리액트 컴포넌트를 화면에 렌더링하기 위해선 어쩔 수 없이 거쳐야 하는 과정이므로 고려하지 않았다.

    하지만 6번 과정에서 리액트 컴포넌트를 직접 그리는 것이 아니라 구글 지도 api의 기본 마커를 사용하면 성능을 향상시킬 수 있지 않냐고 반문할 수도 있을 것이다. 이전에는 이러한 방식을 사용해 마커를 렌더링 했었다. 우리의 서비스는 현재 사용 가능한 충전소 개수를 마커를 통해서도 전달하기 때문에 이를 고려해 기본 마커를 사용할 때 다음의 두 가지 문제가 생긴다.

    1. 사용 가능한 충전소 개수를 기본 마커에 렌더링 할 때 성능이 매우 좋지 않다.
    2. 마커의 디자인을 바꾸고자 할 때 변경에 대응하기 어렵다.

    따라서 마커는 리액트 루트의 render 메서드를 호출해 리액트 컴포넌트를 렌더링하는 것으로 결정했다.

    마지막으로 남은 7번 과정에서는 useSyncExternalState 훅을 사용해 전역적으로 관리하고 있던 상태에 수정을 가하는 연산을 수행한다. 이 과정은 이전에도 성능 저하를 유발할 것으로 예상되던 부분이었다. (하단 링크 참고)

    useSyncExternalStore 훅을 통해 구독한 state가 한번에 업데이트 되는 이유

    요청의 결과로 받아온 마커 정보의 개수가 100개라고 가정해보자. 우리는 이제 마커를 렌더링 할 것이다. 첫 번째 마커의 렌더링을 위해 1번 ~ 6번의 과정을 거친 후 7번 과정을 수행한다. 그러면 리액트 입장에서는 리액트 루트의 render 메서드 호출에 대한 동작을 수행해야 하고, 새로운 마커 인스턴스에 대한 전역 상태를 변경시키는 동작을 수행해야 한다. 리액트가 이 과정을 100번 반복하고 나면 우리는 비로소 모든 마커가 화면에 렌더링 된 모습을 볼 수 있을 것이다.

    나는 이 부분에서 성능 저하의 요소가 있다고 생각했다. 리액트에서의 상태 변화는 곧 리액트 내부의 렌더링을 위한 로직이 수행되게 함을 의미하고, 이 과정을 개선 이전에는 마커의 개수만큼 반복하고 있었던 것이다. 여기까지 생각해보니 전역 상태 변화에 대해 리액트가 렌더링을 위한 연산을 진행할 동안에는 마커의 렌더링(render 메서드 호출)이 멈추는 것이 아닐까 하는 생각이 들었다.

    그래서 크롬 개발자 도구의 퍼포먼스 탭을 들어가 보니 산발적으로 발생하던 마커 렌더링의 문제 원인이 짐작했던 그 원인임을 확인할 수 있었다.

    image

    프레임 이미지 하단을 보면 산발적인 마커 렌더링이 수행될 때마다 수반되는 어떤 함수 호출이 있음을 확인할 수 있다.

    image

    이 부분이 문제의 함수 호출 부분이다. 자세히 살펴보면 상단에 performWorkUntilDeadline이란 함수가 호출됨을 볼 수 있다.

    image

    performWorkUntilDeadline 라는 함수를 조금 알아보니 해당 함수는 간단히 말해 리액트에서 state의 변경이 한번에 많이 발생할 때 5ms의 데드라인 시간을 줄 때 사용하는 함수라는 것을 알게 되었다. 문제의 원인이라고 생각했던 마커 개수 만큼의 전역 상태 변화가 실제로 마커 렌더링을 잠시 중단하게 만들고 있음을 알게 되었다.

    3. 문제 해결

    앞서 분석한 문제를 개선해보고자 마커 렌더링에 필요한 충전소 정보 배열을 부모 컴포넌트에서 받아와 각 충전소 정보를 자식 컴포넌트에 넘겨주고, 자식 컴포넌트에서 마커 생성과 렌더링 로직을 수행하던 기존의 방식을 부수고 부모 컴포넌트에서 모든 것을 일괄 처리하는 방식으로 고쳐보았다.

    고치는 과정에서 기존 방식에서는 리액트 생명 주기에 의존하여 화면에 보여지지 않는 마커를 지워주던 로직을 이제는 모두 직접 구현해야 했다.

    이전의 영역과 겹치는 부분에 있는 충전소는 다시 그리지 않고, 영역 밖의 충전소를 나타내는 마커는 지워주고, 이전의 영역과 겹치지 않는 새로 받아온 충전소는 그리도록 다음과 같이 메서드를 분리해보았다.

    • 기존과 겹치지 않는 새로운 영역에 대한 마커를 생성하는 메서드
    • 기존과 겹쳐지는 영역에 대한 마커들을 반환하는 메서드
    • 새로운 영역 밖에 있는 마커들을 지워주는 메서드
    • 새롭게 생성된 마커를 화면에 렌더링하는 메서드

    이 메서드들을 커스텀 훅으로 분리해 부모 컴포넌트에서 이를 활용하도록 하여 다소 복잡할 수 있는 마커 렌더링 로직을 선언적으로 구현할 수 있도록 했다.

    결과적으로 기존에 사용되던 기능들을 그대로 사용할 수 있으면서 화면에 마커가 산발적으로 렌더링 되던 문제가 해결 되었고, 부가적인 효과로 전체 마커의 렌더링 시점도 앞당길 수 있게 되었다. + 기존에는 구조적인 문제로 연산량이 너무 많아 클러스터링이 늦어져 이를 도입할 수 없었던 문제를 구조 수정으로 인해 적용할 수 있게 되었다.

    작업한 PR

    https://github.com/woowacourse-teams/2023-car-ffeine/pull/737

    결과 분석 (performance 탭 활용)

    before

    마커 조회 요청이 종료된 시점: 약 2499ms

    image

    첫 마커 렌더링 시점: 3093ms

    image

    모든 마커 렌더링 종료 시점: 약 3611ms

    image

    처음으로 마커가 렌더링 될 때까지 소요된 시간: 594ms

    모든 마커 렌더링에 소요된 시간: 1112ms

    after

    마커 조회 요청의 시작점: 약 1875ms

    image

    모든 마커 렌더링 종료 시점: 2395ms

    image

    처음으로 마커가 렌더링 될 때까지 소요된 시간: 519ms

    모든 마커 렌더링에 소요된 시간: 519ms

    개선 결과

    처음으로 마커가 렌더링 되는 시점은 두 방식 모두 비슷한 결과를 보인다. 하지만 개선 후 방식은 한번에 모든 마커가 렌더링 되는 방식이고, 개선 이전의 방식은 산발적으로 마커가 렌더링 되는 방식이므로 개선 후의 방식에서 전체 마커를 렌더링 하는 시점이 훨씬 빨라지게 되었다.

    결과적으로 전체 마커가 렌더링 되는 속도 약 55.6% 단축하게 되었다. 이 결과는 마커가 늘어날 수록 더욱 차이가 극적으로 벌어질 것으로 예상된다.

    before

    before

    after

    after

    - + \ No newline at end of file diff --git a/37.html b/37.html index 5d551f1..e10a216 100644 --- a/37.html +++ b/37.html @@ -5,7 +5,7 @@ 충전소 조회 api 분리 | CAR-FFEINE - + @@ -21,7 +21,7 @@ 이 정보를 제외하고 마커를 띄우기 위해 필요한 최소한의 정보를 조회하도록 수정해 서버의 부하를 낮췄습니다.

    이러한 변경으로 인해 충전소 조회 API의 성능이 개선되었습니다. 필요한 정보만을 조회하므로써 데이터베이스의 부하를 줄이고 응답 시간을 단축할 수 있게 되었습니다. 또한, 프론트엔드에서는 필요한 정보만을 호출하여 불필요한 데이터를 받아오지 않아도 되므로 클라이언트 측의 성능도 향상되었습니다.

    - + \ No newline at end of file diff --git a/38.html b/38.html index f075059..dd0305f 100644 --- a/38.html +++ b/38.html @@ -5,7 +5,7 @@ 카페인 서비스 방문자 분석 - 1 | CAR-FFEINE - + @@ -20,7 +20,7 @@ no offset no offset no offset

    집계 된 자료처럼 방문자들이 단순 방문만 한 것이 아니라, 수 많은 이벤트를 발생시키고 평균 참여 시간도 상당 부분 확보했음을 확인할 수 있습니다.

    - + \ No newline at end of file diff --git a/39.html b/39.html index b06c01f..02fb365 100644 --- a/39.html +++ b/39.html @@ -5,7 +5,7 @@ 카페인 서비스와 함께하는 전기차 여행 1 | CAR-FFEINE - + @@ -27,7 +27,7 @@ 차주 분과 인터뷰 하고 싶었지만, 차 내부에서 너무 바빠보이셔서 그럴 수 없었습니다.

    전기차 충전을 기다리면서 무엇을 할 수 있을까요? 이 분은 다행히도 업무를 보고 계셨지만, 다른 차주들은 무엇을 하고 보낼지 궁금해졌습니다.

    no offset

    휴게소에는 충전소가 하나 더 있었습니다.

    한 곳은 사용중이지만, 다른 한 곳은 사용할 수 있었습니다.

    저희는 이 충전소를 사용해보기로 했습니다.

    no offset

    사용할 수 있으니깐 들어가봐야지! 하고 도착한 순간 아차 싶었습니다.

    "아, 충전소가 외부인 사용 금지일 수 있었지?"

    저희는 분명히 서비스를 직접 개발했으니깐 다 알고 있던 사항이었지만, 전혀 생각치 못했습니다.

    서비스를 개발하는 내내 외부인 개방 충전소에 대한 중요성을 간파하였고, 이 기능을 넣었으면서도 사용하지 않고 충전소를 방문한 것이었습니다.

    바로 앞에 있어서 다행이었지만, 어찌됐든 이 충전소를 사용할 수 없었습니다.

    따라서 저희는 휴게소를 떠나는 내내 이 문제에 대해서 토론을 할 수 밖에 없었습니다.

    분명 우리가 만든 서비스인데 왜 놓쳤을까?

    맛있는 점심

    no offset

    파주닭국수 본점에서 맛있는 식사를 했습니다.

    비록 식당에는 전기차 충전소가 없었지만, 인근에 충전소가 있어 실험을 하나 해볼 수 있었습니다.

    인근 충전소와 식당의 거리가 가까워 보이는데, 과연 걸어갈 수 있을까?

    실제로 걷지는 않았습니다만 차 타면서 지나가면서 확인해본 결과 직접 걸을 수 없는 거리였습니다. (굉장히 걷기 싫은 수준의 먼 거리였습니다.)

    집에 있는 PHEV를 탈 기회가 많아 전기차 충전소를 자주 방문했던 저는 이런 점을 잘 알고 있었습니다.

    다행히 이 부분을 잘 알고 있었기에 저희는 이 부분을 서비스에 반영하였고, 모든 데이터를 포기하지 않았던 것이 옳은 선택이었다는 것을 확인하게 되었습니다.

    no offset

    식사가 끝나고 드디어 마장호수로 출발하게 되었습니다.

    마장호수 도착

    마장호수에 도착하자마자 충전소에 방문했습니다.

    no offset

    통계에서는 사용률이 적을 것이라고 하였는데 저희만 있었습니다.

    no offset no offset

    2기 중 1곳을 저희가 사용하였고, 마장호수를 돌았습니다.

    no offset

    약 50분 간 산책을 하고, 돌아와보니 충전기 다 되어있었습니다.

    사실 마장호수 까지 오는 내내 든 생각이었지만, 전기차의 배터리가 생각보다 오래 간다는 생각이 들었습니다.

    일부러 회생제동 기능도 끄고, 에어컨을 강하게 틀어서 배터리를 소진하려고 하였으나, 85km를 주행하는 동안 겨우 20%를 소모하였습니다.

    충전기를 꽂을 때 50%였으나, 호수를 한바퀴 돌고 오니 이미 100%가 되어있었습니다.

    여담이지만, 저희가 돌아왔을 때 옆 자리에는 전기 화물차가 있어 충전소가 가득 찼습니다.

    또, 앱에서도 충전기 사용 여부가 업데이트 되는 것을 확인했습니다.

    no offset

    배터리 성능에는 좋지 않고 가격도 비싸서 이를 자주 사용하는 것은 좋지 않겠지만, 급한 사람들은 급속 충전기를 사용하면 되겠구나 싶었습니다.

    따라서 급속과 완속은 더더욱 다른 개념으로 봐야겠다는 생각이 들었습니다.

    제가 그동안 경험했던 전기차 충전소는 완속 기준이었기에 신선한 경험이었습니다.

    선릉으로 돌아오다

    no offset

    선릉으로 돌아와서 차량을 반납하였습니다.

    저희는 이번 여정을 통해 카페인 서비스에서 어떤 점을 개선해야할지 좀 더 명확하게 알게되었습니다.

    1. 현재 서비스에서 제공하는 기능들로 충전소를 검색하는 것은 가능하며, 충전소의 위치를 정확하게 파악하는 것도 가능하다.
    2. 하지만 충전소가 없는 목적지는 검색할 수 없고, 현 위치가 어디인지 가늠하기가 어려워진다.
    3. 충전소를 사용할 수 있다고 표기되어 있더라도 외부인 개방이 아닐 수 있다. 정보가 정확히 제공됨에도 불구하고 이를 단번에 눈치채기 어렵다.
    4. 이러한 문제를 예상하여 외부인 개방 여부를 필터링 할 수 있는 기능을 제공하고 있음에도 불구하고 사용하지 않았다.
    5. 충전소의 통계 자료의 적중률은 높았으나, 좀 더 많은 충전소를 들려 확인해봐야 할 것 같았다.
    6. 전기자동차는 생각보다 오래가고 상품성이 있었다. 주행 능력도 충분하고, 인프라가 잘 되어있다. 이걸 왜 욕하지? 라는 생각이 들었다.
    7. 지도 확대 허용 범위가 너무 좁아서 사용하는데 불편한건 실제 상황에서 더 불편했다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git a/4.html b/4.html index 6940dac..8f35cdd 100644 --- a/4.html +++ b/4.html @@ -5,7 +5,7 @@ 큰 틀에서 바라보는 서버 아키텍처 계획 | CAR-FFEINE - + @@ -21,7 +21,7 @@ 물론 이는 계획이고 공부하지 않은 다른 내용이 있을 수 있기 때문에 언제든 바뀔 수 있습니다.

    무중단 배포 아키텍처 적용

    이 또한 아직은 먼 이야기지만, 고려해 볼 상황이라서 적어봤습니다.

    사용자가 이용하고 있는 서비스가 갑자기 중단된다면 어떨까요? 저는 화가 많이 날 것 같습니다.

    피치 못할 사정으로 서버가 터져도, 사용자가 서비스를 계속 이용할 방법이 없을까요?

    이런 고민을 해결하기 위해서 나온 개념이 무중단 배포입니다.

    카나리아 배포, Blue/Green 배포, 롤링등 무중단 배포를 위한 여러가지 전략은 이미 존재합니다. 이 부분은 아직은 서버의 명세가 정확하지 않아서 어떤 방식으로 어떻게 처리할 것인지에 대해서는 아직 정할 수는 없습니다.

    이는 명세가 확실하게 정해진 후 팀원과 장단점을 상의하며 결정할 일이기 때문에 현재까지는 "이 정도를 고려하고 있다." 정도만 알면 될 것 같습니다.

    - + \ No newline at end of file diff --git a/40.html b/40.html index a0c6979..67d4752 100644 --- a/40.html +++ b/40.html @@ -5,7 +5,7 @@ 카페인 서비스와 함께하는 전기차 여행 2 | CAR-FFEINE - + @@ -13,7 +13,7 @@
    본문으로 건너뛰기

    카페인 서비스와 함께하는 전기차 여행 2

    · 약 15분
    가브리엘
    센트

    안녕하세요? 센트와 가브리엘 입니다.

    저희 카페인 팀에서는 지난번 카페인 서비스 1차 체험 진행 이후 일부 기능 개선이 있었습니다. 기능 개선의 유용성을 판별하고자 카페인 서비스 2차 체험을 다녀왔습니다.

    저희 팀에서 1차 체험 이후 개선한 사항은 다음과 같습니다.

    1. 지역검색

    no offset

    • 이제는 검색어를 입력하는 경우, 전국 도시의 주소가 같이 제공됩니다.

    2. 충전소 마커를 확인할 수 있는 지도 영역 확장

    no offset

    (기존에는 위 사진보다 좁은 영역만을 호출하는 것이 허용되었다.)

    • 모바일에서 좀 더 넓은 영역을 호출하는 것을 허용했습니다. 원래는 디바이스 너비를 고려하지 않고 줌 레벨 기준으로 요청을 제한했으나, 이제는 사용자 디바이스에 보이는 지도의 영역 크기를 기반으로 요청을 제한하는 방식을 도입했습니다.
    • 기존에 사용하던 마커의 단점은, 그 크기가 너무 크다는 것이었습니다. 이로인해 더 넓은 영역을 보여주는 경우에 마커들이 겹치는 현상이 있었는데요, 이를 수정하기 위해 특정 영역 크기 이상에서는 마커를 좀 더 간소화 된 디자인으로 보이도록 개선하였습니다.
    • 마커 사이즈가 작아지면서 사용 가능한 충전기 개수가 더이상 들어갈 공간이 없어졌습니다. 따라서 마커 색상은 그대로 유지를 하되, 인포 윈도우에 현재 사용 가능한 충전기 개수를 보여주는 방식으로 디자인을 개선하였습니다.

    체험 규칙 설정

    개선한 기능이 실제로 유용한지 확인해보기 위해 저희는 카페인 서비스 2차 체험의 규칙을 다음과 같이 설정했습니다.

    저희는 좀 더 의미있는 경험을 하기위해 1차 체험 때 정했던 규칙에 더해서 다음과 같은 추가 규칙을 설정하였습니다.

    중간에 목표 지점이 많이 변경된다

    지난 카페인 서비스 1차 체험에서는 지역 검색이 없어 목표 지점을 찾는 것이 불편했습니다. 1차 체험 이후 지역 검색이 추가 되었으므로 이 기능이 얼마나 유용한지 경험해보고자 이 규칙을 설정했습니다.

    추가로 목표 지점 주변의 충전소를 확인할 때 새로 추가된 지도 영역 확장이 얼마나 유용한지도 경험해보고자 했습니다.

    체험 개요

    no offset

    1. 잠실역 출발
    2. 하남 만두집
    3. 다음 목적지 설정
    4. 판교

    체험 후기

    잠실역 출발

    no offset

    쏘카에서 EV6를 대여해서 가브리엘, 센트, 키아라가 잠실역에서 출발하였습니다. 저녁 퇴근 이후에 남이섬을 가려고 목적지를 설정하였으나 배가 너무 고파서 가는 길에 식사를 하자고 얘기가 나왔습니다.

    하남 만두집

    따라서 진정한 처음 목적지는 스타필드였으나, 가브리엘은 동네 주민이라 스타필드를 너무 잘 알고 있었습니다. 따라서 스타필드에 전기차 충전소가 어디에 있는지도 알고있으므로 목적지를 급하게 변경하기로 했습니다. 이 때 목적지 변경을 위해 주변 식당을 둘러보던 중에 괜찮은 식당을 발견해서 해당 식당을 기준으로 주변 충전소를 확인해보기로 했습니다.

    no offset

    식당 주변을 가기 위해 지역 검색을 처음으로 사용하여 식당과 가까운 지역을 탐색할 수 있었습니다.

    이 과정에서 식당에는 충전소가 없다는 사실을 알게되어, 근처 충전소를 찾아보기 위해서 지도를 축소했더니 1차 체험때와는 달리 더 넓은 영역을 보여줬습니다. 이전에는 마커 자체가 보이지 않아 답답하였으나, 이제는 더 넓은 영역을 조회할 수 있게 되어 편리했습니다.

    지난 체험 이후로 피드백을 자체 수집하여 개발한 기능들이 편하다는 것을 식당에 가는 길에 느낄 수 있었습니다.

    다음 목적지 설정

    no offset

    하남 만두집에서 식사를 하다가 알게된 사실은, 남이섬은 생각보다 너무 멀다는 것이었습니다. 식사를 마치고 남이섬에 가면, 충전도 제대로 못하고 돌아올 판이었습니다.

    식사를 하면서 다른 목적지를 알아봤는데, 가브리엘이 예전에 가봤던 곳 중에서 남양주의 물의 정원이 시간을 떼우기 좋다는 소리를 하였습니다. 따라서 물의 정원을 검색해보았습니다.

    놀랍게도 물의정원은 검색결과에 없었습니다!

    어쩔 수 없이 카카오 지도로 물의 정원 위치를 확인하여 주소를 알아내었고, 이 주소를 카페인 검색창에 넣었습니다. 저희는 이 과정에서 카페인 서비스는 업체명 조회가 안된다는 것이 치명적인 단점이라고 생각했습니다. 다만, 이 기능은 검색 할 때마다 많은 비용이 청구되어 현실적으로 지금 당장 기능을 넣는 것은 어렵다고 판단했습니다.

    결국 주소 검색을 통해 물의 정원과 가장 가까운 충전소를 알아내었습니다.

    그런데! 지도를 축소해서 확인해 보니 해당 충전소는 물의 정원과 생각보다 멀었습니다.

    no offset

    no offset

    무려 걸어서 30분이나 걸리는 충전소였습니다!

    전기차 충전을 위해 왕복 1시간이나 걸리는 거리를 걸을 수 없다고 생각하였습니다.

    물론 지난 체험에서 전기차가 생각보다 배터리가 오래간다는 사실을 알고 있었지만, 만약 저희처럼 충전이 급한 사용자라면 목적지를 포기할 수 밖에 없겠구나 라는 생각이 들었습니다.

    마지막으로 정한 목적지는, 의외의 결정이었습니다.

    굉장히 발전된 첨단 도시로 알려진 판교였습니다!

    사실은 앞으로 갈지도 모르는 판교를 미리 구경이나 해보자는게 이유였지만 비밀입니다(?)

    일단 판교역은 IT서비스 회사들이 많이 몰려있는 곳이었습니다.

    따라서 저희는 판교역을 카페인 검색창에 검색했습니다.

    no offset

    지도를 판교역으로 이동하여 외부인 개방인 충전소를 찾았는데, 판교공영주차장이 보여서 해당 충전소를 목적지로 잡고 출발했습니다.

    판교

    하남에서 판교를 가기 위해서는 서하남IC를 지나야했습니다.

    가는 길에 우리 서비스에 나오는 정보와 실제 정보가 일치하는지 점검차 서하남 간이 휴게소를 들려봤습니다.

    이 휴게소에도 충전소가 있다고 검색이 되었기 때문입니다!

    no offset

    검색 당시에는 2대의 충전기가 있다고 나왔고, 둘다 사용이 가능하다고 되어있었는데 실제로 확인해보니 일치하는 것을 확인했습니다.

    먼 길을 달려 판교에 도착하였습니다.

    주차장에 들어오기 전, 카페인 서비스를 확인해보니 판교공영주차장의 충전기 총 12기 중 10기가 사용가능한 상태였습니다.

    정작 들어와서 보니 입구부터 너무 많은 전기차들이 충전기를 사용중이었습니다.

    뭔가 이상하다 싶었지만, 아직 서버에 반영이 안된건가? 하면서 비어있는 충전기를 찾았습니다.

    no offset no offset

    충전기를 꽂고 나서 알게된 것은 카페인 서비스에 나온 충전소 회사명과 방금 꽂은 충전기 회사명이 다르다는 것이었습니다.

    알고보니 음성 인식으로 네비에 검색한 충전소는 판교공영주차장이 아닌 판교역 환승 주차장이라 엉뚱한 곳으로 온 것이었습니다!!!

    다행인 점은 우리 서비스에서 제공하는 충전기 사용 여부 정보가 잘못된 것이 아니었다는 것이었습니다.

    그래서 애초에 가고자 했던 판교공영주자창에 대한 카페인 서비스의 정보가 실제와 동일한지 확인해보러 걸어서 이동했습니다. (바로 앞에 있었기 때문입니다.)

    no offset no offset

    도착해보니 1층의 충전기들이 모두 공사중이었고, 서비스의 정보가 실제로도 불일치 하는 줄 알았습니다. 다시 상세 정보를 보니 3~6층에 충전기들에 대한 정보라는 것이 명시되어 있었고, 실제로도 이와 동일한 것을 확인했습니다.

    no offset

    저희는 시간이 너무 흘러 다시 잠실로 돌아와 차를 반납하고 체험을 마무리 했습니다.

    결론

    불편했던 점

    • 디바이스에 보여지는 지도 영역 확장시에 원하는 정보를 볼 수 없는 것이 불편했다.
      • 지도를 확대해주세요 모달이 뜨고, 원래 있던 충전소 마커가 전부 사라진다.
    • 현재 나의 위치를 알아볼 수 있는 수단이 없어 불편했다.
      • 현위치를 나타내는 핀 (1차 체험기에서도 언급했던 부분)
      • 내 위치를 상대적으로 알 수 있는 랜드마크의 부족
    • 특정 장소(매장명) 검색이 안돼서 카페인 서비스만으로 목적지를 찾아가기 불편했다.
      • 카카오맵 등을 활용해 특정 장소 검색을 진행해야 했다.

    다음 목표

    앞선 불편했던점을 개선하기 위해 다음과 같은 기능 개선을 추가로 진행할 예정입니다.

    • 디바이스에 보여지는 지도 영역 확장에 제한이 생기지 않게 충전소 마커 클러스터링을 우선적으로 도입한다.
    • 현재 나의 위치를 알아볼 수 있도록 지하철 역과 같은 랜드마커를 지웠던 것을 롤백한다.

    카페인 서비스만으로 목적지를 찾아갈 수 있도록 하기 위해서 특정 장소 검색을 추가하고 싶지만, 해당 기능을 구현하기 위해선 검색당 비용이 많이 청구되는 장소 검색 API를 추가해야 했기에 현실적으로 지금 당장 구현하기 어렵다고 판단했습니다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git a/404.html b/404.html index 116412d..b4d58ab 100644 --- a/404.html +++ b/404.html @@ -5,13 +5,13 @@ 페이지를 찾을 수 없습니다. | CAR-FFEINE - +
    본문으로 건너뛰기

    페이지를 찾을 수 없습니다.

    원하는 페이지를 찾을 수 없습니다.

    사이트 관리자에게 링크가 깨진 것을 알려주세요.

    - + \ No newline at end of file diff --git a/41.html b/41.html index c01df7e..132200b 100644 --- a/41.html +++ b/41.html @@ -5,7 +5,7 @@ 카페인 팀의 무중단 배포 | CAR-FFEINE - + @@ -13,7 +13,7 @@
    본문으로 건너뛰기

    카페인 팀의 무중단 배포

    · 약 9분
    제이

    안녕하세요! 카페인팀의 제이입니다.

    저희 카페인 팀에서 무중단 배포를 진행했습니다. 어떤 과정으로 진행을 했는지 작성해보도록 하겠습니다!


    기존 배포 방식과 문제점

    먼저 카페인 팀의 기존 배포 방식은 다음과 같습니다.

    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근하여서 기존에 띄워진 서버를 다운 시킵니다.
    4. Docker Hub에 업로드한 Docker image를 pull해서 서버를 가동시킵니다.

    이런 과정으로 배포 스크립트가 작성되어 있습니다. 하지만 이 방법은 기존 서버를 다운 시키고 새로운 서버를 띄울 때 다운 타임이 존재한다는 문제점이 있습니다.

    사용자 입장에서는 잘 사용하고 있는데 갑자기 서비스가 작동되지 않는다면 서비스에 대한 신뢰성이 낮아질 수도 있고 이런 이유로 이탈할 수도 있습니다.

    기존 문제를 해결하기

    저희는 먼저 제한된 EC2 인스턴스로 인해 롤링 배포의 장점을 가져갈 수 없었고, 카나리 방식 또한 저희 서비스에서 필요로한 전략이 아니기 때문에 비교적 롤백도 빠른 Blue/Green 전략을 선택하였습니다.

    저희의 Blue/Green 무중단 배포 시나리오는 다음과 같습니다. 편의를 위해서 [기존 서버(기존 포트) / 새로운 서버(새로운 포트)] 라는 명칭을 사용하겠습니다.


    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub 에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근해서 Docker Hub에 업로드한 새로운 버전의 Image를 pull 해옵니다.
    4. 만약 8080 포트에 기존 서버가 띄워져 있으면 8081 포트를 새로운 서버가 띄워질 포트로 지정해주고, 반대로 8081 포트에 기존 서버가 띄워져 있으면 8080 포트에 새로운 서버가 띄워질 포트로 지정해줍니다.
    5. 미리 Docker Hub에 업로드한 Docker image를 [image+port]라는 네이밍으로 pull을 한 후 새로운 포트로 서버를 가동시킵니다.
    6. 새로운 서버가 제대로 가동 됐는지 확인하기 위해서 헬스 체크를 진행합니다. 20번 동안 서버가 정상 동작하는지 Spring Actuactor를 통해서 확인을 합니다.
    7. 정상 작동이 됐음을 확인하면 현재 인스턴스에는 2대의 서버가 띄워져있고 요청은 여전히 기존 서버로 들어가게 됩니다. 따라서 Nginx를 통해 포트포워딩을 새로운 서버의 포트로 지정해주고 기존 서버는 내려줍니다.

    여기까지가 카페인 팀의 시나리오입니다. 그렇다면 하나씩 스크립트를 확인해보겠습니다. 설명은 주석으로 달아두겠습니다 :)

    backend-deploy.yml

    (Github Actions에서 사용)

    name: deploy

    # 1. prod/backend branch에 push 작업이 일어나면 해당 작업을 수행한다
    on:
    push:
    branches:
    - prod/backend

    jobs:
    docker-build:
    runs-on: ubuntu-latest
    defaults:
    run:
    working-directory: ./backend

    steps:
    # 2. 도커 허브에 로그인
    - name: Log in to Docker Hub
    uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
    with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_PASSWORD }}
    - uses: actions/checkout@v3

    # 3. JDK 17 설치 및 빌드 (프로젝트 Java version)
    - name: Set up JDK 17
    uses: actions/setup-java@v3
    with:
    java-version: '17'
    distribution: 'adopt'

    - name: Gradle Caching
    uses: actions/cache@v3
    with:
    path: |
    ~/.gradle/caches
    ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
    ${{ runner.os }}-gradle-

    - name: Grant execute permission for gradlew
    run: chmod +x gradlew
    - name: Build for asciiDoc
    run: ./gradlew bootjar

    - name: Build with Gradle
    run: ./gradlew bootjar

    # 4. 산출물을 Image로 빌드 후 Docker Hub에 Image Push하기
    - name: Extract metadata (tags, labels) for Docker
    id: meta
    uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
    with:
    images: woowacarffeine/backend

    - name: Build and push Docker image
    uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
    with:
    context: .
    file: ./backend/Dockerfile
    push: true
    platforms: linux/arm64
    tags: woowacarffeine/backend:latest
    labels: ${{ steps.meta.outputs.labels }}


    deploy:
    # 5. Self-hosted 작동 -> infra 인스턴스에서 작동됨
    runs-on: self-hosted
    if: ${{ needs.docker-build.result == 'success' }}
    needs: [ docker-build ]
    steps:

    # 6. infra 인스턴스에서 prod 인스턴스로 접근 (아래부터는 prod 서버 내에서 작업)
    - name: Join EC2 prod server
    uses: appleboy/ssh-action@master
    env:
    JASYPT_KEY: ${{ secrets.JASYPT_KEY }}
    DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
    DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
    with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USERNAME }}
    key: ${{ secrets.SERVER_KEY }}
    port: ${{ secrets.SERVER_PORT }}
    envs: JASYPT_KEY, DATABASE_USERNAME, DATABASE_PASSWORD

    script: |

    # 7. Docker Hub에서 Image를 pull해온다
    sudo docker pull woowacarffeine/backend:latest

    # 8. 만약 8080 포트가 켜져 있으면 새로운 서버의 포트는 8081로 설정
    if sudo docker ps | grep ":8080"; then
    export BEFORE_PORT=8080
    export NEW_PORT=8081
    export NEW_ACTUATOR_PORT=8089

    # 9. 만약 8081 포트가 켜져 있으면 새로운 서버의 포트는 8080로 설정
    else
    export BEFORE_PORT=8081
    export NEW_PORT=8080
    export NEW_ACTUATOR_PORT=8088
    fi

    # 10. Docker로 새로운 서버를 띄운다.
    sudo docker run -d -p $NEW_PORT:8080 -p $NEW_ACTUATOR_PORT:8088 \
    -e "SPRING_PROFILE=prod" \
    -e "ENCRYPT_KEY=${{secrets.JASYPT_KEY}}" \
    -e "DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}}" \
    -e "DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}}" \
    -e "REPLICA_DATABASE_USERNAME=${{secrets.REPLICA_DB_USER_NAME}}" \
    -e "REPLICA_DATABASE_PASSWORD=${{secrets.REPLICA_DB_USER_PASSWORD}}" \
    -e "SLACK_WEBHOOK_URL=${{secrets.SLACK_WEBHOOK_URL}}" \
    --name backend$NEW_PORT \
    woowacarffeine/backend:latest

    # 11. prod 인스턴스에 있는 bluegreen.sh 를 작동한다. (이 때 port 값을 같이 넣어준다.)
    sudo sh /home/ubuntu/bluegreen.sh $BEFORE_PORT $NEW_PORT $NEW_ACTUATOR_PORT



    bluegreen.sh

    (prod 인스턴스 내부에 존재)

    #!/bin/bash

    # 1. Github Actions를 통해 넘겨 받은 환경변수 값
    BEFORE_PORT=$1
    NEW_PORT=$2
    NEW_ACTUATOR_PORT=$3

    echo "기존 포트 : $BEFORE_PORT"
    echo "새로운 포트: $NEW_PORT"
    echo "새로운 ACTUATOR_PORT: $NEW_ACTUATOR_PORT"


    # 2. 20번 동안 헬스 체크를 진행
    count=0
    for count in {0..20}
    do
    echo "서버 상태 확인(${count}/20)";

    # 3. 새로운 서버가 작동되는지 Actuator를 통해 값을 받아옴
    STATUS=$(curl -s http://127.0.0.1:${NEW_ACTUATOR_PORT}/actuator/health-check)

    # 4. Actuator를 통해 성공적으로 서버가 띄워지지 않은 경우
    if [ "${STATUS}" != '{"status":"up"}' ]
    then
    # 5. 10초를 기다린 후 다시 헬스 체크를 진행한다.
    sleep 10
    continue
    else
    # 6. 헬스 체크를 통해 새로운 서버가 성공적으로 작동된다면 멈춘다.
    break
    fi
    done


    # 7. 20번의 헬스 체크를 하는 동안 새로운 서버가 제대로 작동되지 않은 경우 종료
    if [ $count -eq 20 ]
    then
    echo "새로운 서버 배포를 실패했습니다."
    exit 1
    fi


    # 8. 새로운 서버가 성공적으로 작동한 경우
    # Nginx를 통해 포트포워딩을 기존 포트에서 새로운 포트로 변경해준다.
    # 이 부분은 .inc 파일을 통해 Nginx에서 주입 받아서 포트만 변경해도 됩니다!
    export BACKEND_PORT=$NEW_PORT
    envsubst '${BACKEND_PORT}' < backend.template > backend.conf
    sudo mv backend.conf /etc/nginx/conf.d/
    sudo nginx -s reload


    # 9. 기존 서버를 내려주고, 도커 리소스를 정리해준다
    docker stop backend$BEFORE_PORT
    sudo docker container prune -f

    이렇게 카페인 팀에서는 무중단 배포를 도입할 수 있었습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git a/42.html b/42.html index 748571f..e50724e 100644 --- a/42.html +++ b/42.html @@ -5,7 +5,7 @@ 카페인 팀의 사용자 편의를 위한 협업 | CAR-FFEINE - + @@ -13,7 +13,7 @@
    본문으로 건너뛰기

    카페인 팀의 사용자 편의를 위한 협업

    · 약 3분

    사용자 피드백

    image

    저희 서비스를 배포하고 사용자에게 피드백을 받았는데, 축소했을 때가 많이 불편하다는 피드백이 대부분이였습니다.

    이유는 아래 화면과 같습니다

    asis

    이런 서비스를 본 적도 없고, 이런 서비스를 사용하고 싶지도 않을 것 입니다. 해당 부분의 문제를 알고 있었지만 어떻게 표현해주는 것이 좋고, 구현할 수 있는 방법이 떠오르지 않아 6차 데모데이까지 미루게 되었습니다.

    열심히 팀 회의를 한 결과 화면에 보이는 사이즈만큼 일정 범위로 나눠 충전소 개수를 보여주는 클러스터링 기능을 추가하기로 정했습니다.

    클러스터 기능 추가

    해당 기능을 간단하게 설명드리면 화면의 일정 범위로 나눠 충전소의 개수를 보여주도록 서버에서 계산하여 클라이언트로 전달하도록 했습니다. 하지만 전달한 클러스터링 마커들의 위치가 아래와 같이 예쁘게 보이지 않았습니다.

    image (5)

    화면의 크기에 비해 마커가 몇개 없는 것을 볼 수 있습니다. 이렇게 된다면 사용자는 그렇기에 클라이언트에 해당 기능을 담당한 가브리엘, 센트가 좀 더 유연하게 마커를 보여주는 것이 UX 관점에서 좋다고 얘기하여

    서버 API와 로직을 변경하여 동적으로 화면의 충전소를 클러스터하도록 변경하였습니다. 그렇게 하여 아래와 같은 화면을 제공하도록 하였습니다.

    final

    이상 협업 일화 였습니다.

    - + \ No newline at end of file diff --git a/43.html b/43.html index 494cc84..f1bbc38 100644 --- a/43.html +++ b/43.html @@ -5,7 +5,7 @@ 카페인 서비스 방문자 분석 - 2 | CAR-FFEINE - + @@ -14,8 +14,9 @@ no offset no offset no offset -no offset

    - +no offset +no offset

    + \ No newline at end of file diff --git a/44.html b/44.html index ea5f6f0..ca4dd5e 100644 --- a/44.html +++ b/44.html @@ -5,7 +5,7 @@ 카페인 레벨4 후반기 리포트 | CAR-FFEINE - + @@ -13,7 +13,7 @@
    본문으로 건너뛰기

    카페인 레벨4 후반기 리포트

    · 약 4분
    가브리엘

    지난 4주 간 변경사항 및 신규 기능을 소개합니다!!

    이번 업데이트에는 사용자 경험을 직접 수집하여 피드백을 반영하였답니다!

    마커 클러스터링

    마커 클러스터링이란?

    마커 클러스터링은 지도에 표시되는 마커들을 클러스터로 묶어서 표시하는 것을 말합니다. 마커 클러스터링을 사용하면 지도에 표시되는 마커의 수를 줄일 수 있습니다. 마커 클러스터링은 지도에 표시되는 마커의 수가 많을 때 유용하게 사용할 수 있습니다.

    어디에서 확인할 수 있나요?

    지도를 축소하는 경우, 마커가 클러스터로 묶여 표시됩니다. 클러스터를 클릭하면 해당 지역으로 확대됩니다.

    지역 클러스터링 서버 클러스터링

    도시 검색 기능

    도시 검색 기능이란?

    기존 검색창은 충전소의 이름과 주소를 기반으로 한 검색이 가능했습니다.

    이제는 대한민국의 주요 도시들을 검색할 수 있는 기능이 추가되었습니다.

    원하는 지역을 검색하고, 해당 지역으로 빠르게 이동할 수 있으며 지도 조작에 많은 도움이 됩니다.

    어디에서 확인할 수 있나요?

    검색창에 원하는 지역을 입력하면 바로 확인할 수 있습니다.

    도시 검색결과

    디자인 개선

    인포 윈도우가 개선되었어요!

    기존 인포 윈도우는 충전소의 이름과 주소만을 표시하고 있었습니다.

    이제는 사용량을 제공하며, 길찾기 기능도 제공합니다.

    인포 윈도우

    충전소 사용 통계 정보 디자인이 변경되었어요

    통계 정보

    새로워진 탭 디자인과 색상을 적용하였습니다.

    충전소 마커가 이원화 되었습니다.

    충전소 마커 충전소 마커

    지도를 축소할 수록 마커가 도로를 가리는 현상이 있어 사이즈가 대폭 축소되었습니다.

    단, 확대하는 경우에는 기존과 동일한 형태의 마커를 제공합니다.

    - + \ No newline at end of file diff --git a/5.html b/5.html index 6117e5c..db60db7 100644 --- a/5.html +++ b/5.html @@ -5,13 +5,13 @@ pr 본문에 이슈 번호를 달아주는 기능을 만들었습니다 | CAR-FFEINE - +
    본문으로 건너뛰기

    pr 본문에 이슈 번호를 달아주는 기능을 만들었습니다

    · 약 4분
    누누

    안녕하세요 우테코 카페인팀 누누입니다

    빠르게 결과부터 보고 가시죠.

    어떤 결과가 나왔나요?

    pr의 본문 끝에, 연관된 이슈 번호를 달아주는 기능을 만들었습니다.

    밑에 사진을 보시면 쉽게 이해하실 수 있을 것 같습니다.

    imgimg

    github에서 issue 번호가 pr에 담겨있다면 2가지 장점이 생기는데요.

    1. issue를 클릭했을 때, 자동으로 그 issue로 넘어갈 수 있습니다. (호버만으로 이슈에 대한 간단한 정보를 볼 수 있습니다)
    2. pr 이 merge 되었을 때, 자동으로 issue 가 close 됩니다.

    이 과정을 손으로 진행하는 것보다, 자동으로 진행하게 되면 실수도 줄어들고, 개발 과정이 편해질 것 같아서 이 기능을 제작하게 되었는데요

    중요한 점

    이 과정을 진행하려면 밑에서 소개해드릴 브랜치 네이밍 규칙이 필요합니다.

    브랜치 이름 규칙

    • 브랜치 이름은 타입/이슈번호 으로 구성합니다. ex) feat/1
    • 타입은 feat, fix, docs, refactor, test 등 여러 가지가 있을 수 있습니다.

    이렇게 했을 때, 이슈 번호를 브랜치 명에서부터 가져올 수 있기에, 자동화를 할 수 있습니다.

    이런 규칙이 아닌, feat/action 같은 형태가 된다면 issue 번호를 찾기 어렵겠죠?

    사용 방법

    작성된 코드부터 보시고, 설명을 드리겠습니다.

    아래에 작성된 코드를. github/workflows/assign_issue_number_to_pr_body.yml로 저장하시면 끝입니다.

    name: assign_issue_number_to_pr_body

    on:
    pull_request:
    types: [ opened ]
    branches-ignore:
    - develop

    jobs:
    append_issue_number_to_pr_body:
    runs-on: ubuntu-latest
    steps:
    - name: append feature number to pr body pr branch = feat/(issueNumber)
    uses: actions/github-script@v4
    with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    script: |
    const pr = await github.pulls.get({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number
    });
    const body = pr.data.body;
    const issueNumber= pr.data.head.ref.split('/')[1];
    const newBody = body + "\n\n" + "close #" + issueNumber;
    await github.pulls.update({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number,
    body: newBody
    });

    진행 과정

    1. pr 이 생성되면, pr에 대한 정보를 가져옵니다.
    2. pr의 본문을 가져옵니다.
    3. pr의 브랜치 이름에서 이슈 번호를 가져옵니다.
    4. pr의 본문에 이슈 번호를 추가합니다.
    5. pr의 본문을 업데이트합니다.

    이 과정을 통해서, 직접 pr의 본문을 수정하지 않아도, 자동으로 이슈 번호가 추가되기에, 실수를 줄일 수 있으니, 한 번 시도해 보세요

    - + \ No newline at end of file diff --git a/6.html b/6.html index 82abc62..c91ffd6 100644 --- a/6.html +++ b/6.html @@ -5,13 +5,13 @@ [DB] 대량의 데이터를 DB에 넣는 과정을 최적화해보자 | CAR-FFEINE - +
    본문으로 건너뛰기

    [DB] 대량의 데이터를 DB에 넣는 과정을 최적화해보자

    · 약 9분
    누누
    박스터

    안녕하세요 카페인팀 누누입니다

    이번에는 대량의 데이터를 DB에 넣는 과정을 최적화하는 과정에서 알게 된 내용을 공유하려고 합니다

    이번 최적화의 목표

    전기차 충전소에 대한 공공 데이터를 가져오고, 그 데이터를 DB 에 넣는 과정을 최적화해보자

    대량의 데이터를 삽입하는 과정

    저희 팀의 요구사항을 간단하게 정리하면 다음과 같습니다

    1. 대량의 데이터를 공공 데이터에서 전기차 충전소와 전기차 충전기에 대한 데이터를 가져온다
      • 충전소는 6만 개, 충전기는 23만 개의 데이터가 존재한다.
      • 한 번에 가져올 수 있는 양은 9999개 까지다.
    2. 이 데이터를 DB에 넣는다
      • 충전소와 충전기는 1:N 관계이다

    최적화 전은 어떤 상황이었는데?

    before_optimize

    위 사진을 잘 보시면 아실 수 있으시겠지만, 2000개를 저장하는데, 231.762 초가 사용되었습니다.

    물론 출력을 위한 시간도 포함되었기에, 230초 정도라고 생각하셔도 좋습니다

    1만 개라면? 231.762초 * 5 = 1,158.81초

    23만 개라면? 1158.81 * 23 = 26,652.63초

    시간으로 바꿔보면 7.4 시간이 걸린다는 것을 볼 수 있습니다

    이 과정에서 볼 수 있는 문제점

    1. 데이터를 저장할 때마다, 새로운 Transaction 이 생성된다.

    어떻게 개선할 수 있을까?

    데이터를 저장할 때마다, 새로운 Transaction 이 생성되는 것을 방지하기 위해, 전체를 하나의 트랜잭션으로 묶는다

    전체를 한 트랜잭션으로 묶은 버전

    all_in_transaction

    이 과정에서 2000개를 저장하는데 65초 가 사용되었습니다.

    1만 개라면? 65초 * 5 = 325초

    23만 개라면? 325초 * 23 = 7,475초

    시간으로 바꿔보면 2시간이 걸린다는 것을 볼 수 있습니다

    전체적으로 3배 정도 빨라졌습니다

    이 과정에서 볼 수 있는 문제점

    1. 23만 개의 저장이 모두 한 트랜잭션이 되어서, 하나가 실패하면 23만개를 새로 저장해야 하는 상황에 처한다

    어떻게 개선할 수 있을까?

    23만개의 저장이 모두 한 트랜잭션이 되는 것을 방지하기 위해, 1만 개씩 영속화시킨다

    1만 개가 한 트랜잭션으로 묶인 버전

    separateTransaction

    성능상으로 개선한 부분은 그렇게 크지 않지만, 실패했을 때, 1만 개만 다시 저장하면 되기에, 훨씬 빠르게 복구가 가능합니다.

    여기서 PageNo라는 클래스는, i를 바로 참조했을 경우, effectively final을 보장할 수 없어서 만들었습니다.

    성능은 전체를 한 트랜잭션으로 묶은 버전과 큰 차이가 나지 않습니다.

    이 과정에서 볼 수 있는 문제점

    1. id 생성 전략이 GenerationType.IDENTITY 이기에, 데이터를 저장할 때마다, DB에서 id를 생성해야 한다.

    JPA에 있는 쓰기 지연을 전혀 활용할 수 없고, DB에서 id를 생성하기 위해, DB와 매번 통신을 해야 한다.

    어떻게 개선할 수 있을까?

    id를 미리 생성해서, DB 에서 id 를 생성하는 과정을 생략한다

    ID 생성 전략을 GenerationType.Table의 형태로 바꿔서, DB에서 id를 생성하는 과정을 줄여서, 성능을 개선한다

    1만 개가 한 트랜잭션으로 묶이고, id를 미리 생성한 버전

    이때 batch size를 1000 단위로 설정해서 1000개씩 id 가 늘어나도록 설정했다

    charger_generatorstation_generator

    spring.jdbc.template.fetch-size=10000

    10000batch_size

    1자리 숫자는 앞에서부터 n(만개)를 의미하고, 2번째 숫자는 1만 개를 저장하는 데 걸린 시간(ms)을 의미합니다.

    처음 1만 개는 142초가 걸리고, 2만 개는 285초가 걸렸습니다.

    23만 개라면? 142 * 26 = 3,266초

    처음과 비교하자면 7.4시간이 걸리는 것에서 54분 정도 걸리는 것으로 개선되었습니다.

    이 과정에서 볼 수 있는 문제점

    하나의 스레드에서만 동작하기에, 성능이 개선되었지만, 여전히 느립니다.

    하나의 스레드에서만 동작하기에, 하나의 커넥션을 사용하게 됩니다.

    어떻게 개선할 수 있을까?

    여러 스레드에서 동작하게 하고, 여러 커넥션을 사용하게 합니다.

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전

    multi_thread

    이 버전에서 89991 개를 저장하는데 총 157초가 걸렸습니다.

    23만 개라면? 157 * 3 = 471초

    시간으로 바꿔보면 5분도 채 걸리지 않는 시간이죠

    이 과정에서 볼 수 있는 문제점

    hikari connection pool 사이즈를 10으로 설정했는데, 10개의 커넥션을 사용하면서 저장을 하다 보니, 10개의 커넥션을 모두 사용하고 나서, 11번째부터는 커넥션을 가져오기 위해, 기다려야 하는 상황이 발생합니다.

    어떻게 개선할 수 있을까?

    hikari connection pool 사이즈를 25로 설정해서, 25개의 커넥션을 사용하도록 합니다.

    spring.datasource.hikari.maximum-pool-size=25

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전 2

    multi_thread2

    총 13만 개의 데이터를 저장하는데, 147초가 걸리고, db 인스턴스의 cpu 사용률이 100%에 가까워져서 ec2 가 다운되었습니다.

    이 과정에서 볼 수 있는 문제점

    db의 cpu 사용량을 고려하지 않고, 23만 개가 조금 넘는 데이터를 25개의 커넥션을 활용해 저장하려고 했습니다

    결론

    1. 데이터를 저장할 때마다, transaction을 사용하지 말자
    2. 데이터를 저장할 때마다, id를 생성하지 말자
    3. 여러 스레드에서 동작하고, 여러 커넥션을 사용하자
    4. db의 cpu 사용량을 고려하자

    긴 글 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/7.html b/7.html index 834db91..e555100 100644 --- a/7.html +++ b/7.html @@ -5,14 +5,14 @@ 깃 커밋 메시지에 이슈 번호를 자동으로 입력할 순 없을까? | CAR-FFEINE - +
    본문으로 건너뛰기

    깃 커밋 메시지에 이슈 번호를 자동으로 입력할 순 없을까?

    · 약 3분
    야미

    프로젝트 브랜치명 컨벤션이 feat/이슈번호여서, 브랜치명에서 이슈번호만 가져온 다음 커밋할 때마다 커밋 메시지 아래단(footer)에 이슈 번호를 자동으로 입력해주고 싶었다. 자동으로 입력된다면 깜빡하고 이슈 번호를 안 적는 일도 없고, 시간도 단축할 수 있기 때문이다.

    아래 순서대로 진행한다면 이슈 번호 POSTFIX 자동화를 할 수 있다.

    1) 프로젝트 폴더에 .githooks 폴더 생성

    2) .githooks 폴더에 commit-msg 파일 생성

    #!/bin/bash

    COMMIT_MESSAGE_FILE_PATH=$1
    MESSAGE=$(cat "$COMMIT_MESSAGE_FILE_PATH")

    # 커밋 메시지가 없을 때, 커밋 방지
    if [[ $(head -1 "$COMMIT_MESSAGE_FILE_PATH") == '' ]]; then
    exit 0
    fi

    # 브랜치명에서 이슈 번호만 추출 ('/' 뒤에 있는 문자만 추출)
    POSTFIX=$(git branch | grep '\*' | sed 's/* //' | sed 's/^.*\///' | sed 's/^\([^-]*-[^-]*\).*/\1/')

    COMMIT_SOURCE=$2
    CURRENT_BRANCH=$(git branch --show-current)

    # [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] 👉🏻 현재 브랜치명과 POSTFIX가 똑같으면 POSTFIX 입력 방지
    # [ "$COMMIT_SOURCE" != "merge" ] 👉🏻 merge할 때, POSTFIX 입력 방지
    # [[ "$MESSAGE" != *"[#$POSTFIX]"* ]] 👉🏻 이미 POSTFIX가 존재할 때, POSTFIX 중복 입력 방지
    if [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] && [ "$COMMIT_SOURCE" != "merge" ] && [[ "$MESSAGE" != *"[#$POSTFIX]"* ]]; then
    printf "%s\n\n[#%s]" "$MESSAGE" "$POSTFIX" > "$COMMIT_MESSAGE_FILE_PATH"
    fi

    🧐 이슈 번호 추출에 사용된 명령어 설명

    • grep '*' 👉 * 표시된 브랜치(현재 위치의 브랜치)를 가져온다.
    • sed 's/_ //' 👉 * 제거
    • sed 's/(/)./\1/' 👉 / 이후의 문자만 추출
    • sed 's/^(---)._/\1/' 👉 하나의 이슈에 여러 브랜치를 만들면서 feat/10-1 이런 형태로 브랜치를 만들 경우, 첫 번째 '-' 앞 뒤만 추출 (ex. 10-1)

    3) 프로젝트 폴더에 Makefile 파일 생성

    init:
    git config core.hooksPath .githooks
    chmod +x .githooks/commit-msg
    git update-index --chmod=+x .githooks/commit-msg

    # chmod +x .githooks/commit-msg 👉🏻 macOS, 리눅스에서 스크립트 권한 부여
    # git update-index --chmod=+x .githooks/commit-msg
    # 👉 macOS, 리눅스에서 브랜치가 바뀔 때마다 스크립트 실행시켜줘야 하는 문제 해결

    4) 아래 코드 실행

    새로 git clone을 할 때마다 아래 코드를 실행시켜줘야 한다. 한 번만 실행시키면 계속 적용된다. (window 기준)

    git config core.hooksPath .githooks

    ❗macOS는 git clone 할 때마다 아래 코드를 실행시켜줘야 한다.

    make

    참고 블로그 https://blog.deering.co/commit-convention/

    - + \ No newline at end of file diff --git a/8.html b/8.html index 3686d25..87bd48c 100644 --- a/8.html +++ b/8.html @@ -5,13 +5,13 @@ 스프링에서 발생한 에러 로그를 슬랙으로 모니터링하는 방법 | CAR-FFEINE - +
    본문으로 건너뛰기

    스프링에서 발생한 에러 로그를 슬랙으로 모니터링하는 방법

    · 약 12분
    누누

    안녕하세요 카페인팀 nunu입니다.

    오늘은 스프링에서 발생한 에러 로그를 슬랙으로 모니터링하는 방법에 대해서 알아보려고 합니다.

    목차는 다음과 같습니다.

    1. 스프링에서 로그를 남기는 방법
    2. Slf4 j의 동작원리
    3. Logback의 동작원리
    4. Logback을 사용해서 슬랙으로 에러 로그를 모니터링하는 방법

    스프링에서 로그는 어떻게 찍을까?

    스프링에서 로그를 찍는 방법은 여러 가지가 있지만, 가장 간단한 방법은 System.out.println()을 사용하는 것입니다.

    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test() {
    System.out.println("test");
    return "test";
    }
    }

    당연하지만, 성능이 안 좋아서 실제 서비스에서는 사용하지 않습니다.

    스프링에서는 Slf4 j를 통해서 로그를 남길 수 있습니다.

    @Slf4j // private final Logger log = LoggerFactory.getLogger(this.getClass()); 와 같다.
    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test() {
    log.info("test");
    return "test";
    }
    }

    이 코드를 통해서 로그를 남길 수 있는데, 자동으로 콘솔에 출력이 됩니다.

    스프링에서 로깅은 어떻게 작동하는 거지?

    스프링 4까지는 Commons Logging을 사용했었습니다.

    Commons LoggingJCL이라고도 불리며, JDK Logging, Log4 j, Logback 등 다양한 로깅 프레임워크를 지원합니다.

    JCL 은 런타임에 어떤 로깅 프레임워크를 사용할지 결정할 수 있습니다.

    런타임에 어떤 로깅 프레임워크를 사용할지 결정하는 방식으로 클래스 로더에게 질의를 하는 방식으로 작동하게 되는데

    클래스 로더에게 질의를 했을 경우에 몇 가지 문제점이 생깁니다

    1. 클래스 로더에 명확한 표준이 없고, 부모 자식 모델이 있어서, 클래스 로더에 따라서 다른 결과가 나올 수 있습니다. 참고
    2. 클래스로더는 gc의 동작에 방해를 일으켜서 메모리 누수를 발생시킬 수 있습니다. 참고

    @Slf4j 어노테이션을 붙이면, 컴파일 시점에 private final Logger log = LoggerFactory.getLogger(this.getClass()); 와 같은 코드로 변환됩니다.

    스프링 5에서는 Slf4j 가 사용하는 것처럼, 컴파일 타임에 어떤 로깅 프레임워크를 사용할지 결정하는 기능을 작성했고, Commons Logging을 사용하지 않게 되었습니다.

    spring 5에서 변경되었다는 링크

    Slf4 j에 대해서 알아보자

    Slf4 j는 로깅을 위한 인터페이스를 제공하는 프레임워크입니다.(Simple Logging Facade for Java)

    컴파일 타임에, 어떤 로그 라이브러리를 사용할지 결정하는 기능을 제공합니다.

    로그 라이브러리를 바꾸려고 했을 때, 기존 코드는 하나도 건드리지 않고, 로그 라이브러리만 바꿔주면 되도록 해줍니다.

    조금 더 자세한 동작 원리를 알아보자

    only slf4j

    Slf4 j 만을 사용했을 경우 위 사진 같은 형태로 요청이 처리가 됩니다.

    Slf4 j 라는 인터페이스를 통해서 로그를 남기고, 어떤 로그 라이브러리를 사용할지는 Slf4j binding이라는 것을 통해서 결정합니다.

    Slf4j bindingSlf4j의 인터페이스를 구현하고 있지 않은 라이브러리의 구현체를 연결해 주는 역할을 합니다.

    그 구현체로 Slf4j-log4 j12-{version}. jar 같은 것이 있다.

    이와는 다르게 Logback 은 Slf4 j 를 구현하고 있기에, Slf4j binding 을 사용하지 않아도 됩니다.

    logback example

    위 사진처럼 Slf4j binding 을 사용하지 않고, Logback 바로 사용하는 것도 가능합니다.

    그렇다면 Slf4 j를 바로 사용하지 않은 코드에서 Slf4j 를 사용하려면 어떻게 해야 할까요?

    slf4j working principle

    위 사진처럼 Slf4j bridge 를 통해서 외부 라이브러리를 사용하는 것처럼 갈아 끼울 수 있습니다.

    Log4j2 를 사용하는 코드를 전혀 바꾸지 않아도, BridgeSlf4j 를 통해 Logback으로 자연스럽게 로그를 남길 수 있도록 해줍니다.

    Logback에 대해서 알아보자

    Logback 은 스프링에서 기본으로 사용될 만큼 인기 있는 로그 라이브러리입니다.

    logback 동작 과정

    공식문서에서 아주 핵심적인 동작원리를 설명해주고 있는 사진이라서 가져왔습니다.

    너무 어려워 보여서, 조금 자세하게 각각의 구성요소에 대해서 알아보도록 하겠습니다

    이에 대해 알아보도록 하겠습니다

    로그백의 구성요소

    Appender

    Appender는 로그를 어디에 출력할지를 결정하는 역할을 합니다.

    외부로부터 어떤 데이터를 받아서, 어떤 방식으로 처리할지에 대해서 전체적으로 설정할 수 있습니다.

    기본적으로 수많은 Appender 가 제공되고 있습니다.

    • ConsoleAppender
    • FileAppender
    • RollingFileAppender
    • AsyncAppender
    • DBAppender
    • SMTPAppender
    • SocketAppender
    • SyslogAppender

    저희는 Slack에 알림을 주는 것이 목적이기 때문에, SlackAppender를 사용하면 될 것 같습니다.

    하지만 SlackAppender는 제공되고 있지 않기에 직접 구현을 해야 하는데요

    이를 구현했을 때, Slack API 가 끝날 때까지, 계속 기다리고 있을 필요가 없기에, AsyncAppender를 사용하는 것이 좋을 것 같습니다.

    사용 방법은 다음과 같습니다. xml 기반으로 가능한데요

    <configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>myapp.log</file>
    <encoder>
    <pattern>%logger{35} -%kvp -%msg%n</pattern>
    </encoder>
    </appender>

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE" />
    </appender>

    <root level="DEBUG">
    <appender-ref ref="ASYNC" />
    </root>
    </configuration>

    만약 여기에 있는 기능들로 부족하다면, 직접 Appender 를 구현해서 사용할 수도 있습니다.

    직접 구현하려면 AppenderBase를 상속받아서 구현하면 됩니다.

    이 클래스는 필요한 부분이 대부분 구현되어 있고, appender 만 구현하면 바로 사용할 수 있습니다. 당연하지만 필요하다면 override 도 가능하죠

    Layout

    Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 합니다.

    Appender는 로그를 어디에 출력할지를 결정하는 역할을 하고, Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 하도록 하는 것이 이상적이지만

    Logback 은 Appender에서 Layout 을 직접 지정할 수 있도록 해주고 있습니다.

    따라서, 직접 Layout 을 만들지 않고, Appender 에서 기존에 이미 있는 패턴만 사용하려고 합니다

    Encoder

    Encoder는 Layout 과 비슷한 역할을 합니다.

    Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 하고, Encoder 는 실제 byte 형태로 변환하는 역할을 합니다.

    Slack의 webhook을 사용할 것이지만, AppenderBase를 사용하기에, 이번에는 사용할 수 없습니다.

    Filter

    Filter는 로그를 어떤 조건에 따라서 출력할지를 결정하는 역할을 합니다.

    Filter 는 Appender를 등록하며 같이 등록할 수 있는데요

    이번 프로젝트에서는 Level 이 ERROR 이상인 것만 출력하도록 하고 싶기에, LevelFilter를 사용하면 좋을 것 같습니다.

    <configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>INFO</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>
    <encoder>
    <pattern>
    %-4relative [%thread] %-5level %logger{30} -%kvp -%msg%n
    </pattern>
    </encoder>
    </appender>
    <root level="DEBUG">
    <appender-ref ref="CONSOLE" />
    </root>
    </configuration>

    와 비슷하게 사용할 수 있어 보입니다.

    그러면 실제로 프로젝트에서 error 발생 시 slack으로 알림을 주는 것을 구현해 보도록 하겠습니다.

    슬랙에 추가하는 방법

    이 블로그를 보고서 작성했습니다

    실제 구현

    구현된 결과물은 아래와 같습니다

    slack appender

    SlackAppender 구현하기

    public class SlackAppender extends AppenderBase<ILoggingEvent> {

    @Override
    protected void append(final ILoggingEvent eventObject) {
    final var restTemplate = new RestTemplate();
    final var url = "https://hooks.slack.com/services/";
    final Map<String, Object> body = createSlackErrorBody(eventObject);
    restTemplate.postForEntity(url, body, String.class);
    }

    private Map<String, Object> createSlackErrorBody(final ILoggingEvent eventObject) {
    final String message = createMessage(eventObject);
    return Map.of(
    "attachments", List.of(
    Map.of(
    "fallback", "요청을 실패했어요 :cry:",
    "color", "#2eb886",
    "pretext", "에러가 발생했어요 확인해주세요 :cry:",
    "author_name", "car-ffeine",
    "text", message,
    "fields", List.of(
    Map.of(
    "title", "우선순위",
    "value", "High",
    "short", false
    ),
    Map.of(
    "title", "서버 환경",
    "value", "local",
    "short", false
    )
    ),
    "ts", eventObject.getTimeStamp()
    )
    )
    );
    }

    private String createMessage(final ILoggingEvent eventObject) {
    final String baseMessage = "에러가 발생했습니다.\n";
    final String pattern = baseMessage + "```%s %s %s [%s] - %s```";
    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    return String.format(pattern,
    simpleDateFormat.format(eventObject.getTimeStamp()),
    eventObject.getLevel(),
    eventObject.getThreadName(),
    eventObject.getLoggerName(),
    eventObject.getFormattedMessage());
    }
    }

    이 과정에서 url을 직접 입력하시면 됩니다.

    그리고, 이렇게 만든 SlackAppender를 logback-spring.xml 에 등록하면 됩니다.

    <?xml version="1.0" encoding="UTF-8"?>

    <configuration scan="true" scanPeriod="60 seconds">
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <include resource="org/springframework/boot/logging/logback/file-appender.xml"/>
    <root level="INFO">
    <appender-ref ref="FILE"/>
    <appender-ref ref="CONSOLE"/>
    </root>
    <appender name="SLACK_APPENDER" class="racingcar.SlackAppender">
    </appender>
    <appender name="ASYNC_SLACK_APPENDER" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="SLACK_APPENDER"/>
    </appender>
    <logger name="racingcar" level="ERROR" additivity="true">
    <appender-ref ref="ASYNC_SLACK_APPENDER"/>

    </logger>

    </configuration>

    이렇게 하면, racingcar 패키지에서 에러가 발생할 때만 slack으로 알림을 받을 수 있습니다.

    결론

    slack appender

    이번 글에서는 log 레벨에 따라 slack 으로 알림을 받는 방법을 알아보았습니다.

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/9.html b/9.html index 28a431e..255f55d 100644 --- a/9.html +++ b/9.html @@ -5,7 +5,7 @@ Pull Request 시 자동으로 test 실행하기 | CAR-FFEINE - + @@ -17,7 +17,7 @@ 아까 environment 속성을 보면 test라고 설정해놓은 것을 볼 수 있습니다. 해당 환경이 여기에 적용됩니다.

    Branch rule 정의하기

    이번에는 해당 Repository의 Settings -> Branches 탭으로 들어갑니다. 그리고 원하는 branch에 들어가 edit 버튼을 누릅니다.

    그리고 사진과 같이 Require deployments to succeed before merging 속성을 클릭합니다. 그리고 아래와 같이 어떤 환경을 적용할 것인지 선택할 수 있습니다.

    이 속성은 해당 배포가 성공해야 merge 할 수 있도록 브랜치를 보호하는 기능입니다.

    그리고 저희는 frontend와 backend Job의 환경을 둘 다 test라는 이름으로 정의했기 때문에 하나의 environment만 선택해도 둘 다 적용되는 효과를 볼 수 있습니다. branch rule

    적용 후

    아래와 같이 merge가 안된다는 글과 빨간색으로 경고 표시를 해주고 있습니다. blocked

    결론

    간단한 github action을 통해서 생산성을 많이 올릴 수 있는 좋은 기능인 것 같습니다. 다른 팀들도 이 기능을 도입하여 사용하는 것을 추천드립니다.

    - + \ No newline at end of file diff --git a/archive.html b/archive.html index 08acf95..8cfb955 100644 --- a/archive.html +++ b/archive.html @@ -5,13 +5,13 @@ CAR-FFEINE | CAR-FFEINE - +
    본문으로 건너뛰기

    CAR-FFEINE

    팀 블로그

    2023

    - + \ No newline at end of file diff --git a/assets/images/report-6-038d09fa5b07bf8b8814e5e2463c4ccd.jpg b/assets/images/report-6-038d09fa5b07bf8b8814e5e2463c4ccd.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c26d104c728cbad274c49507caa2ec41103f799 GIT binary patch literal 149632 zcmeFa1z4O-lPEd_2n2Vx;O_1O4baz!(S9e!eHIIvrn*gkra*A>QI5+@60rmrU+yY3-dRmzS z0Lsb$W&i+y41f!>0wBO3SQY>YECK*ea-RGp1+gFp{x2|s?yr}Rz$cQ5($Yq%>MC-I zuVnuy`U$6$%nR8kSO9>Xy{ogj{0k~=9bGDZ@NR*gM#p z{rdZl;d=g$764}0ewFo)@BYmv6ca~hS6D76EOMKgxtPGv;e^399VC-*cf2+vE&Ds6WPjF9o0Dyn%`EO-p z5CH%xUjYCF=-<+KV*r30RsaAq`EP0TTmS$T2LRBK@z@2xMn=#@EP#ij20X!rgU5z@ z>;q83NR0^hNBB##;GV!EAU;Jxeujby%TSL6cmf9x{{#UZ5fK5#uyDSx_W%TJM4abb zQcrQ!-XT#t;c*8f|Yhh7w zN$I!Ry84F3rskHO-oE~U!J*-i>6zKN`Gv)$D^4Y#3fo5D?%IkbdC>_rwDh;js}ApL0FMky1l?=Y&hm9e|AYA|bb? z`xy<7Itc&0^ArjJE$s*-%og_fxDs6-vDjFYoI*fnT5ByRYQs?*2Ldu8WpAPcHLk>Bp{U zEYGd&PJ)SwjTd~VEG&wh4f$~=Sn&-s3TccXeHCr?a`G0&j^dV}xAt_4TH+t`I_&0r zJhs<3y00l{Y(Cl5Dm|C4m&vk$cPv4l8&jrk;|z~AJ)bH-j6c@r*7EfNGi!iRe=NJl zpCBemu^r!hBNrVc@7qJLByfFV%Uvrl9z=wq^O;HL#eS)pk@$}dG7i3t8a-`@58ufZ zl%O~i_%?2+>08#VIVRmV5uhNEox(^BRerTlxeBi54UqgSqKeIjw^wE6RSR1Nwg07e z@WW)2Tbg;Kw~&r$pEnj4Bi0iCOx&o1An1?4p%JO!L1i~bB6Dta5um=_879sloW@HqKv zXkDC|-3-c_6Oz(PU3X7OpSI$DeEEXiwj& zo?}sd^OcZEJi_3MAtPjJ6B7y2G1yJAd(>ZIFg*}m0j+? z(;;vlV)kHXNO&isg1#^avA4@m!pm27braImd`lIyifwpc@(7>k| z){O;+kjhuG;$`6B{QC904NFsBwLXnJHAQqRXV)E;%}-UR=Pi8ig8-zgP-?RZ&vR5S z?#+xiZ8s-B*NQyy^j_N38Ji3a!x_0dll%oL8MJGG6eKW6;vZ3ek8|;AZs2Q)l|Lqe z);F(Eo%gLu!R9)sre{b=84I-#k4?F+IjdIL%N=y72Gc$?4sHYi#@EBBdbi-upC&j> z_UoJ|*)V1SF3}5OAeSBrYS3U*8@Ww|RpK&VSitjd&F{a&uqPgJPZ1L`8UZq%GLT(X zum?wUkUvaIV4ijmTXI9f`mbj^zg#XVPja=5vyYj0d?9<5aeDYvtds+JbUVE&j)G!V z)S|>{InmHlPc`lP<(=XQAmY?^il6l(tR4(+z!Mf@DX*uKEf6lz$92!cJOw=o1b@&qa~Ogq+#hLWzVEy zA$^j+Z;$k5z5ql~Rb|s?^u)~pcU8r$WYxb%(kx}y{nedu!rPWDNB4k5tv6c}m=BX3 zm`Q!YS|P4gn51ITdb3wV_VuJIi32PvVaafz$gh#r1vhRrlPn(rxRgMS-l#_adAjn8 z!{vP)FZ1b&!r19$Vpa_rdG5L}y54u6qjvz@Oq}BKv-J(x{p);5+F;Euwv(Pw8li&q zn0SY2t_OG?XxN&|b2Cm9BzheRo4mZZVL}YYCjEF>z{mHE177ZDjqF6>fa}^7OOzksmKq zHd}Ja45SrkdS?;mBTcGjMTEde+<``(?y`@NW)tCKDoY^XicE+xa5-1Wj z)8s1*1ZFSy-PYJ(*LeHfhhlZA)Tr#hNafQz`HmP)!9 zlC%NwJVCQh=!Yz(>g7~!pGiFe^vxatllf(H|L|BM%`Gaw6^CSVgufBxnvO1O3;%hi zBakVWgg@@7y@v^L*MyXsKoAtWII6X1iuWkDOr}P^xgn%~jD}QGy6!fdlM_Q8 ze)?MnwWmkd@_G#UBcM-;^hX*^`hiKak4|GsG+a|OG?97VQ4_%)u+4W4)U_rt>P1)m zk#gaw)hjLvmdms$TCOxs98~N~LG>z--5$DoP2PiMi(_h>Me;(tycT59`{jKoxrFp| zmKeW#B3-7Ue{CPJUuX<7umJ<~vY6V^G?G$fkN9pIr zI^2%VT+3~weH5sms-7w`sQ8>8g;l(zLz%U5zTt{aPNYbyBAdl0(b6UiP}|GyalOQ6 z-R0!cUF#?{kfg0RR4Dh%ThCOvSzL|zD>u5+=e3R}v|6e1`zcE8E9wla?z(wJNf>rLD2Wo>+5UFYhB@ z3jri&pZzV@(upa7lJY3Kgm?@s#gL8yQOLh%)o3eZ_8y zyD~`SHAnEA#{Nwzc4^`E_#|P@lePf2v&bxwIjE9A0H2tM%uJhrH+?#SR!W@f&4T8S z3Z^gf+R)F9{QZl>O|0H*rm%$yW^~=XH94lr0-J05iv^Xyz0>JY+0KPDkq8g_i=9T4 z=Z&|H!Nm!odH?`pE%s#-01bs@myH8h)aM_bO7V0CN=X+Qy3RDo?j|yPO66VkZP1(`pS)Y8umRdh_dG~6fjbA#{V=httb7afx)9DS_Z0gWRJdu~&wa~@Q1X+_ zNgKI10y66p`xILzz0gJ#`zDnEe$akHR8EuTX*S&k+Xw%j@{Qb@}EX|#LsVV z@bxiUn<9Pkns4aFbKhC;wYTq%sVaHy3F63WSm`spFMc*ko|A#$_^d4X{B;DFQWwl# zv^na4b3kxI=m<&KR#7^N;rCV%#-G`1ianiYsiq59A*DQn?I9fHn3kZgAYFdsdeHYB z9W#VVjh*Lo(}`0z6P6qg1Jk}aqvuL-UnE~N!3)uDIfqPC7gxnxZNyS=cFaLl1VSBT zqYX*ho2%<)#si8EX;f2&jz5ih*>~tm?VyF@(n+E%jk6q{p&y=AK|GWe%<2-muUNF% zb=QqGwO{+^mI1-O2s zqNz0`jkXZq&kw|pjAKe?t2?<{6s-JNeRV>8qtg((KB&_=WkBhaGNzaME|5RFPMUi= z1C+DlHXwD_HG*C7?5$!y@3Kd&L7nnZ!+^$&qqRtu;--J(a&`>V>1hmleYvi+)xyl! z`~te$m#wU2!Cnz*f-$5|cKc39h08TSN)peD-kYSJefeho8xm(xWOkVzHubaPcz%`P z#O@{}$ZMyBtgzlk0Q#AzFbSy&;StV?XycnU?kMyaww**JmR;7lZtkIR&Mx?Xm`*k| zhhWE!T=6o7NWRpTW+m5zWhKp#<PLkx9*Ps?uBS^n(i$X5>Jm_^*C-ZP10erw-I~koK&272%@}Uo~lwC zbs>9AqZ^SQlB+^Q4S;H_4S~J7jWxA;qkLK+^pRtTiA;sH0Af0m7R6M!+Qb}`%V9rq zPt9{SDK`<1j?}j{T1{D2q~jK~8d%&d$y}ez)ipSQTNB`l4#H%Y#fN`q7E*IJsH9^PUU>$-Xm zm}TdJ*&&ozP~~m(4-dUs%{9$+flIV86%Rv6ULn`jOm@W^yIgUfk9W4yI=3;y4H>4~ zDs<|i^n`dF(Bv)Ha~Lkf=QvwTn)FVd%MEo=RoflAqZy}dfZ}>dC7$Z{vwx|u=8++n&PvW)l{JpWQ1!D?z?eZUo{;1j-v!< ztFLbu36wq|SisGQu9OZLw7rPU?xRFGzPhJTn=b4$)jN4}_XZu!xyQ&@h1t?{HA!~h zWrI-n=0|G>0F=8H#GhMySiLGYW9`&S7Qd|9Z(`n|=uB=L701=MmH8zCfB~pXTO4cN zv_MF$J1A@yUb#A39mjJ*X-V=stAgUdRw8HEHsVB}FSDZJlk~YxzJ-f@H`sMkat>$j z>bZHX!KsUMtb`wS0=|20^hppy%|ZQ1Kg7{zT{rMOTc(-LPVIox=CO>&DV!!a@2{oF zVt*WA-KME`A!^8fxb~08BAtV3Y)KdXKnKiHwa--L60%SAoV@cwpC5FUH^ZQ2X`(R^ z>aKb|FNz9&qXr6#^r0z&c=x6mgIztMyxjS`S0bVti>K-;_Ozeu!u2pYKKU3`FF80Q zbG39iW|qchLG=EqU)@hrtKET;j1z$0+n)g;cxsZAk5l*1DrQI$$f4T^pLS*+{L00v z#mRwfGilUZp6Z(b60^cJO${;M0p+r;2lQU{5#U<|nY#a4jh;iO_}%v@)>O*coO0>u2df#IVF^kwPyxOS!+PmCWVQyXp|#@V=6O>k6P z`6x(3WxS)MLfPwp+7H|V=gB+*9LzZy{ZE5~jXPE?oPKVLZu2cZ0<5eyF0KgguRFKp zuTwG~0TOEd8#WJo-QuKOFZIuJNiUdeJLqb!dtO|Mm^jafvcgpdi9Gn_Jpwuwpq3BQGoAZ~ z9Cs)F15t49kzfBF|9>EiG5gfp&cD@@{|^mdMOh@z0>mzAW#dl#FhW#s~W?E4x zjJ7sHFbg4pGkp&XVE%ky**!B8)l3>hGRiUuz4EYHmdNXipJwuTpQ%$)mVdVqJcsBr z;fA>FcDs{x(E6G*_hRP?odr5WHpi0B!Ub1d!+p0Wg0ac!-}T8q(u%!jF#5;{zJf13 z^Yw6>a_I`hLchtT?4Fnc*MT>Y(@Mk4atz$h;tVeMNS0wtrqxth$|wV;AqOA>Ain*( z?uz@nZnUySO169$+0Xatu z0kl~y|EEA@Ltm9%Kl(XW+F3h0+biTv&qAA~;!2uC?cGK9y6~;={WJ8rBcp%S zTO!ppds*lZsjWubZK zOKKvsf_N3AcM=(rM%+NA1-Dy>^<`)p?ikNI3^CSUpRC?FX$yz-hzQhyjvyYo?UVKX zXxf#TcEx((hKg)2^6K6UvO3^J_TyK-wR@LLlkqd`U$CQGj#qzbPnn>eRLg9AyS!${ zAz$cf+znEvMaCazn$dzjppy(jQP#9M&RFL^Wnc}@$_2+{T4z0FN&bj z#`)LWjo1mtn~wlpj}U1IPF^I7H0K}Z*q^7VeXrpUtvY-pHjKtg zWUo;Zr_{Vauw9eCuqed8qcf8%u3A4OGHN31?&x-EzxVzM`eVm8aHsCA;U8a#6Q?|@ z+Lxy}CZx;D!+qh2(V3~FSl9?VAw{tHtvBwLLHG_2vtS=7lqFw;a?@8nm;QOLVb#tq}#Et7UPR%$Z`k<(%DjEXYOX zmVck#{s&L61C81tZ#7suy+Osryz-ATf9AE)4>8BWb|L-u-7EMlywSe+a*Aci>JQ`O zFGjpcL)P$nxH-k^ z^Dl%ytCVQ8cT|nsvJHcM({(P^^eQXQzOe9IwDDTsS)QG+H+QhL-1_TMD*Na|qB~K2 zo<9PfDM?%LAB~D|UEfU=={|4Ee2$~+Y?3-EvzF*i`SP5Fa#`EJQyRz-j#wDs@3N^( zE%uTt-zG?-wk^fAnR7Wkq#<~aXV@OsIA-Du7m(D@5eRk&bECvL052YNJnsOl6a*}L zBOiZFlm*1Ggc2H6e~tE?p{bsrwLtJgJM+bC)f7?*?5yiZ2AlM|cxta6L=&}7U>Kus%rZx6r*%Y%E*4kjxI<#@h1Vnm?1k0i_8I*Tbedzu=!3f#D zGxU_$5`mc9Nx<+#<-lH1al4+Ml_?{UgA223&r=bNa3nCU1yE~-m%JYr?-VL}#sK^wB(+u z)~@c>x{lpskLe%KQ}I9;u%u;!}Ck z_*F6+WzyKI<}^qC=~@DP)KMdiFPFY~h^o%Zmiz#4i6+&Gs3 z;!c$nw_$_$c(JBx@dUjR`v9rSdDZvU*f1fYeTlvYvgtOnoHdAtCO4@_d+r!FuR2pq zZ;93mlyBdH;y*;5j|YvqQpWI$vx8q+t`ml^+iJaOnbF;fDoMzhq3T5YSekrWbUs9N zKP|7106OdcZuAx$nz<#WB3~U+wd~4aR(fBv#d)8r==r@(`wd-rjfDqBfk}`q|Hf0- z3jPz0OFIko`ZfVAt;Dra&ej(1sC2<&>9A&;;&WgGe_(Vc2ThXp10DqF-4-TJRb;u3 zPWhTa(p`VE0pGlg03qS);EKo=mQgn@2eyQ89}AQmPR3uv@rNHIjbIsiE)rB9MZl@$i1rk&VhFU^+__e zB2BLO3r| zAs`v6;)|0&_GW-cVRZ9US<8wi4i2{VCJpflR4MZwaid8H$w|hV%!?A7g5CI(G2+x# z9at@sc9==dKgY{3(y4gL%~zGkV$I$KWp)^HIxSRH-UN&C5eB<5UNTzP1jpKZX{<(3 zY|h-LfuHPlhNV)A^Rt#YA!g_Sg76nIakAAm@%*YbY>SA;tMi$7OMJAb93>5Xf&S zvjdmh7OzO7`m#>XZHl*b7Wbr{2gol!*|v>(!p&tar)h3pYX^EWz_!W4#-f?6(ga4Y zEMhxLcc9Vji=qN3+K<{#^*V>(+IE`1&;L+4L&FE~RVq08>E4v6RQR%X5bvAhadTH9 zP5mHE26l8Ti$I^DD99qXinR`A9dx9bW-y+&Vz=FtY+@yZ8vpDd@u>HH9A)bzmB7cMY4E<7gfGv=WBRqBydT#6s$(VcC`u zg_Xz^=~4Qya{Z$> z)M<$huMf@&+Vn?j`5_#)QkQO#%+CIWH^oC6*l2|o$T))4{RUo8aMM`9;t;+5nCAd& z&FWKJ5Mf3a4)?b5ROL02?l!DPBaJZJ>2D<6DRt6(=T|ZFOfeMyS(yVWOe3P;^H{zCce2Gu| zK=n**?&HvRpYIhA72x-Q%*u&Ei0Vo<*;BfuFyl(wznRjNrOrj;wn!qgQ0MP; zA59YlGiZ_#6g(NSoMhuEww<=m5SAf%9EkcyDG4?+0QKQQHLg_w#+B^0x27EGLh$%~ zz2L#_TFA`<{-M0f^+tXB$Iszy`EF$9n#zGjbpSldj6NFJQ3Da*-p|h733BDi!v~Uv zsc8cdU9ebcIHAFvjO2>6{iMPaQN%^`=+5VS=C3IHiu%|_{63BlF)+YKWA3bj5+ZS; zYg@g1t0ilZD8&raOk|pi4nJZb)+B0w|B|a?3r{S%5>F{7zSJ=e~OB!ZfNe$UCuBO-rQbzd^f!5pUL%b1> zduCTqN1NEjQX1sI4c+_02{HRfyXbXTdhlt7K4QQq2W-QmYfVs_a*j3S(m4SQj>OVd z(`wU6#K--NAR{6l7l<2}RG}H9pN(-DHh+42OcnGd`{o33ti|IKTN)6-fnA-{h2wKf zoFSmf@jk=OkBT7_DqRXyuI2x5T2nskL49jZXL`q0$2?MiA>kIADN8WD>For@_{ea_ zHG3k{-C%o>H}|Mu@EX=uIa|WRQ&ifc@c3cdjLMX$hkvIg@ROu)UHuEZa1^zi^h6} zvj6m5(a-)(o$(|NAzL8`^(5fq&hbp0^nbZBomz0XrnFfX<1USrP@Fejg*{&2qr~v`X zU;l|3ImjC?XK2|C#|gN(Yxnp>zK9=)I?J0Zr`~bAV(UDPKfMq=zO~5~xVsm>>D2ZA zGm+z}b^Ip~pdkRPMeUXN`D<{WlsTd*JL>&**P$cNk>+91KWEg-EBy;~|J(vq#1`h? z*?zIxZ-V?{rQaO#Cjs`?vGtoE|6d|VSN@a#UBb1YaQ+`;T-W~uS(sK@oQw5N9)RNZ z1cfp0BUS2|A3nYk$2ydm86Ep^?yobqO^ZwnZ8Sa>j^ZXjI1S*2e|z`{sFN7Hfmm^z zNId-lGr8)ik2P^jBaYlW;$a2PxTy7U-z!Z`2jGhlj?JRHDxEa=WM(DY)Z4tKHF?(e?<8WmZ zE~#x)H!13y%AYo6~3<@H%Xi%mhAhPvZ~ZLEA;L^Exd!A`O`!xfIP_m7eKRVDkEa zK*8~tuZOxevNUIGdyfE1g}Yt=|1ms~&>>?r)d7L8}&4x zQp0D3oW+;9FT8ASK3RAmTm*LcJ_2NYJnqDxR^YHcm_~U0prt5Uln+SCBEtV=bK2EF z?{%|0dt|I>XF@*HFrwsIBBrFxx*r!*p$;Mq8tgN|K0VQeTBZzWpTwW=j33j4vMq{u zjEod)Zjxp3xvp^hs;0BXtB<9shaV(=H;Bc!&`w_abwqwk{%YtbKui~yX z!3+^lEf7aGtBOt`o`a?rHY>T+gvOtHeeK4uyd@Lv9_~PrAn$;78pV#<10u&68 zeu1z;@JU}9QiyB#mVlC3>7WzqkwZ+^n*JcK6>7q;Mx=7^Hmj_O`nT9nm(u*)(+&Ij zheyB<|DL!;-A91`rNowgT<->{^maR9J&(05M+HOT^z~eZ;*NfQ;0U>=tC^Xr(@jEh zHBaP;X$qs3&p7r6vsk1ft!GRudE2yeQz+}pJct*P427N(`G_)m?o1v63 zQULxLQdPy6Pv1;;qA9e*gJ|%jIn-qF0KenEn+9+Qzs?*>b^LC&)Ko3w9_RlozY7(~ zT;!%+<|cG&BIP0lqKiiYphpe?oprDLQFiY?<;J}fM@W=5&tkf>iQ1W5cHo6MkFcP- z?CxPbSfpqbPw<^;@~2!=*6(N%lzl^W2%8!t`AKbUCJ6;p6_>Q)`hTf&7~qL@I!N6BFjL_S@7m7Kwbp_5rq_JW)9S{Q- zkdYo_&<5xb;>VTRC!Cz*S*w+#e9`c(llrV)ZAtA493zkZRsXIbZy1 zm8ai+uNI4o3vzu+A`>lFFAMccv{$1I8gO0Xk`fWOFdOr&#uL&_4v5MFC6xKwG@k?- z@`#EMD=?rZblv4eO8OXZ1eA*tLzVB-J17nk z>#o$`@}4Lg4LKXuiJlb58-#-EWzU-b)Tc0tb?79=bl0TD)|u2onN zhQSH`)>6;(*YoQm%Hqeg6@>>{B+k9KbzH%#-@HOH!$;zgs7rmMKsSAzSkPBXx9!&y#{N|rLJF6h3!eU^ZOA#s`BHhz@N-$KmHp1Q}b4nF6K&i<6e(=Aw4>c{899|yeYnZSk$dDp(9 zueJL|8ZY1_fXzLM5bW^`X0GfkNrmNArIr^@Nu=#bdK~~M*;avD^Rb!Xje(bgczfNK zu~;=ucrFi=;IX0M6xiXtU&gy2A8B@nxd99fO!|oALh^ek>k3nWER0vEd|<;|mz~kX zP(3>al{9Ke8WB@jTUWvT$h*AHHF@@oxgHrAXr@2YjQx-x0$W90CmuZ{6wMWoU)?$D zMoMZEmG0?-=;egR^Bt!xpkmrSWylF9? zt_{t3cI(15$YM9i$UF{=_$#0;kYEEJ&8R*P+~uKE#jWHSdxzufHl;`?jHfy8ufyV zWi;AyBZF+4m|+hD5R0vJ2NX}aN!=yAbH&?&6zQ9SB8Ez`UfsoA+zn!6~~J)abopJ;!ov1-KA))mqYzQ7n-Gx%uahG6O}94ncF5J-BU z(A4@R&qBEnd8E$TC9^vci7ggW@VXMcgUyE(XJdsS+V!@QcgP}TkEjxuVwqo$SZG2* z;2#g)8Y{*!Q+D1vxQ+_hEr7lP$ApF1UK!ZP&m+!7G`G~UCP|fa6E(Zqu((FQ14*HX zdv+VIo&xS^jR+ue2JclcWPzmobE(YpHg3l^$v}I;|}oa;1mo zHPL<4W65OpjLL3VDH?c26%?7R1CjVO=r;ud+OYlE+QY6j(~IiwW9~mSDml>-yyvl_ zirkxSlJiPg|oA2va zNRZUL1I3`i?DVxIRp?1uDh^ZN?rtmtMox*QB`rKsX~uO`rZ@qV0qhXaL1v_9mF5?M zzaQZ@=-HxBoi^V4vgwP~iBP0cdOo$x>#G3KudH9+=J-?@fHt<02S18>iB+b%dEAE- zLX2c)OxUY$wNg3t54n|DV|LFjQpdEOlfStsV)ir0q1fk;BaNuyWI@rHS?%<1MguT$t^d(C=GEunRM<7m`#`gB>2T6!n~n=$NwA}U@84l*nj;)1MoB)b`!~8 zEhL`=^GlfB+L@Wg?cJ~V9HNr#SnL5PA~67)aGL!3kAMl;yQaI&{-cGW#-le=l2~}n zR6toWG@o`%ze~KPeH_LelieR8HzhxEkoi|F?%!{_`<;P)x*eVUR3+G^;bz*|-#rw; zs29V711Kd#4;2MvCzYIR)br&xo3ZR8j%+ixgmPx#HhXqko<=rvAzi6>dpb(KsD8c{qYpJOD>uM@u;6 zwbB~7FYp*S)kFCwDcKim5@)#&e9d@jQY8H-!Lmevx5=XY&KZw@znS6QnH52ARPKKG z$GGCDjgb{eT#u^Uo@U=n!rcDEm#koGh?$33tMj`$!ax9p(@*UIukB83-u(UA#G)7MCQGK-QDxg>%zbIQkC9FoDV*< z!fu<$jjYi)_1}%VcZXg6CXI(q4%?&Yq~NLPqo4J~Vd(t$+0oG93bOP#DqIbYLt`gV z$nQC0CYDNemKxMIjaEhSmGv^L0!Q_bP=@REEJy2p(hW=XV3oRDDc;Pq&E@&Nt1oH; z|1qdI2_c>*k)_%nR}Xgl$blJoVtZOem4$4NgG_zFu49fOiHu46bQk~*lKptVScMK# zN?IC)>6R_(_R`I2wWQ?Ve?c#wymcWC{%(H~=zgrIHWyACSPr1oXm{CO_~;{X{rL*{ z%Co33c273ej#RZy=V2&@whZk?RT@4%Qv3ZYnA&fqTL1b3OTrw~d-DXT?}J5feT*)u zvWfP@5?f~Vxl6zbYExiYQ_T1HW>HP~5OD^G!vL<$l+~yx>edonBuP7AKgtEReRwQ{mq>&Iu7H0CRI%G zGO>1na~~ac1dVe=gA5%h5}MP?t`8^pJ87u{XJKjP6{SCLjn3wH-?=Xkl+=xtnc1eQ z^Y?U}a5XIt!ll!QzO9B@fS5qLMUfmuP2cJ|XW1`DUA?{N)AZ7r=oOy~+S9$~LWsq@ zk$a7acZ^h~rr#I>wSZl`KrAi#6Cp_u74zaiayIS=f(HL zJh2OUBN54AT7cCerdeK_`@;4@FS@+OOq~I#ma_WM+fHF0O0&N3Y35d*FeDQ>e(Yw& z{YKpbQ9ZD9mm%UT>tz-GA^6}%YqW#UG@)m(O@8m?PqYjw;)$m8#>u>bdt*J6Ssgj=qI9c%5(d2)b05x>i^G{8P!FjZgaVLPYvn*28kG zP+eF%sAf+1M6A%PYwokIpH)(Gq^uGZpeNLA#)~AP!X3G4C0k{abdVhhxLh1(+9Q{d3|6zruMvRHl|go@O-Nmen?0fF@2T?_8oBAeKZ*pBY0QkX)E)8Ki9!Zr2bAA z&ukjJn0N^`tfI(v^J(4IR3u~d?(;J(j~vDGUgY@l{Otts_cya0tPmcVj`rFG`_5XN zUX3XEMDN6e?&~k*>h~(2_25K{;9cx)AM{;mt0|IYz379$McwOk^Os_%3VL76z)!QS zN>&t1G~$kLs+FCveHu3Vscd745CT>c_`iV5w#I8c94x+Wtgg1t<`HG`9?js2@OMY} z#Btc40oUr>#+eo|HS(JM2_O@_hKSQM+F<1$wGAn68{?4AhgqjBY@PaX+7g8TnWY7r zxY6)B1RJ=b4F3EXyrW6pcBKbeANmN@>^S)<(t8q36Onz!n-JXX`mh7bk^E(SRyI4A zYquEUeBKm#O7|`Z;2e_Fwn*~J4ZW0`&g`ds|8!O~A$rpTe`Vdr4f5t)8GjJW8N#xj z@ywo;ww@ggJZgiMio?^#eMA&X|9oiqH9&}leT@AB%ykjs4-^RTk=-omc&@kRn<~{P zFrbtkQ8fYIa#?gOO>tOIK4=#f>*t| zdUD=Z^P0w0y}Tl^tpri(X%yVStX7OHcz>&XxGlB^x~@`GtrY$aWm;z7?oI-uHFRd* zB`EOC9&y}+OJq({OA22D2F=?x_v2OhVq+}^3A?tMbMwjVgj7d$XxH-wUaJp!R@rAr4S zby?4&j8ev7$9$0rpC{OBuYb^HovyOr$1<6qcaYghk=TlM_%-FFy))U3(Is6I>0cN1 z_2~V%UhiEza4oF)6}`G85?%Ok9(I4YhN;4wJ!f2l%ahd2M$JIW5ud26sQ2`+Tur?mxnoHjUQj#i2wxZUu zkv~s+douf_9W|=)1!9zH;x$lc%1q>?Eb)a04vJ~oB+MD0b(IkCP4sodFg5FEDPi)DyYP7y+gtWmyn0*mRPG69s%4;%~wG%+H=+L@6R| z^E=r>T5t_P=F+m<`CfFZ2;r#DzPrs~8k0V&503}v40v(S&|+S)1+hC}2ZH1>oy0;5 z&kgh{Bg&r9r}62z2`$l|d%%&D*p11tNCPK&Mh$F$wv7+ZgZh4kX7ENKSJ?A@wzDTT zjZ_LrA*N6u9)dT`cfsw@66yx{R>%^fQb>c&x-|(X(TsXh7KB?CH@?nVw&eArpL|np zCYB5o?1|o;jn~^7cls!5ezz6YH??`9u7bOAaBgVjM-e1BvpKbH4|>xUYa^2S zJYPpz8joNCi`$roi@Z8*zb6g{61H^~ddWsA-`uP$p)<#qY%@VU{??|svZtS|ZTBL$ znU>RD0Vx{Tv6h$3seQY>sTCq>wV7M||TFz&p54x0#J}umD#q~Gf{~W{#@1)x(v1UZbE{ggqO8kN+oa+%bYr@DFd>;*s>T> zCOe&EFc&0;x=u>NgX^0k?{`2_bd{I<2Y9vm$gWW=Ld2STLh5RDV{Ik33zf1kO)AC% zsi%KW7Uy)mO4tM8TwTF5?_%zq*j^@nqM?$Rl?#d@-w0NR&z@6<-0PPQM%HRCw@fbX zdO8vOXg%dMXRg>pdP`wWzcY&dmkynK5jfSei2YK5$;%|2^Po0P+x(tZPMY0o!qHa#~o>(Xc_5!t{9rl{Q7U8_! zQlBZJ(bo;tHA{O=USJs?G(jX^`2xvK+F zs%4uc&xsc*n1{;6O3aEDcn*%T{oiQS%g59YCEvS86TGFXNzl)Z6{kFETsDfUa=r7u zYOe;PhZcP$b&b@iL=cuu&WJ8^AuC~AHaC0=JzAjdsLRXjHvx8r`J=Ztq)vZWuRqwMdUPlgd(0!|G4xvrkMK%(55!|3*pz~)}`uFtMBni_6VjMFhWNU-q z$Ose#N$+u7>S)|*oo^%XxM<^_VZE-^9VJd<++#eXr=!Jge9^Srw~)xttAhWdX6fJp zDeI(72w2pd)h7_27Ft%UyMgNYzu0>Zu%_BLE221(1imGK$zFjQZ)~!MxGPI8 zn{*`?nm=kjddjq$L5M!P?H%&kd)N}w=)y!<(4^$aAeafrr8dz=!}>8UOB+fz8aCMa z%1g@j3fP?wIOS~k+Qwf+38x0kGah9aiMOuJ%aon~Au!`g18?+g?CTXVF=9Z`6!O04(JmCV1< zT3=a+mbCVi*8PF+S-)Ghh~Pz%qEvbiu!)x&!2DUL_*&9Scz?4>N4|ek!q@BVn0`Fr zjc};SsY>q&$yO6}(9BzB%KLGAxLht9VIwv!tqG1j+NlpU&Gd!ePVzC2zdQ^rPxFvp zplM`tZz&zy4z_7f=Aa-OutKB{_+XlvHT~_WhmQ7XZ%1$39Fz)*xV1N;qxI0z$oUIa zW)QoU6dxc@E2PAkGeD?gDff8S`Xwe5)=@?-7_)*Db<*!12!pPxfZd z>HzdfOq!x7XQsb3Ok@_ z?Nj*!DLM9MY7GN^;Xelz{bSJKPH~lVL%gdFNJmX&(;T^cP`Sm0{`LCNt~-9QW7Ggt z!SF8V$(?=c24LG##(3Id$u8)}u{}!MEQ3zFpfYi)6mE+ds4DXBd;cw~|H83xwvAmE zWc%HU8KkY|4OnDI`|&n_qdC-b!KzVdWU5D`Tg~3qfh-bPd(4&f68ohXwLS6gpK(fg zlg$|26NovU73LqMn>)yr#0&4Cy@c-f9oq*C-dN}IgIOAc zhBTv#Fm62iN5tGc$J``y&xv1sRg$=~#L+l&w@(1Nfk$r)AZLqmdjB&@@17IgpAT2H zvzqkgH(KmTfL{ge^jWGh6s3_1Q2-NJ^mFD9SaLGUe?|&6xO+Rl31GT6Yl5GNp_?;4 zU51c1>=;z!?Ee4o#rFi^$Qxd}AWIF#vtp^6e|_-ir*xkG!jAYYGU6XEKWd<3djDD< zSB5ajE=18}a|L5o{I&SV>7`hcQjN3vBG9RV?KfA|WCW%J9(}(8B(M>HU$oy4h<|(> zZD4A2&+fg$_NrvTJ7fO=?e96lvMSnF*mcJ_ykzyujw^_Ue582k>PTosq?JabBa<{@ z2B+LaB3Cs{M?MS`UOJ+6RkS!O^*<)`+;>Pw`tZ?mNITn7?Gb7kWc$Wu-}nW4-Z^Ba{w}BnNxk>)TmL7;#A!c) zV`I&0>X+P(wxOSfY)>a`crlKQ9R%v0JZu3Rq(C4#Kf`A>jd9XeX6GQ1!U=eizzeOo z89Z?p1Y!h32vQ^reg9(|=`@6#_V@3!{P&&zvl0q$p_vkFY+U!}KhTQU1x-yrR9B4j z83}88cH73Ypk{7nsSJyC5JMX7ekfs)ga_4i^o2f78vFnLOZD zX23J_?_2+Nru=^+rVO`<4mN?D8**XFf;gaWm%%;^fARktr)+MRd8PR*&wTTPvf#C1 z;=TDNPo8M7?7NTqpg_;0zr{_CAtZpr`eQrhN(h)B+=U&@dg8X@)zMq>#icr9UIUx1 z$|CUQyr|&M7+k{YnsNk?&vG?tJ^*mgvY!F4s2tqRJY=0Y+u^Sd9B=Ren$v+_5)}Rx zCiH)!GV`nQ^`wvnrMNjxysA3tG2x`Z;s1l@ZLM}z_Y~+QG)mySH8l;FInp$H=wjnH zbdbh%Ad^TsrA3QhV@NG?XlRp*bLe0Qm$OvF@BswUZH8Kf4{;dVgRmw-USZ|><#b?F zwmN)5aZ-l$FuonS{p0K{B|e1V(xThZwl< zz!S`CYUm$~t`Pk$tk0S0WjUNlm##F2zjr-yD=l-m&Gg{RpaQghCB$qK<4B+j)K~Z| zXZkt{F7o7CeCyG2lD%&-Wx(>(C_LnZU$;P}T*hZOzSas{p3q^@xAfVrG~inN5qs|A z)(NoFmrnxV(5)LxBQFItF;V`4j254nI^PJU1|O?5$E^7TwV3D--AdoGWJXVEcUt!( zeAy<3s2P7a@%>einMe0R9<9@T2|VRwAdk}#G75!lN*SYAkw?vSE_%x>Z3xK0ovlOd zAHL{|&iKJ~izSvgSp&H`WV&a-9xT8(K5wM(c&maQ8Rwfl@Ig&7Pev+X-27Z&A!AyL z>zRRP^QFDqFR$oF=5;hOSbOCT6uswgZJ*fmjvYA_X=~4KR6@OXJ$XiSYzw#S){TXc zP>a4FDdy%~a;zKVPo@KE4`if1mX_%~l?&o*v|IT_6r z&{`L6du0jeVd!TI`-;b#u9BTWYs*6VlOy?~1Mpnv;PtORo@)v7rKIztO}f;$g%45$ zHaB-cZ;Wfv8&n7*jC-riAL&2JQ_?@mP0B9~K0V^A?3raTaRJZ0r7VE_zQzH>MX$%g z6SWvb{hketY1?CZut7Vb%Ks3I@&~vE29RM3jA`#zf{{zqU68gNrXlp#HN{_v=t2w4 z-UZY6i-Ih-yG#0_=ypn zX8jp#Xiz|3sZCAp0`LA7Sn_}7TB50WA}SPQ_Wu$izQW-+A&N?c1NCswoYFR4y^31g zKfY8D@3Y-Jr21fHS|A@N*C#iIug4ut_eBjc>5@Y)XjXB15a`P49U#Xe7U@73bIpS= zNJy?+1Nj_#ArE}!w6iEHRp_LCOeBCiQ;|Mh~)7*p6 z_G?X-AB{-N?o@M1T+2L8ushxo<29B|!KnEM0*$;+lidZS!dw74HGqwgLm`BGe|}__ zOew&*223qV15$Sv^tl{IX9Mh@z?1%V^!Ok^frvfB(>ai!nxU0Z?G*HRoch7=0gx#*9WEGoFRe5&>1@WaZ1b7LV z^dnRxbs2~+3)f>50Ffx`QgICSXUK=vkl8cHaY-5_6{iBkV*c``VA24N12P*2v}4S3 z7eu>{+K9Uh5R?EV{{a*A5-{jhfL(Tv-An`b4qy*(gpqm}ul#WIEuit=#Q#n^{?csl z;5+CJD~!ShQ5MH^A}NnyFVO4SfP7Xek&Fu(ziS2EwhaI<%w=fDL5$W~WalklOqgzy z|4EU97o>q#x&hd&-&r;v9%Td=Gxm0Lgi5S~f0fSPX@#j9a)$>nf%adA6rM+~AHk7$ z{1JMvokhcbdNRiDuUwevcfnn_}jsvmk4^$H#U+1HjS+qv?Q2!SKtBXYybQ8!wO; z6DF_=;%Z0H_u^=*$Ty3-psXICA{H)$c66JZic4Oh+^7PsxGum+V-5oXyVDqlG5di& z+g}GPW%qYM6OVR5FK09u&tZ&xxbKi&U?uJtt7EXzC|$5lFWM<94q&zB2x066w*LA) z3G{xrqNUlM6q9?Ec61(@W$@8G*Q8e&E-}3x5FbFe52|&j?U%^#B9_g5;!#tr35(1_68J-@f}uVL@R}?%NJ% zz%_IDTFI5@)6%-S+}D{zqBbyLM9h)1GkltaG_~69yVWZN4#Uzj#cxPIzIc2vzBXd+ zA~!Q>`qStN{9iJbP#_dQCdPN|5DAKKvj?UWpVbb&n_-*W~s#LRDcl;KKvxZJ@?)Z79hOaHB5 z4vis$2)VwDt$5>{CRUwwW2L0f?E((-hA}2XsVem?e=!n2=%}9Ung@rJ&;3c6OT&@f z&#PJ34}kEvxt)f{T$}xw8D*746)Gb7Hw}sI&Z-i7Ral0K>a8uLL{@kU872O(6renx z|ALXD2i>`jwDz{_Hcc)a2i;K>c>h-hW^CC!zgo zhzAo3)DuI#G|PPPxX6;TZtyb9p|lS(sLKviMQBxvuV!lP%G%fiD9Q5pYX*htHHWlL z-s5b?a+C9Lne%8i#wnU{f2uua&*tMNdZOm*TQ^7Ujpcwii|` zW&S)vCfW~OeCo%kd*rTG>fae(>3{F!zlQk#GJ`&i&g+?&cKEqQei>HpzFI@L^B@Tu zkYL;?pbWu(eu!rrr#X-x2z%5yUTR}P<`c+!zo~5cdSUY27iK+_l!xz@Tz#=D62nQo zC~m#OPl0~L%JqJJtSo?#s<thDHsWCa$3844 zTzwjz*melb{W+0+J2vn;Vh!(ZC%18Yd2hx!z35Hng&q?O{qmLxsi1-!y;xd;#U0%R zb$8tR%j^HP060#e+>=5BJ6yIWcR{ZKbl*+WU@zBuRDFPNm5_B7g2>r_y zprlX-`Hd!KKa`orUmSDBtx1yf6z3{1S;BzyA~WSww>#B-DsoBe&2&Y=7}QB+BI3yY z;0FgX5b>g>n)Hbr(Q}a>$UTiik=XM1*q(*qe=<5_2sBQLka z)x#xTABQ>GS_Zq#CrFGt*=@cf8@~k16oLH0FR1H`(O&nKma9^EZS`#*KbT621+2;k z@4JW{_sqHS)wcak)CRaF*UgB0`K!xbN|;r-W1F^q*JEjEJA`wr{XKQBD>Y3bQECA* zIRL2l;wRF@{i`!A5ovlp(mwekfw#i-rXfA1( zXd-6&=mf5&!~FCv2$3<5!utU4@(MNdhj+>P@sk~brUDyYGJmy!Gm<#pK?l0GlS7ty z9w;mQ_i_O6X#;zmF><39Im>pp_pcB7gMQZi;rb<6(r@9-|9C_Gi#wNl0(=}fVe2<8 za}d)ygn7Ai#+|Xw@1q6Z-fY!Q*xVs=crnDlV474Wk|m_bQsb`6*79U%KZNG-ppj@f z=oxdhQgj!@;_|Ig^sQl!vmsy&0>D8$O`6&_Aui%#=haEp*4v|naeW3iUEkS>E82(}u4=dK& zLewZXc~N3D-|d2`t%!!<8GO(mgX#D-|My2d92%vut=!N8>{CqFwLXW+5ZeB$FTD;tIas_P)ByH@u&Gx zUH)G1lh{@b>c*@s{E^}37>!38jOxcvA97FWzJEfgKw8Bq*QAyMuGj?%dcSdJ`f(h>!RPqz5~4pWcX*Ay@uhTey`7m`-)@An#gQ>`0fkMrB_=f;z)dEGKKMLlu)E; zbXr&DmK5)p6r%g+rU%w>7Zi|m)Xr(i(ZPvztMCmle>Ok(8X{7t{PTB5X@XRdr3xL0 zUU@jx`>XU*U`5E8-`NK24UWa9ec7H-v`m88`gydXM@Z!TmV*( z5?Bw^%4%$gNq_O~wV{VZr38!XcdYFo}U(`!Hd%WGGaE+n~Dxil=yp!sKVYSy6uN7pyQ=r^t#D8kUQ2|#x&egJ~J%0&RNs3 zE-2BU%r{%X`Eo-Wj;H2BWrhGPy3sBVe4`UbkfoTC?~cp78$H|W(NS1x+2V|Q@C=pm zj7uNEf=3q7j3)%*T(;x5PFHtAo-wV!9Fw$qYj1J-=!ngyF>kNos>tWV z-d2f}03g~`2;=4sFm5p&(2SoTtRk3*5K^ix?p~rvSi5;-4qFUcTF@g6wWi))dskH! zI(a#o*Y~@IhXw8ZS`Ey8^rG*Q{T&6P*7t;(h+>}zdR_MCAw zAiTun&VHKaMk!Hjm`89g1jQmJ1Z;c}{dyykuP&Mv*S+J8eXN=GP+kILD4)&5Clsv4 zE%_7f{fRHLoM*6-9w!MaUx-rq;^dvWs+;U||MJ7rm7d2-R)$N%oxfd~GhBBG8Qn`F zULO@AjlTaVV4qR`a%ixQHPRdZpx|SJfMw6r!xJ&%iN-KB(9_qns}x@s4=+EK7qVr` zF3YDY%VHiUbIP@HRGdw}7jMsnwY7Ph`C29Ky-ttpk^r=%Th`a)^W(=vlY6DldBjs+ z?eDv(w#U$b9Y`VNo>TM}&f?#JNn>yhupoa4N_F}perV9ppj|;(g1vIjsR++#rU35p zHb3MZ8++?DAMTtc^|O_w1zkz%e(E0QV@@%9?t)rr?y1)>Wo19rnYH)j>(OFnvSj^5bV zYdiq?{rvi6y6o1qSQE?)`ow{W@jeljH@o50_V-${ek*Fn zj90C9YJTqch5~5HP9fk7!mTqIe~Rd!`I!Qaps}4jAD>mL9lHxuxfK8P!^8?`=qrp0 z6n&3_*{i?$-ucr^+51bH>3=?e{I4``a$LRAtz{?e2}bt4nI))88r-NML{DnG^||IU z$AF^yFzg||-XD;3==-~*dYfo6ETR~FIBLNuZ6)*s)mif?#kiYPfA{lz%cDU6tyHGc zPIgT`Y%AuI8UgGEueG;I6*7Zcgr#)N1e^H^w=5>V5)AUVh*x8xf&b67@t}1IpyYB2 z!9U>zFJ-em-vvc+XO^zn#mL_f$KvQVac zj@$Ukkq<|F<7*{R8IJv30$$PIvN9h~uhuX;HZu^JWN>Y_qw+O!W{^}t0lcHO%W_x< zO+Mc(X3Zru-omv(9I9R+tc2Q-OP?3X@Mt{q(7y7}16(H|zIK$ccN8Cf>j@;-b~A#5hg~G|6wmQZJMiF|7#Xq@D~EV%X$9qIhdGAzEuRDoXTqMgQZn79KC2FtLJmn(79dE_0UQcyCHN zi81|1yP@M7tZtKQg`6&=_TwU&Yv!O)+s@b}92;PkScsYYjnev0=Mz>_Bn{u#;=L%T zr_Oe%{NN{+yISK1L9tZ}6PU1NMegx1y15SYno-%HU8ZVpi=TU~_%Q$(zqVhPnvB1U zB=^+1SvI9p_(xa14Mp2Is--o|s0etjIls%T;I#Ozx&HaPtc)N$v);!zf+1dKoji-> zTU&7|`Y!62&HObyIceVYh|G@^*OX(=biGa-+&E7WV?B#EG9O%~O1R|VuSndFo*-(C zY?;ekTgtSF5jtF2+N;PyI^D+TvQ4>Jv0N$!Q%Z5Y&zXdKR3#+cyDYc5;^U z<$z5@W(sI{6CKgTH}cMR*>|F(n<3Oq#k%F`>XHh#`CJ1NUm8E;s?tLSU8YauxSwoF zdo0yQ3diARt}IWFwsGRMqI?*~yKFNroayu9PI7#TdTfsB#x`Lh{g11=a}HHb=!R6X zei~BqNy{HP>8e3HNWclhY>a58D>jy}8NE0L^RZS}CvjJ##9QinK*hyA-nj8skOSu- z3Se;^1^&S~3G=k3YzOp^u}zkI!maHo6Cc@WE^Q#)?i-pJ5Hx7Bs^-7RKULvF|8&&l z8Yk7>C2Av`grgaZ2~?R+sIHmAAGYIflv+2(XY7J*R|@}d4u*Drm_SqBAfA$p%BnOl z1M>=!_3LAN=HDUUk!BYBXmww@KZf2yV?dfjyg4TTlwq{Ag+Lp&D+LH41R=oU{h5y$ zez_iBL2e9NhX$G1_V$-7LyC*Dm(m|)L^kiQ=aPJ>VwCjc>b}ES^~UP_Aw@A5+e*K; zfQiGuQ7-IjxxS$^9ZKq;s9+`G`qLyx1f;}Ce|dlsDO2Qc{N?dGUl&t|&toBFE>mvcyfd_$#BIL?D7LY@ z%hXJ$JRKgVS&T0dNU!c$xueD2m6(%F6vkN{Pz?NcN#?4L?E_ zekl8GX}ZCbX?(!)>@^`BnA+4Y06k1`2cU)w2MITpseO$?Kk$P?e$fiH!6hFaU*Pzx zb%aw*?I1)2#8TznFm?eepnPTKzRsQ^oUURUl+YU9AhG?{HQl5VYSG^#V&h?+pGl_6{d2lKU9&M zyR|Y}QspM2V#4EWd0*O$hQgWm#$wJPhcFXJnnb0N=NMJmJfvSFzoJj^o%(x_Idi3E zeWo7I%JeNn{;6_x!)^Xk%E$2HdHyIpm;&u6C4`6uJ1C3w(#)lTU&Lovn3;aHW!Gra zn(q0$Vk_fn`@qAf?7{V{E5{|_7!k@Fsv1I*RNCOe1=qM$*wb^n`}0)npfn<1sn;>1 z3GQ;*#=Sr@zdcp-W~uE^ZajB$t^{*O`6En6OUKZqiI3s?V`y4 zwcDgt(uyDX_p`p$ifY!l7*R7Wy_#A+0^Y2qymCXPt8!<=X1gJi>A6KyrK@6>>+0%E z)Whg!r{PoY_@du&AGoGB()9}QS=#??7c4#jvt&zF|0a>O9s#|McamDRK~bFrtX=MX zc3-bFx8vm{e=3T)q*2*E5@c)A8YI4clQKdsH0qBij%bhgsN$PF@AKeJn>A1fE7v;x zV`g=Rc3Hp0hpv0#Ai+VGN)2%fDi$kDFG1)}xUj+9>UgdA`c`CLeXRUoxwtG+`cRMW zijBZo;ekGxU|3OHJyM7U1;B_94@>#ly4(Ga7iSzTQZ3#&yT4m$`@z0t_4v*9%9In& z6t~93?Ps}mjK5{k^wd>cNb(2?>g#dQVDP|ysLFpaJOSHqM*vP)wy=pA`k|c}274Zo zG6g1;VWp9sD|yKAW&lNqG)~lm;l~E~pxbL;8Z+|EHfR4lupLkbX917q-(CvXRDb&{ z?}lT@2iB08RwRj=p|VkdYiDfwKxoo3Vd(X13uwy4azMk4M%GSHfG?_8;J-i#G~=q( zQ#J6PfAF=RGiD7}orL=J;eY2vH8$0cG7yb`uNXE7HqW80tk|UiaS^{fUgMV+h?zO2 zM}GtGe9#vFo6>ljSQ-s29S(?)K08lYa(tq-&pTeLQLad!(ltUN+4$XcII|p;K zkd46|bXO#OUtcseZ2ee`$A6aL`+ve8@XsdB_=Det_=ILw8bt&)umeJacQigqO;{{n zb&U9$aKe4!G&s+PYFuDEb@^qL>E}6Ek!S|ls|~rRbh(~*`*rp;m69Oywk1@dWXRZF zY#e}A=2f(8-y7QK{!U&)_$Fr;_upcCfc7Pb@8f!-hUi*6F%vFVOu`2hsaJK$jpk>d+H5sVVXqmy1D0R zA}pcf*2_cbV-}5U0R0E_P>%O2JPnKfhLutikaO~!IKbyFU*T{j+GMj;SEC<`mZp#H zEwE%i4!eb3L1dC}v0+6FWfE{!@5U;2`;X=Cw1o@9OeBhG1x*q?O-ljVe zU^m2 zS*awKRnV|#yLD z0Om63c-}<;7SPI;t|ocj_g#pL9AKCE;u18ffYG)em7~2?SddB`rjDr=Jv=uxh;g_uHJrQd-Z?0{~v#<+ddV7PV zyPD}Mn0%LJMQ#k|>~Kkw!i3}slvu-x05fvZ)DTKVN749lCS<~}ie%YmS}qx9VgsE= zaPBYWCy`@rS&(J?fUO)O*U-3_9xdY>(Ud`PLr!LfSfPu8 zh;x+ukBNgntTNuCcRcVXhk{Y5t@-|P>q-bep;sOrB#p)~wqy|pWtNI^9;LCC=W%*H z1AYddIJX?Egj_`YS3nHs_exJ0InQ3xC0LQKQp~1&X=8NPnp~tyYUVrO8q7c{SCu$fRTH9OS?tF0m z@~su~N-5)+0>$vh+d1DtQSuRkOM~wIb_4Q83jR@cf|2iCxMR|pSdVGue+#|FdCTI1f9U87Cq}27jFH~Icw~?8m2m^>obfLk+;H8y zZ~LsyO02t##kO}id0x)eEg~*XzJ>bAuP=1DD=auV`bs?dyd=z}vRCqHYWTU-aB-&2 zL+5!&vnO9g1Ga-je;lDKFVA0!CS=RHKTANbP``$2foBm@fbO8 zu8s-12$hZK5{}Tm0F=oQr%0vGADvSOsdqVi!@3{gbRGvEUNIjG9<~K&y^jfBAxniu z$cBrErxSi`<5T$~jy~$yxkQTlu(7}?m!0jzbI3BxP<5PP-#U`he$^piYLVY5TtKzs z>FK&7&Zmr}C&imivL{s;rzaD%Zb*!xr@9cMxHCT{ru9EfZlY%J%dq#dvw1?CuW%fB zNaAoq!tHX#6hQbC%YCV6PB2GPHv1oEbZkiryG-_VD00AQcN{0%gRMC2g_{{X$ObXM zYsrU@j+zYUo^({WpTsiYA^PymVWg+To6~eY;|k*-#ND?*VW<|sb1&qNg(PE8jKc#u%DE(g04to&M}LPZFTz>FFu(mUy|^IlEM=JtTb9zv12sx6x}&)a&XfLp+~)?CK@(@ zp=jRUr}XV*=>;x)N2dp$jN8WQgV%l%OFr9$o3Fdn(=sU!r;MfKXhYgN-ac)K{dPc{ zu_6*B7sqC(_f5Py;Vn%7wtZ2(yy3E>VlEx2xpU*JaDr@tD0?K+SwX!lmZH;kCAJfAAQLMFm_)WSUjpLw3qU zRUJ)*cei&tO8B{U3ls$q4r&J2as(VWzEF3F&pKqEF;^~)o#cc$=N2#d793?JUc4#Hn<+eD@~tPDz(jT2X)_MyCCI zt^BvV`{jPG)lckZK?pRz2r4b$3-1?CRiTN$+?%z@#GRsc@+y$4lDEzZ z(v#Ya(BIcHtSXj}!_n7}=6vqEim$iuLn9+wTalc-D*H?HRlDzZvAh^BF>!Aw;R)O4 zt!A><+nU$;;Ik@r8^Ij2P!O?7V4U+Hd~fq~W9=r&xwMdVStP+bZzkbR7=KZeh2mRm zubSbH`g70rGkePFUEjao4Tc^o4K53;#q!iuPH!Ts8gNtZ<`U?8Fwb!=F5BD)z##_% zfbrRUm>Z##hB`lr*#&9Yy{bnq%PBz&{Ohf+h&{)c%O{@W4%-%3LNGh7?}UYX<`Nofy>v=RZ$Bs=9W z{#db9?(GFa$KI9+3O2I6nh*q2| z8}F9qHKvo_10)L3S411}a~t%iK{Y_DYj0V7c)13}eL39!D8i}&()8%%hoq0yM0DU*M_iW-`zDdE)xp1g($DD11IR{?(!vt|YFWs> zFO;EZB58D?YaX}Xead6xw&EG7P5&Hq#0;5g9;RDGvN&qfT!^dapXz{0s1+m-(Jtp3 zDHD}r0Yf5KA&Nf1PksjSNd7{Hvd{Jw1Q(}F9VvtL-2v^?4M%^muv1uf= z<;`DE6YHV#4_Q#%V2`w=KB6D3>CQPhJ`Rq57oa36KiQ$-vG)T}+%(po~R#GTz-%^(H{(($6w) zaozKkF6PkRd_WotBdUsG#$g3xPA8&7U!4t@1aGBksqEkfR2KL%fMTd}ohe~gjJ)Jotvds3ZUFU0V% zyKpCv>NnriY&qYKuypheeq;ZzgVAYn1 zkmxI_mf#w7ydfymXN($kI3o4sAPbQ2H`wQ6T&CF^zwO7U2bEHbDUk>8UBY)hK~7m= zPZ#baeo~r;l0s?k6R449vTt+vWb(ls3<0vDC)v0TBDf@ak2x-~sI>UCr{j`j{Nd6{ z^Y6qlTbUYBN6!6L6zRLSv7CJ}MM*yI?ngH{`@dinZh(@9MtxoOs}iq?$aswpFF4um z-FTEc?9@!>ADL{sLn^N4H*VxSVlsTi7s5Lyuh3U1Gjh@VsQm?)MM`jl`vk-mF_UK|Vsl6{I^9R>O1^FB zRg_UJ$XWaH;xZ?E2d|DJo{1udiHXJ)Lzd=aW_7;yCu7-yOocrrFJ5VWv#8Pr*LW@^ zg*^@LY1joFEdva^0;+us2xZy}!X0S7jDO>Xaj-<9& zq0K|>a1s*gzU?Dd`BCXB#)dm2AT^D9Mg7Ov=GHDqICaA=J_I9X9FOxfN8mb)XHkxT z2HB`|X%aOwQ|~qaXt|`3SE37gRhLyXt_6oKztUWH%q)d{!@eQGLQauFg4|(j=ER3F zcFZLjWWLvDT_{}p(7A{=L?i{+gB)~@)&)x0C;PqAR7jy-{Gg7YKah7o%9v3-v1mDZ zI}=C9fqFac8dkCt|>)&+~ocN-%X?&PS|v#F*+v$PAVX z7*1HbRqzHk_Q?1N?U0(^{!3%cv(RnZD-5o;w85#at*0@(@CG^_Ht7`&3<$#fp9cio zvve24eYp|EIma|zLh^tA^i$i2e;>lX#X0kbdg1wY}p;VsEwOR1mjFh5n`T|3a;z)M8amF3PKVUI;ByK*hWi!X! zWiwVY1DY*2?S(fUJxQ_XH`47~fSf4azJCMKUzeeI>pNpX2)$>ASE!hG^BP?LE11h4 zJWGE3o)B5e@^T#>x7=ubNjmxE2U1yi*!nb1Gb5_!&Qx^u6t&X*4n<6cCU^})Dr|Ri zj>-nI2KZ+q$ImrZ(*VxSD=1CWip3?0!L{VS)`X&#lu})+8%tN-0e;nBxM-lClOwr% zd7@#<1U^>RpAZBcoc{9q+tX$1q)Y2UeJdW@-)wo8ywL{e_94b)r4wvhSAFHNmJl{( zz1|H8K>?kLzKyG~qwURNN%jKE=})sC+AM-ZF~)%i<7mn-tjnK$ApcRZyrrr+z$ubk z&#_}^xvwA)e?1&o01h|eyCu}^AmcQaI<~Xy_%gR?e?MKxhu5ENu5>8)MJ8F*GN|qo zWyrSDWNgKJjvP^P$Da$a#ONt;JU1_nl|g7a9)E2CSFI^zY@6&4rJ4@c9(Fx0b!-x~ zJzH~PDGxJ&4&4BA+VfZ)C%x34Kptt@6ufX@wfytpn z=c>`o?PZ4LaU*01m=mBS&(!W%lr!kj9JKBNG0G)UD3R_GP-KxrgqG@W2XW6C7)P=9 z90kAl9M|^YZEd%2gI{Bt6*H+JHg_Czg2vK^HE}n8|6cr3lQ!GNWgb^fcwLOp3y8zI zOCVMZONKs^U5K!Yu+zP0KlC;#)_hp>d>eWzbVaCDA_)(~eMM1*33LP^*6Ms=oJP6Q zG0UOp=`K{xMepyBJ$kcfvKq~)-<^qCeXWrvC&w%@RA#aED?AiZMShnKQOCH*(pNno zAI{Pk$LUK~eMG!_AzmVt%Eoh5$d+kGnosY?ZpI->p1I4$mXVGv7!&WKR$w#c9?0E6 zQ|hrsO>~4E5xPfek4c?vwj{0d!PK=Q#8X?12~p;R(%E2Y@B5JjpNTg$HXUy;cX!$Yxpr3BVPnbo1%rL*Ym zv|))xWr!IRQmf;?+2!^$UBoO=qs&S9LHT!@FeKPl(AIItKw&u~Yc>u%U(9BoI|dnv zKM3x`N5tNbVb@XUSZson|5(2uTRzt;*rO52XfJ(oxsDpaKT@ieGC%Yo+OGeU>)K8K zB#SWAW8+GEBdkCBjSN^JGw}Aj5(!eU6q{2g4VsHvw82uB$xD8z!{`J1iXlS;qlKb48W< z*WlJU(Ttrwz-Q&}+Xd^v%>&$_@*l5h_3u?^H%o7Yu*8xqf|? z#M)>mr1uQKoEkvU_;*3yA*E;Ccg~U|7~+yE7FiXb3YDt}qa;)@j&WRK z(+>zX2gE6cK~m^oQWLCjg?g=cX<>FqVimek3P>cm0!=xAYu)KsOcbN-00=fJ z5l487vm4vtMy~p70YsxNfG8G2RR%-_NH_|QqxoYaQc-_>Go;`NdTky;IjH=ogdW8J zX-PHXf(wLS0~DJ~E0R25t0pnh`!JLTn87VZFpQQ|4aj?AWfQ%Cfh+)qWS6}$z$GZTgdYkS{+r46(FhsUZrPgGctdB-#W7HjDH7i9eDWU#tJ}~!lXS)qdT=BXQeY4 zEHZ1H|0NA^1*o>S=xF3DFf^JL#uyL{+C}3kMC z=NV14$PS(}0(`@HyvTvB6LAB?HP`&9&Np73=(;O>W4|rg8SZ(cQG3 zy&qGbDVx)?QsazCTS2q~XQUB^69Q~kd70iZ@p|r@WlPv`Ykv1c6teXl^S%lWA)KP8 zGfq$th%SnMqh7x#Y-gUiN)kEp29EjYIMzyU72=8W00!*%-6>i!>Kf~cQ4T~FJB4g3 zoPY?3j2%Kc;10o4Y#-#45fb*-Kq-hO`C)nFQgRUl-H+{C{Rj;8##Qds#wB0Es|{F) zr@!czZ5M=+?};$cr}@&HQI)fA-=zcnV%+J!pR9xnryWnFl#K0>Gr!_|K#=>x9uMGe zB`Q0{>Tc9UA%SD1g@GH9W7`tyS7b&e&ULM<$Rq)J)v8XoD+RZiT)}oWcBoFz-r^o2 zO}(dE1OhFBVU-$uww73>meUAMmkA%yt}VXAr5l36lT1FQCAQe8$d<>EoYdN?%uO>e zDGJ{BO6~=Txycai5sSLYgc+Q?q8M~fS7JbsjX1`EQ12EnCY7YynOHPD-S=wB zVyIPlUB9+Q(K$2AdimnjywxhVM%aN|MU@>|nVg~SXNrz)^agh@fSx5sGqE{roPUU%mfT+3+J04BXKUNIXtlTQnVe<*T$gf8M( z+LiSQ_jDdVEm(hNa zDpeR=&@&w+&|F`1%w^gpB^ifpT(XZLk{&YHXG`34cd%$g$bP4<^ zbYn?@(P7xLRIH<{r<6OXC3{guBT|k1v(n3x__(kw!oi$kG_Y+wclpp<=UWg1Nmxgc zTqsAj^^qMm+Xw$79jMw9!5|@nv9$rl4b(Z8?M37%pup-0a6N>Fa}WHIA=#kS&i4xL z=>fg?JdsnUUhZkPQa2>_SEbc~3qykR>q>Zq#6tBan)LEyinX<-iYt6uUR-P)eS9eR zndd3DH_xu*;*KC2{K!ZRQh@)O7U}NCLSXUqz+^NJ-0I>l!l zmWEU5{*Tt^(T)bL2^D5CR+#neOSL$H8SQqbIzoUfv}zLf#wpKSH7i9q$#whE^JPm^ zfA;L?F-lG@LZhonjeNrXl^vl{yQ4rkUL?F=WMup4%A@H!O(iGC5p7!)_)b|P*wCfI z3S;}6L9slPqyJ^uop9;TBbAoJP*^7k_wqsu263C>N7!=~o|`9KJp3BeoEv0iu#b8? z^R5zp&T4Q981zc0Nr`V&fX;9f2w@PCBOl*R>CFA?e$7+xGWX>;;e`qlAcn(h&dPpb zogCMQI_x073*wZ^mQ^}hQxIkEcYIq;(Erel%a;AIyC7HH_Qj`VXZ)C&tE;j*6MIkT zvNr2(>iVw^n`3PSq};Jh!-|TGtGUi&gv>c){DySOM~zP=7ISeO)5Dhaq~>7t<3N6Q z$YDn!ieqGKmV}V+EoKYAV>$X)6pv@gvs}>qDBvkpc1G%*9d&dYlY!-h?|@%okA<1O z{W0JWTcDJbdB1U{5LbH5E*KlpoHT2&g#l5rlSfr2z(R009sOxarbb{r{}a8A&5th@ zs)d#=&x)3W+)MR4=AUCLGOD}|7) z16Awh@5brxt^uk1se8nKTcd%Rzbt>8QfbZa8NT4}2J3hB{r5j;6-Xl_B+`eF8V`#~ zX*IX#7YV!F!wWj<4Ij05@Dwmqzg`5}%FS;O#26B|h;cu<7-0m99P~54qpO5^<3xE8 zH&prLye?wZOv89b=6%6-Vp(~ykjaV^WSkZ=IH$}(iopa_No2sG+R^m)+NF`q*VHYt zwiAJ}_DiwKllXXt3oYX-kv^X3XUG@!L!9CSzvaw~%{;@VihbLhi_l2qxCLoC^%9yt)B8crQwTd{H?1C$aK ziXKKTJ>XiK?wKB^S6Ap8)G!ZWdgm1G;A)n2hC+T?U!`DiPF9bH!Wp+3tJ$)ViG4*d2e1X!U_;Gr3s=MS= z3RWxC3?Z0U9j2!)$@zJwk>z7|F2C@|naf37%PoA^4HyV+tXk#_lP2Lb*=t`4PJs}0 z(>qlQ(wOiDJp9q9Y%aLmT&;gI`g7jm`@u_r=WZnlofxk6r$f`L{zcw>*?3Qqt%iZn zgq_wv9g5-jt5zW~@(W7+TvN~bWKBi&WA}5J4@`wGp8q1jaY z#qRAxJqwk+=E8@Nc?VJy&K=QkIlh!R&sSYSlW+YF8KFr;&`})&ikuTrRvaf`$$z!S ze*T!G-G);iKAvz2$`P6dB}8c6f)Em7&?C*>%R2Y7n>NDZ+GK$Uxt-EK&SSkl&Gd6V z5)+&jfxn&<-^=;O?M&m3vmdAp&UqCGJ1|ExBd(EIUZuoiyX@@625a6_FFB}J|DTFT<#U6+?V0nNaU>tFH+ipY*+Bp zb7teH@~j0e*L-9#cP9Htf(85G;wW-dLo7cm!oBhXbf!m<58tEkTp3o8? z#5KM>|7%`z&b{Ye>tOAZeUJ+V7bJ|7@jmbK{O1vi$Tut}}6K`#) zw{OCkM2aZz_-Wiym20Vff>5cQiWv7n*1-Pl{SUKP8rMb=O(z>^qVAv%gCO0Rh9WA` zIVOW(AUH=fIbkml{pVD;$fexjSZ*$3ur(K$B_odc9&Md+96Zd-UM(@7oj&z6H$Kra zfbzr$!pfhYS>On8Uc^8LK0R7cfff^|(q%r;L1U#PzG9^8e=EC)gKl^G@%LSgawbe+ z_z^wLx_M!Og8$oE@*VsIx%d(uTf0*I!rih^?!*~Y;L_bvPbdxlOA%9rhxGrYFvCD7 zEbW>KR%@v#=LE;s$c=SDHT<1d5m=L$+eGDW6>>&}sexG8kBc<0-H;w0;_j9H*pR!_ zXA{GrZBT-^kMU~O4)Yk=MXrvy!5eICJ>u+gx2Y9*c-Zs@8aC~dyTtX9uq55kdT%1{ zOgiRLG^K^3)=cWJoFvP-J!9O*+< zD*xR?puLq=nKnKSs~0Ahbk(H2j=0`SYx# zFTrVxd}%`_0NPG}O;sUZ$MaQs5-d;24kJU42pYYG?wzdV=Edzrw1OQuTFu?AJNg9Q z7~_5EtL+@=TiT`=Y`7r4wXuCUCeI1OH{`P$_PfK{C zAC4<`JOAaeGUhGtD?b%+(gff6F#li5W9YrWh)xhSPgV!KS|pkzy8r2AhWCB!Q@ST6 z?hEQYXX!f$HqbZ!Dqo0AT{bZAnZ>$X)@Su{8hGa{@a~0>A(z-m;GO7G*9U~+F(e5W z6I_eNE5sr+h7kLG$i#n@EQR74k7({#dM|n!j6lT$))=q}?2b7MekMkNX=X;cMH^c-rkW`L91PRWBlXT36z^(;Bm* zlHD={g_G~Zo34bYMQhCIJuc4=;QsPaS-z&9sNON==cb)Bxx3;f4A55sduA3-(Z_$UF*@xZCyjs9f0S)Ue<`ME+H(M9Sdky4c|}vLi@-076XdO* z6_TRUHM0rVb?)ape{N8U@BUQ@i{Nq|dU0L}yXuaC61DD*NYO_5GM7fau3+fASLgg` zUOUhG_|<&$Gq4ut5VslSo=M{-=t?WdsZ|ipu^C`t<&;ogm*FmN!5Zl4FdCH7>q6?d zAjYoHvhr{UoRfSUNHn?^si1HggsG$(Vm&j_pD)Jy(_ZU4P7r$`7QFqFO|>pA<&dS2 z3k#ptRvq((F#B2e>o(PlR||3M8OCEVF25j&uS6*MDjMSbzg`I0dv>G#m2k6I59+*3 z0i5TE=19xSb@G@}KF;!Uv(@$vKR#`jO4^-g8(4kvimEpKf`#q79*Vl6c^DPZ5FX?S z_x{S_Tw=DvamU-MwvTzR?$4^2Md{c3`GIfmI`XJ~L~<4v5Vm)4z6K4AjV-?w#{8K& z*Ijo%s0=*Ul5+4|=h&$8DSJpG||4MdZh=3To;44iRuo}Hs?q_wrKLOMys+v`_iHS)OMIl^G1 zJJMK2wSD>iOVc%x?Q>l28*fELuc+o0i|u(^&a)0dZ9Cb%@%-9_qJl*L3QP5^OtJ|NFkr?uWpZYCS zn`i~($#Ki06!SB#N7wYOr(XGIs4LJpfiA|C9xEZqgCpVzyn_tz1)gy4^Ac`Sk08&Zrhj9Ey zuIvB$p8PKO7Q9aZhdK`_MIAmiIXH*@rte5N;m=|HOYy@fogAq};_ne5z+{L8Cp^1E z5HU4~VQK^*>9N7l|FA^*)j(*8KX8j6$7r=YcY&a-;bc=$MWI-@%A5Wc&z_J$V%qLqyL88|NqjM z95%5CY}~F=@9^R2J82L2BM#4H)Wez>#IrIU0WFPg+8{7ghCHZpW~hvZ#_J8V3gB7t z{Ao^Sz>!};s`vjO^TG32JzwT+RFYW2keDXW*hVXms?32#+Q7`&j5DrcfH>CSLl(ou zS79tluN+xumQ}((^oAL=7Qa=mN5+w_!XDR;!tGI9W=?dr;|d_IgJOm{$A-8SBhc>b zNksy|d3TePOFVJ^4imuxEqzJ(hwO&{R@vhw#(v?k_S@FSz*URRAfh{V9C2e9)Xysy z%t(5CsoDGSSAP76bFE=Hp8KH|wA>Z+rucjA4JsoxinkOu>myiNQr18xK{9;gD>7`P ztW}_L&|hCU^ncI}`jx&g4%jNU-Up^lN)nEQd4};rRD2Fs*tbU9FR9jwOSVG(U$tfm z*>;Zy&xqt$`N_iWKA?KOzNvKYH=jkW4Rd*&y`;TBzx`dSlA^;`D#N+RH%Sjy;=ZP; zs!>mLSip>UZ^Us?>S@DuWe|?Z71})$id16Kk}ED%aihWKV|4$ zaNZU#HK{4o&71V%NGBG}@4#n}P+Z+8Kli=VrTYLpv>L?YCY6IMRCi}%GN13f^~%m# zmX(L6{x&)eb~6o1-&IcHZ}AxOliusE7W5b{n0xIk-M3Yt{ltB&zYPAKBfGm0m)rq= z1KBWUS=0OE%lJp$LOSSx+nEU~LehudXa?iuAW1_T;qObh+vo8Ij$ogQ;MfOBNgVry zVaMJjFA!>@M=?HU6X+5(@e)77P7wq!DG&nn5}fXTZB`k*)`1s^lw7z<4AjRV0 zXJslU;LVsegHlD2=q0Y}m|T(JI<`jIdM@@v90`fEyJ0~LJylS=R|zYsk|P!!e4X2j4^yhd)|u5bA|M_mH9xjnkRXb5&R48FwBxbP2$2c&+^ug3+RtsQZxsuEEhn0jC`BJ z;er%v*Yuq5ICd8;;&&S%M=dR-bRT(viIvdI^#r=#3hG!|i?&AJ_as&cb7B)VZ1A`# z(5uC(OkmT4cIwdcipG~)b|=&{P$`23+b;j|v3c?dl#Vk8}sW3}-7m~ z+m#o*2b@2n7zP5nm3#U#*Oeq19e(>+U|(IACR#iX+vUBL@XwU&&;s%OqAHw77qm4c zB_!8jI#yntZE&fvswE1O`QY+$lI#UzW3IMKp-@Sph#t$cEdZ^ikN=hw=x+O9M0i;? zu!ZG??bUjmeHE|T-0?Qd_Fkj->~rUC=EBkquy%71#G8agfXQ1{cl){wT!q2q^G(Rk z4FzDH$%4cxkT0bZg4yw&gF$)RQ7%D*-R+(imGJV&w@RncgwvB?P92r*dG1!?gQdXt zHcL=?UXy^=Uqc=0S=P>(^h0IkQ{S2W=Dun(5(Xp`&~s`p!LJKV7En}89&T9oJg?cK z+Pzq%8D&Z)!TWPQhmU@kgS_9mfusMR*%WEHhzf_mRpq$at# zjcDPY2D@cV_LsOTG}g2bq{><*R_Fgx_~c}!2QxX!G;U|6cOjW9KGlGj1Pr)JHA8E|D9~uI-jL>lE}Y!6{%belHKj zErC6A=PEXl()N|})asgP8WxH)*ybq+78^V%IICle@k}7Vt8B=1CWZKznb^u`I<~zs zBj_bNX~corM&ar*ifOoOTsNenYj@N-;W>GS-kR9q?}=Hs;0U2<1`0p>NrmUQbq;rE ziyL)%OiV1DJ5^=VD=(Lt4FnF^%uT)=foYk~U`9veR>$|6|7!Z!!5C&qm=&eNgmA7> zE1*;TSjoaNUBfp>TIqyL^{~zy?vbir)x~W!oT)np8Z68<6Si*Vr+1rYr<-vj1>rGH z?u6yjgMK3<3?NxSF>72(m?WnbE#AzU5ObCv33 z6X(S+>kVzMlGw>wG6SxiV2! zvr}E`TfuvLkf+Y;asQQ zZGv;|Fdu9iZxtlY&$eORShw9>N!ju`PqQqD_nF z*)ccR5jlS#b%Fp4NSIGf{*7Dp+v|ef++wJS=s;ZRUm@mowoTl-bsp09Fz2Yp<=sx~ zE!U6lBtos$Vqs?aexME$@&h#Q14kQf>`l9651)Jo`KF3%c@1~#;bFl{6WfyjL^3ZG zOV3RFT$;|X_O`MXdc>li)6e+10Tl4Kb1l@0XnJ22ojLNR3AJy5O4@YC@-CmJ_y(Sc z90ZDFt^4LM;%XBXk#wehQ017u%CRMeFZ_Z%CGt7D+uCBgno@W#fU)ojzAqh(XsO04 zHM^Eq96b2leBb!t_+N?&&)`3VHj+{UF&mqPGwC=F?8JL2rj~|Zh~2G+CsFmB8Hguz zH&Y*5;3|9Qs+JzW{ck=Nq2Cy_f9vcb_@g0%vvC%xut&UhI>eRLv51#VS!ls^5t7k+ zdTGui!S-syl56@?U~&wuQK`O|;FCr;PBxua;|sxX$TBpFKkT5~RxY_|(*A9%QBeW| zqGlO6gj3FJE)Vm+KblkdIi0q*RM~VHZ0vUI6Z2AH%gQ&5SQ-&updL_!=kKUvAQG_DA4>NldaOYamSeq3w4%WBG7ld?np3S=$R?0X7KwqE%aDu`-u$N} zErJpQI~yayVbpn5Q|7adE;oHSYOvD6_ss0#pIUUPr-e}u&1Cri!pPxEv;4|~s_OG4 zGQN9?U&?%T%42*7$ZX%5er!k=^L-n!wj#q*8*77PpNmLYiRd|uJ!=l1D!B7Z`)=+_ zT?)>~KK=r~bdm%j1PNKTn^aK6-Z`H+_c$y($6rqr!NeNXLtZ1_P2&3LNq0EB<^xiF zb0ie%Z9<+TrM;bxy~6WdYNb@#0ujGOmY{M=C8OkSJ#zkXdjHh?s|SBeY&m zf^!j-gd0_78zxHg04UdEjrB@iz62MM0-Te&#Z5&_#L!1hGR_mN34=)!%}pg{&pUh_ zMf`SezvY&`#L(N;b1B|g&{glg%FIZ*B4pJ{A-^u>XyiPNaLdWrK2N?FcWzI90Ew5o zSFNNi!eQE9VCe&`HC@F{eLAgv{R;F5JPYn^eds7t2TYY+kAnDQHgh=AxLyyB55Aap z)Zc0q-H1Oc_K-!E3K#_FJVc!ZK3T{wC9AQGpCjbv<<1vY%VVLjEYh9}eh}5#uqQwh z{9X>>;jMm_oCJ@qmPa?fp8{awJ1eJ2Ybjz2?sV%ZTzqKk2@1{7VvJL9yL!5l_?N;w zq9<4oP4dUeCximznCtYA!()I8-qQDw?BxY*-gXV&H#$Qd^aXnkiER!G78&2X4-*jc zNDfq=ciLOdvupCvlpYoD81#Iextnw_0vW92@;we4^+Mk2`D(T?YVG&3dq;B6*4p^u zXved9?3Jrz1!_a(KCqKf=yjKWvVelQZPaeGe>O*xV z&B2N4rCHx0@b)@?_$u43%dFC`dnrVyjm*ma}M3;QMTr#G;`^y!eK9Z z6=zArt9t4`UGnxbnIlAIxKHc#yusYvmz%2RC6M-oc$7P!pH_vn{jWs}Fq1O}Kbfvz2^=B3l*FGu zyE)e8E8m+olEMDG`?3btW#=8O6Zzv(Vi_RB{BSB0aRjCAziz^sE}?oS^UnS08w~FP zJfS8(_d5kAL)-J$eC@ymRr5Wm*~M-&5U1$`3%7=oGQhM9v?OzWH2_Jzh$8C$=R=cX zc!xdgE#U9bFG9Mc2Lxbp$}dHb$Y$`xyYM);?zw#wd~Fa!x&~biw%@K?SVPPvfPqt{ z2kap~v;gg{3s4d;UHnhS;0Xw(s`Vj!cqN^Dk)Md309DrYKqEmcF!i#H9I>2vQ$ZI- zhYzBqPvkYLm*bmF(m$_}l;5`xvs%Xqo9;Pkoy}B?h5ocV+U!o@2cvjl)c^T^{;#L7 zJHdQ1Oj5t@Mglx`$o38l?7|`6CW?3saNkz{&xg>a=fFKNbO4ze zN_U8VDL%pfkNz37hD=%A@B=3k@y6|?gRoHbmd0{iH#^szXTQ{6UTG^Yk0#EGE=`{d zJZP_x;x?X7-`WghS5ncVexSwZdJa&7$WX=KK#ToiT;b`~@aZ<>(A;P<&yn1CKyIQx zXGsmn0vhoDt(fnEA+U!@ph27hUWcbwOCAtd*9B^G|59+XLXLoO(jk+v;jYMU!wcj8 zFTKKRc|fBOWDMV{BfoPx(g&o32_+E141fzu{$~CDHTuy%oyh-svk@T$0h?Mh@YH@0 z($@b^N9H$x)B5s+@t@SNBD=GNFjeKRkpJ!MFyaU|~m5VU)z*fSbr-Zow-ag{Qj3?^{~l;lZogSf}ZP8R5OY_d9CxZv??9 z(>aaWV^`}mvmrlqs9;h!=eARWkis)L&Is!7fw^n-Bn}m89g!|b)EH2$ff!&?FQrfs z9L#;Ab`|d>0wGhL$1RV4|E1_xzFBI!e-Ood)lw9xN9@Ns15O~syMU9KJOeSN)1&L_ zdM{+<^H|(BS2@0BDCS9OBcawdI`WHP?Ny!SJIZ57Grn(oxd?bcOvVBCs$Ms7$L#G zyR)C|8=zs5$1#ZqeP~v}rCT-=V{o;7N31>3K&batvn`D%7vj>X z&~TRlgOgNE+YR zx4kQTRI7OfK!)?0e?V1n(&*lIpb*D$Q2B$pA(xzj1##OP$S|`Pc>8dBj?-HMby{cOxoL}p4 z<6emTp(bD4$3^ny+F)F~SpCZuLiap#1-|D)joVcx1?6IkX1>Oh*C5`|InL{fKaqob z+$45S8e zmrn7MNukNRtR^ThmuKIjI_z`I_1phF7sdM`QMG8J_tF&qwN{r}H1ClZBA^P=O|!uK z^wh1bg}wRdW*6vrc3f=&4c*m)ULjfQt!uULX7EzUqMhu)sPz`Q(qse9@4YX(j$OLq z@f=`L24~8eClCY2C1+HJ|mPZAzgYb4uGijxID^NWGWcs$7c+^F?i!=Q;q_g*LU}K9y74tEx&r&?ltW< zHy9`9YR(_8I(oJCmw5PSI{Mnxelsc_+VqHP60X}lGlRCKEA)3we?#H(x;b>xlJ?DN zo0IRw)^}`7-^t9uOL&8D}ENp7tCXwoyQKKbI;>%FxsCRV3<&E1lW zzz9)xEifsu%{h>zCl#y;rf01;ACDPj+;gU`i1S<}@9-Tw8RmHZKrqjTesK7AX8O(p zoXFF9X*lka*(NLKpkH_4d#~OX3)K{Kju}al=xDp7jf_0i>)2y1I6A7RLFh+3YGTk~ zunQWO*%-&eNB${9F^^uE6}GN&j}lmf6oO~x@! zTWdjzKDBdY=q)YqPp0E9#Jll#Clnr?O6D(qE3ox{seE`3dlNd@La9td;!s`UJ#H8j ztE1fK{f{i*z;)h_$-m#22qE5>tR3yYvEv&wh)p_(f#z;B8d&6^kbK0ZMIH67Q=xB2 zj3(n)*F1aH&Z%O@+tz?s1x9X&=i7{U<GZC3Nq(~NkmP(ps(@LyJ{?I#K)2z$kxN8Yp`r!A{TJ~%g4() zo>^4P7;|Ljpj)?GTu)f=^$upz&*|gnZXA%~<^^STm~6d`+gbKK4cerRb{H~xf38d&LU%=hA#~2Misy&@ZZ2=)1S@5)MMjDbbmAx|dC_4orRoM^4Az)9jO-k79O3=w=Q^oh-SwKJ z8L%0k?U|7NzE+UVmt&`0Kc>YTag793J=j|DOlLCM&;>D(SWV8oLAIBmmz21jf(7tE zW&Or4&-kOIb#Evk{WQCqk<&rlD!S`s)T5JUu07`XVJb6XVNaTG4mB?A>+x}&uehZj zs*7c9wV@^b+$H?eIel7v4~0HD~bH??XjUhn(uS_g~ClEE&AHki@QS` zadmfs_ib=m$<{kqdS*Z8b|0J-5n)s_n%kHmgbgf8Voq|TW4_tIZo9730)7dw5y#UO z+VyFE{A%$UvzOJ?*z8{T-3|?*H$SnYvo(lQH?H&u(=LMUfakX6w4aPPwut`H`V_3`th@b%iuh7^QH3g6}!flC+l}^A4+XZ_k`8ZUmieF*)=){y51v? z;O4wdqpj=!lzkz{`D zAi*GGKkBw=-;TY@4&>{jm6NABjVjIO8_?3Ex-$b=dp8ef=% z4?!sr489HMt{DC?FS`ZMf(nZE*gcv}GBZFq%$Qc^jOt2+WnW`DV(&$UDR5#r`;n2L zi_JR4q`HHRQ$&RKbwt*V`O9WH&e>nS!BVHJ&`iL<4u$|+iM-lfrFcMLeH4ajnF zxn^y9p6KpyLU7-Let8SsD|#u4Jh4n45)aj$_(e~iQ;1#CcyR2Db0nKUfa_H>iFDKs zi2Z{FiCHG`LJ-Va=Wnky7`3w83`#WW`q*va&sU1`;jr#A0D9L9g~eoVKovVVUkROx~ZP9Ay|WNYcDmaqecPrwY1C`0`yxX_N7K`Lj zfe~|?x4SZ*$^1Z310&c0socCiAPCG4Fd+5^Dr;3GeTtgV*dVQPeV&I$y z3X9NB6ia1^MQ!V?7=W~M<=$z|*=!tbX^7Yude|@RI=dt9;HOyaBPK|+doF!1Mr?Uo}w{I#vK(C(pR0U9wudXJ$g{*kRe|i!yh{4VMKy#8J%{ zUT8m3IH3qKu)8^taj?G$txgC_5h5q#5?ANN>IN$6lQo-Ra3tdd^cgWl8qNur#4=#B)PdgwfMSApT&82e3 z@}>&&^!iE)mxmhEHU|#oc#Lq!ZfFEepi5R9YiF9<&BzYM+aK9h;(Bm>oOY%@HapOV zu-j-U5G_ephq9CLHNl`RS!EpX;_lbcdtx*wY6~`)K|5m1(tX+ zu?loQHXO4-#b~GP z-fYR%?xEE9YGSnc>%;DC+axM$WaLo-HLBVW+a;M>w-DpM24B^c)y_Tg zVDv_LyTzub@b~ETFTtDI4^>L;o_u{WFT)|o3Q?vhPBp&GvPf@DGAua&8|o!+%M;)}e=4P=x2Fjj+C*Yf zv~63!dn$=KLvAdeuSP7Qjo04YQnP!p>{;jH*@6^CDi+B zN57bcB3gKkl8k{*o4Vl6u8ud8<;;xmS(Gu~(d%mv^MixWufQfXpb&5x1Y;HSYL9<) zYED;3$oC#HquKvVvAhNQ@QD7J6LgA0xbD08EHPcP+r^u?CiOMr*LSdn>_p?o^^6bt z0kIb$CV|z@U<~3kS(@8J*nJ6Io#=N4u`4k-uy{f@=fy@}L|N`*KaZg zUYohsx%6W~78fd$6Nc48!2RaX$SisX{1LtYnELVVv$QzO3>1`TW zPS*Y`L#UgU^oQ(Gnn7^IfRU-&DvrO4W-3=#wJ@^GW`f6d-Ib}go^rMcRHio}>J5po zNGG|&Kl>vOG-O{M!~Hj)3`)1x;j}Or{z&h#gQmurY4b-iv-5NPoyNG^EBT&3xxzJn zUP6br@uAwS=uOUZgXOgD;q6G;myFW@tVr5jV`2SNHz)M`Ri*5ODfpyjlI}?6x+I+1 z=~TBS5RW*1Hz_&grT}rfUtysqt|+2a{d|CMjsK+i|qJZml;V|lu9*9@XlmyVYkkDdcN z@H)tduxsPZugNK`%qT?CIlKg40b?c3V8Ip%afHUbW8cmeP2C0hS=lUQtJ|Sfs*RD? zWpAe?Vbq=sbSdi(_c0RH+SkmIO>ul(J@1ZM?jAjvlu3G%t~~LI%_6Xn9RN%aWGlCi zlCJgfFZ$yURW%1|-3#xIK}<^tovqyU2~Y6l2+B6+;E|J$vok0WTR1DJt~$99kwHJq zeqHv6=p0vUQ}S@4@kPW*`I}fLtrYGe)d$K8CyKkyiqk7j^0JB|Yg(0Y>Q@a3K*^tdT(z;_ zy)r@UIIh7-qe`?f^cVfghqY=f3nuFXS~~8p8+T7j*{A7gvR&zMnKeN;HVzfwnM{8x zh~Xe@1fckQ52KaMw8++lZqEenyn*#gjXnM5c!{rhQZ>d?qr;=Pcx{7IDPuyEf-=4y_i2R)HiF{F=Id!qifyT-yJy}Cr z0rmQw!oDUrY2dLf|BrI-%on1EC*t+8!>?M_Mha!k#bAB%YNJjvE@pe*Ks3V!q>_3x@I)O-rK_UUqv`jQxq{5YF@#{dl>0D;xR~3b4YuXgi zgML9hmZo~4AP(d+&2o~*q{7+?T5q(vNkdsN&!jG+J$@!Tp-Ae$u;YL0q2}1AmYio1qr?4+hK*WdG47?_*~%m4T&M)RL%}aC z*qc6%`{75xwiaBbbm4?A9&R43H?`L-XDkA2oRYh0GrtC*CtlZ1Z2t++bQZi{xEUL_ z?%bEg_#u1#4s;Y6pH8~oiqs|tivV-O#KWbp8U=kyIc;H;ehvTL~_-6V=Q6MiQZ=mAISXV>CwE6T)((^ZzE8L%XeY>1j;b8 zC&h|kj_bf2v|0Xg9({xozas%cf7BP>8DlgpxCYmDKD;-tzfJ=N?V{_at<9k`1rxCw zo|itCCjRiU*;Z37ZtRZ9BSz?EsiR9@jK6VJn|jd@XBX9z#}h)2eb79_?A2}9M7G=Q zUmLadHGvPr`^u9vMvP6Kt^dO-$X?<&Nw!Czb;jKLgSy0?uo`(7>wDRJe0~)XYMe2R=F<4lD4$=W6gh+KTmRa) zaD`-!^X=q}K~Qc@COHK2h>2UR%QZW5b*TL;`P4SxEBa%2nB9)2Tq62V^!Xf>Knf~Q zO>0bBE#0SP#o3$Budra+MPc)IW3w4`bSvW@XI2>{K^*hGLuPA!R{0gxdSFCkLvRz) z>%0E~J}C_PynKkH9+eVdg@Gq+N4$CsJ4`$qX1**84S(4b8qdCW9`0i2`t|C=K1#QM zqc{l0zDti{hE@Utk8_ESa2h;T1Y7)&(VyuYFnyfvsXPrb4=uh<>2pf|40%EfAED9Wz3#7e}XE4_1#5!!u^9lcNx0iPXT zmt4MvrI(_bt$x&7+xl!IcSkNQ3b z@+BRkJoOxlKu^8Ok^S~A+|aF z6lK?V`ykEh#LjTzZvEX>#?a$e#2W9S*-dWQ-^oT6VI9&L#qH>%B+;knfP53iR`us< z@#64^TgKmFaLJtuv~&45*a8FWfw1qDFIF-pKWH~6Cs!B%E3s()cB1N@4Sr*MFHdiB z)Z=K>ghUsm71#24B8j|R+6t>ZODIECw@Q#8OXgJxSh_fnMHZE|j=2=q{GY_@%U|3Lm$WVH zahE+C7A{4&B^mgiWMbaRxgeb?MwHlQo@651_2TIBthHps5N0u^HT9kog>}uTeYU?- z9C}a3(Y?5Ox4DIENG(Phao$`CR7I{uTTRX?&|)mdYAX6wnH?D(k)*Z~wvt^ox7RuR zF8LeMUNY=xkM7QX_A*&EHw-sPumy>Whh>50+aQnqRJyrA_@M~uNSKU=u**RHIqBkG;5|uL*jr$NSreTxGzk^XIk#)IM}1;6 zOlRkq)^C4M$N)^8&>(kwU>D(IZ7v2HK0QdzrXCiod(EX}9P80NKW{9dHREq2kd?sB z&U_G_h2IsSS-(JLf^oR8^@`fHItMAL=ZSxp7*MJoE_+EIhroRt;3Y=kwT*FQ^C~Kr z6Y~pdQkxr2o>oGp3+f*Q?^g*VwlTB0QkJK#Q5E_Cty>(2XCcDBX{W0$>+5Qu#HKL7H+pj8&CqQy>1RFY_ORVE&24a}6u^P8!!)Xr4FClW zf==PGtgU2;OYwsGUkV*w+uEUfhnIe-c|7OX7ya|#IiVK7UY<0f_ztAYqV!=UW&rD= z)PFkOgq=wsT98HD!p$wSNFraGJ^iAw68p1SbnDds3CLOM;TmLSa5BpinCGHx7gA2(UGy>Lm*i+s?etTd*nX{% zav(a}S#;pXbkU3JiESRb>>)P@GV%#&`Lx~YmH~S0z`PB5C78`m4gC{xRY(fNJvZa& zeJ|`Kfnm_&Cv}qcx2ZQ@65pSV)jYaIIOn8KnrO3GiuTGX`b!~uTnH*ZbePMYUQ#Otm==#v4a~KA`N7^!y>sES=Rk zLX$$D`@AgnbZqT}?=>dYLmV0fB1UlF)gLF8wgJS92ft3w(9uCFQo;7|dP zsEel;)Z>I4V`$$gN8L6Yfn<$BIk_=!OfN)|9?^bwDQ4Ez;6beTy%D0@6JQ?Ven|so zY7B99LO;W^Zl7H@7*rsGiTZa-RDAO1pbl?;{MrMjHB?e^TE5mmm#N;uStk*mhqYvv%{w_4EKv2=}tgh}RJ_ zD%+-^P7TFL3jomlMGG40O)L=hhmu16_Je!oz zVU@PK*|?^kiLi$_T?Eki$~vnj32y<76U*1Ye@? zgzaU%@oJ4iH=;%}?b~nDXL|L#8RnDoPo-*p$_iaLAOE9dHZ*44j;Kx#F;kI_!oK2t z6^c1m=lO$W1Pkgy;Ycc3#7@o2>Q!e0v7 zJBtaYlz!=e1oj;G+{GGzra9FbmY(43V`^R>MyS+BQm$JsKMp0I0$JG5?xTEkaowtj zt#Hrpn#8>jtvFG^=bh2jO{cJ)cSiiB;5&(^42A|Mz5m)81VEr|cp43$*hgWc8!rC+AMCwn zR21vBHrglx5+!F)f@F}KlS&qmoM|OU5)jD}8YIUiBOp0v$vNjFlBLO6a+4cq+FNUX zYwfen*?Zq_pE2$ocii)>A3eIOM|E}8Tl1astvR3PnV>OMc27xdsRlr5p#Qddtd{k* zK#Is9hCHq;yvJE_iit<6<>S z^^T767?u9nmCu^L;ULMd8E_oFHGF6&z;j(-K#!AF=|hAf$@@|S*X!X55Hv`y@F9J| zd+C}>AZzagC+Jnz7gJ>pthh*M344Zjf&S7pCS9Kp=tXL>jWL~$Bh)?AVx*bG(PXRS zS#j8kQb9e^ zE4Rl~+$E0J4OG%pZ%s82xHxMRIK{SS8=RlnJCy{3XPql>t&)}=&1@Um;)#(163f}y zxEV$JppA4)!3{7TCpV)%P@!MHZF0@4=wZfr-%0*r;>~d9UH;dP=9Aw_;B99#11%@b zZ}X(cY*H4ZGQ1&xELz~%GuqqUFND%CLBsQR^9ESCSkHNdm=bck$bPiCOXwu$BH!Lt zI{+MJlcQChpmCMPcXope>t^N{rf3auj7JX`o{DJPB(@cBuCg!dxJPJSXl3-Wx$aPu zZL+i`B{pZ%T79!|uz=@HpW6=Fd6S{!jzX4eAdx6RTU`hNK$sd8*J^~HXW;;e^>^&{ zxgmA4Wcj+GX6TmIoU{Y$R_GZnaF!kiOym=IZ$^N#T9C<;ae+PNqio4eH|dZ1CUf}< z`yOvmpGnW?KY8J;It2UB*Tee*I2kARx%#pFw_Fbo7lERtPudfXvU0Dn&?ip$U-i5s z3wswmXW}zXRm-Rk@^U;Go8lQVBxc&$XEy-)5HN!z;XVt2GY&^*n#(uOl%}|}yM}8|=b2+N*iKC5PoU4$U?y+W??b zEAOYXCv@O)hy##X)d!RK(E9AHEeGM3<@L`R!`CExY}%I^zrI_L^BWfMv4OtP#XQiw zk&GrlKE8M+84^$5Lo$#BM{(^br=}bcc=(Xl=Tx5%m~n@pg1-x%7WhfsgW@68j|rfH zo?~@$)bpun*kh%xT!qC--cKkxV)flv`=4XpY~VoP)AZHLxA+&*SAck4NUn4(L8l>f z@n^~uY=1i!XvVs^A8>6-p?CM3p&72nxlDi^zoVX_spGRZKLc}71&V8PL|ub(4Xp&3 zRuvhLRTo6H&Xoi`yrd^-J(5v*yCAK<~{%7aA^YMdVG4%e{(JnRL^aJAU-!zdo_}Y?jl8XdXWC| z`al*LNDt&(L>7)A#~=0lb~$UrDIko(Ff8N_#9{(W_813f)&_~EB6!=L>?6J5jVAZE zTaGObZoaC8hP81um`%w#S-%a22Tdy?sh|i;dd!96a(Dlp>w!aVq^yrDk}&~S_nhPmR^GDm38=-8^VsqIHS$ogX==-ZU_cvimdl z^nBUU-{9g5mRyW<(}tLI1CH3WU<4ovyF#2@!mdue{L?vB!5*icK4X_s)Y9`AxLy_| zJ9JFaf}`JZoE{fB6g>g3{PP~E)*@HWx;Lw$(i2R_)?Kq!ZW?N&p;S-cX+05);kte) z-q<_PyX5Hhb#M2R&>%~KXL33%Rn=2=%wMM-M(x5@l>yQOov_YI3CA5Q$xaf;J5L8U zYC4x?YaS&R6Ae}7T7TH`+Egq~2zO@G){I;VrIwi3gQ21Bho3|qMg?fqp3Wj!J)yV@ z!#tfwLNJjmo=rjLq%+zgq!?z7|eU_qd%PwWrlou+tswj2o> zx@T;=rMVb7G(n&!Ij{9M_+@Lf#KyN@q?ne-k4ejlQWd7b&;x44&(yYGA=_(9GTAPB zF%9Otj3o&`al%_n=(EBq-n-{G9_qJkeM&OhHq|vhOF!OmA2Z=dnnud50-BN+#|Uc! z$Q`66eDbzgt!|2hd9iHh4o*)un~OE;Jtp2noo9)lwN_+${EkAb=F1ctWpy{_jk>gY z^*I$4!P`yZ48p*z@V({lNf$4Uwfpi`RF3Z1RHk~XbdguVl+eDJg~?mWZPz$#J(tX> zDB3$+U%iYBio21Ww@s?c_)$;Cr!angD+(nuIQWX2`?3H1of?7xM@G8L^)tE#k#25h z-YBCwO{2k&hjF`ct?8rsWt7_%#6H7w!|vjcq}A8iVR=P1I#iMo*Xc#?6YCO(Oq9%D z7Z!`h+HOK$HOsx-q3TPnus*hFk{mKN7}4VlaUbGU&|KjA%0A?Q{y^s8Ou|2a{r@P) z{gX-|yghOm!?_}otVt@ec$eRL(2TR1<7shk=&J_tl3jNW{+v&vtZS9{-^1PZ#&d?h zeXTAFOk?+KKb0oV8!c5oduCOSxgG+hQg``AMFt4-Y zs}c<{9zj3=4S+gqO`iDzybREPdHs*)X^h`)0KlqePyTn^n@F$t3Ho`k+&wR{0VLtA zuEKPS_8J$}i7LO!gn%cdectIKS({6u2SN`C`v~#%zaF}t@L!LyTJ zD%=whW_{%h;TkCSMOBkS&eDvhwWE9j+e^OU)Vd-Q*zMQ|a;p`QdSm6K9bFJ4ahJ!h z#+sTk{cvL907zMXT>GiYK!3?uCEfa><~G%bYv5%`#m{uKV~i{wKB0P-Qkx`u$b4(U zCjf$^F{kB9dM#cl-NgDMW07O zi4*G&uW1O%yet0rpHa<_ZFPfE2Y{WYb{+@zO&8)>%Wu1aq)M3q`G8dLHM5WSFHolh z1Q`REI-W@TnKIH1Ada~I0$nk*r2(f(@=onAZ1sAJ8s!Dq7=bz{puAC>SK%lss4^5K zg`#mkBmg4{?3w#-ulO{8d7zIJ3P(K^;|Z$)^r36qEwzjw%R&d>m%klGh~-j_r*xI> z3_Jwh`s`^#Yu(BShO=5 zC%q*ZCTe4%wYl|pM*q{t2L+$xWhz90G1Eo>10~A?q#sw+N+2iN-t>jT4%QZe2VIq6 zb1zC6BlA8~B6~O;gIrl?*gq+)0A(kP%Qc@s>s2c+OR_X?m)v2JYJnI--*L_tTEUTX zM!XMwGs##YEW^%N-!>FGzA8}9Z8A>NI-bsKG!sfH+H8jpW!#tk$qXpJu$?=glq&#* zVz~dRbXN}qAuE8x#kcnEYF5UhK8|>5*oDm_cW`^l$s|2X|Jl1f`S%J8X~4Cz>5MuG zG7f5#rX0AE^48`zS_sV+j@8HlV#egBB;MHsS3lc77wrFjhu+iV{EP7-0sZkK(lK{v zw-nxSWYUFYcufi3hY^}9;qP)zbZXwQB?J>qs=wcv;(!IPfbr+`$7$oIs>}PSH{wd~ zFPjvCCcEq3eRw?1WA9v`xqI)+IL}tDr?vy)l@HT+yT^({#1iMkg7=SeoK#~QkfhME z%nAL?6jPloKR_Y zPT7kMX9V__ueMQh0JW81y@GNV#WJb+0(SkR*b?eT zdCzok=M%?4pTYc zXIT1g7;3KUPeI%@^%u6FZof6{zRP{aQMp zG1lwEVE}RlMS2uJb?X2Yn7nh01nXu+##b^oS1-KpdB)@1Xd%+E4$9`cGH{(2v(y=h zZR@`mu7IS+x#Tf39Zf2EdU-JCe^S+lyAV6RvCS;Jd7(>o03cc0wPI;2)AfcZpQ3$x zIdS(6$An$pr^6+VLWMy-j&gZ?3UggLo)8wDh3_xtorF0)KE8sSXjnvn-{Ne$UF{&~ z`5@im#-=Y+IQojlY;V#MUMa0d-3&?Y+0n`i6&f z8H}6D2m?qBeSi;`*6T_AF=>t0py#Tp2HOv)x`hu{-dNSZFVGA?(hAC%05a3i_8^pa z{V!1CM-N9J+#4ugyO~0q)qnE^*9Z5#oXH1kuj&z)Gi=gfDOW=6BUexqRsU*_8C81V zTC0THjY5!%?A3u^$)|S2@D20sNy{u9UaC$*bnF$}7+J3O3*_DkWjU^>`{UeebD-Um zZUIag@jz0gE>b76&L0Q_?Khe$Dj6iG;mf%jo{ln8INb#bvmwIgEjnpT^MzNUn$YZp z!?D0Nsuz((A3sY7ooIhv_xorx=k!!VyS4%FKl8O-UU>fB4w!+wyTakoMvV(GM-5$n zY0I#g6i{rJ7CJ65Qsazn5dv4`f-T=a#5HGApz=060%% z3PE{4%DbWsyyauIn@$6tNfv+w2TKupJw-Sak6;07>`_a{yU@d7bhiol zV?-HzuaD5Zq?-)&Xp*@uy2VWcVRF(dWp?WEy_345P+^%6lVIAP>h|D|P;yOE-(zq_ zlB1FG#k_PXHDT6vIC*~P;zV?BC=eW#vG5BtD%}m{haax5LHa8w_YT&kp@!47{25gy z)>33Gtx?m|#fG<)52FaKf5se1E?kbqtuFUNscmcvZQC9kM^%1!Dc6^82c|#F+o+TP z!znR1E;WJ6)dBFvqr%+=W;RV>&GJlPB#P04OtE7Oy(CsSCE{Q2e*LBdA`)kH&(Qf) zM~;W(JP+=gy-NP@%0QkTx@Qiky`QSA_IEmjB|5cUk@0*a1XUl|$Gmj#Z(7m6l9PF5k z6(D8ct>>s13)_pJq@Ks%>crXvM%{XSiGhunc!q&Cl}`bO13OW+?Z2%VPLeZ7O&v8a z60{t4AdM=?Jh2Kd9KRHQ4+CEbqvGdIgP=D8ND+U%nq$mJ7gfVGjGTh@)ORSRDFYM* zke5l1F?hqJ-v5@++_keJyXRuvj{#+x`^Rm8SBebPrns0d)}5RkobZ-~q^U(NSi*pM zReJbU*zfT`^Wt|yY>ZIDMkK>^WsloR2%@}riw6O3MEB$Iu(4~mL&|`dtlRx7?y|wV zyn|mL_ccd9gcqS^Q^fq8%vK;o*|_u!hrd87$;Zp@VvHZM-0tg6{WjHq^R^}l65njF zNf~kzXAM1Aa_STIUAifmNhz6`04PKH_J}mR(MQxeLpY=j+gOF@c6&l_QS1hFr39NE zyFFC_7BGxuYB#C${%yjs>sk7CoDXHZaiJecZgu~(mZ_+C<@p^tID8(*u_tgn0rva_ z67mAzf;iCi+U?OiFT$qhl2pLjW#XV+F!YeZM+*Ybe|2o=1v7lTxN`lG!s(3ZAEfee zaCIk}e?M)7i$LypAio%gsv;?%EwDCe!LuRSBZK7eUeDbWK;E&g0>UnzF z0X@g#FDfHRA88NLx(RpdU|)oeGTuCr{?@W|sc{>bMa&X~#4JVfC%UkYdgkwn85u>; zpM{fonqn5kIn3tyK!1y%>n-~04)_Q`QxxpfkyCR)I1Wc;bKc#==)LHW#b;Apqh}DlxXg``l^aIr!KPh>+Z&M~2qVZo{MzxR8OXkR9pTvdq!; zNT6?nS#4jWC;m}U7-IcIU>WgJ+D454nKIkGOn}IPZ0Cno2N}MXZ-E#d2Z@{wD&1G(aR2y5|eyctqdmT5A}s8_Jsbh zv)N-6cW!Ex&?7R6twggOO2Hr_oWMU_IR1LrwxFZ^iU>hJk%S^I=(Wwb!azQl&5)EDFS5D-6c$KVD++{u zx1*$4E3QJG)TA9Idi{cl*qi06oGtp_?n7JIO)JZ)RC12}4tK?x-pl1uSDj1xobd|> z*q+W6ckL0n`v;-jnL4-YFC}Nz1+~u*dES!0KM|2Ytb4skUbm_K0`0t%J@07)gvUIQ z^HQcjUf<~kAgz>2({QzPPXhoDi7eFaZH|vrIWTwu{1;iZKz{PrLALdx0X~*=-Cy`; z_t*;F3`U_5pq@*$O983I4l|U(2>8uyd6}qxFCFHT%z;!=pjnHWO5o?&SQrFD!qS>6$0OQ!?nk$$ z;Fp}-x>+%ok!r>}XhVKqK47i2{rDp`!h&pHewkX8lPj-`pS)T4Jj0#_*h^9lq{MPH zsb4zu`_9Lxj0(fOq^8Kj&+yMj=UnWk%G2ns2Tdex`YTcQHv%#ijh-*P#VN_cYlOV@ zp>v;qiSS#ICO~WrU%s-fdu3c)r|Xd77Z%w;gf@VgMFa3s=i;9}(R$!rsJE`IrH!Wb z8YBX8CEY#7BvGIb%8{L@DO#FnnwCmb;-XuO607-n+7zEX>AB|&%#k*_G`mq+yjWY3r7{x4ojb-szt2D20U}2f#<;_o zDxJ$T6@4ApWXWVG@o?f9wegDybOnp2owA@x?S(lJ%3fD3U$l<3#Q)Su{_pJ2-z0!@ zq>&JVTPD=g3T@Gt$jxS7UuJU#Ov=1+*WZ1E~{Z1(a_s z*YiJbSUm&I+3$hMWjl6QUeb{dDJe$CuZe#%&Gkp_^PjzIO$p*~vG=WOP8Fyaswl$r z^I|^bI%W>@6L)}c<4iCm$NU8{?z+F>2Igj+)y#FJ>w|f5!Ob2n!IAfTpI*qbktogw zKjtc+t~8llv%{b5X``QV#+l-K20@Shif=0Yq@&wcIobmkt>1_);j}-j4G$3uz^NU`Z-%0xGxP& zzEToOP^fP^l}M!iI>+#s=>gVSlfl*YinN~ucx86B+{>6g{&H(U#AV)pK2aX|(yFngbAvh=e&EUZmW4ow%ty=*_hiJy?C zzY{6Gkg8}FJ{m{8Y#O-mW1WSvjEb#}>|ffQ2Q<8v2iHBZTWg|B;+dr-WYxM%5GPr$ zbkH2GvaN6tfd5(NBu!tXX#UHC_qLNIgGlWaE=}VpFLe)V3105XJnOo|&TvP^H;!)a zy}px&ERV@XD>1q%yr{u^=+HvCnugPPeg3WWqL9-?_38S^r@8q9-6ZwJ)Px_opIrs2 zM0EzKUA=cNyvcj66+)O3CE4W@OoL$S9va2ti;vugw$ruDKcSJX01xEqYifk6kCd6DvY zBndL6)c$7(#TSM^^>hsO^e)NkFxEnff7XapHw6LM2@)m9I@1)kGs+x@N_cN@SL#Mm zVd(Uesp5sQM8c2l_cWVSt?Xgeej6sV2yOCU)Wc=Oq4Y#BTPI7OVg4E`D$f~hk{BgwhQ)I?=tNH`QC|M1%xiqieD#;V zRWkmi?h)vpUG*Q|>G~nsdzz-9J8rV$v%@1w z6rz?snD^E%#n|#O2ZVK&go4)yB5HZpK68 zZ^a*xo)ZMFr_NPVF;fy>Pz5bQDubq7qp{c;IShcylnm;2@6SLqg0Gp9SVTr{P)ep%Yq zcLX@AfXMqKnw!M?y$mB}v@P9acN_iJH2YHLVMC8NaT5`B3iH=1zE}ey#apD2VXWD? zZ~mmc`)8ebOyXlk?U^%aG+S7EFbVC;iEsK3IfdCpz-{u>kED(%MEjn6)E#Z(XggMj z@FHIQA&};^6Yfy7VAz%~NXT4^Ok5cx3Y{TgW`PZFfr`i|-o`^lX=f|rW(?R$(fQI!tRi+Sc>lKT8OF|StuwclqZ}^Acy%1 z0t*h)5_;b)IGvls)Y7IkLh4l| z@^m5U{t7$=d?1&H$z14V!*LOW$51aI^6GXrupQLDiLWs`~xYs7-rpT5IkEIXG1%4iu!4#1{ z!eYT=CuKtxiq`z;PhRKD{j7)*B!D>?!7qv&v%v_V8};elo+H7?!t|Z8*VU6}&8eT) zZB>+B#NKxS6>WvAzvK1DxkqwDw zwyQ^TsTWX0&iZm_(JDQK(c==c!d?l#M66X?O(Jr1ii8ir)KBI_8PglDy-ETq^?{vE@MHGj7>|;+aA6yRG)6;0ZmctH)T) zxv?7$VhgZ(@%WzHsZ1Pya*npO0RhXqNX=KbnG%M1HrG-99xKC|p1HlbWu3u#`fqOR z>x-B^23HV`)E`HUod}WmO{-tBUMKZkpN|E4QC7I8f16rbuqGqdjp^V|Ho;1f zdntMqwjMSUNU8P3y5Dub&D0*2CP{iFdn1UNS*G;zIL=XpCxw?YOxKioVsD6(JT4xe|cF#!5eP<#MnN<-RZ~&7m@0=CXK%5BRL~rmT!G+ z@hv}EfuwJD&C{FP`n3m@G&>L1VwPRyW)yECdbE_*!g(>rS~CPTYmlMfZQOEKvc(l9 zRIo-lI@&v$P0Rlxs!yM8Vc|#5UCR5lP?(e*qy98k0sKrQ50(#{ zj|O-@(KZs_$}V4ts|Rt4WMT(N0WI^xINmmU z8g=O>hJv5C$ack*o^wfNt8g?Al@>8QNiaHs&1)Apz&v$Zj%mk&YY6TxECf#<3fOX# z6mh>hU{0Sn`!QmXwx7a$yTTogv%njv*AUq^1iD@eySgI{OGnY;50}}r;k$ElXH8m6 z+8rF`weUl%!EImAQF1v-WP@?83{f8jx~=%=xht%eYl1`0=WA;HI~87@mPaMFJ2WRQ znNOvvBxyu4)qBb^ba>@ZMz405zabpH2fFSHTah9`WEdP*bn{>XDJh+ciN0jk`iSn;pa)}z0q&W;m%?SZXxw7 z{sHU8OmIR=TTR?un}Hn=e~X$v7K`v3B`bWZGe=ec4JfPB3;{sh!Z7=Ws{0YU+O+2cbNDnM)4 zFHA+_Cby_#i*-(-|Mo#>*`b*=c|4u@bKH%H2;J{*zX*qRuKrxJnNQ4as-CsoJ2`Ng z>f0o>(kL)+tpRe9pS*eHydb$g6K#y`@D{42YAjL8c99DV1hRwZY7xjrZP|Qgc%EtR571N=)PbUC*!Xf z^c{Hrk+5&;>U3BV7IvkD8g~Nk5$?Fc1QN^gcKubSc^xV5yW(e3{h%aNSg2O&PE=bQ zC4NtlvMQ(si|Z5qD86h(M^{@pW5+2-9a?jp%3`Cf=!NSH&3 zn(;g4LXFn@Uh?V0Hg+S{S7!rIj0V;z4&nP|Q||0KTl5Yikb<$w66W%~ zTcpY~C1FF873s6B&d#jvLQ>y|B01nbNu}0!fOe+q_1X7P*QUU{oZO&gw^&?tS(4{j zZyya&Qhu&(<{fp_fnXzXEV#P40+PrsRVqsl?aR4KwV#!SRnCnu&i4_0E4zxbr`bHVrL|FRNsg>YJqUQPXWnFRkdr%oQ1jpU< z+m)b-D#JINfsKY_OBMGS?b!XIhWD#(7@v!;fg}6uokm z@b%TLw7i9%u5{!R2Q_=cF16_}4pt&cyYY<#Q}?cWR?}MvUC}buH0zu_7tT7&U(%9% zTk!JxE%DP<7sX~IMNL&K!X`)_aN@8*f+xzL8R&uaOa6twyy}#tA_{S zvT|#(X9BMes=4J*ZyiYVv~`Y>$NeC$g?q>2;cMc-GTLVy=~&Y(&urHYbh}|LTg~QK zTt{0qK`IT}Di110hKyx6e}E#)uTypl%Z@3EYxBl*UaB>_K|X;|CvZJOPNcaHs-@bJmS(fr9_pJSh+&LN5#A%D0#v%^(TbwkbHPa3&79ocr7 zknifwl^4ar!^kh{%p59SZ9Glt`;Dl7)uEe<8@4s93Xh7hcJRa-Z8`3JV;{;XoXzV* zU7U#x< zUYWWjy&lS+ev1iX%_P7+4&L8{m-GoyNkvLQ9rCTkBu>613JE2?pC6cvw-(0mOX2Iq zFd>V2%7?QOV-f)sQ+&&TUmad($Z=&%Nb)&~Pt>b~wXGHn{a@3MG6$plx6u zS(O!iVEBmyvd*GBMO=n}lWNC-#eR1TtbE+yTgyhg#p8+j;qQ%FaZ1A3B7-Ozq-4_s z6a028NTAWVF?LC~#+DTqH_;czg*P7d{2>}e{~$DpG^5aq4Sw%N{&)vG7pp)bgnA1g zHq0aMRV4(mc(Mehi^RuJ@v^rtpX>(zE1CcIJniqv{r{7;|GVu7 zr~l31Poni9M=_9e1i+*atuvmY`y}Y@C()nY2yc=DGV?Ozg~ih50BkB_uAQ|V5hgVV zV4b9^ebgbxBpt1C>Nm{&NYv<++%Hg%`h^Pg>NfQR04;}P;!J7NR~bRZ_Lt+qNICjH z9Rc*$ul^d1zii{Lnevz4_?t)l%OCy~9sY`y|Ie-*^n-*WU0cf7#&qU0kj`j|_pJbI zMi&@y4ZuFja{ySOK_XCT`O{X-BLEH{6T>#6X~#}G5t6NV z_lcf?iq!BR6^Cs>T{0K9`HRg*4d#T6E9;ne{`G%0P^pnIAOZiqv9JBNQ zS~(v#6JyHt08v6PKRdX3o-2DlVwh2Cr>D5E*o_woS}{+gJa8UH)EbM;YxpsRi5Jeu zDdkw@zUmfneod)(h*d!gH1BA6i{kh1@xkIVObu5EJ9OW!RjTJ;i4U7itoLMuQN)M7 zBD+fEQhJm|CPWzS0NvCW2(m}mmf*CTX-1KJp^ zr1)pfTX7T?EWgtaH4Q&vV{L4zWqry|Dq72Z_jSCp2xZ4AQXJj}5aGKxEZ62FB5e=E za;Sq0ZTp_skmH|la6IK2Ou!E2z&=g@-I1+cfd;26cb92usuIR&S2fmEN3pad*v%<6 zw57Yyy-;Bq=YmG{%>FK)P;U@|*%kXV2Zi-JW7V$KO8IdNqlLC0; zJyb9ovafsJOzZTV_lDOjP9^dpQ@&9q7|$QFJ*mg@!C;fI`=U~%vzR!j#zrN%WaZiLQiLA;9=S86MKv2I!?r-*644- z3pwxF8*MABICQr{Ixb-cGZoeswuV36W!Bz2o0mK$J91+BBRMZTI&KE_;+(RI_$Y1p_w- z2h+rMm)J0Jg2MF6AGIsJhJEK$Wbz|>hoQq{+FI}o;Xb>JU6XMKF{*(RQ15?GQlP@n zdlB%4_jm`-zUakLX@;pfg_Dxt4tA=2BCcSQ>lvX(W#*GP z@CcoBmeiiH)e=LTG({`}tw~6&fqwa$Phd{2rZ#ONaIy@p?7#(2@X{0ieiZh2id-qL2PxJGHkez)(2LKfr+OY?A;8LTwIG4^`&NFvQE?8mE%NfqjGSiFX$=eeK5r!l zA4k)Y0=1kXLw-oQ7%tz!G z;|t*)WL7Ob1XWr$<&BuQtsxt)?m7>z7g=9i`_(5-9$-~@Xcj8HdNP2YnO3&TAM^cG zw-7*PZY=lHkwbfr-S1a8m5?zTz5 zx5Ky!6z!bDC0{O?sHL@C?xPIlv8&SBG%X#^R!)8KON?gDD)6jk+aAtxlTE5kW_fHU z_&s8bus8F)FM$t2PxmD@`j&Cb(Fse8O&KBo(2oOF$fww@OUDUlNth<1!T11k8yhkeNR5(YDSqH5y+cpNr>UV z{D8K06i6>BtU>W@g@#K@$zZCHOIamAMrr{T-s|mw>slGy)Tp@Kz}tV3#KXRQIS}UB z!xnO(hJjRoL!c-Cii}mZzUZAI!XjnSa&dET=ypZiyPfS3J-peT#BMJSd*6@I9Cp>d zLf(1cxoVW+Hw~NC*jAf-S2w??=C-YBwtbZ(nl6GGrAfD%vnG2JjA^+fdj0foaGn1y zJZJXbT)dEI8WYK}F$8N)5yohvx-Ed|24JR_?;=_6Oq1&F3xQ9M=x^cDUC^&{kg*oX zC4k|b^3@&;&dZI}r43u*uy4@Dv9S7pcwi`(A-Qehc@0Ur6GNWA}Jw^#Ry z*(-j$Yu{dyzFC0;(*JokG@G!J|2T@mf9wVN>#zPk&i_9cz$)|V8r1hdi2#^sd`!*j z=t2LodPgX-p3fa(6&^6WrbcHP!I4qd$A_ z|K#cKVxk_5;Q`Sk3y>B7mKn#+kS@yA_Nz1L%L_1yyT0wuVd2k4fGy_gm%GDPbMfGF z+N?G6V#BwOyiy63|E~v}qV+IVU2{c3Z<5zO@xIyfG61 z;Idf>(c3-Yg~j?lm#gZptf0UC-L4w{jL~CZLv<0HYh(c1ZHjD-zdYKU@0S>)9zl9l z20g6Yc+Y23fn=s4fE{wGu9aTYJN{5CrF3)>P!f~B58}1j&{=2vV)@W-O`6P$a*ftj ze+H}yUqkpw$Ytfn*u9^S@mFcke`_XwRSvCenfmYs_a^HVhHH%f`WS(c4HagOZ7|OJ zYx!EsLUy%xv-h1Y?AV9d$8+Qr%`%$Y84GN31D%$+T0B>&N9DVfYNlds%hVYX=hTe@ zLiyB~-cfLWr?kswWbz(y;Yb=@U%vay^@7+Ps;W}B&@(Q$!65rkTseU&y5l&)Qt;%c z`v@Ch`0ThKXaio}1Z&!3sZR|2oQhhlsZs`c8ROo|@)-d*4fNor_<; zp}lTGQ0ZZuMpL`Ct!Lnsy?y!$Bg1fYKsEjfaosj-N+d3nrx+E+{?be;zi zS)MX!qoq>2JrET+$WZBpzcnw|NtNDhA{JpApmzQI;wp1ZAL^K9*KLiOan8Q&QH zwhhBM3n-mKr_Jd$r-gJ~ngoFZj_VfNJOe7nZwCp-59~729LxtrhUF*kbOd%i+36Q8 zM^@mxY-1oP9!uoPT^<9TqmrC(RrQ~K$s=`zW+x;}iG3--_20Li?sg~dPG^8`MpU|o z_tcv(deo8F7ILjT@oiwwUJ~gXyU~}>buyaX=_7WGXiRc}OtT=UG~;Xz^m15`0hvJZ zm+u$or~35nf7u-P`cL=}f8yi_PF3-hkT5-WIQE73p86ZW>G)JC|M2Hr>r=$ zI>>F*$Yn44;Y3EAH4s@Hm0)Q+>kP%S7N**2cCK?33Zqg2i%>q20hz~+$h<;hiUu*w zj-oIS9>=N5h_aI1^oO&Ic6f=yF1t)%>uI8K3q4ck4Y?X+uPtmGQjGxf7(nvlftPp@ zBB^`WHd}(NaBgvK*$^kT3o~(Ol|?H)^DXv`&>^;REX5rel7~11AwCq-N%C=uX-5n* zhg+4Q3D^|d^+Ea@`mw4|N*tU!*oT9Z6rwb-NH>IsW0rJ?zuApN5v%4GkJc_ zK8zWonGIp-vSa9e(FV{yC2jV!z9#MKQ29-k9eS1EFmmdYki>9ug)+RiU`jNb%`=XM z7N66Hw&72()YVMczm3}3n&IdT64yVZee?ONyu%|VoY+!PN!jvx#pzO)Q88NGmzK8H zovh}OYi2bGqDrKCAH@CkCfbaSXS)eV5&k_ou=0#tk3rjfnDri0?EKnu-h}W<%>n|1pk>F<+bD*QY3&J8{=i6~IGs z8TMWWf8unARn>eMNjv+&?!(+tOxm1Vh#jA=lPEY~fx>X_q-}2=w zcdkW^OppmS@Gz=uuJvQT%BIQGJf(?Cxqcak4*2K4RT=&jzv*w^zXAHMr|<*{3<^g2 z>~J11ig0g(D$xV!wrO-W-N*VNIJ<3}aFQn17}}mwt{(jF^C1~thFt?DZ0{dyJp~Wp zrXJTztkhr>bZ11Q-$fLVM6Qaja*mTP;v}33bxIC*)yhs z=*8YLyy|!mNutaroM^`pvde-Nktr$0V_dFEvP0^- z(p6mqeM+fS2BDW^S$=tOy-<=EHsi9Pkzvyq_CV=?YoXoEr#d0Bov7N=x@P#iQN{|r zzXT6+U5@LrMOC`#i_Fe?5~vRWBYu`=p<^s@T<;p0}Zg!Lbc z;U1>XQ$*}pUXSt>ERt2R%{K&hJp7=LGdrTQjNPKXI)8-iZY;=lPLDsgr|` ztoyaoS-&UV2p@si`?2&RUfxNh746t+8oEmDZO8L+k?>(Y9wB#BEw{PQz4q=6c&kwF z%|GmDjVp2YB4qwDHOzOEGqme~{X1R%8a@3_6Dn0U9vzRv^RBL;nI%TfNfI?)-@BW} zC4-zDo>K;DJrG=*YpJ}lr;xdg3KC zC76NZZYjfSLzS3H^b3@)#Gq94*@RPxSTcK;nzbVyp{j z=ox&+U_T&_Tez2k5uCTHS|CvM%;L@VE-}o+aG{WIVDh$+iwkAV)TQ)po;SLp6ORdV z7HBPtl+3l_L*%@gdIVJpMGx8#LJU=6H!E*Dn##OtWQxX6Dz*JL{WYQcUe{&Qb`axa zixAhrk-$v}$Gqa!kfA`L?tQWT#~q6DbM=puG@gMJ?=;W~chRv`&7N^N8Ghoa+m&Ky znmRJ8a!Y}YQ!Ax>6C@Io*Yk=wG!1W3kZ@9B!l?RpB0>I?&trtRVmfS6y)i{RHC3;I zjN-tu!6nGFld>fsnp^qn#yLitmRbNzE-2YMd->6xN8JU>bW?-m#?T>y_!S zG3Xf*Jzp78;_RKYKP{l5tVjgi7@JOQ;1Fm80djiU+A>*?1N4DIWKQycSw4Eyd@P9rZZOfV*C(sv~(kioZ#S zX5w(9aY3pD0we8l1}3iDE4$)ZgIP(C{Y+a4(e(cpd*2xrRhF(x5RoL3qkvFI2Fa2I zB&LBC1 z%%myaHX~}cou8#c5Pu7sAG&xkok1Z>s5?XKy#ESnqnqzT(F48Ddm363E=r<4VBLnO z{^2RC0iI$!JQ;OeD9cf$5}aubulW7Qy0x5l(x_)mi1IVxmz{90XGMvYA6z36*=gBm zpOGm(#H|A|TM+ZzCE0zB`3hFIqgj-&*s2ryj%1Mm6re)wOP12>;S*>nIi zk0v$<2Rb$7oWXOKx;LKt*qyA2ne=ST!-ZgzZseuB7t%PY;gFDEi}8c%yW_Cyucq5= zJon6M>a?0($Y7@#w0Fx3^x^Dw95AW%UI@dTMY?0t7-0(se#P)~>ELDEn!1Ye+JPbR zWD~dA+A#BL3N$(~Pp@o(Py`}nRzsqAaEK8iVrmsrnay<-+}+2uM}~c9&F!S4`U}?F zM0iXf?;u~;eB$kJ8qt)@Fy^cO>_zC?caJzOdqE@1D(7<-fUZJl}Rk^rrF#+q-kz)OGvDI&aT4+(wrW zzZ#(a45b=wz$X@$TG_-k;&KH{^ZnHpxyn{huRHb2)l}BjcIm5vPnZ_28+hN$U@k z<&)au@?xg@?MmD?LkF1fpu z*l~SlDY0QXKa}iKT(n5sq1s$C$sQPNBTm~>VMV7e-w1Z1L$epON53hJV?B0{vQzZ> zXvz2@EjU82i+d=rMGd_xO$&3!qHF`2 zXcmAYc=Id7w%n<=#joIgPWPg_v;g4eHYNCQA&KPMa>Q#SH)QPLjdkb);S`VW2iSOL zvECV4c=hVjS2+wU0IeEHYy=nZyx$f#+V2xQN5ZJJ4X_Z8tz)}*u+i`s3F-P|mHH1o zIIxm{B9JKu@c<3?5f-#DKbF3Jt&Lhuf82v#JnAk)vu^%oQ({qSW2|?96ByJNyUf{< z{?d&Or6Gl}j^)dRZ`ZgrFr%jh_hg3TT^o07X&T1YC_k(5ErP5t= z*xc$H>ga~OQDnIumZ4tr6`y@F@et}8WNW9W;D`twuktG0r%3LmcH6rC1?k}|*2P^w zE$rxQxB)JtbpDLfYj#!Y?~)tqO)6{;`nd3WHtw1v8uG>u42&Po?#}zv`}}C!mTV z!{xR@!136%B)i4-FG%^MN{yxzcPaO}4Pt67J=JA7Y=+U(pDN9=8l?L2Tcrhv5y!Kbug35nRk8M zD(vTOKYO)pgt-pAjzmFwjK-2mHxOKC|@a2#t3fO-1>ZRT;ycBV+QZbQ3T;pzRz*{QS zmRwp&>(5qXLx*%2B8<~mf%+DhTp5WCol97T+pLPX&38N*M3BR5pGbBUo!*hLvp?Gn z$B9=5#j)=1?)!^lJ6$_rGDhw_L-}1sr+@kK{Z)}f{T?HowZ4R-*;BhN;E@~p=(6ME zo$VFnZrvMRp|qiRGK2g3NT~JiMCnR=$BzXqI!d@9xvy6uy&gdKGf;Yu^lDy~IdH2S zo7Gz8wS=xGfOOU;TO`x?kBtyn$+)7-T&4%GNn(Ek7Z3$`<|nM()1GFr-nriMPR?+ zo(w~3mGTB+S#@o@=>o!REYzDCD6f^^$0GUovG)l4eD=uG@YR0Fki~KEXqA$l zjogJukwWg4jEa(tx>U_Mdo*8mc@w98@GJSj0#*mkU;$!~tNC3}$U{;8f4$;H_-Ek` zKh8$-XN2_QZGROres%B%0YqnHTCB?bS=oiRFghW>`~hQ4)NW@xdIKv-A5Mh*l~`%# zr^-y(=%YM32_7Mk&5$Pk62Eqlf)3v1^8p(|3<2514+i=|B*E0s z^eq_NqGO3RWq5tQGZYzMXHXF|I09D>FY!eu@))b!WPE#b&(p1S<))4*>?8bH=Jwco zBQV?-C=y)zfbHUg-2R5Y$3Cfp(g1}7H#J4&Nf40YgJmNfb*^6BR9ROYtq@7&7SCLf zba)5TBEl!b-4qtE2Tn{A8>_zQ$s@vX$#TgK=1(S5G?pzfX*`vWtFujzvE}n&ti*cd z7vxJkwECJBAhAhR(s|dHHDZ@9>4l!^n!l~19+C!e17)$cV!*E{3YZlThXIvM% z1U9PZwKw*7L!xYW2LtmhS6|x7Q?5=nk$ccfi(^}ucFw918l2LjWqMNq+T4;(R^L1x zIG4~o6p<~wP%`u6oR{XMM~dvfBEzJW4Bg(Z+cJ2Rp=x?K-othy-sg#Wg_=9 zA|mej2OS0^JJ2TPyjQ;>u*{SFo8|RiaSah29fzFDcu*U{mM>_I-!9V0LP*)uZr@N6 zpdpQqiR_mfVW3drd3ts=?2v=N+Z1{bbg*A#C$VJd%1AY3>~BITz0O|hJ6MLys_;}& zU`12-P4sZw%ckiOnnya##He+dO%*ec?ogh&Hw`r@ z<2X!vbTJ?1uAetbMkh;)KZ4Ih(kCF`` zK^y`l;tuE`zDIF*Yq%jn`9xHP zTWw5}%Hz2C%M|IlTH^c)<-^Q?peTNAkj1Sed&s>EeO&Sh1d_MJcy$?49V{oYIs<;a zeSB_dL~Fw0b;>sV;YN#2*fIUNq)dEZf9!K)dU4}H--=SI5eP$2nYWzds&1*SQ5)_3 zLd}GPCBCF<($0bEax~c35m7dE8Bc$7I{2$8V;R~ELc~gxqG87FvmNo_mC^eM(-Qi+ z$hlj1s$x%cJ=0vgoa)=Q+K85SM7bw(xu`xFXXF^-YNF z*G4DSA33(-3-gx2YuhJ2rct$2RHeA7@jQ9$zpc&h_JJsIZeyXzEGt17Lr49(U_`+Y z?zIcFbcwMRD!9AejC5X(2f0n-k!4~xsYPp|v<+97sUQ1XmsyP6iqJ8Hpp_H*?0O}P zYes5<=hEB&AK^uDuDgHHJ2#{#cblt>GKq@N0u19rcNnNE;OHN*R zG4I~U*YOHh0?OZwL(-#{x#f1Fi!c%|2?a6iNrJ>T9fpy1?{SBKq0q!$3x^edqG06b zme(%f7WV9`M~e6K-bT;l3spR`1!3GV^`vfK7kmiyrvcF#tLg1|LzcN%^mzF^9}Y zU%(s&{UWLD#g4jKIT1X%e>MN#AsIoHQw;GhNcuTI#iXpF>aZEl$y+95q+x1(H)oko z=Fv}Zh_R9w3;k@V7kGT$`aK?dSnm{;cieW_A@grOY5P8M=h#hGKStWQx$J>bFL4)t$Ts$v_u5FcV8CK2b}~ZF60bVNS|kijVs8s z4@NaUFf-<=r_}NJ<@v~;LFiUu6^cB<5GPvvY!JqT+YOetNEF9j3b%5b+7JXCs!$+x zm^i3Ep~m&*6s4Tnt7hZ~8aZ?A+C2@vi)>}PR;3krOY0s|sN5PT&M@`w9S4UHLBx>2 z9*H}Kv4?xHI9}3%!p}pGXyZ7!u8uI^Lpi&-(;f>blm+Pl#aXA6TQO};YPp9*#^2Yz zFsZAI>WiV_PE^MyeB3R4+6E#LvUxB1UaZdY{2^jBOLg>c(Qj3D(nvmCVrqRXZ)_!O z8a-0^zHBrFzS^3(Nj#mS8O0sVtlpBHDV4o|^Wf+j+N-Q? zVqRgVoH_q;-_{@Xs7xTyfpZqmiyflpZwuzEe{MdQ%QN@MoHI?JxfVotKR=g;RLGs| z`X(#baP!>^zn**WYKTs%QWNyz5wVSF=*b2(eK&&~$rUG2{TYYvbrh=WjUToEPZTPV zmG_pg&5hmZ4b+ztdHuO-#dDG-OfeJ>w;vhvo|ZoH7*Iqy13KL3P$cfnE`8pYH^|wR zR3A1+bnQi9WaNmsei*`l8IFyfADzpyQ^3=HJq=^Aj7`v(%F zYk%k9=4gN!eD&@BQ8V~=1(kFGkMtF~C3vdFj4*qHef(UY8BLmyA1#SH409`m_k)IRhqd;=+HL6EnNQPPUk^tQM_yOTl&InWIl=sk) zr?x!Q$Uu@EDGbb+!bH5le<%EW4N(97AxWDy zZph$$7cq)Fz(oFM*G+$vMItz!rLyI{{U*eNn z@HI&>O*{S7%c?hLj>kz?0Zc(d)-_Vyw0fqx$m=;f8G)jKAeQ!>Vy^x)-BfV)=rW36xWi1bBkDd?n-2F9^^Ab}paQ?}Q!q!)Za z(qf*w*x5ZfS6&WAgrr{P-7G#sU~YcQAv#SjxR8LS@`c5IG^Cr-kdM}!c@*ZUEi*~9 zVj0`1Lg>g7L~;^fLnE;gD%<7b08t60!k@Wd@>|*C=y_tEzQ&Tu5JgRU#<&p%m#UXF zn16a*03GgxV-m62FzLzLJl`BQK0Nb;MBz@vLGE&N#yYfY7gltkI z_iogW;CSDIXPH2cbYWK-rj~LFE#1kbKDd!k8SCpelp9DaDn1X^bg_FUP0nj}ib2|a z5M&LOEBe@CuPo-4x@V{O1*vLn7FZkE{1{1eIJ0}6e<9I4T|LC#GuJnney`TQ9pg$y zR(tJvwn=pK6!BJYk-OG`-&=2pH*C=`^fNVAU9DSUV@PVIL z*Lkytwn;YvsHJ$9-dFvaFjRaaZTd7S^aX9(RK8J$5` zr*{ry1WmeNf}y|L)7#il6l^Xnb|FcX=5yz=tdW>ioDp=n0lqSLLZuT=8=4!Pa8Iu1 z>K7#J3rWbq?7*5qPuRsRRDGMFLTg{@@rM(jXR$!hNFiAZ|8~10Dd9M}3MLeZPFbP{ zuzS?b)@gU4+9BRN%{HCnzUdxqy5f%Rgz7JrT>{;30_LM^^2<=*8LQ%J(fDw&w#dv- zm(zU`jcJa#dB&O)FzCgcXP09OWIeOren27)<&Mp=C9;>g`f>K(@R~Hhe%~MN3zizG z!Fv3wTpsZH-LN{AJygI<-iJIVdq^Yyv=TQN135&C;guF{C~6PKijN^j@PNIy0*|Oc zoPk&pp#vS7mE7DZX90VmX7EG86?~huXcP~Y?lcNG2ZSR83YV}G(we+b@XXzE9%@zf z(sjj<7%GtCc~Lfi{b+-Le}Lzij1J0EQAl(rf;>gxn(fxqYrUljNc~pwg?kg&Io4nwIWi?I?-%-Tir0yg_1_mCO8T%D;opDDGXLD*H5+u? z)APzed6aPR9rR@q9qnoDLxZKk7vh+So}h=%3k4HR-l0xT6KAYZU)y4{Oew85F`x4f zsUgtsB6|%c<=lVNL>WOeqIcSJ98^B(?YU}%GdE&aFlY5d<#|bJC#UXKbbQN@%$~JQf`8&(>yAAArIc zVeFx>Die9Zu{zI&-i4rUp1LrBpg|MXgR^eNSLVczZXYf$XV=PM=463Sqghk?JyVYh zVk#AJr!WmCgPMF{@>s+)4;lZ#jbLV&3_U8pPag)8wjkCn*9C2jWC)iBU zcX$CC;Jd1#@hm0ms11QX$y&-YzlYxs8Q&AG&xoDli@M)ySDN!!`rciUcf2!Skbb_) ze8fmOu*`#Ts$E1Nw48kT;hgbUfI{V(hl)yh?F6d}n8iy-iV2%@zPr3TJ63{GpAi(- zHiO+usG%P%=8K02x6v2 zbsKk%_}uNKnc)1*^?r8SsE6xJV>;j5^cTb1xUQaOEe|-X*+lQ(eVhHCHZC~&bI$!| z0>NKx|9tTLOJ@Hc$SkQE{B2-U``JsA{^@9X8W5i+rT<;qpNHU=0`e|e-@+mF&%?FA zpKYv1yu1x;nMD5_RkUfpCLPM-Jy}t=wZc~e;K_HA5Td1DkamEQcca8P`M)|Ux%LQ{ zZ+K>)JYWbPBQP%s}HAOoOyZAlfh9}iM zaSmuZCIejhG@duQ?_Dm0`x6Rzm3VA+lm%?A4wn^L*R*g@|@q=5CjXZhP+nkN^2N99!q0^t_@nkn7nPdaSIJ%3b$H?HrKnfCeD1OfN;!7O2oCLEY+^$=h&RHaQc~CX4rw;PNvhX z6x`|pgVkZdj|GdI@<{1K3syKtwhfu4ge}bSrLe757wtaHJLA7)!<+*vlPe9*`b^yE zefN&i0tCnO9cbvCIB+Qt)*_sa1E!1Sdfms$(&?ra3W>opmvli^bc)Hs-E$e@NXE^Z zt3U|>*Ys;f@Yr+B$&!PSP!3>OA^XO>FqQruw(1>&@xJ(3%p15g`vNpi>Fz`sk#j{+ z={%YDI-qbd;5cfH(WJUHBp9xel%Afk<)S9#UBC6CZwCL^jxhTR(u;WN&;3T1n1YR( zzdHH>s05)ZvOap33ek4URk$mJn#9tLL1WUTQSNqa4F)L9_RS;0?0db^vb}1rTuKpd zFXS@vLS0yft}6_b+%?D@b2el0-ypt1aNitJvo%MOl;X5~aCkmkuzVv=}O zNg0Q0Lt((tY5d+H6TuK2XRU{VJ+rUs8y`BkKLu~tywLE3P~e~(?zE@4JSi%abiW>4x$L^-Jp?xhbM39Bx_7sImRJ7a%shBT2^U0d;#IYHdh%#l{OKdhp+ z^k#d0FB0b2)nEV@^uv(v@0Wm3qhKvrkVPTs2f}0)HqJhv-e|T@Em|enq_-799fKb~ zeHfb*aBj7!gHJXr1;(DsDtr*Rwa8jh5XqepijG@IvO#xt z4r??k(il2hky*VT=v!g3utZ=*U54G zq%pLt-Rn|FChSBP2Jx^z&eopp`ei*2#l%6ds}8RBZ}ui*QG6~ z1PcnYsY#sRslhYo!7~uhLkk$Fo+T$TC{)&OTz_vrU#&CPx*8*T)vuiih>w^xf!Q9O zE>%${c$XEcJjt<^S#XV7~L={ge7ag``J|Rqy4!TYgU0rWchs zVpYGY2Z}enBFuPF3>jc2hj5(Q_-v zWqt8HzDlr|7UO1k_lG{rh^Z1XQ9}Jvxd7#XkQ%%S;w2W81=EI>~T2rpJ@z`@E z8r|UMlm1AGwJ#@U!6_`Q9BXgLT!r^Zi%?DdD|f;7=J);QKUcTpM;Q;KxT_qKq_8_E z37(w-8x_o1$Ma>t+Q$h{<_AXPwsxuDOFdj^9ZO$fRI}(eas^0U2m!dsv-WC@fHjc= zK%Ed~=D~{TDj)gn?H=V;|7}Q@zrGY<{sC5+L~o&-#;&7s+n}va%?_|ga(N*J@R-{H zWlGa9z99Jl)uZv$)T9DlCVkf%5cvPrUS{6Db<^K( z8CVwY)YMdaxIzD2d(n9S|1C^_Ku{xSGc&`{IXS*-t@izc5fSPNg!+EhJo@{m%g@hj z8&vqNJ>hrvFC|5M-S}S!gYm61{^fu7aleV@ugCqnHT*`qs7v~nEyRZ!v`klpI~L;i^*hjR+Ki?GK^$|U!D8jhWn}jD@hDEd8Hkm-Te7ATS<{m@uhMQb>0Of48{GK-!3w*xBoZcT$sh=##Xqc?9Gxm%zt$WvZ z82_pTU5t^p?3-%4eu_umN08`h`oFyOyGZ?C4^AaKW8luC;T=jx|G~j~T55fuG$f+n zB{pc`7M{nm>eO2~!)<-+2D6~R8`7HoLej17X3c$ZR`2N>b`rYvQZ;pG~Rv9%x`|Kdy%uofP zdd$MCSUF)N5gWdPPV(#NMA4~BMQ59pMHqCM>YhQi}UYH1%@VF z*@R>(3`b{5B}NeL z3n7$+m=*W%DaOB927gU4zBAe4(9e;ybM7_uE&fbdcZ%rx3)@O{vuXUD zup=<@PM*K(%h%`>yplvb9kcB=B(f&|Li)DEVHLKvYWS=2$YQQc1gya7BK@C5E&XUt z{V)5T-=mg{ZeGxi=7g+8WfX5wQ8$mdFuEwDa-$8$pIKHk@R#?Fkqv*C)LS`tsYKDx zO&GDEVPveP_~N^l6Vz21_=s!ng)$ZXq_-rGm@$m1?&P|uN+o-m%c;^UU)zXVLKaA@ zwjH(sHVcVjeTW2T6T*0}E2&&rJq~!I1ulSgO;Y+cVbes!8cm~c)c$Jb$kKc`P zHX$YG2LgHWeHJh{b*2F*?nqCGy2*N#&-7H;t&&4g9Cos1>dr3+O2=t(wMXrSHbSPJ^?twdz#yy5iSQYj*v>W5vWAbvbZM%(LC;zO>Q*1M3`2Dh7Q=@qI44&p*Dl@_Xyq> z98B4$EUC%C4Yp@@SWS`LGmcjCksx)~l>>2*iIY7dlk$El;d{h;Ca%8}v~qyqVd&ME zj$>J)r#TmJnuKfPK#9Zsc_3a@_e0!BL1~!2WQbZ-zHMfMje`)2k#9o`E1JRbx1fo%C?K zmY?Hox1joXbyJ&0Z7Pr5MtPtBYVi&#^L<~K&EygG;)n}PqKu4z_lylfjS08tnzqXG zD3Yt1W;1bQ?(#%Y4yXO}umS~{j7~I!sPf7Uy|$Yr#WvV?^6%eA*3yzj$)`j@N5X9V z9EW3p7_R;_;oH*Jya3Pp?*xE}27AS^aPOLr&BN~hNm04Gt&4*YE&<&!}97%Ucl zmmKkkH!qxCZD^f}GaXJX60C`Z?IUh`xcS(0^Bvca+}Oe%!w|2sG#t{qw7|?|%ppkH z(9u!H)5L3Nz5g-aTTVPT?WT3gqLsVf`stHf#Vyv;>6-|P_0meno=PY6_0I+jTCmsM z*=Nf^pTVJfr5stO9D5E~#Bbl6%2&PMM(nQVsHHg?Ww+o=RoS_>rzn@K?#xW`?1WoJ zaQ6?6_hvXID_ks@OHHOpI^%QK)~Q)!}SyLrse|_yNe}(c`F8yZ$qd zjtwAv)PuqD2LWOKeIS?)GECwopC8J$+73C-+2xx8#If5p z)SfwCubqVUR^d)5O2@k;8emqh@BkKHqhXCM2Mf31Q#J6ghuxZj)Qy`=lwUp%s{}t! zk=BZNiVY)6$6OEj7!pU4!tlPuQauC8Z^v+VMBk!X?jCiryBHdR(Q*#AV~C4|u}oN{ zV5^0+yk2J__m!ojMC&8Ex+AzBWt`uOk&G`{;Vc4GsYbyZfKw8diS9ZIW9#K9@QPFvb7 zXjDP5hnV!_r}LYV3XQGPpY2q%EH7B{7w4@`_|F_`!seFc>ubW~GMTDz#(QdquJ6?j znNjD)tDD6kXtf6N-W2L|V_G?Ny<#V-Fy@F82(7&58oy8zqC)$C{)mt4 zP|MvjxK;f3NN;}Nd*OKqusp@nN_N2mqY5e9QrnR~=I4^MVWnN+ZNJ}*U;e`Vt~cSz z>-Ti8bOyd45q|PvKX@mK4ppvP6_NLm0YeY1bmpq`*bsS_k$J+ZmnG^#mJG9=EX>6Z zw3r7r1Ues4@24=LZuo_B>)K3sP`#xojVmylI!)|FnVDoSAWIv1%5c+>)$` z95{`)PdJcNNo)k9qgv&D!l79LI{Zre_Cs0OzGlAf(=kQ);o*>==ulB|pHe%)O?mz} zw#@x`_f=;$L^^@-A`DbZas}+)<9zjs327%ZsXdwkZq!kuhZ}w&=2Zmm>BL%P8NiVi zVzN+R%9vVjZI!c+B~p;CJ)(NRAAiKZr$J}ls>zs(tS}dwRDCPspWPo(h8>~|R($sJ z2p^;Nb%aOzvm^YE@;PwHt|eeQ<-3ZAlTpVMenFZ&rasyuxrDZ^YJYr#3BZ@W9m*X0 zgj}2hZ{gk?aIFR?2kM1odXEu_x73Oczp@r|KgPIHZ(AzVX6O07;oOJrX5p9Xk~i#& zKYs9TltVfJmYH!+kHZmgBPalR>OlBp0>t$s7tD)<$Qs`rGa^Rb0@U@Etp~tbDQ2w{ zlE$yTF#3BhMJ}pnDt|xxzEhUI4fxiIf}KgHpwd`lm>5>iF)_>MsE$xmGiZGZ&|~r+A8d^!C9*56GALr&pV*)4XHHy2r1gCeom& zAf?#c?h-q7J99bk^1TOh`GWNFWaM}5V4x~JQWS4m*a9epAksuKw#2))@hWb>R(o2Z zk@RBLSk5{45h=UKvyUDnV$>HJ(3y_VPmLn>EJdl!4~LE|Sy#CGcWeuR0<95y*rHkk zHIEB>!g|j6T7|8gM9oW+5*rZMWyO*S{bzJ7zfui=oDtic<2YaSYj2XDS*wmSfcLW; zT(@=>AJ-k#pa;nccEq&s^Of^YV}7gD<&)I~uIE^h z5_3Wd1|KA?5t=PltXIrTPp_{J&q5payXP2D^1gEq0EqAiAkiQ5TgE~V*@qz&e0NL;K#TlFs*Fwm zVuv7b=2PbHj{*3>H+TD&-uJ)e=eVf4d#!K0sg1S@Q0zWA=@L74VUk5k$cI|!1~BG5sOfY8n#Dr za01{sO}V=97JN+P-+j{&kV*YD>VMbu{|+Vn!4UTC@qQZ9{wczkL1A&V>Fa&8XIp3|0#viE5#= zjDyE@=qW{Ryw07Ia}+XE-lCrx{uDK@Topw#Y|s86EbE4*XcuNz)F`gy?-@x`z#4UD zv(7u>G_n9Wa-W2m&}xE*9K6XXH$_M6vQMP)JVsa%GVx)gD7&Kecf4bb=D+}7c{*;B zolUM)`IaaTxh9kS374F^yQhy6+kR#xqrXUEVY?b@HtV|;+w;j?^ckbSk14;Nr!xKP z>E-{ZP^FOmSN|N#n#z9?C1JcvBD(l{CR8#e`}^-A|5s;2nlLFsmI^}MnNmU|G5Oe^ zmJR-VBFYc5XK88(fy`c@$dss`8}ytI9Eq(ezq4{(nThaFIQE0WX~=flB6(@bnon|TcrSaud-U!I?{=$A9RlKO0TgNZxeKY6`s10EzB7)OofjiO zHL=F%eTEIc_HeWzW)}WEcj^TdAZ0v=`rBo1f2Z|Gs<7lauUM$}gJO?#O&Di`fga1e zU*(8t#kG=yiGf&7uKqZ#!e`|V`lWp>r>=AA;m%g-)j&HD#39{XcKhrm_3nuNnt@Zt zQyanDcN)eC-(V%$JIv(mNm(9?8inVsWx?gW{1DH|)QbPl+0 z9>F`cMz;&2({wgn#6ay6fGRVj>=rCAnmQPN(KaKoSH3*e)N-__sax|3=Q5=e$llYG z2T0j8YfSzG#zFRe1me?9F(v2Oy7|(m*6MiJG>Rf_(+QQ^yRZE1Ah*AikIFZ(ME`s% zL=K^xGp%B))M6Z*eimwT%i*Hl(JL~cwx93v0=4d7l73c2MmDOITmG_%2dcF;R{S(| z>SKY<)siT1kW41??;+$5{Mk+~qUJ)wV z=h3Dlx7j3CkSbxsBM+K1n~z5SsDeCJVB}}?;%5_~n_FzO#6p%5HLd>a+dRy_YVph- zLE*tQ&=$A7SKO8xaI-iXvwdtn!uEL(gWK@4Xpo73aQgk?<$vt5jJf&2RBD09QrVe| zB5h{8cJDtk%!+b%p8VfloZ03qA2#&OsAcB%#p=?Nff*$$$^ToeogRpNphy1+oDx+! zO<&F8&RdpZmIU*3zBu}dC}R}&(~q45NiAVblT@{ZKeD-f{4<8I!pai(}(}jo;>M8 z+9@LG#K$hsvOJ;?J?&4lLtS%Tt~wHD(07($_e6a9Z1Wk|cj!zyp}@mI9Xb>Kd65EvrzBE!PHK!8t`EIZYUl~`G-IsR95E7Q3|0zHWMqyQw&mnny6 zjqgX8gKYwFDPF!dl>m}cU8i)P+`#c<+8)oUG#?688 z2Ysf-+D@aNOy(ssd9e^!ezYn(&CA)9szIv5s9zq@TfD0_dIh6<*>|LcpuT)uSmaTm zb&5r>OOcP@RFy76WHGz(c<#|&QyY)Ed=w=v=V~l}JQB$KT~Fn&Le6fNvqad>nvDC& zRkUvok^e)eYWsVmpZwJF?QcDdgop7T{1BM1{A1AjpYBN*4C&cjhPJNpRa%w7hx^iw z2b8CHx?=J06GOBD#p%jD7LsqNy`&IpG*-TDBj9JQf~5Ba$z;y*L}yMrF1IZkykIq5 z;&8Jxvw^aEqjwP%*{6?*b%Wt~-?-91m)Feqky&|A`X3_m_}l$|qjCL zJJhsEU5xz9uzP==*8fL&>i#JNL7ya~g~5ZmE9DYc3>FqC@TpZBUxEk)>Y87Rw&IB4 zIxY6+GfiTYC>KBC{D?M+8xkVdFE`#K@y(EX z8`_A}F-|nKWd_>6V4uBqgbF%~`i{@wI_Tm)^f?S|IT&rNCc}icxoNxRh{tin1yu^~ z<*jHhma8m{G&jH~jn;6zP1%|;;YSdxOVMEktI`YCyloDU6~jm#xQit1!?Ruj&sL5l z`!_eFJQ1~T7NnqO?QEhws*SKKXK6*QUZjZ@kxhMh#D=y`a`-PVY9z?XVr5kZ^M`eD z-|F+rdp9MJH`gi@+b?bRi3sk;tT%Zl98i5hNXZ{7Ox7*l3n`5yBo;yEg@phRXgs*r>Oo`^7^38?&?q$nnE-)PS-_2><_cyC zFT(+4zy9KAB;F7#uvRlQo%pBOZO<4 z#w6pTxLCRzZs^`B+=_P2NZ-iviG8Yr8-1)%MH>%4@;=!`ZJ6!{JOJyVtGL71=|Hs* zH|HUv62?JvAuwHSz~?ov$gK(^)M3>MZ==fa0o8EDzE^(+1~IWVK4ob`Ev?0F$8$PP zTAFuJwk(@c6N@ra2@eqXi%$Sg-}?#36S;u@)YgNLQpwUC9WB_ms@ zwnQ0byeR;l8tf?z$?39^ISQN2h<>(YIM)^IdRKLM$`wA0E7;V}SzAyq)S#Pz-GM0< z2`1U_o^x$rmx`4yCFOl-;;}ziz@Zddeu$1)aBy{wj?ZbZ3i+6=ryDouxbkDo1U#+F zsPj&nH9d#5{MULSC;-pJ@0WG? zcJ6Sth3A4hT!p+?Z)BczXlg3`wUvXTn?IzlT{*Cg;#qkVmxoAbTS;puIZ2qX(u$8! z#}dv|oW#^;Qt#(w2IMNY44x*$)6t3*2dy_YC>bMmRazBHAEZK|v3Gw~y$SuE_U50O8yQj;M_^cO6(-@?OYb6~8* zwSpd}A+K?Eh^oC4+A-b}h5IgZKGW*EqLVk%nNHw~10PX?mzWiJsL0om^O&@6+1AwZ zjas3wk*Iow>S)%+N~LLF+mg@pN>jY0K+>6a`3-Z=4vX>6H;Q|lb4wr3RG>YHl(D(4y40e=Yr^swUg!CbQ#X;U5>mgsav-KtSg(VUv33#Dp6#v zHN1k96RgR!*YmR6yy>lEaXcii?SE}xrOF?oa4$P1Xjxc=(ChipP5GX>F!;`K)#Xc3 zx{C+!&^&m@yIO}2IJXZ*!uN2Hz?WS7sd9=wnRmPgl8rVog){jH`tpV8&qXd2p$vh0 z2&&w~vsoC(kSGzLsk{+5QR>nxUgi)8v6{0&2_C$PcY6z?H}bL7voqfRIO^a>+0b+v z%w)-41))-=q3iXYYt>$SRyqHueToG2%-vPl53>r*!rIebKAI9c$dH*UY27UJTb8JG zV>pqbAUMxVzf-TsQ4RJR0p3Mgf~c(lBxG8_7bG9d#rNrpgtox-K>Bw3w_eqbiyTh3 z(@#cA7DF$`q&pEGvtA1Pb^TxR`|sF~vcbL`ViDs=2Sq?&K}5aevTz>pB=7PQi-eTX z9_iJq!UbRJnCt__mN!V5OY+TxqkoaQ{Z}*R0}L9V61medW%mV1L<4a~=<}@~cVj;n zwQ{ir#*OP5|H2gj){?)j{^wG|-QxrD?ezf1&;Aw)w7)7O>#Jn{Kq&w#&ZBRtVEpUq zzohmrUHC6+x%~7;UQ#vxbGZE_@ckvVKbG2Whl_uo)R=sy?4m#aZQiu-zj5AVJ=-96 z{p0&PeIF>NB7vEmJkrgpOnf{+0M9A*pWlC-zWe~Z*-PFjJP|wWK{Ns|*Nm`ot$n}s z=u5ZoFG!MvI5z=~ln04l!Nf#|t?ybRMQPw65TW-?1`dunJ*8}yOV6yQ%Vq4(+>+go zO0a`sghK3QSGtxP_Pl-rjl?SN;VFhwM1XABUxV2KW(^!lmtNCwIDS{mM&4 zhnylj@qLd1nO=RMM2;0yvRUDaEcLUe?|IIv`v``1w(X9AQKg=wRtR#EWSEU8=dmJZ zj(;r89ord~nui!Rq#7?7pHYEbFTu9TGqBR7HU`Zu%hK0L-=0jg0wJeJC?@wU_LY^n zR`f{;(EErba|AC6f4%^FQR&>K7h=d8Xg&2aQ1()}%s$wxQ`zG{TX;WvYs|F%)lwTn zoI_b%*sxOgK-}$>kD?#R`WT3IzaS9^yZGMpFt|{-Ol8k4;8CsUb^}Rh&uAwc?gjo7=FtcVnaW1 zA+rW7f*EyC76=>hr8|JQrG4d$Q|9`a)VgK+>sDe$XGF}B0Jn0oc zRs^UrT*b~^n|O3<+)9F;&pO(IC7E0RsTxIcgY)r0k`8^qRe>TshTX+C9#1^jt40WI z(tK*(xN62)m6U?`OYH5)&8R$N5DC$cKMwqOss<&X^&9Wu)tT2E`;i((- zwF^f`H03Nj%#UEJ=FE9ZaOybqTJ4Koa-j#9-lvFLqi^5YXXxZSS@fKF=1chie*H+V zxpJzk`sz%n(t5iAkEm{)Lo3J%TaB4au;nTSS+|qTn9ar!nRuWv0pZ+LoBQMO}c#A+2i`E-|^h*72lud z{?5JcJ@?%6UjE=S?3pL_^WAH$y`R0-`mUL^pbRJ^YFw?{e-dm`Eg*TeU#(!mg3lV0 zoyox#$7x_h<#3Q;r#_2QOKfaB=OxhpK!&aMI%A4#7=0KA%y= z!akt6{2wcH#L_Ozw+@~S5F+qcz@}{>CgnT4$HfF0~#|;BZIPpO3L++(_^wwse~pUgF5i(?XWjLnQ7f)2ax1ScY#P(je0{O=6cKil8Y z==CYP8Um+ zp4UR*^ZF~$$U}$ujqBFhV5Y6n)at4-Q&9Ef%2dbLkd(2%KATWSilbGTu>vYL8X@HA z{6R!>KR|O`+a$y2WIV`O+iYk?rf5o5s5oADn2BCAfTUTT3WW#*&)empxA*SR7SDW= zmN4ba^lf4PdZGcI9fI{3-Xfj1RbzBfQ(cdT+ZQFt2jrIH(h?;5c;8A}oXUDq&-F8J z&scIgU~gt>gwM@5N^#QNd2Vcio7titMU{;MB#RIFl=z9b$vLEXtn(eWI-IzyqNpT5 zuqJz+x5Bza47iSzE0?2B%%d0B4ciLd^hZz}m)KD}Mw!?WduA;PFIMa3)T zhx(Q!HJ~AmG&)gUF)KTv>dNkp{L^m9u!oV)Jl}){=YHY8xyVBStBSt<$W6p{6rb3p*~*EHAJA2+}Eu% zJa6iTn^BOD-{?j%(cMyWQ?@&U2eOned$@?OVmlWu^z*ngC^x+)x*C~jGf zJ{je;q%B)I{oH=^A?L?Rp;6r{2f>W*(!cWYAy}V&`ga-s3!r^;_RNX$1ZkxFhej$E z!U=#P#xVn9W{%OXgQ}{M(*9;1KMC6*)(3;QB!(!V;q|`3eP45JV4p+aXP_?tVr@Rk zb>J@CDN!|&E#wffA~{|oK0CF6vqzNaBS?s48g{09Adb;16gQxtPge;oH|DH}mE3qV z{DIM+n6c&Q1d{9c()t42C@eC*pLR9fhl>i_rNIb(xs`PNqpb#LH6mL9u?Inepy)yD zMYIknN&3&9A#!x!?ucG^PkjM|q-_y@0qhTPoF2}?5XFHt**VUgp7W2LZe$5wj-5+F zVArDkan@wGtb?wqGS*VKBUO2O>LJmchYA3?!)xZ%+p)R*4pvz`#!xSJjsZWozU9Y1X690h{my`O#L{R1L{=RuDOPQ>{~;Q4<< z_u}+_S}=bC-I8?j(bjGfWP5M1y~8f7Gi5#(k#L6CLyq*BMQkO{uO}k(8z%CV`w*RZ zS`fAM2SxlrY3?)qs0Kf|+l83?iUo0?V1c9r2wlx;eGkn%W2LHmEk^XIZ?C7Tk2ahe zYkNv95d$5Z{uu>PbZq2DW&XhuxrL`fUd?4DvWH=vEZTzt*`oHEHzD2jrBnAsWusH2 zFOd{A<6r7^w<5v}S(*KAD*vxbBB*{af8DOWRx+q;I4>isKgfivF3 z5ZVtj6cGA|XFT~PnDT~7d!C6dH|Q58IN%h2Z+yHbVgs0IbYx=tRJt{=B}T{%&TVSMz%$qw>jy3;!-`L z&Vs5j^C77HGCyebkCy&Ij{dU5{BclzS2>zL@Co$B7~tXOwzAX+=sP4H9(?!mWn;P4 z47kU!aX&L`&~946PBRFSHm8#i9UflKPT!6JrYysIr{NB>q?rfDbnX7Jq>x9|+Z zj|dx`oN|5<7lGhC`MyF-N6-NI&;9WGd86~c7_@)q46uGy+67sDjA;rLL*Ans^D+ba zn+l%Tv$|L2E^XqpDdQnir6=vB*BFmN6X47j|G*ieu5$5sJEn$Zdai2dk1I?XX7w zcoX5COWSg8*q=mr`X8vXDyhN8EfMb_ufF<13&!CdHxZlUm_&>SVggG<-gk26S2w@# z-wr_yxu)h*4o1xCM!$ptd_M-M{X;!}F@$g|4RTe%2Xo^jGb=~3K*BPo{l?KGT}4$n zr?yreg$m2&D_*@l>}Ec8G|wcu4fq!~Z#y zTZ5S!zV+@L4woLc68_Kb`xZYAF8m*(E~;59sqw9Ih}5IM2*q30|FFInwIf)PpgtR~ z8&2@ms(U?hZ7N!3EDWTn`sPBTqAkCTm1y2i`z`p-i142PT*9SUB7^a5ArTZ|ioqQx zS);i0k~Iz9it3ElHUtsEy@k#n^qnUrxC1>wkI`u@-4%Z(EQWKBc0=a%p3n6o16Q?g zY-NRT=VQtbtEdd~JooSFlk<2=H@MCl#{GDigEgurL?mv>SG7r2-*xm?RKGQOBpMC8 zWe2Z97!}$7Sc&>0v>_r4&hE*C2ty4L=4_pp$8Xy9K%Z@9jX)hO(*|PGDn+gPY?;wV zS~Hl99!cf%LDz;mfxks6Wug5KVAX0chKKfO5dhYl}gQG zIdJZ`@0q;EEh7$oSv!e6cK>(J*y_L3=qvu)!j3T4l;)e~e(2?(^*>j+@3%5o{>5RT zkLBG@Ap(7ls33wrApVs7&BDs3+3SnMyw_qIpp)^C*(m%vkVErBI0= zv4QZ)4=yErlB@_JA4JUlb=M8`gn7*40#ErzbNO6?LEHFEZT$J~;^gSWx(so+)Ff)N z$o!*`#itH^A#FvECnv@t8KO=Dv(wa z6jpDMw#ptz>=Sl0EYowA8+uIp&s{@Bg7$nmb1 zyYogD2oqgL%1;KI>UQjV!*0$~EzzP^P#dn{W6$Xp&64kvJ8_yBe2k_DU4!tl*|G6# z-IO7^rN2rvei;mW$`sKes7_0tIPjwEw`z(tguY9(%I}6CslD&*;MenmVadHyO zSP0nuO(_N6QVS5DOtN19)Qloumw?xtoZtV@ibFV@WsuIL3;q57clCo@gc*jsnLSx* zhHJ%KY%(lKdrpjJVnVKzCrL%TpFS?(i(QGu7b6;=yTZF-ZWy_v`j5x_>tQa`q!5_< zT!)j^r3A7s3?3xrZU}}kspDC|6_T86=}q=v%U-K-m*t<@U`EM%jQ%E`yq~)O^)MlA zD=lC-*W20yhzH8}AS9iprK|O!=vX6CZ&rdJuMbj|DozBDTE*E(v;{mwt5eb=h9D@J zXVrJno^zi)UXU1u$rZ>MXHRQYx7#gAX6os-W8817O`|Jnk0E({c|dCYn5ccey=7Xz zsnoULsCE3-jH{u__Kz&I}njTjYJ66xMa5t;LP{aq@&%w07Q`Ch;W#V0n}>dtaI(bB|V>hgcG)2)`k2nr<2?`@14L5%4+y@LkUL7RC1~i&B{8z^HcNMLcFU9PA0>I&uAOHYKB}NgWti z!2`T!IXQ3Z7s^#l{nQMgY;#j)Y5p_tZN}6x4-{27Nww0K{)eA1J+H+1YN2(>TqoDa z{pn3~H*Ob1zb=DEK5Z61Dqh0gRcW>&YZ)ImrU$Cpl?-FWuvL#Lo`}#~UZ#ejMK|Wr zpS^+69F7xdhnE*v9LC-!0(vQ&m9scwH2UpXk7+idAd#?3=X?AL3I}$5oSfh%ish+ zz^s{)kp(`8E#a?t?AY zlNh_qt4GQo-Xwh9Vcv&(qn#n%BgWIZY7vBueh6AfpM!M@Q{7w#A9c{uTX;TB>#vHz z!3XPK9d4r4oaCQB$G=#v{c!_uU(?P==L@po*1*df%=2S-{H`+**4#iHTlz6ZU>k6JzaISC>V4$7q5wr)mmN)$qZAUGlL@)6;`mKB!D- z_PZQU7h_vG5x9#B#IJhBewo*hgk(*Z?_amjxDcnmuF+C*T}_+aVUfKBYw8K1krH>S zd3x}A)X6f?g$ye1$f}}7y59ouN}A|Gkw3(q#JtLzb#;)h9N|V)9WJ0Nd%j$vMxtcB zTw5{P$7 zKU}$^28`g0*d1>|!pnhQeoF18B!4|hhza~5KxMVdUPMWhtW4cVJBS&z1t(l!0juhe z7BqtK>QtO#8}XLuIigt;yP87@#`CdPJYA01y3$Z}^K4)8h5Z?!#J6Jx&r*qx=nx}_ zU`lC_dma4n}-d zF_lMdrTYrK!ZnHx;T6Ez1)Km-<<(8TZoyrlKb?Ipsoec_@_u^H=gT*Os&~I$Rrh>_ z-HZSKd7O~0U)D~WQf|;TMy~qujYH&*#JzxJ9xar1S0o^7oD@$e*tspVRfAftlppXi zJGgHGASk>!ooxq>Mww+8XABqi-O$iNO=Rg zp$e)QD&)TN9RsBM6p$mYPbw|gS4sj zAm3y|DL^nFN+s)VMZ%QcIt_7;)63Ua8ny^3v%4*)U^u(A6r`iI+#EZH8+duQbh(m*7nSx+|PsbZR;nMa_TB(Odv7f-qjgzk{`0X;;E=PJsq*SC&d z+vi+*G%UZ+>)t+}AM3lbl~>=HBPmn?SB*+Uuy`M#8?lGref*Ra6RPF5!Fps>P!Yffw6y_tW!OEU(qhyo)w3o-joWe%F0gu`rjGe&vhZ^SCUbK#r2Bg{_ zmUcVYnDexq%CIv8e%Nb!W`ok!lPdRGx835Yl&{?k1JrN+^{9|&0?LW{s|;6dO*ZR- za@7QV;dNKTH&Xt@icCoIB>On9Qd|PeSMP7OHK0+;vh|#Vy|$aGn@KgsOK2O=c_U>U z%WUf&l{2M*bt=~T#Bvs!d&^L>YW#_luAF#H!@cm`wQ(do$KtSTBdl=b9N4>iVCaWw zU(N`TMv=?Ka+bt3!)0y3W0*<8GgcKKyGdVaf&g`g=a)2d-#Y<>=R~nzA<8dog$~bV zEM>lVx7p~!R&~lVr5SKLqlSsK2Z{5cstv|{Yr^|S3s~0JnJF2%6Bg>=fEP9nHt!{t zxEk~HypnXC1uqM#bu?0uUtZ*TJ_5huYQNldIKyAS{)toHE1+rHnV0C5K5bqENhoWY z;KbE8aif6|R)$cHIf~-Ld=+)b*;KEShj0X|S!|*Ex}$lrtJbnB6^S_|DiIZLXbgML z$fXI_+Y6NbFt@ylptrrW_Xj#1thaILd+-F>_N2?>eNkhB2ub`&kodF9z5ponwLJwP z9AP|~ts@D9W3@!wm-t#UXLMTe5gyGFden%KqkNIeFwTKmOvhm}5TBKGBsbAvNk=B8 znby-Mq0B6|NG?4H-OS;Ak4&}c>e9qp&HlpY?To-W(aH1W3@4ge119%f4Z*D)xJ zv5!PrLqC}!-&YP!aL~{gABfK~wdzlS$w8jBB`~B>1|0a5YFWJOrZ4L^ycUZq%MqU` zi>E@vDYiJ>5z!F#fjl(dT^yzwt*<<|Sp4eL8C%>EsHrz}#->M~XrwIbKv=sP+__JD znpS^VrqpZKu*u5O%KRPLP}Hcg+LZ^XJ^SFkE$WxCh}!Lv_mqLp7v2KweXe~0nD+9` zM+B?19D##B41y&sXk1A}g+P%*KsovzSwfWN6&Ec3Np^0d+ZWB01 zhO@g!V;H5}Zo>=Mo$Bc6QdgHP8uU#%OAVT}?mPVEB*-Z{D<2&TQbeI%kLG&qcyd`b zx}z#efVb+Z685CHI>PmPlcs&i09S6MWh1{?P$Bb;;G?t2wVYNjpN<)q&9(7xBPQjU zF4%gvvGxUk0=bF}lZ70nzTI$GCpIo*>kX!rK$Y|DT%%2t-7&2q?7 z7ownhTPH+nH#~*C-mz-*VAzsC{*X*Oi`uzlr;=6enOxoBWkGZ>_O$7z!e-t3mwAQB zhaJ0j1enIQF-eFVU6-`D3a;S-t#<%km`9PVt=t z*C2(&kU^U>ozHGOy<*<9Q&2OmvjBMpL}sPb4$sZ-TP{v*V?r7nxt=QJgK#<>S9Y_- zs^D3<@O|U_G7#}q!QP^_cU%r7Tm_vVTXpQDs)A`4e?yxsv}TKBFORNx&)I38Ue;x& z-zu(6aOG~!YyGRW^aOP8x*Z|win*-4wDHdu<56L~Q^iHc9aRV zpNFPT5xXIMCxeL~TA^l1Xz_cAEU5F5ItW1(sa-ct`%==78)SYXdgQ%z{XNVo=eHQA zFO!80Yv^WnQ(C6#9-m+Xbt@y}heS$wSgK-F_lT&?y$!VJ_Prfx=#QImFZCn8T_>0` zdBKomZtUgYY+PJ3!akQgwrt7taGe-mk;d9@XQP8KSdpB(Ld1yR3f5oKQ@y>!GZbHqlVuK>| z=H0h2gYbR5*Op^kYr^ECbOCH=O37&9f^8zSL73z%dJ?C8G+A-A1@m^=J_Ine`F`x~ zEa^FDkMj|pPHil$LerW@NFAO3tVGnTBzj_4WueZ(&H}h>#JSjko^B(C0?eMZMYM$76lV9%TF!=w!Wt z9l6{+_V*Vn2F|Y8!m*%B^CAQr!jyjOY)*j(n1L>h8O`LkNlc4kobl6-RIF~v5W8tz z>WT&=U#&71!RE$BlY;R2rM6QfW9G={dM6uVBAYB`$@t`k*V)o+ zPy2*=t&_*b5v#W&n#pU+S$qmL>Z`i3J z$x**W?}+yesPz|sBIHKGy~BOijQg`Xxx`6u-C$(d8jPpU#P$jl&=_iU3hGFm_;cGf z$`M{o$-?Az69$6UR%j-!M=;h|d>u1wwo{9cjJ1>KF;IhQqGHs7=_E%2i=ZKfn+K^( zX?IwAHw(PD6c_^+hNytcw<~(nx`b|9zL!G+b8lgPjH)iyyb~V;xPc>h+`B z;zg_}M#0mAgdv8Go{>=sS0#GgM~mZx)#nInhQL$t<%d=vm*IQm4Rnm7)iqus9C}{> zyDI^!4Y>6z4FOc72PLhAk~wP?)vv`-LAD(#E{8#7?xUwfXbYMHuIMa2(6Z`29ZkCxIE_ETWpX%OLojhu$ac-AN7THPbx5i}~8* z5hB{4&Jk#<#{?~$pT+c~*lh^QC=1oMaCUc$1rbP+Nnjnx;xZ519nFRHt-PENO)vV8 zZ|=>clVqq8zU+|35cYO@W~SZY$#6z_3l}3k9(qp~!ObUJZ=Qy3Hx`Ns1BN&P;%^4u zOlff;)c^~{+w{0HEID(!8pynTjQd*IutWLdo@{3&RAp{$i%dp~A>zIlkrzP+PTz)& zAkzJ=r9|e-qiA)!13#B8gH>~9)(+tfg-{!AL}86E z2yT4TT&cw?b7qjwga%lFbVb>JM$^uFFNjZktIJn|zt2{JhcbjhFr?KbnMjw%YA#?$ zpc0|>z{PA6)5&jJObM%tshr? z@U3pbB=O&#j7C`)n1e2S@{V7y@_0D5ZDWY03JM2HR+acew#ImvN&94_tR`0M+U6>) z+uRDONqVZ4%Vl!4Hms7~y&Co|G+xW^$x&*d->#qb@_>bVwABkj-<$6qH4j5=pdOl-HVP7V;#f#Ot9p2_!!79c9V-X4R==C0xCb-fb|qwoltmzm~tAndw_ zP9S+LXUZNU8kzqC?d57p64>2Rh|4o8AzGd(I-)4;$|$pi8q-r2B$SX_M)niOASSc| z7J?76S3d6`y9WECU`~Hg&f7|cPRqm8I#ULA)oJ-r;F^x~A9E~|cKWR{PmzTn6FDmmRD zj%^7h{LZ*U=KG8b?3j$ajvOyy1xy3CY8CUKiAoUE(Xg_^icaSkJ82A^GI=JXF))(s z-b3sb-%B5s-ytgf+Cb$F0uR^}RgqZKHOdu>G!+he04VKjBLujz*Zc0k7?JtZph3A4 z=Jhv6AB8C5nvn(rkv)XQ{N7r!c=&Zm3JUeoO%Wntf4sEswHbloX;cZJ4CVGgnK2(+ zODL;Q{6#S<7L62;;+0)S66)cL$6pM;tMCkA2M_!*Eos=K1bZKQ=}oB>)zwJu z)0$D2KR-I=Y$HsUF%m~)k_rn(>N3QqL7R43)V2GA7lvhq=aZ-{dEdxndZm_7E>S(d z;dUb%`KkuA6zBQ;v_`XryL{`=1bt0#PG^Em;~GRJ z*J~Bn?`Ip_(>KMMu&(O35utCfQft23(4xmNnl?f=!c7+rUb^|Be_&?bK62%W$^#pf z9zC~9*C@q+_MUe~3%9rU$%KuaYG-I%0CtKNzh6t8dYnJiT&7lOWOLmeRi=jPLJmveh(Ly+c|fb?OB zW~wi5PHIJ9a7|!_CXCiWZSbKSTDylSjz^?1pm=th9h^0520^J~3`PLz!<_6?HBk&e zn&@5eb97X{#*guPe~+R#7O&`C)&=1eNUCd! znK$oyA;KfRWg!2TJoz8#COU`uxAd6{)ER&85jltYx9I!})c>8Re+$66K>go|`i1-a zcSrl11 CP#mrR literal 0 HcmV?d00001 diff --git a/assets/js/06db3492.6ec62fef.js b/assets/js/06db3492.6ec62fef.js new file mode 100644 index 0000000..e9254c3 --- /dev/null +++ b/assets/js/06db3492.6ec62fef.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcar_ffeine=self.webpackChunkcar_ffeine||[]).push([[3861],{3905:(e,t,r)=>{r.d(t,{Zo:()=>p,kt:()=>m});var n=r(67294);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function o(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function i(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var c=n.createContext({}),l=function(e){var t=n.useContext(c),r=t;return e&&(r="function"==typeof e?e(t):i(i({},t),e)),r},p=function(e){var t=l(e.components);return n.createElement(c.Provider,{value:t},e.children)},f="mdxType",g={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},u=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,o=e.originalType,c=e.parentName,p=s(e,["components","mdxType","originalType","parentName"]),f=l(r),u=a,m=f["".concat(c,".").concat(u)]||f[u]||g[u]||o;return r?n.createElement(m,i(i({ref:t},p),{},{components:r})):n.createElement(m,i({ref:t},p))}));function m(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=r.length,i=new Array(o);i[0]=u;var s={};for(var c in t)hasOwnProperty.call(t,c)&&(s[c]=t[c]);s.originalType=e,s[f]="string"==typeof e?e:a,i[1]=s;for(var l=2;l{r.r(t),r.d(t,{assets:()=>c,contentTitle:()=>i,default:()=>g,frontMatter:()=>o,metadata:()=>s,toc:()=>l});var n=r(87462),a=(r(67294),r(3905));const o={slug:43,title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",authors:["gabriel"],tags:["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},i=void 0,s={permalink:"/43",source:"@site/blog/2023-10-19-visitors/index.mdx",title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",description:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",date:"2023-10-19T00:00:00.000Z",formattedDate:"2023\ub144 10\uc6d4 19\uc77c",tags:[{label:"\uce74\ud398\uc778",permalink:"/tags/\uce74\ud398\uc778"},{label:"\uc11c\ube44\uc2a4 \uacbd\ud5d8",permalink:"/tags/\uc11c\ube44\uc2a4-\uacbd\ud5d8"},{label:"\ud53c\ub4dc\ubc31",permalink:"/tags/\ud53c\ub4dc\ubc31"},{label:"\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30",permalink:"/tags/\uc804\uae30\ucc28-\uc0ac\uc6a9\uae30"},{label:"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571",permalink:"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"}],readingTime:.3,hasTruncateMarker:!1,authors:[{name:"\uac00\ube0c\ub9ac\uc5d8",title:"Frontend",url:"https://github.com/gabrielyoon7",imageURL:"https://github.com/gabrielyoon7.png",key:"gabriel"}],frontMatter:{slug:"43",title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",authors:["gabriel"],tags:["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},prevItem:{title:"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5",permalink:"/42"},nextItem:{title:"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec",permalink:"/41"}},c={authorsImageUrls:[void 0]},l=[],p={toc:l},f="wrapper";function g(e){let{components:t,...o}=e;return(0,a.kt)(f,(0,n.Z)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"no offset",src:r(8190).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(38707).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(7699).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(59493).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(50753).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(69573).Z,width:"1444",height:"1114"})))}g.isMDXComponent=!0},8190:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-1-24a19304076da351d45ffd4fad22dd3c.jpg"},38707:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-2-0e4dcf52281a9327ab66d76e8889ee5a.jpg"},7699:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-3-4d57784043b803d60241e3d4417c3582.jpg"},59493:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-4-73cea9b32c1a5f8557a64a28c7867923.jpg"},50753:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-5-914a0377b591f9d9dc58ea570c6fc8d1.jpg"},69573:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-6-038d09fa5b07bf8b8814e5e2463c4ccd.jpg"}}]); \ No newline at end of file diff --git a/assets/js/06db3492.853f24a9.js b/assets/js/06db3492.853f24a9.js deleted file mode 100644 index b26a9bd..0000000 --- a/assets/js/06db3492.853f24a9.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcar_ffeine=self.webpackChunkcar_ffeine||[]).push([[3861],{3905:(e,t,r)=>{r.d(t,{Zo:()=>p,kt:()=>m});var n=r(67294);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function o(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function i(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var s=n.createContext({}),c=function(e){var t=n.useContext(s),r=t;return e&&(r="function"==typeof e?e(t):i(i({},t),e)),r},p=function(e){var t=c(e.components);return n.createElement(s.Provider,{value:t},e.children)},f="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},g=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),f=c(r),g=a,m=f["".concat(s,".").concat(g)]||f[g]||u[g]||o;return r?n.createElement(m,i(i({ref:t},p),{},{components:r})):n.createElement(m,i({ref:t},p))}));function m(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=r.length,i=new Array(o);i[0]=g;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[f]="string"==typeof e?e:a,i[1]=l;for(var c=2;c{r.r(t),r.d(t,{assets:()=>s,contentTitle:()=>i,default:()=>u,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var n=r(87462),a=(r(67294),r(3905));const o={slug:43,title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",authors:["gabriel"],tags:["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},i=void 0,l={permalink:"/43",source:"@site/blog/2023-10-19-visitors/index.mdx",title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",description:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",date:"2023-10-19T00:00:00.000Z",formattedDate:"2023\ub144 10\uc6d4 19\uc77c",tags:[{label:"\uce74\ud398\uc778",permalink:"/tags/\uce74\ud398\uc778"},{label:"\uc11c\ube44\uc2a4 \uacbd\ud5d8",permalink:"/tags/\uc11c\ube44\uc2a4-\uacbd\ud5d8"},{label:"\ud53c\ub4dc\ubc31",permalink:"/tags/\ud53c\ub4dc\ubc31"},{label:"\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30",permalink:"/tags/\uc804\uae30\ucc28-\uc0ac\uc6a9\uae30"},{label:"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571",permalink:"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"}],readingTime:.29,hasTruncateMarker:!1,authors:[{name:"\uac00\ube0c\ub9ac\uc5d8",title:"Frontend",url:"https://github.com/gabrielyoon7",imageURL:"https://github.com/gabrielyoon7.png",key:"gabriel"}],frontMatter:{slug:"43",title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",authors:["gabriel"],tags:["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},prevItem:{title:"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5",permalink:"/42"},nextItem:{title:"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec",permalink:"/41"}},s={authorsImageUrls:[void 0]},c=[],p={toc:c},f="wrapper";function u(e){let{components:t,...o}=e;return(0,a.kt)(f,(0,n.Z)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"no offset",src:r(8190).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(38707).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(7699).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(59493).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(50753).Z,width:"1444",height:"1114"})))}u.isMDXComponent=!0},8190:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-1-24a19304076da351d45ffd4fad22dd3c.jpg"},38707:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-2-0e4dcf52281a9327ab66d76e8889ee5a.jpg"},7699:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-3-4d57784043b803d60241e3d4417c3582.jpg"},59493:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-4-73cea9b32c1a5f8557a64a28c7867923.jpg"},50753:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-5-914a0377b591f9d9dc58ea570c6fc8d1.jpg"}}]); \ No newline at end of file diff --git a/assets/js/2e801cce.448d8484.js b/assets/js/2e801cce.448d8484.js new file mode 100644 index 0000000..a70f08a --- /dev/null +++ b/assets/js/2e801cce.448d8484.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcar_ffeine=self.webpackChunkcar_ffeine||[]).push([[9450],{16029:n=>{n.exports=JSON.parse('{"blogPosts":[{"id":"44","metadata":{"permalink":"/44","source":"@site/blog/2023-10-20-level-4-last/index.mdx","title":"\uce74\ud398\uc778 \ub808\ubca84 \ud6c4\ubc18\uae30 \ub9ac\ud3ec\ud2b8","description":"\uc9c0\ub09c 4\uc8fc \uac04 \ubcc0\uacbd\uc0ac\ud56d \ubc0f \uc2e0\uaddc \uae30\ub2a5\uc744 \uc18c\uac1c\ud569\ub2c8\ub2e4!!","date":"2023-10-20T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 20\uc77c","tags":[{"label":"\uce74\ud398\uc778","permalink":"/tags/\uce74\ud398\uc778"},{"label":"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571","permalink":"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"},{"label":"\uc6b0\ud14c\ucf54","permalink":"/tags/\uc6b0\ud14c\ucf54"}],"readingTime":3.205,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"44","title":"\uce74\ud398\uc778 \ub808\ubca84 \ud6c4\ubc18\uae30 \ub9ac\ud3ec\ud2b8","authors":["gabriel"],"tags":["\uce74\ud398\uc778","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571","\uc6b0\ud14c\ucf54"]},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5","permalink":"/42"}},"content":"\uc9c0\ub09c 4\uc8fc \uac04 \ubcc0\uacbd\uc0ac\ud56d \ubc0f \uc2e0\uaddc \uae30\ub2a5\uc744 \uc18c\uac1c\ud569\ub2c8\ub2e4!!\\n\\n\uc774\ubc88 \uc5c5\ub370\uc774\ud2b8\uc5d0\ub294 \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc744 \uc9c1\uc811 \uc218\uc9d1\ud558\uc5ec \ud53c\ub4dc\ubc31\uc744 \ubc18\uc601\ud558\uc600\ub2f5\ub2c8\ub2e4!\\n\\n- [\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 1](https://car-ffeine.github.io/39)\\n- [\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 2](https://car-ffeine.github.io/40)\\n\\n## \ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\\n\\n### \ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc774\ub780?\\n\\n\ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc740 \uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ub418\ub294 \ub9c8\ucee4\ub4e4\uc744 \ud074\ub7ec\uc2a4\ud130\ub85c \ubb36\uc5b4\uc11c \ud45c\uc2dc\ud558\ub294 \uac83\uc744 \ub9d0\ud569\ub2c8\ub2e4. \ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc744 \uc0ac\uc6a9\ud558\uba74 \uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ub418\ub294 \ub9c8\ucee4\uc758 \uc218\ub97c \uc904\uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc740 \uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ub418\ub294 \ub9c8\ucee4\uc758 \uc218\uac00 \ub9ce\uc744 \ub54c \uc720\uc6a9\ud558\uac8c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uc5b4\ub514\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\ub098\uc694?\\n\\n\uc9c0\ub3c4\ub97c \ucd95\uc18c\ud558\ub294 \uacbd\uc6b0, \ub9c8\ucee4\uac00 \ud074\ub7ec\uc2a4\ud130\ub85c \ubb36\uc5ec \ud45c\uc2dc\ub429\ub2c8\ub2e4. \ud074\ub7ec\uc2a4\ud130\ub97c \ud074\ub9ad\ud558\uba74 \ud574\ub2f9 \uc9c0\uc5ed\uc73c\ub85c \ud655\ub300\ub429\ub2c8\ub2e4.\\n\\n![\uc9c0\uc5ed \ud074\ub7ec\uc2a4\ud130\ub9c1](./region.png)\\n![\uc11c\ubc84 \ud074\ub7ec\uc2a4\ud130\ub9c1](./clustered.png)\\n\\n## \ub3c4\uc2dc \uac80\uc0c9 \uae30\ub2a5\\n\\n### \ub3c4\uc2dc \uac80\uc0c9 \uae30\ub2a5\uc774\ub780?\\n\\n\uae30\uc874 \uac80\uc0c9\ucc3d\uc740 \ucda9\uc804\uc18c\uc758 \uc774\ub984\uacfc \uc8fc\uc18c\ub97c \uae30\ubc18\uc73c\ub85c \ud55c \uac80\uc0c9\uc774 \uac00\ub2a5\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc81c\ub294 \ub300\ud55c\ubbfc\uad6d\uc758 \uc8fc\uc694 \ub3c4\uc2dc\ub4e4\uc744 \uac80\uc0c9\ud560 \uc218 \uc788\ub294 \uae30\ub2a5\uc774 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc6d0\ud558\ub294 \uc9c0\uc5ed\uc744 \uac80\uc0c9\ud558\uace0, \ud574\ub2f9 \uc9c0\uc5ed\uc73c\ub85c \ube60\ub974\uac8c \uc774\ub3d9\ud560 \uc218 \uc788\uc73c\uba70 \uc9c0\ub3c4 \uc870\uc791\uc5d0 \ub9ce\uc740 \ub3c4\uc6c0\uc774 \ub429\ub2c8\ub2e4.\\n\\n### \uc5b4\ub514\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\ub098\uc694?\\n\\n\uac80\uc0c9\ucc3d\uc5d0 \uc6d0\ud558\ub294 \uc9c0\uc5ed\uc744 \uc785\ub825\ud558\uba74 \ubc14\ub85c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![\ub3c4\uc2dc \uac80\uc0c9\uacb0\uacfc](./city.png)\\n\\n\\n## \ub514\uc790\uc778 \uac1c\uc120\\n\\n### \uc778\ud3ec \uc708\ub3c4\uc6b0\uac00 \uac1c\uc120\ub418\uc5c8\uc5b4\uc694!\\n\\n\uae30\uc874 \uc778\ud3ec \uc708\ub3c4\uc6b0\ub294 \ucda9\uc804\uc18c\uc758 \uc774\ub984\uacfc \uc8fc\uc18c\ub9cc\uc744 \ud45c\uc2dc\ud558\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc81c\ub294 \uc0ac\uc6a9\ub7c9\uc744 \uc81c\uacf5\ud558\uba70, \uae38\ucc3e\uae30 \uae30\ub2a5\ub3c4 \uc81c\uacf5\ud569\ub2c8\ub2e4.\\n\\n![\uc778\ud3ec \uc708\ub3c4\uc6b0](./info-window.png)\\n\\n### \ucda9\uc804\uc18c \uc0ac\uc6a9 \ud1b5\uacc4 \uc815\ubcf4 \ub514\uc790\uc778\uc774 \ubcc0\uacbd\ub418\uc5c8\uc5b4\uc694\\n\\n![\ud1b5\uacc4 \uc815\ubcf4](./statistics.png)\\n\\n\uc0c8\ub85c\uc6cc\uc9c4 \ud0ed \ub514\uc790\uc778\uacfc \uc0c9\uc0c1\uc744 \uc801\uc6a9\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n### \ucda9\uc804\uc18c \ub9c8\ucee4\uac00 \uc774\uc6d0\ud654 \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![\ucda9\uc804\uc18c \ub9c8\ucee4](./marker-small.png)\\n![\ucda9\uc804\uc18c \ub9c8\ucee4](./marker-big.png)\\n\\n\uc9c0\ub3c4\ub97c \ucd95\uc18c\ud560 \uc218\ub85d \ub9c8\ucee4\uac00 \ub3c4\ub85c\ub97c \uac00\ub9ac\ub294 \ud604\uc0c1\uc774 \uc788\uc5b4 \uc0ac\uc774\uc988\uac00 \ub300\ud3ed \ucd95\uc18c\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e8, \ud655\ub300\ud558\ub294 \uacbd\uc6b0\uc5d0\ub294 \uae30\uc874\uacfc \ub3d9\uc77c\ud55c \ud615\ud0dc\uc758 \ub9c8\ucee4\ub97c \uc81c\uacf5\ud569\ub2c8\ub2e4."},{"id":"42","metadata":{"permalink":"/42","source":"@site/blog/2023-10-19-co-work.mdx","title":"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5","description":"\uc0ac\uc6a9\uc790 \ud53c\ub4dc\ubc31","date":"2023-10-19T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 19\uc77c","tags":[{"label":"collaboration","permalink":"/tags/collaboration"}],"readingTime":2.35,"hasTruncateMarker":false,"authors":[],"frontMatter":{"slug":"42","title":"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5","tags":["collaboration"]},"prevItem":{"title":"\uce74\ud398\uc778 \ub808\ubca84 \ud6c4\ubc18\uae30 \ub9ac\ud3ec\ud2b8","permalink":"/44"},"nextItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2","permalink":"/43"}},"content":"## \uc0ac\uc6a9\uc790 \ud53c\ub4dc\ubc31\\n\\n![image](https://github.com/woowacourse/service-apply/assets/106640954/e38ba7d6-7a56-43e3-926d-fdd5e1a8f80f)\\n\\n\uc800\ud76c \uc11c\ube44\uc2a4\ub97c \ubc30\ud3ec\ud558\uace0 \uc0ac\uc6a9\uc790\uc5d0\uac8c \ud53c\ub4dc\ubc31\uc744 \ubc1b\uc558\ub294\ub370, \ucd95\uc18c\ud588\uc744 \ub54c\uac00 \ub9ce\uc774 \ubd88\ud3b8\ud558\ub2e4\ub294 \ud53c\ub4dc\ubc31\uc774 \ub300\ubd80\ubd84\uc774\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc720\ub294 \uc544\ub798 \ud654\uba74\uacfc \uac19\uc2b5\ub2c8\ub2e4\\n\\n![asis](https://github.com/woowacourse-teams/2023-car-ffeine/assets/106640954/8792447a-5e3b-4afe-b556-2ef20e1c9cfd)\\n\\n\uc774\ub7f0 \uc11c\ube44\uc2a4\ub97c \ubcf8 \uc801\ub3c4 \uc5c6\uace0, \uc774\ub7f0 \uc11c\ube44\uc2a4\ub97c \uc0ac\uc6a9\ud558\uace0 \uc2f6\uc9c0\ub3c4 \uc54a\uc744 \uac83 \uc785\ub2c8\ub2e4. \ud574\ub2f9 \ubd80\ubd84\uc758 \ubb38\uc81c\ub97c \uc54c\uace0 \uc788\uc5c8\uc9c0\ub9cc \uc5b4\ub5bb\uac8c \ud45c\ud604\ud574\uc8fc\ub294 \uac83\uc774 \uc88b\uace0, \uad6c\ud604\ud560 \uc218 \uc788\ub294 \ubc29\ubc95\uc774 \ub5a0\uc624\ub974\uc9c0 \uc54a\uc544 6\ucc28 \ub370\ubaa8\ub370\uc774\uae4c\uc9c0 \ubbf8\ub8e8\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc5f4\uc2ec\ud788 \ud300 \ud68c\uc758\ub97c \ud55c \uacb0\uacfc \ud654\uba74\uc5d0 \ubcf4\uc774\ub294 \uc0ac\uc774\uc988\ub9cc\ud07c \uc77c\uc815 \ubc94\uc704\ub85c \ub098\ub220 \ucda9\uc804\uc18c \uac1c\uc218\ub97c \ubcf4\uc5ec\uc8fc\ub294 \ud074\ub7ec\uc2a4\ud130\ub9c1 \uae30\ub2a5\uc744 \ucd94\uac00\ud558\uae30\ub85c \uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \ud074\ub7ec\uc2a4\ud130 \uae30\ub2a5 \ucd94\uac00\\n\\n\ud574\ub2f9 \uae30\ub2a5\uc744 \uac04\ub2e8\ud558\uac8c \uc124\uba85\ub4dc\ub9ac\uba74 \ud654\uba74\uc758 \uc77c\uc815 \ubc94\uc704\ub85c \ub098\ub220 \ucda9\uc804\uc18c\uc758 \uac1c\uc218\ub97c \ubcf4\uc5ec\uc8fc\ub3c4\ub85d \uc11c\ubc84\uc5d0\uc11c \uacc4\uc0b0\ud558\uc5ec \ud074\ub77c\uc774\uc5b8\ud2b8\ub85c \uc804\ub2ec\ud558\ub3c4\ub85d \ud588\uc2b5\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uc804\ub2ec\ud55c \ud074\ub7ec\uc2a4\ud130\ub9c1 \ub9c8\ucee4\ub4e4\uc758 \uc704\uce58\uac00 \uc544\ub798\uc640 \uac19\uc774 \uc608\uc058\uac8c \ubcf4\uc774\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\\n![image (5)](https://github.com/woowacourse-teams/2023-car-ffeine/assets/106640954/133cb411-68bb-48f7-85a3-eca8a91d23bf)\\n\\n\ud654\uba74\uc758 \ud06c\uae30\uc5d0 \ube44\ud574 \ub9c8\ucee4\uac00 \uba87\uac1c \uc5c6\ub294 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub807\uac8c \ub41c\ub2e4\uba74 \uc0ac\uc6a9\uc790\ub294\\n\uadf8\ub807\uae30\uc5d0 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0 \ud574\ub2f9 \uae30\ub2a5\uc744 \ub2f4\ub2f9\ud55c \uac00\ube0c\ub9ac\uc5d8, \uc13c\ud2b8\uac00 \uc880 \ub354 \uc720\uc5f0\ud558\uac8c \ub9c8\ucee4\ub97c \ubcf4\uc5ec\uc8fc\ub294 \uac83\uc774 UX \uad00\uc810\uc5d0\uc11c \uc88b\ub2e4\uace0 \uc598\uae30\ud558\uc5ec\\n\\n\uc11c\ubc84 API\uc640 \ub85c\uc9c1\uc744 \ubcc0\uacbd\ud558\uc5ec \ub3d9\uc801\uc73c\ub85c \ud654\uba74\uc758 \ucda9\uc804\uc18c\ub97c \ud074\ub7ec\uc2a4\ud130\ud558\ub3c4\ub85d \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4. \uadf8\ub807\uac8c \ud558\uc5ec \uc544\ub798\uc640 \uac19\uc740 \ud654\uba74\uc744 \uc81c\uacf5\ud558\ub3c4\ub85d \ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n![final](https://github.com/woowacourse-teams/2023-car-ffeine/assets/106640954/c65d9a51-5d3d-407b-9d72-13f06580a502)\\n\\n\\n\uc774\uc0c1 \ud611\uc5c5 \uc77c\ud654 \uc600\uc2b5\ub2c8\ub2e4."},{"id":"43","metadata":{"permalink":"/43","source":"@site/blog/2023-10-19-visitors/index.mdx","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2","description":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.","date":"2023-10-19T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 19\uc77c","tags":[{"label":"\uce74\ud398\uc778","permalink":"/tags/\uce74\ud398\uc778"},{"label":"\uc11c\ube44\uc2a4 \uacbd\ud5d8","permalink":"/tags/\uc11c\ube44\uc2a4-\uacbd\ud5d8"},{"label":"\ud53c\ub4dc\ubc31","permalink":"/tags/\ud53c\ub4dc\ubc31"},{"label":"\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","permalink":"/tags/\uc804\uae30\ucc28-\uc0ac\uc6a9\uae30"},{"label":"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571","permalink":"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"}],"readingTime":0.3,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"43","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2","authors":["gabriel"],"tags":["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5","permalink":"/42"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec","permalink":"/41"}},"content":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./report-1.jpg)\\n![no offset](./report-2.jpg)\\n![no offset](./report-3.jpg)\\n![no offset](./report-4.jpg)\\n![no offset](./report-5.jpg)\\n![no offset](./report-6.jpg)"},{"id":"41","metadata":{"permalink":"/41","source":"@site/blog/2023-10-18-zero-time-deploy.mdx","title":"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec","description":"\uc548\ub155\ud558\uc138\uc694! \uce74\ud398\uc778\ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.","date":"2023-10-18T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 18\uc77c","tags":[{"label":"infra","permalink":"/tags/infra"},{"label":"ec2","permalink":"/tags/ec-2"},{"label":"cd","permalink":"/tags/cd"},{"label":"aws","permalink":"/tags/aws"},{"label":"zero-time","permalink":"/tags/zero-time"},{"label":"blue-green","permalink":"/tags/blue-green"}],"readingTime":8.93,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"41","title":"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec","authors":["jay"],"tags":["infra","ec2","cd","aws","zero-time","blue-green"]},"prevItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2","permalink":"/43"},"nextItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 2","permalink":"/40"}},"content":"\uc548\ub155\ud558\uc138\uc694! \uce74\ud398\uc778\ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\\n\uc800\ud76c \uce74\ud398\uc778 \ud300\uc5d0\uc11c \ubb34\uc911\ub2e8 \ubc30\ud3ec\ub97c \uc9c4\ud589\ud588\uc2b5\ub2c8\ub2e4.\\n\uc5b4\ub5a4 \uacfc\uc815\uc73c\ub85c \uc9c4\ud589\uc744 \ud588\ub294\uc9c0 \uc791\uc131\ud574\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4!\\n\\n---\\n\\n## \uae30\uc874 \ubc30\ud3ec \ubc29\uc2dd\uacfc \ubb38\uc81c\uc810\\n\\n\uba3c\uc800 \uce74\ud398\uc778 \ud300\uc758 \uae30\uc874 \ubc30\ud3ec \ubc29\uc2dd\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n

    \\n\\n\\n1. Target branch\uc5d0 push\uac00 \ub418\uba74 Github Actions\uac00 \uc791\ub3d9\ud569\ub2c8\ub2e4.\\n2. Target branch\uc758 \uc18c\uc2a4 \ucf54\ub4dc\uac00 \ube4c\ub4dc\ub418\uc5b4\uc11c Docker Hub\uc5d0 \uc62c\ub77c\uac00\uac8c \ub429\ub2c8\ub2e4.\\n3. Github Actions\uc758 self-hosted runner\ub97c \ud1b5\ud574 infra \uc11c\ubc84\uc5d0\uc11c prod \uc11c\ubc84\ub85c \uc811\uadfc\ud558\uc5ec\uc11c \uae30\uc874\uc5d0 \ub744\uc6cc\uc9c4 \uc11c\ubc84\ub97c \ub2e4\uc6b4 \uc2dc\ud0b5\ub2c8\ub2e4.\\n4. Docker Hub\uc5d0 \uc5c5\ub85c\ub4dc\ud55c Docker image\ub97c pull\ud574\uc11c \uc11c\ubc84\ub97c \uac00\ub3d9\uc2dc\ud0b5\ub2c8\ub2e4.\\n\\n\\n
    \\n\uc774\ub7f0 \uacfc\uc815\uc73c\ub85c \ubc30\ud3ec \uc2a4\ud06c\ub9bd\ud2b8\uac00 \uc791\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uc774 \ubc29\ubc95\uc740 \uae30\uc874 \uc11c\ubc84\ub97c \ub2e4\uc6b4 \uc2dc\ud0a4\uace0 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\ub97c \ub744\uc6b8 \ub54c \ub2e4\uc6b4 \ud0c0\uc784\uc774 \uc874\uc7ac\ud55c\ub2e4\ub294 \ubb38\uc81c\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n
    \\n\\n
    \\n\uc0ac\uc6a9\uc790 \uc785\uc7a5\uc5d0\uc11c\ub294 \uc798 \uc0ac\uc6a9\ud558\uace0 \uc788\ub294\ub370 \uac11\uc790\uae30 \uc11c\ube44\uc2a4\uac00 \uc791\ub3d9\ub418\uc9c0 \uc54a\ub294\ub2e4\uba74 \uc11c\ube44\uc2a4\uc5d0 \ub300\ud55c \uc2e0\ub8b0\uc131\uc774 \ub0ae\uc544\uc9c8 \uc218\ub3c4 \uc788\uace0 \uc774\ub7f0 \uc774\uc720\ub85c \uc774\ud0c8\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n## \uae30\uc874 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uae30\\n\\n\uc800\ud76c\ub294 \uba3c\uc800 \uc81c\ud55c\ub41c EC2 \uc778\uc2a4\ud134\uc2a4\ub85c \uc778\ud574 \ub864\ub9c1 \ubc30\ud3ec\uc758 \uc7a5\uc810\uc744 \uac00\uc838\uac08 \uc218 \uc5c6\uc5c8\uace0, \uce74\ub098\ub9ac \ubc29\uc2dd \ub610\ud55c \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \ud544\uc694\ub85c\ud55c \uc804\ub7b5\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \ube44\uad50\uc801 \ub864\ubc31\ub3c4 \ube60\ub978 Blue/Green \uc804\ub7b5\uc744 \uc120\ud0dd\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uc800\ud76c\uc758 Blue/Green \ubb34\uc911\ub2e8 \ubc30\ud3ec \uc2dc\ub098\ub9ac\uc624\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\ud3b8\uc758\ub97c \uc704\ud574\uc11c [\uae30\uc874 \uc11c\ubc84(\uae30\uc874 \ud3ec\ud2b8) / \uc0c8\ub85c\uc6b4 \uc11c\ubc84(\uc0c8\ub85c\uc6b4 \ud3ec\ud2b8)] \ub77c\ub294 \uba85\uce6d\uc744 \uc0ac\uc6a9\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n
    \\n\\n1. Target branch\uc5d0 push\uac00 \ub418\uba74 Github Actions\uac00 \uc791\ub3d9\ud569\ub2c8\ub2e4.\\n2. Target branch\uc758 \uc18c\uc2a4 \ucf54\ub4dc\uac00 \ube4c\ub4dc\ub418\uc5b4\uc11c Docker Hub \uc5d0 \uc62c\ub77c\uac00\uac8c \ub429\ub2c8\ub2e4.\\n3. Github Actions\uc758 self-hosted runner\ub97c \ud1b5\ud574 infra \uc11c\ubc84\uc5d0\uc11c prod \uc11c\ubc84\ub85c \uc811\uadfc\ud574\uc11c Docker Hub\uc5d0 \uc5c5\ub85c\ub4dc\ud55c \uc0c8\ub85c\uc6b4 \ubc84\uc804\uc758 Image\ub97c\\n pull \ud574\uc635\ub2c8\ub2e4.\\n4. \ub9cc\uc57d 8080 \ud3ec\ud2b8\uc5d0 \uae30\uc874 \uc11c\ubc84\uac00 \ub744\uc6cc\uc838 \uc788\uc73c\uba74 8081 \ud3ec\ud2b8\ub97c \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \ub744\uc6cc\uc9c8 \ud3ec\ud2b8\ub85c \uc9c0\uc815\ud574\uc8fc\uace0, \ubc18\ub300\ub85c 8081 \ud3ec\ud2b8\uc5d0 \uae30\uc874 \uc11c\ubc84\uac00 \ub744\uc6cc\uc838 \uc788\uc73c\uba74 8080 \ud3ec\ud2b8\uc5d0 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \ub744\uc6cc\uc9c8 \ud3ec\ud2b8\ub85c \uc9c0\uc815\ud574\uc90d\ub2c8\ub2e4.\\n5. \ubbf8\ub9ac Docker Hub\uc5d0 \uc5c5\ub85c\ub4dc\ud55c Docker image\ub97c [image+port]\ub77c\ub294 \ub124\uc774\ubc0d\uc73c\ub85c pull\uc744 \ud55c \ud6c4 \uc0c8\ub85c\uc6b4 \ud3ec\ud2b8\ub85c \uc11c\ubc84\ub97c \uac00\ub3d9\uc2dc\ud0b5\ub2c8\ub2e4.\\n6. \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \uc81c\ub300\ub85c \uac00\ub3d9 \ub410\ub294\uc9c0 \ud655\uc778\ud558\uae30 \uc704\ud574\uc11c \ud5ec\uc2a4 \uccb4\ud06c\ub97c \uc9c4\ud589\ud569\ub2c8\ub2e4. 20\ubc88 \ub3d9\uc548 \uc11c\ubc84\uac00 \uc815\uc0c1 \ub3d9\uc791\ud558\ub294\uc9c0 Spring Actuactor\ub97c \ud1b5\ud574\uc11c \ud655\uc778\uc744 \ud569\ub2c8\ub2e4.\\n7. \uc815\uc0c1 \uc791\ub3d9\uc774 \ub410\uc74c\uc744 \ud655\uc778\ud558\uba74 \ud604\uc7ac \uc778\uc2a4\ud134\uc2a4\uc5d0\ub294 2\ub300\uc758 \uc11c\ubc84\uac00 \ub744\uc6cc\uc838\uc788\uace0 \uc694\uccad\uc740 \uc5ec\uc804\ud788 \uae30\uc874 \uc11c\ubc84\ub85c \ub4e4\uc5b4\uac00\uac8c \ub429\ub2c8\ub2e4. \ub530\ub77c\uc11c Nginx\ub97c \ud1b5\ud574 \ud3ec\ud2b8\ud3ec\uc6cc\ub529\uc744 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uc758 \ud3ec\ud2b8\ub85c\\n \uc9c0\uc815\ud574\uc8fc\uace0 \uae30\uc874 \uc11c\ubc84\ub294 \ub0b4\ub824\uc90d\ub2c8\ub2e4.\\n\\n
    \\n\uc5ec\uae30\uae4c\uc9c0\uac00 \uce74\ud398\uc778 \ud300\uc758 \uc2dc\ub098\ub9ac\uc624\uc785\ub2c8\ub2e4.\\n\uadf8\ub807\ub2e4\uba74 \ud558\ub098\uc529 \uc2a4\ud06c\ub9bd\ud2b8\ub97c \ud655\uc778\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc740 \uc8fc\uc11d\uc73c\ub85c \ub2ec\uc544\ub450\uaca0\uc2b5\ub2c8\ub2e4 :)\\n\\n
    \\n
    \\n\\n### backend-deploy.yml\\n(Github Actions\uc5d0\uc11c \uc0ac\uc6a9)\\n\\n```yml\\nname: deploy\\n\\n# 1. prod/backend branch\uc5d0 push \uc791\uc5c5\uc774 \uc77c\uc5b4\ub098\uba74 \ud574\ub2f9 \uc791\uc5c5\uc744 \uc218\ud589\ud55c\ub2e4\\non:\\n push:\\n branches:\\n - prod/backend\\n\\njobs:\\n docker-build:\\n runs-on: ubuntu-latest\\n defaults:\\n run:\\n working-directory: ./backend\\n\\n steps:\\n # 2. \ub3c4\ucee4 \ud5c8\ube0c\uc5d0 \ub85c\uadf8\uc778\\n - name: Log in to Docker Hub\\n uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a\\n with:\\n username: ${{ secrets.DOCKERHUB_USERNAME }}\\n password: ${{ secrets.DOCKERHUB_PASSWORD }}\\n - uses: actions/checkout@v3\\n\\n # 3. JDK 17 \uc124\uce58 \ubc0f \ube4c\ub4dc (\ud504\ub85c\uc81d\ud2b8 Java version)\\n - name: Set up JDK 17\\n uses: actions/setup-java@v3\\n with:\\n java-version: \'17\'\\n distribution: \'adopt\'\\n\\n - name: Gradle Caching\\n uses: actions/cache@v3\\n with:\\n path: |\\n ~/.gradle/caches\\n ~/.gradle/wrapper\\n key: ${{ runner.os }}-gradle-${{ hashFiles(\'**/*.gradle*\', \'**/gradle-wrapper.properties\') }}\\n restore-keys: |\\n ${{ runner.os }}-gradle-\\n\\n - name: Grant execute permission for gradlew\\n run: chmod +x gradlew\\n - name: Build for asciiDoc\\n run: ./gradlew bootjar\\n\\n - name: Build with Gradle\\n run: ./gradlew bootjar\\n\\n # 4. \uc0b0\ucd9c\ubb3c\uc744 Image\ub85c \ube4c\ub4dc \ud6c4 Docker Hub\uc5d0 Image Push\ud558\uae30\\n - name: Extract metadata (tags, labels) for Docker\\n id: meta\\n uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7\\n with:\\n images: woowacarffeine/backend\\n\\n - name: Build and push Docker image\\n uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671\\n with:\\n context: .\\n file: ./backend/Dockerfile\\n push: true\\n platforms: linux/arm64\\n tags: woowacarffeine/backend:latest\\n labels: ${{ steps.meta.outputs.labels }}\\n\\n\\n deploy:\\n # 5. Self-hosted \uc791\ub3d9 -> infra \uc778\uc2a4\ud134\uc2a4\uc5d0\uc11c \uc791\ub3d9\ub428\\n runs-on: self-hosted\\n if: ${{ needs.docker-build.result == \'success\' }}\\n needs: [ docker-build ]\\n steps:\\n\\n # 6. infra \uc778\uc2a4\ud134\uc2a4\uc5d0\uc11c prod \uc778\uc2a4\ud134\uc2a4\ub85c \uc811\uadfc (\uc544\ub798\ubd80\ud130\ub294 prod \uc11c\ubc84 \ub0b4\uc5d0\uc11c \uc791\uc5c5)\\n - name: Join EC2 prod server\\n uses: appleboy/ssh-action@master\\n env:\\n JASYPT_KEY: ${{ secrets.JASYPT_KEY }}\\n DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}\\n DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}\\n with:\\n host: ${{ secrets.SERVER_HOST }}\\n username: ${{ secrets.SERVER_USERNAME }}\\n key: ${{ secrets.SERVER_KEY }}\\n port: ${{ secrets.SERVER_PORT }}\\n envs: JASYPT_KEY, DATABASE_USERNAME, DATABASE_PASSWORD\\n\\n script: |\\n\\n # 7. Docker Hub\uc5d0\uc11c Image\ub97c pull\ud574\uc628\ub2e4\\n sudo docker pull woowacarffeine/backend:latest\\n\\n # 8. \ub9cc\uc57d 8080 \ud3ec\ud2b8\uac00 \ucf1c\uc838 \uc788\uc73c\uba74 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uc758 \ud3ec\ud2b8\ub294 8081\ub85c \uc124\uc815\\n if sudo docker ps | grep \\":8080\\"; then\\n export BEFORE_PORT=8080\\n export NEW_PORT=8081\\n export NEW_ACTUATOR_PORT=8089\\n\\n # 9. \ub9cc\uc57d 8081 \ud3ec\ud2b8\uac00 \ucf1c\uc838 \uc788\uc73c\uba74 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uc758 \ud3ec\ud2b8\ub294 8080\ub85c \uc124\uc815\\n else\\n export BEFORE_PORT=8081\\n export NEW_PORT=8080\\n export NEW_ACTUATOR_PORT=8088\\n fi\\n\\n # 10. Docker\ub85c \uc0c8\ub85c\uc6b4 \uc11c\ubc84\ub97c \ub744\uc6b4\ub2e4.\\n sudo docker run -d -p $NEW_PORT:8080 -p $NEW_ACTUATOR_PORT:8088 \\\\\\n -e \\"SPRING_PROFILE=prod\\" \\\\\\n -e \\"ENCRYPT_KEY=${{secrets.JASYPT_KEY}}\\" \\\\\\n -e \\"DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}}\\" \\\\\\n -e \\"DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}}\\" \\\\\\n -e \\"REPLICA_DATABASE_USERNAME=${{secrets.REPLICA_DB_USER_NAME}}\\" \\\\\\n -e \\"REPLICA_DATABASE_PASSWORD=${{secrets.REPLICA_DB_USER_PASSWORD}}\\" \\\\\\n -e \\"SLACK_WEBHOOK_URL=${{secrets.SLACK_WEBHOOK_URL}}\\" \\\\\\n --name backend$NEW_PORT \\\\\\n woowacarffeine/backend:latest\\n\\n # 11. prod \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc788\ub294 bluegreen.sh \ub97c \uc791\ub3d9\ud55c\ub2e4. (\uc774 \ub54c port \uac12\uc744 \uac19\uc774 \ub123\uc5b4\uc900\ub2e4.)\\n sudo sh /home/ubuntu/bluegreen.sh $BEFORE_PORT $NEW_PORT $NEW_ACTUATOR_PORT\\n\\n```\\n\\n
    \\n
    \\n\\n### bluegreen.sh\\n(prod \uc778\uc2a4\ud134\uc2a4 \ub0b4\ubd80\uc5d0 \uc874\uc7ac)\\n\\n```shell\\n#!/bin/bash\\n\\n# 1. Github Actions\ub97c \ud1b5\ud574 \ub118\uaca8 \ubc1b\uc740 \ud658\uacbd\ubcc0\uc218 \uac12\\nBEFORE_PORT=$1\\nNEW_PORT=$2\\nNEW_ACTUATOR_PORT=$3\\n\\necho \\"\uae30\uc874 \ud3ec\ud2b8 : $BEFORE_PORT\\"\\necho \\"\uc0c8\ub85c\uc6b4 \ud3ec\ud2b8: $NEW_PORT\\"\\necho \\"\uc0c8\ub85c\uc6b4 ACTUATOR_PORT: $NEW_ACTUATOR_PORT\\"\\n\\n\\n# 2. 20\ubc88 \ub3d9\uc548 \ud5ec\uc2a4 \uccb4\ud06c\ub97c \uc9c4\ud589\\ncount=0\\nfor count in {0..20}\\ndo\\n echo \\"\uc11c\ubc84 \uc0c1\ud0dc \ud655\uc778(${count}/20)\\";\\n\\n # 3. \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \uc791\ub3d9\ub418\ub294\uc9c0 Actuator\ub97c \ud1b5\ud574 \uac12\uc744 \ubc1b\uc544\uc634\\n STATUS=$(curl -s http://127.0.0.1:${NEW_ACTUATOR_PORT}/actuator/health-check)\\n\\n # 4. Actuator\ub97c \ud1b5\ud574 \uc131\uacf5\uc801\uc73c\ub85c \uc11c\ubc84\uac00 \ub744\uc6cc\uc9c0\uc9c0 \uc54a\uc740 \uacbd\uc6b0\\n if [ \\"${STATUS}\\" != \'{\\"status\\":\\"up\\"}\' ]\\n \\tthen\\n # 5. 10\ucd08\ub97c \uae30\ub2e4\ub9b0 \ud6c4 \ub2e4\uc2dc \ud5ec\uc2a4 \uccb4\ud06c\ub97c \uc9c4\ud589\ud55c\ub2e4.\\n \\t\\tsleep 10\\n \\t\\tcontinue\\n \\telse\\n # 6. \ud5ec\uc2a4 \uccb4\ud06c\ub97c \ud1b5\ud574 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \uc131\uacf5\uc801\uc73c\ub85c \uc791\ub3d9\ub41c\ub2e4\uba74 \uba48\ucd98\ub2e4.\\n \\t\\tbreak\\n fi\\ndone\\n\\n\\n# 7. 20\ubc88\uc758 \ud5ec\uc2a4 \uccb4\ud06c\ub97c \ud558\ub294 \ub3d9\uc548 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \uc81c\ub300\ub85c \uc791\ub3d9\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc885\ub8cc\\nif [ $count -eq 20 ]\\nthen\\n\\techo \\"\uc0c8\ub85c\uc6b4 \uc11c\ubc84 \ubc30\ud3ec\ub97c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4.\\"\\n\\texit 1\\nfi\\n\\n\\n# 8. \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \uc131\uacf5\uc801\uc73c\ub85c \uc791\ub3d9\ud55c \uacbd\uc6b0\\n# Nginx\ub97c \ud1b5\ud574 \ud3ec\ud2b8\ud3ec\uc6cc\ub529\uc744 \uae30\uc874 \ud3ec\ud2b8\uc5d0\uc11c \uc0c8\ub85c\uc6b4 \ud3ec\ud2b8\ub85c \ubcc0\uacbd\ud574\uc900\ub2e4.\\n# \uc774 \ubd80\ubd84\uc740 .inc \ud30c\uc77c\uc744 \ud1b5\ud574 Nginx\uc5d0\uc11c \uc8fc\uc785 \ubc1b\uc544\uc11c \ud3ec\ud2b8\ub9cc \ubcc0\uacbd\ud574\ub3c4 \ub429\ub2c8\ub2e4!\\nexport BACKEND_PORT=$NEW_PORT\\nenvsubst \'${BACKEND_PORT}\' < backend.template > backend.conf\\nsudo mv backend.conf /etc/nginx/conf.d/\\nsudo nginx -s reload\\n\\n\\n# 9. \uae30\uc874 \uc11c\ubc84\ub97c \ub0b4\ub824\uc8fc\uace0, \ub3c4\ucee4 \ub9ac\uc18c\uc2a4\ub97c \uc815\ub9ac\ud574\uc900\ub2e4\\ndocker stop backend$BEFORE_PORT\\nsudo docker container prune -f\\n```\\n\\n\\n\uc774\ub807\uac8c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ubb34\uc911\ub2e8 \ubc30\ud3ec\ub97c \ub3c4\uc785\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uae34 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4 :)"},{"id":"40","metadata":{"permalink":"/40","source":"@site/blog/2023-10-15-carffeine-tester-2/index.mdx","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 2","description":"\uc548\ub155\ud558\uc138\uc694? \uc13c\ud2b8\uc640 \uac00\ube0c\ub9ac\uc5d8 \uc785\ub2c8\ub2e4.","date":"2023-10-15T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 15\uc77c","tags":[{"label":"\uce74\ud398\uc778","permalink":"/tags/\uce74\ud398\uc778"},{"label":"\uc11c\ube44\uc2a4 \uacbd\ud5d8","permalink":"/tags/\uc11c\ube44\uc2a4-\uacbd\ud5d8"},{"label":"\ud53c\ub4dc\ubc31","permalink":"/tags/\ud53c\ub4dc\ubc31"},{"label":"\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","permalink":"/tags/\uc804\uae30\ucc28-\uc0ac\uc6a9\uae30"},{"label":"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571","permalink":"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"}],"readingTime":14.665,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"},{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"}],"frontMatter":{"slug":"40","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 2","authors":["gabriel","scent"],"tags":["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec","permalink":"/41"},"nextItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 1","permalink":"/39"}},"content":"\uc548\ub155\ud558\uc138\uc694? \uc13c\ud2b8\uc640 \uac00\ube0c\ub9ac\uc5d8 \uc785\ub2c8\ub2e4.\\n\\n\uc800\ud76c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc9c0\ub09c\ubc88 [\uce74\ud398\uc778 \uc11c\ube44\uc2a4 1\ucc28 \uccb4\ud5d8](https://car-ffeine.github.io/39) \uc9c4\ud589 \uc774\ud6c4 \uc77c\ubd80 \uae30\ub2a5 \uac1c\uc120\uc774 \uc788\uc5c8\uc2b5\ub2c8\ub2e4. \uae30\ub2a5 \uac1c\uc120\uc758 \uc720\uc6a9\uc131\uc744 \ud310\ubcc4\ud558\uace0\uc790 \uce74\ud398\uc778 \uc11c\ube44\uc2a4 2\ucc28 \uccb4\ud5d8\uc744 \ub2e4\ub140\uc654\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c \ud300\uc5d0\uc11c 1\ucc28 \uccb4\ud5d8 \uc774\ud6c4 \uac1c\uc120\ud55c \uc0ac\ud56d\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n### 1. \uc9c0\uc5ed\uac80\uc0c9\\n\\n![no offset](./city-search.png)\\n\\n- \uc774\uc81c\ub294 \uac80\uc0c9\uc5b4\ub97c \uc785\ub825\ud558\ub294 \uacbd\uc6b0, \uc804\uad6d \ub3c4\uc2dc\uc758 \uc8fc\uc18c\uac00 \uac19\uc774 \uc81c\uacf5\ub429\ub2c8\ub2e4.\\n\\n### 2. \ucda9\uc804\uc18c \ub9c8\ucee4\ub97c \ud655\uc778\ud560 \uc218 \uc788\ub294 \uc9c0\ub3c4 \uc601\uc5ed \ud655\uc7a5\\n\\n![no offset](./mobile-markers.png)\\n\\n(\uae30\uc874\uc5d0\ub294 \uc704 \uc0ac\uc9c4\ubcf4\ub2e4 \uc881\uc740 \uc601\uc5ed\ub9cc\uc744 \ud638\ucd9c\ud558\ub294 \uac83\uc774 \ud5c8\uc6a9\ub418\uc5c8\ub2e4.)\\n- \ubaa8\ubc14\uc77c\uc5d0\uc11c \uc880 \ub354 \ub113\uc740 \uc601\uc5ed\uc744 \ud638\ucd9c\ud558\ub294 \uac83\uc744 \ud5c8\uc6a9\ud588\uc2b5\ub2c8\ub2e4. \uc6d0\ub798\ub294 \ub514\ubc14\uc774\uc2a4 \ub108\ube44\ub97c \uace0\ub824\ud558\uc9c0 \uc54a\uace0 \uc90c \ub808\ubca8 \uae30\uc900\uc73c\ub85c \uc694\uccad\uc744 \uc81c\ud55c\ud588\uc73c\ub098, \uc774\uc81c\ub294 \uc0ac\uc6a9\uc790 \ub514\ubc14\uc774\uc2a4\uc5d0 \ubcf4\uc774\ub294 \uc9c0\ub3c4\uc758 \uc601\uc5ed \ud06c\uae30\ub97c \uae30\ubc18\uc73c\ub85c \uc694\uccad\uc744 \uc81c\ud55c\ud558\ub294 \ubc29\uc2dd\uc744 \ub3c4\uc785\ud588\uc2b5\ub2c8\ub2e4.\\n- \uae30\uc874\uc5d0 \uc0ac\uc6a9\ud558\ub358 \ub9c8\ucee4\uc758 \ub2e8\uc810\uc740, \uadf8 \ud06c\uae30\uac00 \ub108\ubb34 \ud06c\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4. \uc774\ub85c\uc778\ud574 \ub354 \ub113\uc740 \uc601\uc5ed\uc744 \ubcf4\uc5ec\uc8fc\ub294 \uacbd\uc6b0\uc5d0 \ub9c8\ucee4\ub4e4\uc774 \uacb9\uce58\ub294 \ud604\uc0c1\uc774 \uc788\uc5c8\ub294\ub370\uc694, \uc774\ub97c \uc218\uc815\ud558\uae30 \uc704\ud574 \ud2b9\uc815 \uc601\uc5ed \ud06c\uae30 \uc774\uc0c1\uc5d0\uc11c\ub294 \ub9c8\ucee4\ub97c \uc880 \ub354 \uac04\uc18c\ud654 \ub41c \ub514\uc790\uc778\uc73c\ub85c \ubcf4\uc774\ub3c4\ub85d \uac1c\uc120\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n- \ub9c8\ucee4 \uc0ac\uc774\uc988\uac00 \uc791\uc544\uc9c0\uba74\uc11c \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ucda9\uc804\uae30 \uac1c\uc218\uac00 \ub354\uc774\uc0c1 \ub4e4\uc5b4\uac08 \uacf5\uac04\uc774 \uc5c6\uc5b4\uc84c\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \ub9c8\ucee4 \uc0c9\uc0c1\uc740 \uadf8\ub300\ub85c \uc720\uc9c0\ub97c \ud558\ub418, \uc778\ud3ec \uc708\ub3c4\uc6b0\uc5d0 \ud604\uc7ac \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ucda9\uc804\uae30 \uac1c\uc218\ub97c \ubcf4\uc5ec\uc8fc\ub294 \ubc29\uc2dd\uc73c\ub85c \ub514\uc790\uc778\uc744 \uac1c\uc120\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n## \uccb4\ud5d8 \uaddc\uce59 \uc124\uc815\\n\\n\uac1c\uc120\ud55c \uae30\ub2a5\uc774 \uc2e4\uc81c\ub85c \uc720\uc6a9\ud55c\uc9c0 \ud655\uc778\ud574\ubcf4\uae30 \uc704\ud574 \uc800\ud76c\ub294 \uce74\ud398\uc778 \uc11c\ube44\uc2a4 2\ucc28 \uccb4\ud5d8\uc758 \uaddc\uce59\uc744 \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 \uc880 \ub354 \uc758\ubbf8\uc788\ub294 \uacbd\ud5d8\uc744 \ud558\uae30\uc704\ud574 1\ucc28 \uccb4\ud5d8 \ub54c \uc815\ud588\ub358 \uaddc\uce59\uc5d0 \ub354\ud574\uc11c \ub2e4\uc74c\uacfc \uac19\uc740 \ucd94\uac00 \uaddc\uce59\uc744 \uc124\uc815\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n### \uc911\uac04\uc5d0 \ubaa9\ud45c \uc9c0\uc810\uc774 \ub9ce\uc774 \ubcc0\uacbd\ub41c\ub2e4\\n\\n\uc9c0\ub09c \uce74\ud398\uc778 \uc11c\ube44\uc2a4 1\ucc28 \uccb4\ud5d8\uc5d0\uc11c\ub294 \uc9c0\uc5ed \uac80\uc0c9\uc774 \uc5c6\uc5b4 \ubaa9\ud45c \uc9c0\uc810\uc744 \ucc3e\ub294 \uac83\uc774 \ubd88\ud3b8\ud588\uc2b5\ub2c8\ub2e4. 1\ucc28 \uccb4\ud5d8 \uc774\ud6c4 \uc9c0\uc5ed \uac80\uc0c9\uc774 \ucd94\uac00 \ub418\uc5c8\uc73c\ubbc0\ub85c \uc774 \uae30\ub2a5\uc774 \uc5bc\ub9c8\ub098 \uc720\uc6a9\ud55c\uc9c0 \uacbd\ud5d8\ud574\ubcf4\uace0\uc790 \uc774 \uaddc\uce59\uc744 \uc124\uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ucd94\uac00\ub85c \ubaa9\ud45c \uc9c0\uc810 \uc8fc\ubcc0\uc758 \ucda9\uc804\uc18c\ub97c \ud655\uc778\ud560 \ub54c \uc0c8\ub85c \ucd94\uac00\ub41c \uc9c0\ub3c4 \uc601\uc5ed \ud655\uc7a5\uc774 \uc5bc\ub9c8\ub098 \uc720\uc6a9\ud55c\uc9c0\ub3c4 \uacbd\ud5d8\ud574\ubcf4\uace0\uc790 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \uccb4\ud5d8 \uac1c\uc694\\n\\n![no offset](./routes.png)\\n1. \uc7a0\uc2e4\uc5ed \ucd9c\ubc1c\\n2. \ud558\ub0a8 \ub9cc\ub450\uc9d1\\n3. \ub2e4\uc74c \ubaa9\uc801\uc9c0 \uc124\uc815\\n4. \ud310\uad50\\n\\n## \uccb4\ud5d8 \ud6c4\uae30\\n\\n### \uc7a0\uc2e4\uc5ed \ucd9c\ubc1c\\n\\n![no offset](./from-jamsil.png)\\n\\n\uc3d8\uce74\uc5d0\uc11c EV6\ub97c \ub300\uc5ec\ud574\uc11c `\uac00\ube0c\ub9ac\uc5d8`, `\uc13c\ud2b8`, `\ud0a4\uc544\ub77c`\uac00 \uc7a0\uc2e4\uc5ed\uc5d0\uc11c \ucd9c\ubc1c\ud558\uc600\uc2b5\ub2c8\ub2e4. \uc800\ub141 \ud1f4\uadfc \uc774\ud6c4\uc5d0 \ub0a8\uc774\uc12c\uc744 \uac00\ub824\uace0 \ubaa9\uc801\uc9c0\ub97c \uc124\uc815\ud558\uc600\uc73c\ub098 \ubc30\uac00 \ub108\ubb34 \uace0\ud30c\uc11c \uac00\ub294 \uae38\uc5d0 \uc2dd\uc0ac\ub97c \ud558\uc790\uace0 \uc598\uae30\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\\n### \ud558\ub0a8 \ub9cc\ub450\uc9d1\\n\\n\ub530\ub77c\uc11c \uc9c4\uc815\ud55c \ucc98\uc74c \ubaa9\uc801\uc9c0\ub294 \uc2a4\ud0c0\ud544\ub4dc\uc600\uc73c\ub098, \uac00\ube0c\ub9ac\uc5d8\uc740 \ub3d9\ub124 \uc8fc\ubbfc\uc774\ub77c \uc2a4\ud0c0\ud544\ub4dc\ub97c \ub108\ubb34 \uc798 \uc54c\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc2a4\ud0c0\ud544\ub4dc\uc5d0 \uc804\uae30\ucc28 \ucda9\uc804\uc18c\uac00 \uc5b4\ub514\uc5d0 \uc788\ub294\uc9c0\ub3c4 \uc54c\uace0\uc788\uc73c\ubbc0\ub85c \ubaa9\uc801\uc9c0\ub97c \uae09\ud558\uac8c \ubcc0\uacbd\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4. \uc774 \ub54c \ubaa9\uc801\uc9c0 \ubcc0\uacbd\uc744 \uc704\ud574 \uc8fc\ubcc0 \uc2dd\ub2f9\uc744 \ub458\ub7ec\ubcf4\ub358 \uc911\uc5d0 \uad1c\ucc2e\uc740 \uc2dd\ub2f9\uc744 \ubc1c\uacac\ud574\uc11c \ud574\ub2f9 \uc2dd\ub2f9\uc744 \uae30\uc900\uc73c\ub85c \uc8fc\ubcc0 \ucda9\uc804\uc18c\ub97c \ud655\uc778\ud574\ubcf4\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./go-to-starfield.png)\\n\\n\uc2dd\ub2f9 \uc8fc\ubcc0\uc744 \uac00\uae30 \uc704\ud574 \uc9c0\uc5ed \uac80\uc0c9\uc744 \ucc98\uc74c\uc73c\ub85c \uc0ac\uc6a9\ud558\uc5ec \uc2dd\ub2f9\uacfc \uac00\uae4c\uc6b4 \uc9c0\uc5ed\uc744 \ud0d0\uc0c9\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c \uc2dd\ub2f9\uc5d0\ub294 \ucda9\uc804\uc18c\uac00 \uc5c6\ub2e4\ub294 \uc0ac\uc2e4\uc744 \uc54c\uac8c\ub418\uc5b4, \uadfc\ucc98 \ucda9\uc804\uc18c\ub97c \ucc3e\uc544\ubcf4\uae30 \uc704\ud574\uc11c \uc9c0\ub3c4\ub97c \ucd95\uc18c\ud588\ub354\ub2c8 1\ucc28 \uccb4\ud5d8\ub54c\uc640\ub294 \ub2ec\ub9ac \ub354 \ub113\uc740 \uc601\uc5ed\uc744 \ubcf4\uc5ec\uc92c\uc2b5\ub2c8\ub2e4. \uc774\uc804\uc5d0\ub294 \ub9c8\ucee4 \uc790\uccb4\uac00 \ubcf4\uc774\uc9c0 \uc54a\uc544 \ub2f5\ub2f5\ud558\uc600\uc73c\ub098, \uc774\uc81c\ub294 \ub354 \ub113\uc740 \uc601\uc5ed\uc744 \uc870\ud68c\ud560 \uc218 \uc788\uac8c \ub418\uc5b4 \ud3b8\ub9ac\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc9c0\ub09c \uccb4\ud5d8 \uc774\ud6c4\ub85c \ud53c\ub4dc\ubc31\uc744 \uc790\uccb4 \uc218\uc9d1\ud558\uc5ec \uac1c\ubc1c\ud55c \uae30\ub2a5\ub4e4\uc774 \ud3b8\ud558\ub2e4\ub294 \uac83\uc744 \uc2dd\ub2f9\uc5d0 \uac00\ub294 \uae38\uc5d0 \ub290\ub084 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n### \ub2e4\uc74c \ubaa9\uc801\uc9c0 \uc124\uc815\\n\\n![no offset](./mandu-mandu.png)\\n\\n\ud558\ub0a8 \ub9cc\ub450\uc9d1\uc5d0\uc11c \uc2dd\uc0ac\ub97c \ud558\ub2e4\uac00 \uc54c\uac8c\ub41c \uc0ac\uc2e4\uc740, \ub0a8\uc774\uc12c\uc740 \uc0dd\uac01\ubcf4\ub2e4 \ub108\ubb34 \uba40\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4. \uc2dd\uc0ac\ub97c \ub9c8\uce58\uace0 \ub0a8\uc774\uc12c\uc5d0 \uac00\uba74, \ucda9\uc804\ub3c4 \uc81c\ub300\ub85c \ubabb\ud558\uace0 \ub3cc\uc544\uc62c \ud310\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc2dd\uc0ac\ub97c \ud558\uba74\uc11c \ub2e4\ub978 \ubaa9\uc801\uc9c0\ub97c \uc54c\uc544\ubd24\ub294\ub370, \uac00\ube0c\ub9ac\uc5d8\uc774 \uc608\uc804\uc5d0 \uac00\ubd24\ub358 \uacf3 \uc911\uc5d0\uc11c \ub0a8\uc591\uc8fc\uc758 \ubb3c\uc758 \uc815\uc6d0\uc774 \uc2dc\uac04\uc744 \ub5bc\uc6b0\uae30 \uc88b\ub2e4\ub294 \uc18c\ub9ac\ub97c \ud558\uc600\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \ubb3c\uc758 \uc815\uc6d0\uc744 \uac80\uc0c9\ud574\ubcf4\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\ub180\ub78d\uac8c\ub3c4 \ubb3c\uc758\uc815\uc6d0\uc740 \uac80\uc0c9\uacb0\uacfc\uc5d0 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4!\\n\\n\uc5b4\uca54 \uc218 \uc5c6\uc774 \uce74\uce74\uc624 \uc9c0\ub3c4\ub85c \ubb3c\uc758 \uc815\uc6d0 \uc704\uce58\ub97c \ud655\uc778\ud558\uc5ec \uc8fc\uc18c\ub97c \uc54c\uc544\ub0b4\uc5c8\uace0, \uc774 \uc8fc\uc18c\ub97c \uce74\ud398\uc778 \uac80\uc0c9\ucc3d\uc5d0 \ub123\uc5c8\uc2b5\ub2c8\ub2e4. \uc800\ud76c\ub294 \uc774 \uacfc\uc815\uc5d0\uc11c \uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub294 \uc5c5\uccb4\uba85 \uc870\ud68c\uac00 \uc548\ub41c\ub2e4\ub294 \uac83\uc774 \uce58\uba85\uc801\uc778 \ub2e8\uc810\uc774\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\ub9cc, \uc774 \uae30\ub2a5\uc740 \uac80\uc0c9 \ud560 \ub54c\ub9c8\ub2e4 \ub9ce\uc740 \ube44\uc6a9\uc774 \uccad\uad6c\ub418\uc5b4 \ud604\uc2e4\uc801\uc73c\ub85c \uc9c0\uae08 \ub2f9\uc7a5 \uae30\ub2a5\uc744 \ub123\ub294 \uac83\uc740 \uc5b4\ub835\ub2e4\uace0 \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uacb0\uad6d \uc8fc\uc18c \uac80\uc0c9\uc744 \ud1b5\ud574 \ubb3c\uc758 \uc815\uc6d0\uacfc \uac00\uc7a5 \uac00\uae4c\uc6b4 \ucda9\uc804\uc18c\ub97c \uc54c\uc544\ub0b4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7f0\ub370! \uc9c0\ub3c4\ub97c \ucd95\uc18c\ud574\uc11c \ud655\uc778\ud574 \ubcf4\ub2c8 \ud574\ub2f9 \ucda9\uc804\uc18c\ub294 \ubb3c\uc758 \uc815\uc6d0\uacfc \uc0dd\uac01\ubcf4\ub2e4 \uba40\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./near-water.png)\\n\\n![no offset](./30-minutes.png)\\n\\n\ubb34\ub824 \uac78\uc5b4\uc11c 30\ubd84\uc774\ub098 \uac78\ub9ac\ub294 \ucda9\uc804\uc18c\uc600\uc2b5\ub2c8\ub2e4!\\n\\n\uc804\uae30\ucc28 \ucda9\uc804\uc744 \uc704\ud574 \uc655\ubcf5 1\uc2dc\uac04\uc774\ub098 \uac78\ub9ac\ub294 \uac70\ub9ac\ub97c \uac78\uc744 \uc218 \uc5c6\ub2e4\uace0 \uc0dd\uac01\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ubb3c\ub860 \uc9c0\ub09c \uccb4\ud5d8\uc5d0\uc11c \uc804\uae30\ucc28\uac00 \uc0dd\uac01\ubcf4\ub2e4 \ubc30\ud130\ub9ac\uac00 \uc624\ub798\uac04\ub2e4\ub294 \uc0ac\uc2e4\uc744 \uc54c\uace0 \uc788\uc5c8\uc9c0\ub9cc, \ub9cc\uc57d \uc800\ud76c\ucc98\ub7fc \ucda9\uc804\uc774 \uae09\ud55c \uc0ac\uc6a9\uc790\ub77c\uba74 \ubaa9\uc801\uc9c0\ub97c \ud3ec\uae30\ud560 \uc218 \ubc16\uc5d0 \uc5c6\uaca0\uad6c\ub098 \ub77c\ub294 \uc0dd\uac01\uc774 \ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub9c8\uc9c0\ub9c9\uc73c\ub85c \uc815\ud55c \ubaa9\uc801\uc9c0\ub294, \uc758\uc678\uc758 \uacb0\uc815\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uad49\uc7a5\ud788 \ubc1c\uc804\ub41c \ucca8\ub2e8 \ub3c4\uc2dc\ub85c \uc54c\ub824\uc9c4 \ud310\uad50\uc600\uc2b5\ub2c8\ub2e4!\\n\\n\uc0ac\uc2e4\uc740 \uc55e\uc73c\ub85c \uac08\uc9c0\ub3c4 \ubaa8\ub974\ub294 \ud310\uad50\ub97c \ubbf8\ub9ac \uad6c\uacbd\uc774\ub098 \ud574\ubcf4\uc790\ub294\uac8c \uc774\uc720\uc600\uc9c0\ub9cc \ube44\ubc00\uc785\ub2c8\ub2e4(?)\\n\\n\uc77c\ub2e8 \ud310\uad50\uc5ed\uc740 IT\uc11c\ube44\uc2a4 \ud68c\uc0ac\ub4e4\uc774 \ub9ce\uc774 \ubab0\ub824\uc788\ub294 \uacf3\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\ub294 \ud310\uad50\uc5ed\uc744 \uce74\ud398\uc778 \uac80\uc0c9\ucc3d\uc5d0 \uac80\uc0c9\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./pangyo.png)\\n\\n\uc9c0\ub3c4\ub97c \ud310\uad50\uc5ed\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \uc678\ubd80\uc778 \uac1c\ubc29\uc778 \ucda9\uc804\uc18c\ub97c \ucc3e\uc558\ub294\ub370, \ud310\uad50\uacf5\uc601\uc8fc\ucc28\uc7a5\uc774 \ubcf4\uc5ec\uc11c \ud574\ub2f9 \ucda9\uc804\uc18c\ub97c \ubaa9\uc801\uc9c0\ub85c \uc7a1\uace0 \ucd9c\ubc1c\ud588\uc2b5\ub2c8\ub2e4.\\n\\n### \ud310\uad50\\n\\n\ud558\ub0a8\uc5d0\uc11c \ud310\uad50\ub97c \uac00\uae30 \uc704\ud574\uc11c\ub294 \uc11c\ud558\ub0a8IC\ub97c \uc9c0\ub098\uc57c\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uac00\ub294 \uae38\uc5d0 \uc6b0\ub9ac \uc11c\ube44\uc2a4\uc5d0 \ub098\uc624\ub294 \uc815\ubcf4\uc640 \uc2e4\uc81c \uc815\ubcf4\uac00 \uc77c\uce58\ud558\ub294\uc9c0 \uc810\uac80\ucc28 \uc11c\ud558\ub0a8 \uac04\uc774 \ud734\uac8c\uc18c\ub97c \ub4e4\ub824\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \ud734\uac8c\uc18c\uc5d0\ub3c4 \ucda9\uc804\uc18c\uac00 \uc788\ub2e4\uace0 \uac80\uc0c9\uc774 \ub418\uc5c8\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4!\\n\\n![no offset](./hanam_station.png)\\n\\n\uac80\uc0c9 \ub2f9\uc2dc\uc5d0\ub294 2\ub300\uc758 \ucda9\uc804\uae30\uac00 \uc788\ub2e4\uace0 \ub098\uc654\uace0, \ub458\ub2e4 \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud558\ub2e4\uace0 \ub418\uc5b4\uc788\uc5c8\ub294\ub370 \uc2e4\uc81c\ub85c \ud655\uc778\ud574\ubcf4\ub2c8 \uc77c\uce58\ud558\ub294 \uac83\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c \uae38\uc744 \ub2ec\ub824 \ud310\uad50\uc5d0 \ub3c4\ucc29\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc8fc\ucc28\uc7a5\uc5d0 \ub4e4\uc5b4\uc624\uae30 \uc804, \uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \ud655\uc778\ud574\ubcf4\ub2c8 \ud310\uad50\uacf5\uc601\uc8fc\ucc28\uc7a5\uc758 \ucda9\uc804\uae30 \ucd1d 12\uae30 \uc911 10\uae30\uac00 \uc0ac\uc6a9\uac00\ub2a5\ud55c \uc0c1\ud0dc\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc815\uc791 \ub4e4\uc5b4\uc640\uc11c \ubcf4\ub2c8 \uc785\uad6c\ubd80\ud130 \ub108\ubb34 \ub9ce\uc740 \uc804\uae30\ucc28\ub4e4\uc774 \ucda9\uc804\uae30\ub97c \uc0ac\uc6a9\uc911\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ubb54\uac00 \uc774\uc0c1\ud558\ub2e4 \uc2f6\uc5c8\uc9c0\ub9cc, \uc544\uc9c1 \uc11c\ubc84\uc5d0 \ubc18\uc601\uc774 \uc548\ub41c\uac74\uac00? \ud558\uba74\uc11c \ube44\uc5b4\uc788\ub294 \ucda9\uc804\uae30\ub97c \ucc3e\uc558\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./empty_station.png)\\n![no offset](./charging.png)\\n\\n\ucda9\uc804\uae30\ub97c \uaf42\uace0 \ub098\uc11c \uc54c\uac8c\ub41c \uac83\uc740 \uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc5d0 \ub098\uc628 \ucda9\uc804\uc18c \ud68c\uc0ac\uba85\uacfc \ubc29\uae08 \uaf42\uc740 \ucda9\uc804\uae30 \ud68c\uc0ac\uba85\uc774 \ub2e4\ub974\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc54c\uace0\ubcf4\ub2c8 \uc74c\uc131 \uc778\uc2dd\uc73c\ub85c \ub124\ube44\uc5d0 \uac80\uc0c9\ud55c \ucda9\uc804\uc18c\ub294 \ud310\uad50\uacf5\uc601\uc8fc\ucc28\uc7a5\uc774 \uc544\ub2cc \ud310\uad50\uc5ed \ud658\uc2b9 \uc8fc\ucc28\uc7a5\uc774\ub77c \uc5c9\ub6b1\ud55c \uacf3\uc73c\ub85c \uc628 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4!!!\\n\\n\ub2e4\ud589\uc778 \uc810\uc740 \uc6b0\ub9ac \uc11c\ube44\uc2a4\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 \ucda9\uc804\uae30 \uc0ac\uc6a9 \uc5ec\ubd80 \uc815\ubcf4\uac00 \uc798\ubabb\ub41c \uac83\uc774 \uc544\ub2c8\uc5c8\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc560\ucd08\uc5d0 \uac00\uace0\uc790 \ud588\ub358 \ud310\uad50\uacf5\uc601\uc8fc\uc790\ucc3d\uc5d0 \ub300\ud55c \uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \uc815\ubcf4\uac00 \uc2e4\uc81c\uc640 \ub3d9\uc77c\ud55c\uc9c0 \ud655\uc778\ud574\ubcf4\ub7ec \uac78\uc5b4\uc11c \uc774\ub3d9\ud588\uc2b5\ub2c8\ub2e4. (\ubc14\ub85c \uc55e\uc5d0 \uc788\uc5c8\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.)\\n\\n![no offset](./no-chargers.png)\\n![no offset](./real-pangyo.png)\\n\\n\ub3c4\ucc29\ud574\ubcf4\ub2c8 1\uce35\uc758 \ucda9\uc804\uae30\ub4e4\uc774 \ubaa8\ub450 \uacf5\uc0ac\uc911\uc774\uc5c8\uace0, \uc11c\ube44\uc2a4\uc758 \uc815\ubcf4\uac00 \uc2e4\uc81c\ub85c\ub3c4 \ubd88\uc77c\uce58 \ud558\ub294 \uc904 \uc54c\uc558\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc0c1\uc138 \uc815\ubcf4\ub97c \ubcf4\ub2c8 3~6\uce35\uc5d0 \ucda9\uc804\uae30\ub4e4\uc5d0 \ub300\ud55c \uc815\ubcf4\ub77c\ub294 \uac83\uc774 \uba85\uc2dc\ub418\uc5b4 \uc788\uc5c8\uace0, \uc2e4\uc81c\ub85c\ub3c4 \uc774\uc640 \ub3d9\uc77c\ud55c \uac83\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./full-charged.png)\\n\\n\uc800\ud76c\ub294 \uc2dc\uac04\uc774 \ub108\ubb34 \ud758\ub7ec \ub2e4\uc2dc \uc7a0\uc2e4\ub85c \ub3cc\uc544\uc640 \ucc28\ub97c \ubc18\ub0a9\ud558\uace0 \uccb4\ud5d8\uc744 \ub9c8\ubb34\ub9ac \ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\\n### \ubd88\ud3b8\ud588\ub358 \uc810\\n\\n- \ub514\ubc14\uc774\uc2a4\uc5d0 \ubcf4\uc5ec\uc9c0\ub294 \uc9c0\ub3c4 \uc601\uc5ed \ud655\uc7a5\uc2dc\uc5d0 \uc6d0\ud558\ub294 \uc815\ubcf4\ub97c \ubcfc \uc218 \uc5c6\ub294 \uac83\uc774 \ubd88\ud3b8\ud588\ub2e4.\\n - \uc9c0\ub3c4\ub97c \ud655\ub300\ud574\uc8fc\uc138\uc694 \ubaa8\ub2ec\uc774 \ub728\uace0, \uc6d0\ub798 \uc788\ub358 \ucda9\uc804\uc18c \ub9c8\ucee4\uac00 \uc804\ubd80 \uc0ac\ub77c\uc9c4\ub2e4.\\n- \ud604\uc7ac \ub098\uc758 \uc704\uce58\ub97c \uc54c\uc544\ubcfc \uc218 \uc788\ub294 \uc218\ub2e8\uc774 \uc5c6\uc5b4 \ubd88\ud3b8\ud588\ub2e4.\\n - \ud604\uc704\uce58\ub97c \ub098\ud0c0\ub0b4\ub294 \ud540 (1\ucc28 \uccb4\ud5d8\uae30\uc5d0\uc11c\ub3c4 \uc5b8\uae09\ud588\ub358 \ubd80\ubd84)\\n - \ub0b4 \uc704\uce58\ub97c \uc0c1\ub300\uc801\uc73c\ub85c \uc54c \uc218 \uc788\ub294 \ub79c\ub4dc\ub9c8\ud06c\uc758 \ubd80\uc871\\n- \ud2b9\uc815 \uc7a5\uc18c(\ub9e4\uc7a5\uba85) \uac80\uc0c9\uc774 \uc548\ub3fc\uc11c \uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub9cc\uc73c\ub85c \ubaa9\uc801\uc9c0\ub97c \ucc3e\uc544\uac00\uae30 \ubd88\ud3b8\ud588\ub2e4.\\n - \uce74\uce74\uc624\ub9f5 \ub4f1\uc744 \ud65c\uc6a9\ud574 \ud2b9\uc815 \uc7a5\uc18c \uac80\uc0c9\uc744 \uc9c4\ud589\ud574\uc57c \ud588\ub2e4.\\n\\n### \ub2e4\uc74c \ubaa9\ud45c\\n\\n\uc55e\uc120 \ubd88\ud3b8\ud588\ub358\uc810\uc744 \uac1c\uc120\ud558\uae30 \uc704\ud574 \ub2e4\uc74c\uacfc \uac19\uc740 \uae30\ub2a5 \uac1c\uc120\uc744 \ucd94\uac00\ub85c \uc9c4\ud589\ud560 \uc608\uc815\uc785\ub2c8\ub2e4.\\n\\n- \ub514\ubc14\uc774\uc2a4\uc5d0 \ubcf4\uc5ec\uc9c0\ub294 \uc9c0\ub3c4 \uc601\uc5ed \ud655\uc7a5\uc5d0 \uc81c\ud55c\uc774 \uc0dd\uae30\uc9c0 \uc54a\uac8c \ucda9\uc804\uc18c \ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc744 \uc6b0\uc120\uc801\uc73c\ub85c \ub3c4\uc785\ud55c\ub2e4.\\n- \ud604\uc7ac \ub098\uc758 \uc704\uce58\ub97c \uc54c\uc544\ubcfc \uc218 \uc788\ub3c4\ub85d \uc9c0\ud558\ucca0 \uc5ed\uacfc \uac19\uc740 \ub79c\ub4dc\ub9c8\ucee4\ub97c \uc9c0\uc6e0\ub358 \uac83\uc744 \ub864\ubc31\ud55c\ub2e4.\\n\\n\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub9cc\uc73c\ub85c \ubaa9\uc801\uc9c0\ub97c \ucc3e\uc544\uac08 \uc218 \uc788\ub3c4\ub85d \ud558\uae30 \uc704\ud574\uc11c \ud2b9\uc815 \uc7a5\uc18c \uac80\uc0c9\uc744 \ucd94\uac00\ud558\uace0 \uc2f6\uc9c0\ub9cc, \ud574\ub2f9 \uae30\ub2a5\uc744 \uad6c\ud604\ud558\uae30 \uc704\ud574\uc120 \uac80\uc0c9\ub2f9 \ube44\uc6a9\uc774 \ub9ce\uc774 \uccad\uad6c\ub418\ub294 \uc7a5\uc18c \uac80\uc0c9 API\ub97c \ucd94\uac00\ud574\uc57c \ud588\uae30\uc5d0 \ud604\uc2e4\uc801\uc73c\ub85c \uc9c0\uae08 \ub2f9\uc7a5 \uad6c\ud604\ud558\uae30 \uc5b4\ub835\ub2e4\uace0 \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc0c1 \uce74\ud398\uc778 \uc0ac\uc6a9\uae30\uc600\uc2b5\ub2c8\ub2e4."},{"id":"39","metadata":{"permalink":"/39","source":"@site/blog/2023-10-07-carffeine-tester-1/index.mdx","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 1","description":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \uac1c\ubc1c\ud558\uba74\uc11c \uac00\uc7a5 \ub9ce\uc774 \ubc1b\uc740 \ud53c\ub4dc\ubc31 \uc911 \ud558\ub098\ub294 \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc774 \ubc18\ub4dc\uc2dc \ud544\uc694\ud558\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.","date":"2023-10-07T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 7\uc77c","tags":[{"label":"\uce74\ud398\uc778","permalink":"/tags/\uce74\ud398\uc778"},{"label":"\uc11c\ube44\uc2a4 \uacbd\ud5d8","permalink":"/tags/\uc11c\ube44\uc2a4-\uacbd\ud5d8"},{"label":"\ud53c\ub4dc\ubc31","permalink":"/tags/\ud53c\ub4dc\ubc31"},{"label":"\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","permalink":"/tags/\uc804\uae30\ucc28-\uc0ac\uc6a9\uae30"},{"label":"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571","permalink":"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"}],"readingTime":18.085,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"39","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 1","authors":["gabriel"],"tags":["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},"prevItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 2","permalink":"/40"},"nextItem":{"title":"\ucda9\uc804\uc18c \uc870\ud68c api \ubd84\ub9ac","permalink":"/37"}},"content":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \uac1c\ubc1c\ud558\uba74\uc11c \uac00\uc7a5 \ub9ce\uc774 \ubc1b\uc740 \ud53c\ub4dc\ubc31 \uc911 \ud558\ub098\ub294 `\uc0ac\uc6a9\uc790 \uacbd\ud5d8`\uc774 \ubc18\ub4dc\uc2dc \ud544\uc694\ud558\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\ubb34\ub798\ub3c4 \uc804\uae30\uc790\ub3d9\ucc28\ub97c \ubcf4\uc720\ud55c \ud300\uc6d0\ub4e4\uc774 \uc544\ubb34\ub3c4 \uc5c6\ub2e4\ubcf4\ub2c8 \uc2e4\uc81c \uc0ac\uc6a9\uc790\ub4e4\uc774 \uacaa\ub294 \uc5b4\ub824\uc6c0\uc744 \uc608\uc0c1\ud560 \uc218 \ubc16\uc5d0 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc804\uae30 \uc790\ub3d9\ucc28 \uc6b4\uc804\uc790\ub4e4\uc744 \ucc3e\uc544\ub0b4\uc5b4 \uc218\ucc28\ub840 \uc778\ud130\ubdf0\ub97c \uc9c4\ud589\ud558\uc600\ub294\ub370 \uc2e4\uc81c \ucc28\uc8fc\ub4e4\uc774 \uc6d0\ud558\ub294 \uae30\ub2a5\uc774 \ubb34\uc5c7\uc778\uc9c0, \uc5b4\ub5a4 \uc5b4\ub824\uc6c0\uc744 \uacaa\ub294\uc9c0\ub97c \ud655\uc778\ud558\uc5ec \uc774\ub97c \ubc14\ud0d5\uc73c\ub85c \uc11c\ube44\uc2a4\ub97c \uac1c\ubc1c\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc11c\ube44\uc2a4\ub97c \ucc98\uc74c \uac1c\ubc1c\ud558\uc600\uc744 \ub54c \uac00\uc7a5 \ub9ce\uc774 \ubc1b\uc558\ub358 \ud53c\ub4dc\ubc31\uc740 \uc571 \ub85c\ub4dc \uc18d\ub3c4\uac00 \ub108\ubb34 \ub290\ub9ac\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc11c\ube44\uc2a4 \ucd08\uae30\uc5d0\ub294 \ub85c\ub529 \uc18d\ub3c4\uac00 \ub108\ubb34 \ub290\ub824\uc11c \uc0ac\uc6a9\uc790\ub4e4\uc5d0\uac8c \uc11c\ube44\uc2a4 \uc0ac\uc6a9\uc744 \uad8c\uc7a5\ud558\uae30 \ubbf8\uc548\ud55c \uc0c1\ud0dc\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc2e4\uc0ac\uc6a9\uc790\ub97c \ubaa8\uc9d1\ud558\ub294 \uac83 \ubcf4\ub2e4 `\uc11c\ube44\uc2a4 \uc548\uc815\ud654\uc5d0 \uc9d1\uc911\ud558\ub294 \uac83`\uc774 \ucd5c\uc6b0\uc120\uc774\ub77c\ub294 \ubaa9\ud45c \uc544\ub798\uc5d0 \uc11c\ube44\uc2a4\ub97c \uac1c\uc120\ud558\ub294 \uc2dc\uac04\uc744 \uac00\uc84c\uace0, \uc9c0\uae08\uc740 \ub85c\ub529 \uc18d\ub3c4\uac00 \ube60\ub974\ub2e4\ub294 \ud53c\ub4dc\ubc31\uc744 \ubc1b\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc9c0\ub09c \ud55c \ub2ec\uac04 \uc11c\ube44\uc2a4 \uc548\uc815\ud654\uc5d0 \uc9d1\uc911\uc744 \ud588\ub2e4\uba74, \uc774\uc81c\ub294 \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc744 \uac1c\uc120\ud558\ub294\ub370 \uc9d1\uc911\uc744 \ud574\uc57c\ud560 \ub54c\uac00 \uc654\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc0ac\uc6a9\uc790 \uc720\uce58\ub97c \uc704\ud574 \uc804\uae30\ucc28 \ub3d9\ud638\ud68c \uce74\ud398, \uce74\uce74\uc624\ud1a1 \uc624\ud508\ucc44\ud305, \uc790\ub3d9\ucc28 \ucee4\ubba4\ub2c8\ud2f0 \ub4f1\uc744 \ub3cc\uba74\uc11c \ud64d\ubcf4\ub97c \uc9c4\ud589\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e4\ud589\ud788\ub3c4 \ubd88\ud2b9\uc815 \ub2e4\uc218\uc758 \uc775\uba85 \uc0ac\uc6a9\uc790\ub4e4\uc744 \uc190\uc27d\uac8c \uad6c\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790\ub4e4\ub85c\ubd80\ud130 \ub9ce\uc740 \ud53c\ub4dc\ubc31\uc744 \ubc1b\uc558\uace0, \ud574\ub2f9 \ud53c\ub4dc\ubc31\uc744 \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0 \ucd5c\ub300\ud55c \ubc18\uc601\ud558\uace0\uc790 \ub178\ub825\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc, \ub300\ubd80\ubd84\uc758 \uc0ac\uc6a9\uc790\ub4e4\uc774 \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0 \ub2e8\uc21c\ud788 \ubc29\ubb38\ud558\uc5ec \ud53c\ub4dc\ubc31\uc744 \uc900 \uac83\uc77c \ubfd0 `\uc2e4\uc81c\ub85c \uc0ac\uc6a9\ud558\uba74\uc11c \ud53c\ub4dc\ubc31\uc744 \uc900 \uac83 \uac19\uc9c0\ub294 \uc54a\ub2e4\ub294 \ub290\ub08c`\uc744 \ubc1b\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790\ub4e4\uc774 \uc571\uc5d0 \uba38\ubb34\ub978 \uc2dc\uac04\uc744 GA4\ub97c \ud1b5\ud574 \ud655\uc778 \ud558\uc600\uc744 \ub54c \ud3c9\uade0 3\ubd84 \uc774\uc0c1\uc774\ub77c\ub294 \uae34 \uc2dc\uac04\uc744 \uba38\ubb3c\ub7ec\uc11c \uc571\uc744 \uaf3c\uaf3c\ud558\uac8c \uc0ac\uc6a9\ud588\uc744 \uac83\uc774\ub77c\uace0 \uae30\ub300\ub294 \ud558\uc600\uc73c\ub098, \uc0ac\uc6a9 \uc911\uc5d0 \ud53c\ub4dc\ubc31\uc744 \uc900\ub2e4\uac70\ub098 \uc0ac\uc6a9 \ud6c4\uc5d0 \ud53c\ub4dc\ubc31\uc744 \uc900 \uac83\uc774 \ub9de\ub294\uc9c0 \ud655\uc2e0\ud558\uae30 \uc5b4\ub824\uc6e0\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub807\ub2e4\uace0 \uc8fc\ubcc0\uc5d0\uc11c \uc804\uae30\ucc28\uc8fc\ub4e4\uc744 \ucc3e\uc790\ub2c8 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uc600\uc2b5\ub2c8\ub2e4. \uc77c\ub2e8 \uc804\uae30\uc790\ub3d9\ucc28 \ubcf4\uae09\ub960\uc774 \uad49\uc7a5\ud788 \ub0ae\uc558\uc73c\uba70, 40~50\ub300\uc5d0 \ud3b8\uc911\ub418\uc5b4 \uc788\uc5b4 \uc800\ud76c\uc5d0\uac8c \ud611\uc870\ud574 \uc904 \ucc28\uc8fc\ubd84\ub4e4\uc744 \uc8fc\ubcc0\uc5d0\uc11c \ucc3e\uae30 \uc5b4\ub824\uc6e0\uc2b5\ub2c8\ub2e4. (\ub300\ubd80\ubd84 \uc0dd\uc5c5\uc73c\ub85c \uc778\ud574 \ubc14\uc058\uc2ed\ub2c8\ub2e4 \u3160\u3160)\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\ub294 \uadf8\ub0e5 \uc9c1\uc811 \uc11c\ube44\uc2a4\ub97c \uc0ac\uc6a9\ud558\uba74\uc11c \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc744 \ud558\uae30\ub85c \ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n## \uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294\uc694\\n\\n\uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \uc9c0\uc6d0\ud558\ub294 \ud575\uc2ec \uae30\ub2a5\uc740 \ub2e4\uc74c\uacfc \uac19\uc558\uc2b5\ub2c8\ub2e4.\\n\\n- \uc804\uad6d \ucda9\uc804\uc18c \uc870\ud68c\\n - \uc9c0\ub3c4 \ud0d0\uc0c9\uc744 \ud1b5\ud55c \uac80\uc0c9\\n - \uac80\uc0c9\ucc3d\uc744 \ud1b5\ud55c \uac80\uc0c9\\n- \ucda9\uc804\uc18c\uc758 \uc6b4\uc601 \uc815\ubcf4 \ud655\uc778\\n- \ucda9\uc804\uc18c \ubcc4 \ucda9\uc804\uae30 \uc0c1\ud0dc \uc870\ud68c (\uc2e4\uc2dc\uac04)\\n- \ucda9\uc804\uc18c \ubc0f \ucda9\uc804\uae30 \uace0\uc7a5 \uc2e0\uace0\\n- \ucda9\uc804\uc18c \ubcc4 \ucda9\uc804\uae30 \uc0ac\uc6a9\ub7c9 \ud1b5\uacc4 \uc870\ud68c\\n- \ucda9\uc804\uc18c \ubcc4 \ub9ac\ubdf0 \uc870\ud68c\\n\\n\uc774\uc678\uc5d0\ub3c4 \ub9ce\uc740 \uae30\ub2a5\ub4e4\uc774 \uc788\uc5c8\uc9c0\ub9cc, \uc704\uc758 \uae30\ub2a5\ub4e4\uc774 \uc0ac\uc6a9\uc790\ub4e4\uc774 \uac00\uc7a5 \uc8fc\ub825\uc73c\ub85c \uc0ac\uc6a9\ud560 \uac83 \uac19\uc740 \uae30\ub2a5\ub4e4\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \uacc4\ud68d\uc744 \uc138\uc6cc\ubcf4\uc790\\n\\n\uc804\uae30\uc790\ub3d9\ucc28 \ub80c\ud2b8\uc5d0 \uc55e\uc11c \uc5b4\ub514\uc5d0 \ubc29\ubb38\ud560 \uc9c0 \ubd80\ud130 \uc815\ud574\uc57c \ud588\uc2b5\ub2c8\ub2e4.\\n\uc800\ud76c\ub294 \uba87 \uac00\uc9c0 \uc6d0\uce59\uc744 \uac00\uc9c0\uace0 \ubc29\ubb38\uc9c0\ub97c \uc815\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc798 \ubaa8\ub974\ub294 \uc9c0\uc5ed\uc77c \uac83\\n2. \ub3c4\ucc29\uc9c0\uc5d0 \ucda9\uc804\uc18c\uac00 \ubc18\ub4dc\uc2dc \uc788\uc744 \uac83\\n3. \ud0c0\uc0ac \uc571\uc744 \uc804\ud600 \uc0ac\uc6a9\ud558\uc9c0 \ub9d0 \uac83\\n\\n\uc77c\ub2e8, \uc81c\uac00 \ucc98\uc74c \uc815\ud588\ub358 \ubaa9\ud45c\ub294 \uacbd\uc0c1\ub0a8\ub3c4 \uc9c4\uc8fc\uc2dc\uc600\uc2b5\ub2c8\ub2e4.\\n\uc9c4\uc8fc\uc2dc\uc5d0\uc11c \ubcf5\uadc0\ud574\uc57c\ud558\ub294 \ud300\uc6d0\uc774 \uc788\ub358 \uc810, \ubc29\ubb38\ud574 \ubcf8 \uc801\uc774 \uc5c6\ub294 \ub3c4\uc2dc\uc778 \uc810, \uc7a5\uac70\ub9ac\ub77c\uc11c \ucda9\uc804\uae30 \uc0ac\uc6a9\uc774 \ud544\uc5f0\uc801\uc778 \uc810 \ub4f1 \uc5ec\ub7ec \uac00\uc9c0 \uc774\uc720\ub85c \uc9c4\uc8fc\uc2dc\ub97c \ubc29\ubb38\ud558\uae30\ub85c \uacb0\uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \ud0a8 \uc21c\uac04 \ub208\uc55e\uc774 \uce84\uce84\ud574\uc84c\uc2b5\ub2c8\ub2e4.\\n\\n\\"\uc9c4\uc8fc\uc2dc\uac00 \uc5b4\ub514\uc5d0 \uc788\uc9c0?\\"\\n\\n![no offset](./search-jinju.png)\\n\\n\ub2e4\ud589\ud788 \uc9c4\uc8fc\uc2dc\ub97c \uac80\uc0c9\ud558\ub2c8 \uc8fc\uc18c \uae30\ubc18\uc73c\ub85c \uac80\uc0c9\uc774 \ub418\uc5c8\uc2b5\ub2c8\ub2e4!\\n\uc9c4\uc8fc\uc2dc\ub97c \uac80\uc0c9\ud55c \uac83\uc740 \uc544\ub2c8\uc9c0\ub9cc \uac04\uc811\uc801\uc774\ub77c\ub3c4 \uac80\uc0c9\uc774 \ub418\ub294 \uac83\uc744 \ubcf4\uace0 \uc548\uc2ec\ud588\uc2b5\ub2c8\ub2e4.\\n\uc544\ubb34 \ucda9\uc804\uc18c\ub97c \ub20c\ub7ec\uc11c \uc9c4\uc8fc\uc2dc\ub85c \uc774\ub3d9\ud558\ub294 \uac83\uc740 \uac00\ub2a5\ud588\uc2b5\ub2c8\ub2e4.\\n\\n```\\n\uc5ec\uae30\uc5d0\uc11c \uc800\ub294 \uc774 \uacfc\uc815\uc5d0\uc11c \ub3c4\uc2dc\ub098 \uc9c0\uc5ed \uac80\uc0c9 \uae30\ub2a5\uc774 \ubc18\ub4dc\uc2dc \ud544\uc694\ud558\ub2e4\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n```\\n\\n\ud558\uc9c0\ub9cc \ub108\ubb34 \uba40\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uc655\ubcf5 700km\ub97c \uc0dd\uac01\ud574\uc57c\ud558\uc5ec 1\ubc15 2\uc77c\uc774 \ud544\uc218\uc600\uace0, \ud300\uc6d0\ub4e4 \uac04\uc5d0 \uc77c\uc815\uc744 \uc870\uc815\ud558\uae30\uac00 \ub108\ubb34 \uc5b4\ub824\uc6e0\uc2b5\ub2c8\ub2e4.\\n\ub530\ub77c\uc11c \ub2e4\ub978 \ub3c4\uc2dc\ub97c \ucc3e\uc544\ubcf4\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./gangnam-to-majang-road.png)\\n\\n\uadf8\ub7ec\ub358 \uc911, \uc81c\uac00 \uc804\uc5d0 \ubc29\ubb38\ud588\ub358 \ud30c\uc8fc\uc2dc\uc758 `\ub9c8\uc7a5\ud638\uc218`\uac00 \uc0dd\uac01\ub0ac\uc2b5\ub2c8\ub2e4.\\n\uc11c\uc6b8\uc5d0\uc11c \uaf64\ub098 \uba3c \uac70\ub9ac(\uc57d 50km)\uc5d0 \uc788\uc5c8\uace0, \uc801\ub2f9\ud788 \uc2dc\uac04\uc744 \ubcf4\ub0bc\ub9cc\ud55c \uc7a5\uc18c\uc600\uc2b5\ub2c8\ub2e4.\\n\ub2e4\ud589\ud788\ub3c4 \ucda9\uc804\uc18c\uc758 \uc774\ub984\uc774 `\ub9c8\uc7a5\ud638\uc218\uad00\ub9ac\uc0ac\ubb34\uc18c`\uc5ec\uc11c \uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \ud1b5\ud574 \ubc14\ub85c \ucc3e\uc744 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uc2ec\uc9c0\uc5b4 \ub9c8\uc7a5\ud638\uc218 \uc8fc\ubcc0\uc5d0\ub294 \ucda9\uc804\uc18c\uac00 \ub9ce\uc9c0 \uc54a\uc740 \ud3b8\uc774\uc5c8\uace0, \ucd08\uae09\uc18d \ucda9\uc804\uae30\uac00 \uc788\uc5b4 \uc800\ud76c \uc571\uc744 \uc2e4\ud5d8\ud558\uae30\uc5d0 \ub531 \uc88b\uc558\uc2b5\ub2c8\ub2e4.\\n\\n## \ub9c8\uc7a5\ud638\uc218\ub85c \ucd9c\ubc1c\\n\\n\uc800 `\uac00\ube0c\ub9ac\uc5d8`\uacfc `\uc81c\uc774`, `\ubc15\uc2a4\ud130`\ub294 \uc11c\uc6b8 \uc120\uc815\ub989\uc5ed\uc5d0\uc11c \uc544\uc774\uc624\ub2c95\ub97c \ub80c\ud2b8\ud558\uace0 \ub9c8\uc7a5\ud638\uc218\ub85c \ucd9c\ubc1c\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./go-to-majang-1.png)\\n\\n\ucc98\uc74c \uacc4\ud68d\ud588\ub358 \uac83 \ucc98\ub7fc \ud0c0\uc0ac\uc758 \uc571\uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0 \ub9c8\uc7a5\ud638\uc218\ub97c \uac80\uc0c9\ud558\uc5ec \uc774\ub3d9\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./go-to-majang-2.png)\\n\\n\uc804\ub0a0 \uc774\ubbf8 \uac80\uc0c9\uc744 \ud588\uc9c0\ub9cc, \ud639\uc2dc \uc0ac\uc6a9 \uc911\uc77c\uc218\ub3c4 \uc788\uae30\uc5d0 \ud55c\ubc88 \ub354 \uac80\uc0c9\ud574\ubd24\uc73c\uba70 \ud574\ub2f9 \uc2dc\uac04\ub300\uc5d0 \ucda9\uc804\uc18c\uac00 \ud3c9\uc18c\uc5d0 \ub35c \ubd90\ube4c \uac83\uc774\ub77c\ub294 \ud1b5\uacc4 \uc790\ub8cc\ub97c \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./go-to-majang-3.png)\\n\\n![no offset](./go-to-majang-4.png)\\n\\n![no offset](./go-to-majang-5.png)\\n\\n\ub9c8\uc7a5 \ud638\uc218\uae4c\uc9c0 20\ubd84 \uac70\ub9ac\ub97c \ub0a8\uae30\uace0, \uac11\uc790\uae30 \ubc30\uac00 \uace0\ud30c\uc9c4 \uc800\ud76c\ub294 \ubaa9\uc801\uc9c0\ub97c \ud2c0\uc5b4 `\ud30c\uc8fc\ub2ed\uad6d\uc218 \ubcf8\uc810`\uc744 \uac00\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \ud30c\uc8fc\ub2ed\uad6d\uc218\uac00 \uc5b4\ub514\uc5d0 \uc788\uc9c0?\\n\\n\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \ud65c\uc6a9\ud558\uc5ec \ud30c\uc8fc\ub2ed\uad6d\uc218 \ubcf8\uc810 \uadfc\ucc98\uc758 \ucda9\uc804\uc18c\ub97c \uac80\uc0c9\ud574\ubcf4\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\uc790\ub3d9\ucc28 \ub0b4\ube44\uac8c\uc774\uc158\uc5d0\ub294 \ud30c\uc8fc\ub2ed\uad6d\uc218\uac00 \uc5b4\ub514\uc778\uc9c0 \ub098\uc640\uc788\uc9c0\ub9cc, \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\ub294 \uc2dd\ub2f9 \uc815\ubcf4\ub294 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\ud574\ub2f9 \uc2dd\ub2f9\uc774 \ub3c4\ub300\uccb4 \uc5b4\ub514\uc5d0 \uc788\ub294\uc9c0 \ud655\uc778\ud560 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4. (\ud30c\uc8fc\ub2ed\uad6d\uc218\uc5d0\uc11c\ub294 \uc804\uae30\ucc28 \ucda9\uc804\uc18c\uac00 \uc5c6\uc5c8\uae30 \ub584\ubb38\uc785\ub2c8\ub2e4.)\\n\\n![no offset](./songchoo-to-noodle-1.png)\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\ub294 \uc790\ub3d9\ucc28 \ub0b4\ube44\uac8c\uc774\uc158\uc5d0 \uc788\ub294 \ub3c4\ub85c\uba85 \uc8fc\uc18c\ub97c \uac80\uc0c9\ud558\uc5ec \uc704\uce58\ub97c \ud30c\uc545\ud558\ub824\uace0 \ud558\uc600\uace0, \ub2e4\uc18c \ubd80\uc815\ud655 \ud558\uc9c0\ub9cc \ub3d9\ub124\uc5d0 \uc788\ub294 \uc778\uadfc \ucda9\uc804\uc18c\ub97c \ucc3e\uc744 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \ud734\uac8c\uc18c\uc5d0 \ub4e4\ub9ac\ub2e4\\n\\n\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub85c \uac80\uc0c9\ud574\ubcf4\ub2c8 \uc2dd\ub2f9\uc73c\ub85c \uac00\ub294 \uae38 \ud734\uac8c\uc18c\uc5d0\ub3c4 \ucda9\uc804\uc18c\uac00 \uc788\ub2e4\uace0 \ud569\ub2c8\ub2e4.\\n\ud734\uac8c\uc18c \uc774\ub984\uc744 \uc785\ub825\ud558\ub2c8 \ubc14\ub85c \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./yangju-station-3.png)\\n\\n\uc2ec\uc9c0\uc5b4 \uc9c0\uae08 \uc0ac\uc6a9\uc911\uc774\ub77c\uace0 \ud569\ub2c8\ub2e4! \ub530\ub77c\uc11c \uc800\ud76c\ub294 \ud655\uc778\ud574\ubcf4\uae30 \uc704\ud574 \ud734\uac8c\uc18c\uc5d0 \ub4e4\ub9ac\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./yangju-station-1.png)\\n![no offset](./yangju-station-2.png)\\n\\n\uc2e4\uc81c\ub85c \uc0ac\uc6a9 \uc911\uc784\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4. \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \uc0ac\uc6a9\uc911\uc774\ub77c\uace0 \ub098\uc654\ub294\ub370 \uc2e4\uc81c\ub85c \uc0ac\uc6a9\uc911\uc778 \uac83\uc744 \ubcf4\ub2c8 \uacf5\uacf5 api\uac00 \ub098\ub984 \uc2e4\uc2dc\uac04\uc73c\ub85c \ub370\uc774\ud130\ub97c \uc798 \ubcf4\ub0b4\uc8fc\uace0 \uc788\ub2e4\uace0 \uc0dd\uac01\ud558\uac8c \ub418\uc5c8\uace0, \uc800\ud76c \ud300 \uc11c\ubc84\uc5d0\uc11c\ub3c4 \uc774\ub97c \uc81c\ub300\ub85c \uc218\uc9d1\ud558\uace0 \uc788\ub2e4\uace0 \uc0dd\uac01\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./yangju-station-5.png)\\n\\n\ub9d0\ub85c\ub9cc \ub4e3\ub358 \uace0\uc18d\ub3c4\ub85c \ud734\uac8c\uc18c\uc758 \uc804\uae30\ucc28 \ucda9\uc804\uc18c \ub300\uae30\uc904\uc744 \uc9c1\uc811 \ud655\uc778\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\ucc28\uc8fc \ubd84\uacfc \uc778\ud130\ubdf0 \ud558\uace0 \uc2f6\uc5c8\uc9c0\ub9cc, \ucc28 \ub0b4\ubd80\uc5d0\uc11c \ub108\ubb34 \ubc14\ube60\ubcf4\uc774\uc154\uc11c \uadf8\ub7f4 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc804\uae30\ucc28 \ucda9\uc804\uc744 \uae30\ub2e4\ub9ac\uba74\uc11c \ubb34\uc5c7\uc744 \ud560 \uc218 \uc788\uc744\uae4c\uc694?\\n\uc774 \ubd84\uc740 \ub2e4\ud589\ud788\ub3c4 \uc5c5\ubb34\ub97c \ubcf4\uace0 \uacc4\uc168\uc9c0\ub9cc, \ub2e4\ub978 \ucc28\uc8fc\ub4e4\uc740 \ubb34\uc5c7\uc744 \ud558\uace0 \ubcf4\ub0bc\uc9c0 \uad81\uae08\ud574\uc84c\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./yangju-station-4.png)\\n\\n\ud734\uac8c\uc18c\uc5d0\ub294 \ucda9\uc804\uc18c\uac00 \ud558\ub098 \ub354 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ud55c \uacf3\uc740 \uc0ac\uc6a9\uc911\uc774\uc9c0\ub9cc, \ub2e4\ub978 \ud55c \uacf3\uc740 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 \uc774 \ucda9\uc804\uc18c\ub97c \uc0ac\uc6a9\ud574\ubcf4\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./yangju-station-6.png)\\n\\n\uc0ac\uc6a9\ud560 \uc218 \uc788\uc73c\ub2c8\uae50 \ub4e4\uc5b4\uac00\ubd10\uc57c\uc9c0! \ud558\uace0 \ub3c4\ucc29\ud55c \uc21c\uac04 \uc544\ucc28 \uc2f6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\"\uc544, \ucda9\uc804\uc18c\uac00 \uc678\ubd80\uc778 \uc0ac\uc6a9 \uae08\uc9c0\uc77c \uc218 \uc788\uc5c8\uc9c0?\\"\\n\\n\uc800\ud76c\ub294 \ubd84\uba85\ud788 \uc11c\ube44\uc2a4\ub97c \uc9c1\uc811 \uac1c\ubc1c\ud588\uc73c\ub2c8\uae50 \ub2e4 \uc54c\uace0 \uc788\ub358 \uc0ac\ud56d\uc774\uc5c8\uc9c0\ub9cc, \uc804\ud600 \uc0dd\uac01\uce58 \ubabb\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc11c\ube44\uc2a4\ub97c \uac1c\ubc1c\ud558\ub294 \ub0b4\ub0b4 \uc678\ubd80\uc778 \uac1c\ubc29 \ucda9\uc804\uc18c\uc5d0 \ub300\ud55c \uc911\uc694\uc131\uc744 \uac04\ud30c\ud558\uc600\uace0, \uc774 \uae30\ub2a5\uc744 \ub123\uc5c8\uc73c\uba74\uc11c\ub3c4 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0 \ucda9\uc804\uc18c\ub97c \ubc29\ubb38\ud55c \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ubc14\ub85c \uc55e\uc5d0 \uc788\uc5b4\uc11c \ub2e4\ud589\uc774\uc5c8\uc9c0\ub9cc, \uc5b4\ucc0c\ub410\ub4e0 \uc774 \ucda9\uc804\uc18c\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\ub294 \ud734\uac8c\uc18c\ub97c \ub5a0\ub098\ub294 \ub0b4\ub0b4 \uc774 \ubb38\uc81c\uc5d0 \ub300\ud574\uc11c \ud1a0\ub860\uc744 \ud560 \uc218 \ubc16\uc5d0 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n```\\n\ubd84\uba85 \uc6b0\ub9ac\uac00 \ub9cc\ub4e0 \uc11c\ube44\uc2a4\uc778\ub370 \uc65c \ub193\ucce4\uc744\uae4c?\\n```\\n\\n## \ub9db\uc788\ub294 \uc810\uc2ec\\n\\n![no offset](./noodle-1.png)\\n\\n\ud30c\uc8fc\ub2ed\uad6d\uc218 \ubcf8\uc810\uc5d0\uc11c \ub9db\uc788\ub294 \uc2dd\uc0ac\ub97c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ube44\ub85d \uc2dd\ub2f9\uc5d0\ub294 \uc804\uae30\ucc28 \ucda9\uc804\uc18c\uac00 \uc5c6\uc5c8\uc9c0\ub9cc, \uc778\uadfc\uc5d0 \ucda9\uc804\uc18c\uac00 \uc788\uc5b4 \uc2e4\ud5d8\uc744 \ud558\ub098 \ud574\ubcfc \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc778\uadfc \ucda9\uc804\uc18c\uc640 \uc2dd\ub2f9\uc758 \uac70\ub9ac\uac00 \uac00\uae4c\uc6cc \ubcf4\uc774\ub294\ub370, \uacfc\uc5f0 \uac78\uc5b4\uac08 \uc218 \uc788\uc744\uae4c?\\n\\n\uc2e4\uc81c\ub85c \uac77\uc9c0\ub294 \uc54a\uc558\uc2b5\ub2c8\ub2e4\ub9cc \ucc28 \ud0c0\uba74\uc11c \uc9c0\ub098\uac00\uba74\uc11c \ud655\uc778\ud574\ubcf8 \uacb0\uacfc \uc9c1\uc811 \uac78\uc744 \uc218 \uc5c6\ub294 \uac70\ub9ac\uc600\uc2b5\ub2c8\ub2e4. (\uad49\uc7a5\ud788 \uac77\uae30 \uc2eb\uc740 \uc218\uc900\uc758 \uba3c \uac70\ub9ac\uc600\uc2b5\ub2c8\ub2e4.)\\n\\n\uc9d1\uc5d0 \uc788\ub294 PHEV\ub97c \ud0c8 \uae30\ud68c\uac00 \ub9ce\uc544 \uc804\uae30\ucc28 \ucda9\uc804\uc18c\ub97c \uc790\uc8fc \ubc29\ubb38\ud588\ub358 \uc800\ub294 \uc774\ub7f0 \uc810\uc744 \uc798 \uc54c\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e4\ud589\ud788 \uc774 \ubd80\ubd84\uc744 \uc798 \uc54c\uace0 \uc788\uc5c8\uae30\uc5d0 \uc800\ud76c\ub294 \uc774 \ubd80\ubd84\uc744 \uc11c\ube44\uc2a4\uc5d0 \ubc18\uc601\ud558\uc600\uace0, \ubaa8\ub4e0 \ub370\uc774\ud130\ub97c \ud3ec\uae30\ud558\uc9c0 \uc54a\uc558\ub358 \uac83\uc774 \uc633\uc740 \uc120\ud0dd\uc774\uc5c8\ub2e4\ub294 \uac83\uc744 \ud655\uc778\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./noodle-2.png)\\n\\n\uc2dd\uc0ac\uac00 \ub05d\ub098\uace0 \ub4dc\ub514\uc5b4 \ub9c8\uc7a5\ud638\uc218\ub85c \ucd9c\ubc1c\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \ub9c8\uc7a5\ud638\uc218 \ub3c4\ucc29\\n\\n\ub9c8\uc7a5\ud638\uc218\uc5d0 \ub3c4\ucc29\ud558\uc790\ub9c8\uc790 \ucda9\uc804\uc18c\uc5d0 \ubc29\ubb38\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./majang-1.png)\\n\\n\ud1b5\uacc4\uc5d0\uc11c\ub294 \uc0ac\uc6a9\ub960\uc774 \uc801\uc744 \uac83\uc774\ub77c\uace0 \ud558\uc600\ub294\ub370 \uc800\ud76c\ub9cc \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./majang-2.png)\\n![no offset](./majang-4.png)\\n\\n2\uae30 \uc911 1\uacf3\uc744 \uc800\ud76c\uac00 \uc0ac\uc6a9\ud558\uc600\uace0, \ub9c8\uc7a5\ud638\uc218\ub97c \ub3cc\uc558\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./majang-3.png)\\n\\n\uc57d 50\ubd84 \uac04 \uc0b0\ucc45\uc744 \ud558\uace0, \ub3cc\uc544\uc640\ubcf4\ub2c8 \ucda9\uc804\uae30 \ub2e4 \ub418\uc5b4\uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc2e4 \ub9c8\uc7a5\ud638\uc218 \uae4c\uc9c0 \uc624\ub294 \ub0b4\ub0b4 \ub4e0 \uc0dd\uac01\uc774\uc5c8\uc9c0\ub9cc, \uc804\uae30\ucc28\uc758 \ubc30\ud130\ub9ac\uac00 \uc0dd\uac01\ubcf4\ub2e4 \uc624\ub798 \uac04\ub2e4\ub294 \uc0dd\uac01\uc774 \ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc77c\ubd80\ub7ec \ud68c\uc0dd\uc81c\ub3d9 \uae30\ub2a5\ub3c4 \ub044\uace0, \uc5d0\uc5b4\ucee8\uc744 \uac15\ud558\uac8c \ud2c0\uc5b4\uc11c \ubc30\ud130\ub9ac\ub97c \uc18c\uc9c4\ud558\ub824\uace0 \ud558\uc600\uc73c\ub098, 85km\ub97c \uc8fc\ud589\ud558\ub294 \ub3d9\uc548 \uaca8\uc6b0 20%\ub97c \uc18c\ubaa8\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ucda9\uc804\uae30\ub97c \uaf42\uc744 \ub54c 50%\uc600\uc73c\ub098, \ud638\uc218\ub97c \ud55c\ubc14\ud034 \ub3cc\uace0 \uc624\ub2c8 \uc774\ubbf8 100%\uac00 \ub418\uc5b4\uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc5ec\ub2f4\uc774\uc9c0\ub9cc, \uc800\ud76c\uac00 \ub3cc\uc544\uc654\uc744 \ub54c \uc606 \uc790\ub9ac\uc5d0\ub294 \uc804\uae30 \ud654\ubb3c\ucc28\uac00 \uc788\uc5b4 \ucda9\uc804\uc18c\uac00 \uac00\ub4dd \ucc3c\uc2b5\ub2c8\ub2e4.\\n\\n\ub610, \uc571\uc5d0\uc11c\ub3c4 \ucda9\uc804\uae30 \uc0ac\uc6a9 \uc5ec\ubd80\uac00 \uc5c5\ub370\uc774\ud2b8 \ub418\ub294 \uac83\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./majang-5.png)\\n\\n\ubc30\ud130\ub9ac \uc131\ub2a5\uc5d0\ub294 \uc88b\uc9c0 \uc54a\uace0 \uac00\uaca9\ub3c4 \ube44\uc2f8\uc11c \uc774\ub97c \uc790\uc8fc \uc0ac\uc6a9\ud558\ub294 \uac83\uc740 \uc88b\uc9c0 \uc54a\uaca0\uc9c0\ub9cc, \uae09\ud55c \uc0ac\ub78c\ub4e4\uc740 \uae09\uc18d \ucda9\uc804\uae30\ub97c \uc0ac\uc6a9\ud558\uba74 \ub418\uaca0\uad6c\ub098 \uc2f6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n```\\n\ub530\ub77c\uc11c \uae09\uc18d\uacfc \uc644\uc18d\uc740 \ub354\ub354\uc6b1 \ub2e4\ub978 \uac1c\ub150\uc73c\ub85c \ubd10\uc57c\uaca0\ub2e4\ub294 \uc0dd\uac01\uc774 \ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n```\\n\\n\uc81c\uac00 \uadf8\ub3d9\uc548 \uacbd\ud5d8\ud588\ub358 \uc804\uae30\ucc28 \ucda9\uc804\uc18c\ub294 \uc644\uc18d \uae30\uc900\uc774\uc5c8\uae30\uc5d0 \uc2e0\uc120\ud55c \uacbd\ud5d8\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \uc120\ub989\uc73c\ub85c \ub3cc\uc544\uc624\ub2e4\\n\\n![no offset](./end.png)\\n\\n\uc120\ub989\uc73c\ub85c \ub3cc\uc544\uc640\uc11c \ucc28\ub7c9\uc744 \ubc18\ub0a9\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 \uc774\ubc88 \uc5ec\uc815\uc744 \ud1b5\ud574 \uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc5d0\uc11c \uc5b4\ub5a4 \uc810\uc744 \uac1c\uc120\ud574\uc57c\ud560\uc9c0 \uc880 \ub354 \uba85\ud655\ud558\uac8c \uc54c\uac8c\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n1. \ud604\uc7ac \uc11c\ube44\uc2a4\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 \uae30\ub2a5\ub4e4\ub85c \ucda9\uc804\uc18c\ub97c \uac80\uc0c9\ud558\ub294 \uac83\uc740 \uac00\ub2a5\ud558\uba70, \ucda9\uc804\uc18c\uc758 \uc704\uce58\ub97c \uc815\ud655\ud558\uac8c \ud30c\uc545\ud558\ub294 \uac83\ub3c4 \uac00\ub2a5\ud558\ub2e4.\\n2. \ud558\uc9c0\ub9cc \ucda9\uc804\uc18c\uac00 \uc5c6\ub294 \ubaa9\uc801\uc9c0\ub294 \uac80\uc0c9\ud560 \uc218 \uc5c6\uace0, \ud604 \uc704\uce58\uac00 \uc5b4\ub514\uc778\uc9c0 \uac00\ub2a0\ud558\uae30\uac00 \uc5b4\ub824\uc6cc\uc9c4\ub2e4.\\n3. \ucda9\uc804\uc18c\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub2e4\uace0 \ud45c\uae30\ub418\uc5b4 \uc788\ub354\ub77c\ub3c4 \uc678\ubd80\uc778 \uac1c\ubc29\uc774 \uc544\ub2d0 \uc218 \uc788\ub2e4. \uc815\ubcf4\uac00 \uc815\ud655\ud788 \uc81c\uacf5\ub428\uc5d0\ub3c4 \ubd88\uad6c\ud558\uace0 \uc774\ub97c \ub2e8\ubc88\uc5d0 \ub208\uce58\ucc44\uae30 \uc5b4\ub835\ub2e4.\\n4. \uc774\ub7ec\ud55c \ubb38\uc81c\ub97c \uc608\uc0c1\ud558\uc5ec `\uc678\ubd80\uc778 \uac1c\ubc29 \uc5ec\ubd80`\ub97c \ud544\ud130\ub9c1 \ud560 \uc218 \uc788\ub294 \uae30\ub2a5\uc744 \uc81c\uacf5\ud558\uace0 \uc788\uc74c\uc5d0\ub3c4 \ubd88\uad6c\ud558\uace0 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc558\ub2e4.\\n5. \ucda9\uc804\uc18c\uc758 \ud1b5\uacc4 \uc790\ub8cc\uc758 \uc801\uc911\ub960\uc740 \ub192\uc558\uc73c\ub098, \uc880 \ub354 \ub9ce\uc740 \ucda9\uc804\uc18c\ub97c \ub4e4\ub824 \ud655\uc778\ud574\ubd10\uc57c \ud560 \uac83 \uac19\uc558\ub2e4.\\n6. \uc804\uae30\uc790\ub3d9\ucc28\ub294 \uc0dd\uac01\ubcf4\ub2e4 \uc624\ub798\uac00\uace0 \uc0c1\ud488\uc131\uc774 \uc788\uc5c8\ub2e4. \uc8fc\ud589 \ub2a5\ub825\ub3c4 \ucda9\ubd84\ud558\uace0, \uc778\ud504\ub77c\uac00 \uc798 \ub418\uc5b4\uc788\ub2e4. \uc774\uac78 \uc65c \uc695\ud558\uc9c0? \ub77c\ub294 \uc0dd\uac01\uc774 \ub4e4\uc5c8\ub2e4.\\n7. \uc9c0\ub3c4 \ud655\ub300 \ud5c8\uc6a9 \ubc94\uc704\uac00 \ub108\ubb34 \uc881\uc544\uc11c \uc0ac\uc6a9\ud558\ub294\ub370 \ubd88\ud3b8\ud55c\uac74 \uc2e4\uc81c \uc0c1\ud669\uc5d0\uc11c \ub354 \ubd88\ud3b8\ud588\ub2e4.\\n\\n\uc774\uc0c1 \uce74\ud398\uc778 \uc0ac\uc6a9\uae30\uc600\uc2b5\ub2c8\ub2e4."},{"id":"37","metadata":{"permalink":"/37","source":"@site/blog/2023-09-22-station-api-separate.mdx","title":"\ucda9\uc804\uc18c \uc870\ud68c api \ubd84\ub9ac","description":"\uc131\ub2a5 \uac1c\uc120\uc744 \uc704\ud574 \ucda9\uc804\uc18c \uc870\ud68c API\uc758 \uc124\uacc4\ub97c \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4.","date":"2023-09-22T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 22\uc77c","tags":[{"label":"\ud611\uc5c5","permalink":"/tags/\ud611\uc5c5"},{"label":"\uc11c\ubc84 \ubd80\ud558 \uc904\uc774\uae30","permalink":"/tags/\uc11c\ubc84-\ubd80\ud558-\uc904\uc774\uae30"}],"readingTime":2.78,"hasTruncateMarker":false,"authors":[{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"}],"frontMatter":{"slug":"37","title":"\ucda9\uc804\uc18c \uc870\ud68c api \ubd84\ub9ac","authors":["scent"],"tags":["\ud611\uc5c5","\uc11c\ubc84 \ubd80\ud558 \uc904\uc774\uae30"]},"prevItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 1","permalink":"/39"},"nextItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 1","permalink":"/38"}},"content":"\uc131\ub2a5 \uac1c\uc120\uc744 \uc704\ud574 \ucda9\uc804\uc18c \uc870\ud68c API\uc758 \uc124\uacc4\ub97c \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\uae30\uc874\uc5d0\ub294 \ucda9\uc804\uc18c \uac04\ub2e8 \uc815\ubcf4\uc640 \ub9c8\ucee4 \uc815\ubcf4\ub97c \ud55c \ubc88\uc5d0 \ubc1b\uc544\uc624\ub3c4\ub85d \uc124\uacc4\ub418\uc5b4 \uc788\uc5c8\uc9c0\ub9cc,\\n\ubc31\uc5d4\ub4dc\uc640 \ud504\ub860\ud2b8\uc5d4\ub4dc\uac00 \ud611\uc5c5\ud558\uc5ec \uac04\ub2e8 \uc815\ubcf4\uc640 \ub9c8\ucee4 \uc815\ubcf4\ub97c \uac01\uac01 \ud544\uc694\ud55c \ub9cc\ud07c\ub9cc \uc870\ud68c\ud558\ub3c4\ub85d \uba85\uc138\ub97c \uc218\uc815\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c \uba3c\uc800, \ubc31\uc5d4\ub4dc\uc640 \ud504\ub860\ud2b8\uc5d4\ub4dc\ub294 \ud568\uaed8 \ubaa8\uc5ec \uae30\ub2a5 \uc694\uad6c\uc0ac\ud56d\uacfc \uc131\ub2a5 \uac1c\uc120 \ubaa9\ud45c\ub97c \ub17c\uc758\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \ucda9\uc804\uc18c \uac04\ub2e8 \uc815\ubcf4\uc640 \ub9c8\ucee4 \uc815\ubcf4\ub97c \uac01\uac01 \uc870\ud68c\ud558\ub294 API \uc5d4\ub4dc\ud3ec\uc778\ud2b8\ub97c \uc0c8\ub85c \uc124\uacc4\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e4\uc74c\uc73c\ub85c, \ubc31\uc5d4\ub4dc\uc5d0\uc11c \uac04\ub2e8 \uc815\ubcf4 \uc870\ud68c\ub97c \uc704\ud55c API\ub97c \uad6c\ud604\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\ud544\uc694\ud55c \ud544\ub4dc\ub9cc\uc744 \uc870\ud68c\ud558\uc5ec \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \ubd80\ud558\ub97c \uc904\uc774\uace0 \uc751\ub2f5 \uc2dc\uac04\uc744 \uac1c\uc120\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\uc774\ud6c4\uc5d0\ub294 \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0\uc11c \ud574\ub2f9 API\ub97c \ud638\ucd9c\ud558\uc5ec \ud544\uc694\ud55c \uc815\ubcf4\ub97c \ubc1b\uc544\uc624\ub3c4\ub85d \uc218\uc815\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ub9c8\uc9c0\ub9c9\uc73c\ub85c, \ub9c8\ucee4 \uc815\ubcf4 \uc870\ud68c\ub97c \uc704\ud55c API\ub97c \uad6c\ud604\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\ub9c8\ucee4 \uc815\ubcf4\ub294 \uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ub418\ub294 \uc815\ubcf4\ub85c\uc11c, \uc694\uccad\ud55c \uc601\uc5ed \uc678\ubd80\ub85c \uc9c0\ub3c4\uac00 \uc774\ub3d9\ud560 \uacbd\uc6b0 \ud638\ucd9c\ub418\ub3c4\ub85d \uc124\uacc4\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uae30\uc874\uc5d0\ub294 \uac04\ub2e8 \uc815\ubcf4 \ub9ac\uc2a4\ud2b8\ub97c \ubcf4\uc5ec\uc8fc\uae30 \uc704\ud574 \uc870\ud68c\ud558\ub358 \uc815\ubcf4\ub4e4\uc774 \ub2e4\uc218 \ud3ec\ud568\ub418\uc5b4 \uc788\uc5c8\uc9c0\ub9cc,\\n\uc774 \uc815\ubcf4\ub97c \uc81c\uc678\ud558\uace0 \ub9c8\ucee4\ub97c \ub744\uc6b0\uae30 \uc704\ud574 \ud544\uc694\ud55c \ucd5c\uc18c\ud55c\uc758 \uc815\ubcf4\ub97c \uc870\ud68c\ud558\ub3c4\ub85d \uc218\uc815\ud574 \uc11c\ubc84\uc758 \ubd80\ud558\ub97c \ub0ae\ucdc4\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7ec\ud55c \ubcc0\uacbd\uc73c\ub85c \uc778\ud574 \ucda9\uc804\uc18c \uc870\ud68c API\uc758 \uc131\ub2a5\uc774 \uac1c\uc120\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\ud544\uc694\ud55c \uc815\ubcf4\ub9cc\uc744 \uc870\ud68c\ud558\ubbc0\ub85c\uc368 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \ubd80\ud558\ub97c \uc904\uc774\uace0 \uc751\ub2f5 \uc2dc\uac04\uc744 \ub2e8\ucd95\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\ub610\ud55c, \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0\uc11c\ub294 \ud544\uc694\ud55c \uc815\ubcf4\ub9cc\uc744 \ud638\ucd9c\ud558\uc5ec \ubd88\ud544\uc694\ud55c \ub370\uc774\ud130\ub97c \ubc1b\uc544\uc624\uc9c0 \uc54a\uc544\ub3c4 \ub418\ubbc0\ub85c \ud074\ub77c\uc774\uc5b8\ud2b8 \uce21\uc758 \uc131\ub2a5\ub3c4 \ud5a5\uc0c1\ub418\uc5c8\uc2b5\ub2c8\ub2e4."},{"id":"38","metadata":{"permalink":"/38","source":"@site/blog/2023-09-22-visitors/index.mdx","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 1","description":"\uc800\ud76c \ud300\uc740 \ub2e8\uc21c \ubc29\ubb38\uc790 100\uba85\uc744 \ubaa8\uc544\uc57c\ud558\ub294 \ubbf8\uc158\uc744 \ubc1b\uc558\uc2b5\ub2c8\ub2e4.","date":"2023-09-22T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 22\uc77c","tags":[{"label":"ga4","permalink":"/tags/ga-4"},{"label":"google analytics 4","permalink":"/tags/google-analytics-4"},{"label":"\uce74\ud398\uc778","permalink":"/tags/\uce74\ud398\uc778"},{"label":"\ubc29\ubb38\uc790 \ubd84\uc11d","permalink":"/tags/\ubc29\ubb38\uc790-\ubd84\uc11d"}],"readingTime":3.82,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"38","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 1","authors":["gabriel"],"tags":["ga4","google analytics 4","\uce74\ud398\uc778","\ubc29\ubb38\uc790 \ubd84\uc11d"]},"prevItem":{"title":"\ucda9\uc804\uc18c \uc870\ud68c api \ubd84\ub9ac","permalink":"/37"},"nextItem":{"title":"\ub9c8\ucee4 \ub80c\ub354\ub9c1 \ucd5c\uc801\ud654","permalink":"/36"}},"content":"\uc800\ud76c \ud300\uc740 \ub2e8\uc21c \ubc29\ubb38\uc790 100\uba85\uc744 \ubaa8\uc544\uc57c\ud558\ub294 \ubbf8\uc158\uc744 \ubc1b\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\ubaa9\ud45c \ub2ec\uc131\uc744 \uc704\ud574 \uc57d 2\uc8fc \uc804\uc5d0 \uc2e4\ud589 \uacc4\ud68d\uc744 \uc81c\ucd9c\ud574\uc57c \ud588\ub294\ub370\uc694\\n\\n100\uba85\uc744 \ubaa8\uc9d1\ud558\uae30 \uc704\ud574 \ub2e4\uc74c\uacfc \uac19\uc740 \uacc4\ud68d\uc744 \uc138\uc6e0\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n![no offset](./plan.png)\\n\\n---\\n\\n\uc774 \ub2f9\uc2dc \uc800\ud76c \ud300\uc758 \uac00\uc7a5 \ud070 \uace0\ubbfc\uc740, \uc804\uae30\ucc28\uac00 \uc5ec\uc804\ud788 \uc18c\uc218\uc758 \uc6b4\uc804\uc790\uc5d0\uac8c\ub9cc \ubcf4\uae09\ub418\uc5c8\ub2e4\ub294 \uc810\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ud2b9\ud788, \uc804\uae30\ucc28 \ubcf4\uae09 \uad00\ub828 \ud1b5\uacc4 \uc790\ub8cc\ub97c \ucc3e\uc544\ubcf4\uba74 \ub300\ubd80\ubd84\uc758 \ucc28\uc8fc\ub4e4\uc740 40~60\ub300\uc5d0 \uc555\ub3c4\uc801\uc73c\ub85c \ubab0\ub824\uc788\uc5b4 \uc80a\uc740 \uc5f0\ub839 \uce35\uc5d0\uc11c\ub294 \uac70\uc758 \uad6c\ub9e4\ub97c \ud558\uc9c0 \uc54a\uace0 \uc788\ub2e4\ub294 \uc0ac\uc2e4\uc744 \uc54c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\\n![no offset](./statistics.png)\\n\\n\uc704 \uc790\ub8cc\ub294 2021\ub144 7\uc6d4 \uae30\uc900\uc774\uc9c0\ub9cc, \ucd5c\uc2e0 \uc790\ub8cc\uc5d0\uc11c\ub3c4 \ub9c8\ucc2c\uac00\uc9c0\ub85c \uc80a\uc740 \uc5f0\ub839\uce35\uc5d0\uc11c\ub294 \uc804\uae30\ucc28\ub97c \ubcf4\uc720\ud55c \uc0ac\ub78c\uc744 \ucc3e\uae30 \uc5b4\ub835\ub2e4\uace0 \ub098\uc635\ub2c8\ub2e4. \uc2e4\uc81c\ub85c \uc8fc\ubcc0 \ub610\ub798\uc758 \uc6b4\uc804\uc790\ub97c \ucc3e\uc544\ubcf4\uba74 \ub300\ubd80\ubd84 \uac00\uc194\ub9b0 \ubaa8\ub378\uc744 \ud0c0\uace0 \ub2e4\ub2c8\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\ub294 \ud64d\ubcf4 \ub300\uc0c1\uc744 \uc8fc\ubcc0\uc5d0\uc11c \ucc3e\uc9c0 \uc54a\uace0 \ubd88\ud2b9\uc815 \ub2e4\uc218\uc758 \uc0ac\ub78c\ub4e4\uc744 \ubaa8\uc9d1\ud558\uae30 \uc704\ud574 \ub2e4\uc74c\uacfc \uac19\uc740 \ubc29\ubc95\uc744 \uc0ac\uc6a9\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n# \ud64d\ubcf4 \ubc29\ubc95\\n\\n## \uce74\ud398\\n\\n![no offset](./insta1.png)\\n![no offset](./naver1.png)\\n\\n\ub124\uc774\ubc84\uc5d0 \uc788\ub294 \uc804\uae30\uc790\ub3d9\ucc28 \ub3d9\ud638\ud68c \uce74\ud398 \uc911 \uac00\uc7a5 \ud070 \uacf3\uc5d0 \uae00\uc744 \uc62c\ub824 \ubc29\ubb38\uc790\ub97c \ubaa8\uc9d1\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uce74\ud398\uc5d0 \uae00\uc744 \uc62c\ub9ac\ub294 \uac83\uc740 \ubb34\ub8cc\uc774\uba70, \uce74\ud398\uc5d0 \uac00\uc785\ud55c \uc0ac\ub78c\ub4e4\uc740 \uc804\uae30\ucc28\uc5d0 \uad00\uc2ec\uc774 \uc788\ub294 \uc0ac\ub78c\ub4e4\uc774\uae30 \ub54c\ubb38\uc5d0 \uc800\ud76c\uac00 \uc6d0\ud558\ub294 \ubc29\ubb38\uc790\ub97c \ubaa8\uc9d1\ud558\uae30\uc5d0 \uc801\ud569\ud558\ub2e4\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \uce74\uce74\uc624\ud1a1 \uc624\ud508\ucc44\ud305\\n\\n![no offset](./kakao1.png)\\n![no offset](./kakao2.png)\\n\\n\uce74\uce74\uc624\ud1a1 \uc624\ud508\ucc44\ud305\uc5d0\ub294 \uc218\ub9ce\uc740 \ub300\ud654\ubc29\uc774 \uc874\uc7ac\ud569\ub2c8\ub2e4.\\n\\n\ud2b9\uc815 \uc8fc\uc81c\ub85c \ub9cc\ub4e4\uc5b4\uc9c4 \ub300\ud654\ubc29\uc774 \ub300\ubd80\ubd84\uc774\uae30\uc5d0 \uc804\uae30\ucc28\ub97c \uc8fc\uc81c\ub85c \ud55c \uc624\ud508\ucc44\ud305 \ub300\ud654\ubc29\uc744 \ucc3e\ub294 \uac83\uc740 \uc804\ud600 \uc5b4\ub835\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\uc548\ud0c0\uae5d\uac8c\ub3c4 \uc77c\ubd80 \ub2e8\ud1a1\ubc29\uc5d0\uc11c \uac15\ud1f4\ub97c \ub2f9\ud588\uc9c0\ub9cc, \ucc28\uc8fc\ub4e4\uacfc \ucc44\ud305\ud558\uba74\uc11c \ud53c\ub4dc\ubc31\uc744 \ubc1b\uc544\ubcfc \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \uae30\ud0c0 \ud64d\ubcf4 \uc218\ub2e8\\n\\n\uae30\ud0c0 \ud64d\ubcf4 \uc218\ub2e8\uc740 \uc544\uc9c1 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\ub124\uc774\ubc84 \ubc34\ub4dc, \ubcf4\ubc30\ub4dc\ub9bc\uc740 \uc0ac\uc6a9\ud558\ub294 \ud06c\ub8e8\uac00 \uc5c6\uc5b4\uc11c \ud64d\ubcf4\ub97c \ud558\uae30 \uc5b4\ub824\uc6e0\uace0, \uad6c\uae00 \uc560\ub4dc\uc13c\uc2a4\uc640 \uac19\uc740 \ub3c4\uad6c\ub294 \ube44\uc6a9\uc774 \ubc1c\uc0dd\ud558\uae30\uc5d0 \uc544\uc9c1\uc740 \uc774\ub974\ub2e4\uace0 \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n# Google Analytics 4 \ud1b5\uacc4 \uc9d1\uacc4 \uacb0\uacfc\\n\\n## \ub2e8\uc21c \ubc29\ubb38\uc790\\n\\n![no offset](./ga1.png)\\n![no offset](./ga2.png)\\n![no offset](./ga3.png)\\n![no offset](./ga4.png)\\n\uc774\ucc98\ub7fc \uc678\ubd80 \uc9c0\uc5ed\uc5d0\uc11c\ub3c4 \ub9ce\uc774 \uc811\uc18d\ud574\uc8fc\uc2e0 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n![no offset](./ga5.png)\\n![no offset](./ga6.png)\\n![no offset](./ga7.png)\\n\\n\uc9d1\uacc4 \ub41c \uc790\ub8cc\ucc98\ub7fc \ubc29\ubb38\uc790\ub4e4\uc774 \ub2e8\uc21c \ubc29\ubb38\ub9cc \ud55c \uac83\uc774 \uc544\ub2c8\ub77c, \uc218 \ub9ce\uc740 \uc774\ubca4\ud2b8\ub97c \ubc1c\uc0dd\uc2dc\ud0a4\uace0 \ud3c9\uade0 \ucc38\uc5ec \uc2dc\uac04\ub3c4 \uc0c1\ub2f9 \ubd80\ubd84 \ud655\ubcf4\ud588\uc74c\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."},{"id":"36","metadata":{"permalink":"/36","source":"@site/blog/2023-09-21-marker-rendering-optimization.mdx","title":"\ub9c8\ucee4 \ub80c\ub354\ub9c1 \ucd5c\uc801\ud654","description":"1. \uac1c\uc694","date":"2023-09-21T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 21\uc77c","tags":[{"label":"react","permalink":"/tags/react"},{"label":"useSyncExternalState","permalink":"/tags/use-sync-external-state"},{"label":"googleMap","permalink":"/tags/google-map"}],"readingTime":12.04,"hasTruncateMarker":false,"authors":[{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"}],"frontMatter":{"slug":"36","title":"\ub9c8\ucee4 \ub80c\ub354\ub9c1 \ucd5c\uc801\ud654","authors":["scent"],"tags":["react","useSyncExternalState","googleMap"]},"prevItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 1","permalink":"/38"},"nextItem":{"title":"Scale-out \uc2dc Scheduling \uc911\ubcf5 \uc2e4\ud589 \ub9c9\uae30","permalink":"/35"}},"content":"### 1. \uac1c\uc694\\n\\n\uae30\uc874\uc758 \uad6c\uc870\uc5d0\uc11c\ub294 \ub9c8\ucee4 \ud558\ub098\ub97c \ub80c\ub354\ub9c1\ud558\uae30 \uc704\ud574 \ub2e4\uc74c\uacfc \uac19\uc740 \uacfc\uc815\uc744 \uac70\ucce4\ub2e4.\\n\\n1. StationMarkersContainer \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \ucda9\uc804\uc18c \uc815\ubcf4 \uc694\uccad\\n2. \ucda9\uc804\uc18c \uc815\ubcf4\ub97c props\ub85c \ub118\uaca8 Marker \ucef4\ud3ec\ub10c\ud2b8 \ud638\ucd9c\\n3. \uc9c0\ub3c4\uc5d0 \ubd80\ucc29\ub420 DOM\uc694\uc18c \uc0dd\uc131\\n4. createRoot\ub97c \ud1b5\ud574 \ub9ac\uc561\ud2b8 root \uc0dd\uc131\\n5. 2\ubc88\uc5d0\uc11c \uc0dd\uc131\ud55c DOM \uc694\uc18c\ub97c \uc804\ub2ec\ud574 \uad6c\uae00 \uc9c0\ub3c4 api\uc758 Marker \uc0dd\uc131\uc790 \ud568\uc218 \ud638\ucd9c\\n6. 3\ubc88\uc5d0\uc11c \uc0dd\uc131\ud588\ub358 root\uc758 render \uba54\uc11c\ub4dc \ud638\ucd9c\\n7. \ub9c8\ucee4 \uc778\uc2a4\ud134\uc2a4 \uc804\uc5ed \uc0c1\ud0dc\uc5d0 \uc0c8\ub85c \uc0dd\uc131\ud55c \ub9c8\ucee4 \ucd94\uac00\\n\\n\uc704 \uacfc\uc815\uc744 \uac70\ucce4\uc744 \ub54c\uc758 \ub9c8\ucee4 \ub80c\ub354\ub9c1 \ubaa8\uc2b5\uc744 \ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n![before](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/28520ee3-2fa6-4110-b4e4-8a0bb706324e)\\n\\n\ub9c8\ucee4\ub4e4\uc774 \ud55c\ubc88\uc5d0 \ub80c\ub354\ub9c1 \ub418\ub294 \uac83\uc774 \uc544\ub2c8\ub77c \uc0b0\ubc1c\uc801\uc73c\ub85c \ub80c\ub354\ub9c1 \ub418\ub294 \ubaa8\uc2b5\uc744 \ud655\uc778\ud560 \uc218 \uc788\ub2e4.\\n\\n### 2. \ubb38\uc81c \uc6d0\uc778 \ubd84\uc11d\\n\\n\ub9c8\ucee4\ub97c \ub80c\ub354\ub9c1 \ud558\uae30 \uc704\ud574 \uac70\uce58\ub294 \uacfc\uc815\uc744 \ubd84\uc11d\ud574 \ubcf4\uc558\ub2e4.\\n\\n1 ~ 3 \uacfc\uc815\uc5d0\uc11c\ub294 \uc131\ub2a5\uc5d0 \ud06c\uac8c \uc601\ud5a5\uc744 \ub07c\uce60 \uc694\uc18c\uac00 \uc5c6\uc9c0\ub9cc 4\ubc88 \uacfc\uc815\uc740 \uc77c\ubc18\uc801\uc778 \ub9ac\uc561\ud2b8 \ud504\ub85c\uc81d\ud2b8\ub97c \uac1c\ubc1c\ud560 \ub54c \uacaa\ub294 \uacfc\uc815\uc774 \uc544\ub2c8\ub2e4. \ub530\ub77c\uc11c createRoot\ub97c \ud1b5\ud574 \ub9ce\uc740 \uac1c\uc218\uc758 \ub8e8\ud2b8\ub97c \uc0dd\uc131\ud588\uc744 \ub54c\uc758 \uc601\ud5a5\uc5d0 \ub300\ud574 \uc54c\uc544\ubcf4\uc558\ub2e4.\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/494a5bc5-be5d-4a58-b5b2-77ce7d3e5de7)\\n\\n\ub9ac\uc561\ud2b8 \uacf5\uc2dd \ubb38\uc11c\ub97c \ubcf4\ub2c8 \ud398\uc774\uc9c0\uc758 \uc77c\ubd80\uc5d0 \ub9ac\uc561\ud2b8\ub97c \ubfcc\ub824\uc11c \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0\uc5d0\ub294 \ub8e8\ud2b8\ub97c \ud544\uc694\ud55c \ub9cc\ud07c \uc0dd\uc131\ud574\ub3c4 \ub41c\ub2e4\ub294 \uc774\uc57c\uae30\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\uc5c8\ub2e4. \ub530\ub77c\uc11c 4\ubc88 \uacfc\uc815 \ub610\ud55c \ubb38\uc81c\uc758 \uc6d0\uc778\uc774\ub77c\uace0 \ubcfc \uc218 \uc5c6\uc5c8\ub2e4.\\n\\n5\ubc88 \uacfc\uc815\uc740 \uad6c\uae00 \uc9c0\ub3c4\uc5d0 \ub9c8\ucee4\ub97c \ud2b9\uc815 \uc704\ub3c4 \uacbd\ub3c4\uc5d0 \uc704\uce58\uc2dc\ud0a4\uae30 \uc704\ud574\uc11c \uc5b4\uca54 \uc218 \uc5c6\uc774 \uac70\uccd0\uc57c \ud558\ub294 \uacfc\uc815\uc774\ubbc0\ub85c \uc774 \uacfc\uc815\uc740 \ubb38\uc81c\uac00 \uc788\ub354\ub77c\ub3c4 \uac1c\uc120\uc774 \ubd88\uac00\ub2a5\ud574 \uc77c\ub2e8 \uace0\ub824\ud558\uc9c0 \uc54a\uc558\ub2e4.\\n\\n6\ubc88 \uacfc\uc815\uc740 4\ubc88 \uacfc\uc815\uc5d0\uc11c \uc0dd\uc131\ud588\ub358 \ub9ac\uc561\ud2b8 \ub8e8\ud2b8\uc758 render \uba54\uc11c\ub4dc\ub97c \ud638\ucd9c\ud574 \uc2e4\uc81c\ub85c \ud654\uba74\uc5d0 \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\ub97c \uadf8\ub9ac\ub3c4\ub85d \ud558\ub294 \uacfc\uc815\uc774\ub2e4. \uc774 \uacfc\uc815 \ub610\ud55c \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\ub97c \ud654\uba74\uc5d0 \ub80c\ub354\ub9c1\ud558\uae30 \uc704\ud574\uc120 \uc5b4\uca54 \uc218 \uc5c6\uc774 \uac70\uccd0\uc57c \ud558\ub294 \uacfc\uc815\uc774\ubbc0\ub85c \uace0\ub824\ud558\uc9c0 \uc54a\uc558\ub2e4.\\n\\n> \ud558\uc9c0\ub9cc 6\ubc88 \uacfc\uc815\uc5d0\uc11c \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc9c1\uc811 \uadf8\ub9ac\ub294 \uac83\uc774 \uc544\ub2c8\ub77c \uad6c\uae00 \uc9c0\ub3c4 api\uc758 \uae30\ubcf8 \ub9c8\ucee4\ub97c \uc0ac\uc6a9\ud558\uba74 \uc131\ub2a5\uc744 \ud5a5\uc0c1\uc2dc\ud0ac \uc218 \uc788\uc9c0 \uc54a\ub0d0\uace0 \ubc18\ubb38\ud560 \uc218\ub3c4 \uc788\uc744 \uac83\uc774\ub2e4. \uc774\uc804\uc5d0\ub294 \uc774\ub7ec\ud55c \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud574 \ub9c8\ucee4\ub97c \ub80c\ub354\ub9c1 \ud588\uc5c8\ub2e4. \uc6b0\ub9ac\uc758 \uc11c\ube44\uc2a4\ub294 \ud604\uc7ac \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ucda9\uc804\uc18c \uac1c\uc218\ub97c \ub9c8\ucee4\ub97c \ud1b5\ud574\uc11c\ub3c4 \uc804\ub2ec\ud558\uae30 \ub54c\ubb38\uc5d0 \uc774\ub97c \uace0\ub824\ud574 \uae30\ubcf8 \ub9c8\ucee4\ub97c \uc0ac\uc6a9\ud560 \ub54c \ub2e4\uc74c\uc758 \ub450 \uac00\uc9c0 \ubb38\uc81c\uac00 \uc0dd\uae34\ub2e4.\\n>\\n> 1. \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ucda9\uc804\uc18c \uac1c\uc218\ub97c \uae30\ubcf8 \ub9c8\ucee4\uc5d0 \ub80c\ub354\ub9c1 \ud560 \ub54c \uc131\ub2a5\uc774 \ub9e4\uc6b0 \uc88b\uc9c0 \uc54a\ub2e4.\\n> 2. \ub9c8\ucee4\uc758 \ub514\uc790\uc778\uc744 \ubc14\uafb8\uace0\uc790 \ud560 \ub54c \ubcc0\uacbd\uc5d0 \ub300\uc751\ud558\uae30 \uc5b4\ub835\ub2e4.\\n>\\n> \ub530\ub77c\uc11c \ub9c8\ucee4\ub294 \ub9ac\uc561\ud2b8 \ub8e8\ud2b8\uc758 render \uba54\uc11c\ub4dc\ub97c \ud638\ucd9c\ud574 \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\ub97c \ub80c\ub354\ub9c1\ud558\ub294 \uac83\uc73c\ub85c \uacb0\uc815\ud588\ub2e4.\\n\\n\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ub0a8\uc740 7\ubc88 \uacfc\uc815\uc5d0\uc11c\ub294 useSyncExternalState \ud6c5\uc744 \uc0ac\uc6a9\ud574 \uc804\uc5ed\uc801\uc73c\ub85c \uad00\ub9ac\ud558\uace0 \uc788\ub358 \uc0c1\ud0dc\uc5d0 \uc218\uc815\uc744 \uac00\ud558\ub294 \uc5f0\uc0b0\uc744 \uc218\ud589\ud55c\ub2e4. \uc774 \uacfc\uc815\uc740 \uc774\uc804\uc5d0\ub3c4 \uc131\ub2a5 \uc800\ud558\ub97c \uc720\ubc1c\ud560 \uac83\uc73c\ub85c \uc608\uc0c1\ub418\ub358 \ubd80\ubd84\uc774\uc5c8\ub2e4. (\ud558\ub2e8 \ub9c1\ud06c \ucc38\uace0)\\n\\n[useSyncExternalStore \ud6c5\uc744 \ud1b5\ud574 \uad6c\ub3c5\ud55c state\uac00 \ud55c\ubc88\uc5d0 \uc5c5\ub370\uc774\ud2b8 \ub418\ub294 \uc774\uc720](https://www.notion.so/useSyncExternalStore-state-67e686eead8b4750b3015a1f75ea3e76?pvs=21)\\n\\n\uc694\uccad\uc758 \uacb0\uacfc\ub85c \ubc1b\uc544\uc628 \ub9c8\ucee4 \uc815\ubcf4\uc758 \uac1c\uc218\uac00 100\uac1c\ub77c\uace0 \uac00\uc815\ud574\ubcf4\uc790. \uc6b0\ub9ac\ub294 \uc774\uc81c \ub9c8\ucee4\ub97c \ub80c\ub354\ub9c1 \ud560 \uac83\uc774\ub2e4. \uccab \ubc88\uc9f8 \ub9c8\ucee4\uc758 \ub80c\ub354\ub9c1\uc744 \uc704\ud574 1\ubc88 ~ 6\ubc88\uc758 \uacfc\uc815\uc744 \uac70\uce5c \ud6c4 7\ubc88 \uacfc\uc815\uc744 \uc218\ud589\ud55c\ub2e4. \uadf8\ub7ec\uba74 \ub9ac\uc561\ud2b8 \uc785\uc7a5\uc5d0\uc11c\ub294 \ub9ac\uc561\ud2b8 \ub8e8\ud2b8\uc758 render \uba54\uc11c\ub4dc \ud638\ucd9c\uc5d0 \ub300\ud55c \ub3d9\uc791\uc744 \uc218\ud589\ud574\uc57c \ud558\uace0, \uc0c8\ub85c\uc6b4 \ub9c8\ucee4 \uc778\uc2a4\ud134\uc2a4\uc5d0 \ub300\ud55c \uc804\uc5ed \uc0c1\ud0dc\ub97c \ubcc0\uacbd\uc2dc\ud0a4\ub294 \ub3d9\uc791\uc744 \uc218\ud589\ud574\uc57c \ud55c\ub2e4. \ub9ac\uc561\ud2b8\uac00 \uc774 \uacfc\uc815\uc744 100\ubc88 \ubc18\ubcf5\ud558\uace0 \ub098\uba74 \uc6b0\ub9ac\ub294 \ube44\ub85c\uc18c \ubaa8\ub4e0 \ub9c8\ucee4\uac00 \ud654\uba74\uc5d0 \ub80c\ub354\ub9c1 \ub41c \ubaa8\uc2b5\uc744 \ubcfc \uc218 \uc788\uc744 \uac83\uc774\ub2e4.\\n\\n\ub098\ub294 \uc774 \ubd80\ubd84\uc5d0\uc11c \uc131\ub2a5 \uc800\ud558\uc758 \uc694\uc18c\uac00 \uc788\ub2e4\uace0 \uc0dd\uac01\ud588\ub2e4. \ub9ac\uc561\ud2b8\uc5d0\uc11c\uc758 \uc0c1\ud0dc \ubcc0\ud654\ub294 \uace7 \ub9ac\uc561\ud2b8 \ub0b4\ubd80\uc758 \ub80c\ub354\ub9c1\uc744 \uc704\ud55c \ub85c\uc9c1\uc774 \uc218\ud589\ub418\uac8c \ud568\uc744 \uc758\ubbf8\ud558\uace0, \uc774 \uacfc\uc815\uc744 \uac1c\uc120 \uc774\uc804\uc5d0\ub294 \ub9c8\ucee4\uc758 \uac1c\uc218\ub9cc\ud07c \ubc18\ubcf5\ud558\uace0 \uc788\uc5c8\ub358 \uac83\uc774\ub2e4. \uc5ec\uae30\uae4c\uc9c0 \uc0dd\uac01\ud574\ubcf4\ub2c8 \uc804\uc5ed \uc0c1\ud0dc \ubcc0\ud654\uc5d0 \ub300\ud574 \ub9ac\uc561\ud2b8\uac00 \ub80c\ub354\ub9c1\uc744 \uc704\ud55c \uc5f0\uc0b0\uc744 \uc9c4\ud589\ud560 \ub3d9\uc548\uc5d0\ub294 \ub9c8\ucee4\uc758 \ub80c\ub354\ub9c1(render \uba54\uc11c\ub4dc \ud638\ucd9c)\uc774 \uba48\ucd94\ub294 \uac83\uc774 \uc544\ub2d0\uae4c \ud558\ub294 \uc0dd\uac01\uc774 \ub4e4\uc5c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \ud06c\ub86c \uac1c\ubc1c\uc790 \ub3c4\uad6c\uc758 \ud37c\ud3ec\uba3c\uc2a4 \ud0ed\uc744 \ub4e4\uc5b4\uac00 \ubcf4\ub2c8 \uc0b0\ubc1c\uc801\uc73c\ub85c \ubc1c\uc0dd\ud558\ub358 \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc758 \ubb38\uc81c \uc6d0\uc778\uc774 \uc9d0\uc791\ud588\ub358 \uadf8 \uc6d0\uc778\uc784\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc5c8\ub2e4.\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/20926d19-79a5-4d49-b733-de1c2b87059c)\\n\\n\ud504\ub808\uc784 \uc774\ubbf8\uc9c0 \ud558\ub2e8\uc744 \ubcf4\uba74 \uc0b0\ubc1c\uc801\uc778 \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc774 \uc218\ud589\ub420 \ub54c\ub9c8\ub2e4 \uc218\ubc18\ub418\ub294 \uc5b4\ub5a4 \ud568\uc218 \ud638\ucd9c\uc774 \uc788\uc74c\uc744 \ud655\uc778\ud560 \uc218 \uc788\ub2e4.\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/20b8f1e4-eceb-4e18-82f0-8ef6cc5ee8a1)\\n\\n\uc774 \ubd80\ubd84\uc774 \ubb38\uc81c\uc758 \ud568\uc218 \ud638\ucd9c \ubd80\ubd84\uc774\ub2e4. \uc790\uc138\ud788 \uc0b4\ud3b4\ubcf4\uba74 \uc0c1\ub2e8\uc5d0 `performWorkUntilDeadline`\uc774\ub780 \ud568\uc218\uac00 \ud638\ucd9c\ub428\uc744 \ubcfc \uc218 \uc788\ub2e4.\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/d7a91ce6-4907-4c79-948b-d80a205a0697)\\n\\n\uc774 `performWorkUntilDeadline` \ub77c\ub294 \ud568\uc218\ub97c \uc870\uae08 \uc54c\uc544\ubcf4\ub2c8 \ud574\ub2f9 \ud568\uc218\ub294 \uac04\ub2e8\ud788 \ub9d0\ud574 \ub9ac\uc561\ud2b8\uc5d0\uc11c state\uc758 \ubcc0\uacbd\uc774 \ud55c\ubc88\uc5d0 \ub9ce\uc774 \ubc1c\uc0dd\ud560 \ub54c 5ms\uc758 \ub370\ub4dc\ub77c\uc778 \uc2dc\uac04\uc744 \uc904 \ub54c \uc0ac\uc6a9\ud558\ub294 \ud568\uc218\ub77c\ub294 \uac83\uc744 \uc54c\uac8c \ub418\uc5c8\ub2e4. \ubb38\uc81c\uc758 \uc6d0\uc778\uc774\ub77c\uace0 \uc0dd\uac01\ud588\ub358 \ub9c8\ucee4 \uac1c\uc218 \ub9cc\ud07c\uc758 \uc804\uc5ed \uc0c1\ud0dc \ubcc0\ud654\uac00 \uc2e4\uc81c\ub85c \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc744 \uc7a0\uc2dc \uc911\ub2e8\ud558\uac8c \ub9cc\ub4e4\uace0 \uc788\uc74c\uc744 \uc54c\uac8c \ub418\uc5c8\ub2e4.\\n\\n### 3. \ubb38\uc81c \ud574\uacb0\\n\\n\uc55e\uc11c \ubd84\uc11d\ud55c \ubb38\uc81c\ub97c \uac1c\uc120\ud574\ubcf4\uace0\uc790 \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc5d0 \ud544\uc694\ud55c \ucda9\uc804\uc18c \uc815\ubcf4 \ubc30\uc5f4\uc744 \ubd80\ubaa8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \ubc1b\uc544\uc640 \uac01 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uc790\uc2dd \ucef4\ud3ec\ub10c\ud2b8\uc5d0 \ub118\uaca8\uc8fc\uace0, \uc790\uc2dd \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \ub9c8\ucee4 \uc0dd\uc131\uacfc \ub80c\ub354\ub9c1 \ub85c\uc9c1\uc744 \uc218\ud589\ud558\ub358 \uae30\uc874\uc758 \ubc29\uc2dd\uc744 \ubd80\uc218\uace0 \ubd80\ubaa8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \ubaa8\ub4e0 \uac83\uc744 \uc77c\uad04 \ucc98\ub9ac\ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \uace0\uccd0\ubcf4\uc558\ub2e4.\\n\\n\uace0\uce58\ub294 \uacfc\uc815\uc5d0\uc11c \uae30\uc874 \ubc29\uc2dd\uc5d0\uc11c\ub294 \ub9ac\uc561\ud2b8 \uc0dd\uba85 \uc8fc\uae30\uc5d0 \uc758\uc874\ud558\uc5ec \ud654\uba74\uc5d0 \ubcf4\uc5ec\uc9c0\uc9c0 \uc54a\ub294 \ub9c8\ucee4\ub97c \uc9c0\uc6cc\uc8fc\ub358 \ub85c\uc9c1\uc744 \uc774\uc81c\ub294 \ubaa8\ub450 \uc9c1\uc811 \uad6c\ud604\ud574\uc57c \ud588\ub2e4.\\n\\n\uc774\uc804\uc758 \uc601\uc5ed\uacfc \uacb9\uce58\ub294 \ubd80\ubd84\uc5d0 \uc788\ub294 \ucda9\uc804\uc18c\ub294 \ub2e4\uc2dc \uadf8\ub9ac\uc9c0 \uc54a\uace0, \uc601\uc5ed \ubc16\uc758 \ucda9\uc804\uc18c\ub97c \ub098\ud0c0\ub0b4\ub294 \ub9c8\ucee4\ub294 \uc9c0\uc6cc\uc8fc\uace0, \uc774\uc804\uc758 \uc601\uc5ed\uacfc \uacb9\uce58\uc9c0 \uc54a\ub294 \uc0c8\ub85c \ubc1b\uc544\uc628 \ucda9\uc804\uc18c\ub294 \uadf8\ub9ac\ub3c4\ub85d \ub2e4\uc74c\uacfc \uac19\uc774 \uba54\uc11c\ub4dc\ub97c \ubd84\ub9ac\ud574\ubcf4\uc558\ub2e4.\\n\\n- \uae30\uc874\uacfc \uacb9\uce58\uc9c0 \uc54a\ub294 \uc0c8\ub85c\uc6b4 \uc601\uc5ed\uc5d0 \ub300\ud55c \ub9c8\ucee4\ub97c \uc0dd\uc131\ud558\ub294 \uba54\uc11c\ub4dc\\n- \uae30\uc874\uacfc \uacb9\uccd0\uc9c0\ub294 \uc601\uc5ed\uc5d0 \ub300\ud55c \ub9c8\ucee4\ub4e4\uc744 \ubc18\ud658\ud558\ub294 \uba54\uc11c\ub4dc\\n- \uc0c8\ub85c\uc6b4 \uc601\uc5ed \ubc16\uc5d0 \uc788\ub294 \ub9c8\ucee4\ub4e4\uc744 \uc9c0\uc6cc\uc8fc\ub294 \uba54\uc11c\ub4dc\\n- \uc0c8\ub86d\uac8c \uc0dd\uc131\ub41c \ub9c8\ucee4\ub97c \ud654\uba74\uc5d0 \ub80c\ub354\ub9c1\ud558\ub294 \uba54\uc11c\ub4dc\\n\\n\uc774 \uba54\uc11c\ub4dc\ub4e4\uc744 \ucee4\uc2a4\ud140 \ud6c5\uc73c\ub85c \ubd84\ub9ac\ud574 \ubd80\ubaa8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \uc774\ub97c \ud65c\uc6a9\ud558\ub3c4\ub85d \ud558\uc5ec \ub2e4\uc18c \ubcf5\uc7a1\ud560 \uc218 \uc788\ub294 \ub9c8\ucee4 \ub80c\ub354\ub9c1 \ub85c\uc9c1\uc744 \uc120\uc5b8\uc801\uc73c\ub85c \uad6c\ud604\ud560 \uc218 \uc788\ub3c4\ub85d \ud588\ub2e4.\\n\\n\uacb0\uacfc\uc801\uc73c\ub85c \uae30\uc874\uc5d0 \uc0ac\uc6a9\ub418\ub358 \uae30\ub2a5\ub4e4\uc744 \uadf8\ub300\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc73c\uba74\uc11c \ud654\uba74\uc5d0 \ub9c8\ucee4\uac00 \uc0b0\ubc1c\uc801\uc73c\ub85c \ub80c\ub354\ub9c1 \ub418\ub358 \ubb38\uc81c\uac00 \ud574\uacb0 \ub418\uc5c8\uace0, \ubd80\uac00\uc801\uc778 \ud6a8\uacfc\ub85c \uc804\uccb4 \ub9c8\ucee4\uc758 \ub80c\ub354\ub9c1 \uc2dc\uc810\ub3c4 \uc55e\ub2f9\uae38 \uc218 \uc788\uac8c \ub418\uc5c8\ub2e4. + \uae30\uc874\uc5d0\ub294 \uad6c\uc870\uc801\uc778 \ubb38\uc81c\ub85c \uc5f0\uc0b0\ub7c9\uc774 \ub108\ubb34 \ub9ce\uc544 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc774 \ub2a6\uc5b4\uc838 \uc774\ub97c \ub3c4\uc785\ud560 \uc218 \uc5c6\uc5c8\ub358 \ubb38\uc81c\ub97c \uad6c\uc870 \uc218\uc815\uc73c\ub85c \uc778\ud574 \uc801\uc6a9\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\ub2e4.\\n\\n### \uc791\uc5c5\ud55c PR\\n\\nhttps://github.com/woowacourse-teams/2023-car-ffeine/pull/737\\n\\n## \uacb0\uacfc \ubd84\uc11d (performance \ud0ed \ud65c\uc6a9)\\n\\n### before\\n\\n\ub9c8\ucee4 \uc870\ud68c \uc694\uccad\uc774 \uc885\ub8cc\ub41c \uc2dc\uc810: \uc57d `2499ms`\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/033e8519-a1aa-43a4-959d-afeba93c1917)\\n\\n\uccab \ub9c8\ucee4 \ub80c\ub354\ub9c1 \uc2dc\uc810: `3093ms`\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/b4fc47ca-4ef3-43f4-a9a5-7117edabc225)\\n\\n\ubaa8\ub4e0 \ub9c8\ucee4 \ub80c\ub354\ub9c1 \uc885\ub8cc \uc2dc\uc810: \uc57d `3611ms`\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/2b8a4c4c-218b-419a-8a47-e3b768d35bc2)\\n\\n\ucc98\uc74c\uc73c\ub85c \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub420 \ub54c\uae4c\uc9c0 \uc18c\uc694\ub41c \uc2dc\uac04: `594ms`\\n\\n\ubaa8\ub4e0 \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc5d0 \uc18c\uc694\ub41c \uc2dc\uac04: `1112ms`\\n\\n### after\\n\\n\ub9c8\ucee4 \uc870\ud68c \uc694\uccad\uc758 \uc2dc\uc791\uc810: \uc57d `1875ms`\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/b7b8ff0c-2314-4e3f-a9f4-72c445636283)\\n\\n\ubaa8\ub4e0 \ub9c8\ucee4 \ub80c\ub354\ub9c1 \uc885\ub8cc \uc2dc\uc810: `2395ms`\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/d75c323e-5c04-42a2-ad3e-1d13ea52216e)\\n\\n\ucc98\uc74c\uc73c\ub85c \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub420 \ub54c\uae4c\uc9c0 \uc18c\uc694\ub41c \uc2dc\uac04: `519ms`\\n\\n\ubaa8\ub4e0 \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc5d0 \uc18c\uc694\ub41c \uc2dc\uac04: `519ms`\\n\\n### \uac1c\uc120 \uacb0\uacfc\\n\\n\ucc98\uc74c\uc73c\ub85c \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub418\ub294 \uc2dc\uc810\uc740 \ub450 \ubc29\uc2dd \ubaa8\ub450 \ube44\uc2b7\ud55c \uacb0\uacfc\ub97c \ubcf4\uc778\ub2e4. \ud558\uc9c0\ub9cc \uac1c\uc120 \ud6c4 \ubc29\uc2dd\uc740 \ud55c\ubc88\uc5d0 \ubaa8\ub4e0 \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub418\ub294 \ubc29\uc2dd\uc774\uace0, \uac1c\uc120 \uc774\uc804\uc758 \ubc29\uc2dd\uc740 \uc0b0\ubc1c\uc801\uc73c\ub85c \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub418\ub294 \ubc29\uc2dd\uc774\ubbc0\ub85c \uac1c\uc120 \ud6c4\uc758 \ubc29\uc2dd\uc5d0\uc11c \uc804\uccb4 \ub9c8\ucee4\ub97c \ub80c\ub354\ub9c1 \ud558\ub294 \uc2dc\uc810\uc774 \ud6e8\uc52c \ube68\ub77c\uc9c0\uac8c \ub418\uc5c8\ub2e4.\\n\\n\uacb0\uacfc\uc801\uc73c\ub85c \uc804\uccb4 \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub418\ub294 \uc18d\ub3c4 \uc57d `55.6%` \ub2e8\ucd95\ud558\uac8c \ub418\uc5c8\ub2e4. \uc774 \uacb0\uacfc\ub294 \ub9c8\ucee4\uac00 \ub298\uc5b4\ub0a0 \uc218\ub85d \ub354\uc6b1 \ucc28\uc774\uac00 \uadf9\uc801\uc73c\ub85c \ubc8c\uc5b4\uc9c8 \uac83\uc73c\ub85c \uc608\uc0c1\ub41c\ub2e4.\\n\\nbefore\\n\\n![before](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/28520ee3-2fa6-4110-b4e4-8a0bb706324e)\\n\\nafter\\n\\n![after](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/1b1521c6-d220-4140-bbe9-fff40051c6a2)"},{"id":"35","metadata":{"permalink":"/35","source":"@site/blog/2023-09-18-scheduling.mdx","title":"Scale-out \uc2dc Scheduling \uc911\ubcf5 \uc2e4\ud589 \ub9c9\uae30","description":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4","date":"2023-09-18T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 18\uc77c","tags":[{"label":"java","permalink":"/tags/java"}],"readingTime":8.89,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"35","title":"Scale-out \uc2dc Scheduling \uc911\ubcf5 \uc2e4\ud589 \ub9c9\uae30","authors":["boxster"],"tags":["java"]},"prevItem":{"title":"\ub9c8\ucee4 \ub80c\ub354\ub9c1 \ucd5c\uc801\ud654","permalink":"/36"},"nextItem":{"title":"\uce90\uc2dc\uc640 \uc774\ubd84 \ud0d0\uc0c9\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/34"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4\\n\\n## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\\n\uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \uc8fc\uae30\uc801\uc73c\ub85c \ucda9\uc804\uae30\uc758 \uc0c1\ud0dc\uc640 \uc815\ubcf4\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\uac70\ub098, \ud1b5\uacc4\ub97c \uc800\uc7a5\ud558\ub294 \uc2a4\ucf00\uc904\ub9c1 \uc791\uc5c5\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\uc9c0\uae08\uc758 \uc800\ud76c \uc11c\ubc84\ub294 \ub2e8\uc77c \uc11c\ubc84\ub85c \uad6c\uc131\ub418\uc5b4\uc788\uc5b4 \ubb38\uc81c\uac00 \uc5c6\uc9c0\ub9cc, \ub9cc\uc57d **\uc11c\ubc84\ub97c scale-out** \ud558\uac8c \ub41c\ub2e4\uba74 \uc5b4\ub5bb\uac8c \ub420\uae4c\uc694?\\n\\n**\ub611\uac19\uc740 schedule\uc774 \uc911\ubcf5**\ub418\uc5b4 \uc2e4\ud589\ub420 \uac83\uc785\ub2c8\ub2e4. \uadf8\ub807\ub2e4\uace0 \uc5b4\ub5a4 \uc11c\ubc84\ub294 schedule\uc744 \ub3d9\uc791\ud558\uc9c0 \uc54a\ub3c4\ub85d \ud558\uace0, \uc5b4\ub5a4 \uc11c\ubc84\ub294 schedule\uc744 \ub3d9\uc791\ud558\ub3c4\ub85d \ud55c\ub2e4\uba74 \uc2a4\ucf00\uc904\uc774 \ub3d9\uc791\ud558\ub294 \uc11c\ubc84\uac00 \ub2e4\uc6b4\ub41c\ub2e4\uba74 \ub3d9\uc791\ud558\ub294\\n\uc11c\ubc84\uc758 \ub2e4\uc6b4\ud0c0\uc784\ub9cc\ud07c \uc800\ud76c \uc11c\ubc84\uc758 \ub370\uc774\ud130\ub97c \ucd5c\uc2e0\ud654\ud560 \uc218 \uc5c6\uace0, \ucd5c\uc2e0\ud654\uac00 \uc911\uc694\ud55c \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \uc0ac\uc6a9\uc790\uc758 \ubd88\ub9cc\uc744 \ucd08\ub798\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uad6c\ud604\ud574\ubcf4\uae30\\n\\nSchedule \uc815\ubcf4\ub97c \uc5b4\ub5bb\uac8c \ub2e4\ub978 \ud658\uacbd\uc5d0\uc11c \uac19\uc774 \uacf5\uc720\ud558\uc5ec \uad00\ub9ac\ud560 \uc218 \uc788\uc744\uae4c\uc694?\\n\uac04\ub2e8\ud788 \uc0dd\uac01\ud558\uba74 Local \ud658\uacbd\uc774 \uc544\ub2cc, Global \ud658\uacbd\uc5d0\uc11c \uc815\ubcf4\ub97c \uad00\ub9ac\ud558\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c Schedule\uc758 \uc815\ubcf4\ub97c \uc800\uc7a5\ud560 \uc218 \uc788\ub294 \ud14c\uc774\ube14\uc744 \uc544\ub798\uc758 Entity \uc758 \ud544\ub4dc\uc640 \uac19\uc774 \uc0dd\uc131\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Entity\\npublic class ScheduleTask extends BaseEntity {\\n\\n @Id\\n private String id;\\n\\n private String jobName;\\n\\n @Enumerated(EnumType.STRING)\\n private JobStatus status;\\n}\\n```\\n\\n\uba3c\uc800 id\ub294 \ud574\ub2f9 \uc2a4\ucf00\uc904\uc744 \uad6c\ubd84\ud560 \uc218 \uc788\ub294 id\uc5ec\uc57c \ud560 \uac83\uc785\ub2c8\ub2e4. \uac00\uc7a5 \uc27d\uac8c \uc815\ud560 \uc218 \uc788\ub294 id\ub294 \uc2a4\ucf00\uc904\uc758 **job \uc774\ub984**\uacfc,\\nSchedule\uc73c\ub85c \ub4f1\ub85d\ud55c **\uc2dc\uac04**\uc744 \uc870\ud569\ud558\uc5ec \uc0dd\uc131\ud55c\ub2e4\uba74 unique\ud558\uace0 \ubd84\uc0b0 \ud658\uacbd\uc5d0\uc11c\ub3c4 \uc27d\uac8c \uad6c\ubd84\ud560 \uc218 \uc788\ub294 id\uac00 \ub420 \uac83 \uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc544\ub798\uc640 \uac19\uc740 Business Logic \uc788\ub2e4\uace0 \uac00\uc815\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n```java\\n@Service\\npublic class BusinessLogic {\\n\\n private final ApplicationEventPublisher applicationEventPublisher;\\n\\n @Scheduled(cron = \\"0/2 * * * * *\\")\\n public void complexJob() {\\n log.info(\\"\ubcf5\uc7a1\ud55c Job \uc2dc\uc791\\");\\n }\\n\\n @Scheduled(cron = \\"0/4 * * * * *\\")\\n public void moreComplexJob() {\\n log.info(\\"\uc880 \ub354 \ubcf5\uc7a1\ud55c Job \uc2dc\uc791\\");\\n try {\\n Thread.sleep(3000);\\n } catch (InterruptedException e) {\\n throw new RuntimeException(e);\\n }\\n }\\n}\\n```\\n\ud558\ub098\ub294 \ub9e4 2\ucd08\ub9c8\ub2e4 \uc2e4\ud589 \ud6c4 \ubc14\ub85c \uc885\ub8cc\ub418\uace0, \ud558\ub098\ub294 \ub9e4 4\ucd08\ub9c8\ub2e4 \uc2e4\ud589 \ud6c4 3\ucd08\uc758 \ub300\uae30\uc640 \uc885\ub8cc\ub418\ub294 \uba54\uc11c\ub4dc\uc785\ub2c8\ub2e4.\\n\uc774\ub7f0 \uc2a4\ucf00\uc904\uc740 \uc5b4\ub5bb\uac8c \ub3d9\uc791\ud560\uae4c\uc694? \uc800\ub294 \ub2f9\uc5f0\ud788 2\ucd08\uc640 4\ucd08\ub9c8\ub2e4 \ud574\ub2f9 \uba54\uc11c\ub4dc\uac00 \uc2e4\ud589\ub420 \uc904 \uc54c\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\ub85c\uadf8\ub97c \uc0b4\ud3b4\ubcf4\uba74 \uc544\ub798\uc640 \uac19\uc740 \uacb0\uacfc\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\\n![log](https://github.com/drunkenhw/comments/assets/106640954/5e275085-fce6-43ae-88ca-d3f9c484b6f3)\\n\ubcf5\uc7a1\ud55c job\uc774 2\ubc88 \uc2e4\ud589\ub420 \ub54c, \uc880 \ub354 \ubcf5\uc7a1\ud55c job\uc774 1\ubc88 \uc2e4\ud589\ub418\ub294 \uac78 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc608\uc0c1\ud588\ub358 \uacb0\uacfc\uc785\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc2e4\ud589\ub41c \uc2dc\uac04\uc744 \uc0b4\ud3b4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n![log-with-time](https://github.com/drunkenhw/comments/assets/106640954/abbe2c65-c26b-46ba-a4e3-fc0f4e5a6612)\\n\\n\ubd84\uba85 \ub9e4 2\ucd08\uc640 4\ucd08\ub9c8\ub2e4 \uc2e4\ud589\ud558\uae30 \ub54c\ubb38\uc5d0 \uc791\uc5c5 \uc2dc\uac04\uc774 2\uc758 \ubc30\uc218\uac00 \ub418\uc5b4\uc57c\ud560\ud150\ub370\\n\\n34, 36, 36, **39**, 40, 40, **43**, 44, 44, **47**\ucd08 \ub85c \uc810\uc810 \uc791\uc5c5\uc774 \ubc00\ub9ac\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc65c \uadf8\ub7f4\uae4c\uc694? \uc2a4\ud504\ub9c1 \uacf5\uc2dd \ubb38\uc11c\uc5d0\uc11c\ub294 \uc544\ub798\uc640 \uac19\uc774 \uc124\uba85\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n> A ThreadPoolTaskScheduler can also be auto-configured if need to be associated to scheduled task execution (using @EnableScheduling for instance). The thread pool uses one thread by default and its settings can be fine-tuned using the spring.task.scheduling namespace, as shown in the following example:\\n\\n\\n[\ucc38\uace0 - \uc2a4\ud504\ub9c1 \uacf5\uc2dd \ubb38\uc11c](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.task-execution-and-scheduling)\\n\\n\uc2a4\ud504\ub9c1\uc758 Schedule\uc740 Default\ub85c \ud558\ub098\uc758 \uc2f1\uae00 \uc2a4\ub808\ub4dc\uc5d0\uc11c \ub3d9\uc791\ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\uadf8\ub807\uae30 \ub54c\ubb38\uc5d0 \ub9e4\ubc88 \uc791\uc5c5\uc774 \ubc00\ub824 \uc6d0\ud558\ub294 \uc2dc\uac04\uc5d0 \ub3d9\uc791\ud558\uc9c0 \uc54a\ub294 \ud604\uc0c1\uc774 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc Schedule\uc744 \ubd84\uc0b0 \ud658\uacbd\uc5d0\uc11c \uad6c\ubd84\ud558\uae30 \uc704\ud574\uc11c\ub294 job\uc774 \uc2e4\ud589\ub41c \uc2dc\uac04\uc774 \uc911\uc694\ud558\uae30 \ub54c\ubb38\uc5d0 \uc774\ub807\uac8c \uc791\uc5c5\uc774 \ubc00\ub824\ubc84\ub9b0\ub2e4\uba74 \uad6c\ubd84\uc744 \ud560 \uc218 \uc5c6\uac8c \ub429\ub2c8\ub2e4.\\n\ub530\ub77c\uc11c Schedule Thread Pool Size\ub97c \ub298\ub9ac\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Configuration\\npublic class ScheduleConfig implements SchedulingConfigurer {\\n @Override\\n public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {\\n ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();\\n taskScheduler.setPoolSize(10);\\n taskScheduler.setThreadNamePrefix(\\"schedule-task-\\");\\n taskScheduler.initialize();\\n taskRegistrar.setTaskScheduler(taskScheduler);\\n }\\n}\\n```\\nSchedulingConfigurer \ub97c \uad6c\ud604\ud558\uc5ec Thread Pool size\ub97c \uc77c\ub2e8 10\uac1c\ub85c \uc815\uc758\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![success](https://github.com/drunkenhw/comments/assets/106640954/14b225bc-297e-4e7d-b196-23d779f635c0)\\n\uc2a4\ub808\ub4dc \ud480\uc744 \ub298\ub838\ub354\ub2c8 \uc704\uc640 \uac19\uc774 2\uc758 \ubc30\uc218\uc758 \uc2dc\uac04\uc5d0 \uc815\ud655\ud788 \uc791\ub3d9\uc774 \ub418\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774\ub807\uac8c \uc5ec\ub7ec \uc791\uc5c5\uc744 \ub3d9\uc2dc\uc5d0 \uc2e4\ud589\ub41c\ub2e4\uba74 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \ubcd1\ubaa9\ud604\uc0c1\uc774 \ubc1c\uc0dd\ub418\uc5b4 \uc624\ud788\ub824 \uc791\uc5c5\uc774 \ub354 \ub290\ub9ac\uac8c \ub05d\ub0a0 \uc218\ub3c4 \uc788\ub2e4\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \ud574\ub2f9 \ubd80\ubd84\uc758 \uc2e4\ud589\uc744 \uad00\ub9ac\ud558\ub294 \ud074\ub798\uc2a4\ub97c \uc0dd\uc131\ud558\uc5ec \ud574\ub2f9 \ud074\ub798\uc2a4\uc5d0\uc11c Schedule\uc758 \uc791\uc5c5\uc744 \uad00\ub9ac\ud558\ub3c4\ub85d \uad6c\ud604\ud588\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Service\\npublic class BusinessLogic {\\n\\n private final ApplicationEventPublisher applicationEventPublisher;\\n\\n @Scheduled(cron = \\"0/2 * * * * *\\")\\n public void complexJobSchedule() {\\n applicationEventPublisher.publishEvent(new SchedulingEvent(this::complexJob, \\"complexJob\\", LocalDateTime.now()));\\n }\\n\\n @Scheduled(cron = \\"0/4 * * * * *\\")\\n public void moreComplexJobSchedule() {\\n applicationEventPublisher.publishEvent(new SchedulingEvent(this::moreComplexJob, \\"moreComplexJob\\", LocalDateTime.now()));\\n }\\n}\\n```\\n\ub85c\uc9c1\uc774 \uc788\ub294 BusinessLogic \uc11c\ube44\uc2a4\uc5d0\uc11c \uc2a4\ucf00\uc904\uc758 \uc2dc\uac04\ub9c8\ub2e4 \uc2e4\ud589\ud574\uc57c\ud560 \uba54\uc11c\ub4dc\ub97c Event\ub85c \ubc1c\ud589\ud569\ub2c8\ub2e4.\\n\\n```java\\n@Component\\npublic class ScheduleService {\\n\\n private final ExecutorService executorService = Executors.newFixedThreadPool(1);\\n private final Queue scheduleTasks = new ConcurrentLinkedQueue<>();\\n private final AtomicBoolean isRunning = new AtomicBoolean(false);\\n\\n @EventListener\\n public void addTask(SchedulingEvent schedulingEvent) {\\n scheduleTasks.add(schedulingEvent);\\n }\\n\\n @Scheduled(cron = \\"0/1 * * * * *\\")\\n public void polling() {\\n if (!scheduleTasks.isEmpty() || isRunning.compareAndSet(false, true)) {\\n SchedulingEvent schedulingEvent = scheduleTasks.poll();\\n executorService.execute(() -> execute(schedulingEvent));\\n }\\n }\\n}\\n```\\n\uadf8\ub9ac\uace0 \uc704\uc640 \uac19\uc740 \uc2a4\ucf00\uc904\uc744 \uad00\ub9ac\ud558\ub294 \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 Schedule Event\ub97c \ubc1b\uc544 \uc2e4\ud589\ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ud074\ub798\uc2a4\uc5d0\uc11c\ub294 ThreadPool\uc744 \uc0c8\ub85c \uc0dd\uc131\ud558\uc5ec, schedule\uc758 \uc2a4\ub808\ub4dc\uc5d0 \uc601\ud5a5\uc744 \ubc1b\uc9c0 \uc54a\ub3c4\ub85d \uad6c\ud604\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 1\ucd08\ub9c8\ub2e4 \uc2e4\ud589\ub418\ub294 \uc2a4\ucf00\uc904\uc744 \ub9cc\ub4e4\uc5b4 queue\uc5d0 \uc791\uc5c5\uc774 \uc788\ub294\uc9c0, \ud604\uc7ac \uc791\uc5c5 \uc911\uc778\uc9c0 \ud655\uc778\ud558\uc5ec \uadf8\ub807\uc9c0 \uc54a\ub2e4\uba74 queue\uc5d0\uc11c \uc791\uc5c5\uc744 \uaebc\ub0b4 \uc2e4\ud589\ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uac70\uc758 \uad6c\ud604\uc774 \ub05d\ub098\uac11\ub2c8\ub2e4. \uc774\uc81c\ub294 \ud574\ub2f9 Schedule\uc758 \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud558\uace0, \uc791\uc5c5\uc774 \uc2e4\ud328\ud588\uc744 \uc2dc\uc5d0 \ub2e4\uc2dc \uc791\uc5c5\uc744 \ud558\uae30 \uc704\ud55c \uae30\ub2a5\ub9cc \uad6c\ud604\ud558\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Component\\npublic class ScheduleService {\\n\\n ...\\n\\n private void execute(SchedulingEvent schedulingEvent) {\\n String jobId = schedulingEvent.jobId();\\n LocalDateTime executionTime = schedulingEvent.executionTime();\\n\\n if (isJobInProgressOrDone(jobId)) {\\n log.info(\\"\uc791\uc5c5\uc774 \uc2e4\ud589\uc911\uc785\ub2c8\ub2e4. {} {}\\", executionTime, jobId);\\n return;\\n }\\n ScheduleTask entity = new ScheduleTask(jobId, executionTime, JobStatus.RUNNING);\\n scheduleTaskJdbcRepository.save(entity);\\n\\n try {\\n schedulingEvent.runnable().run();\\n scheduleTaskJdbcRepository.updateById(entity.getId(), JobStatus.DONE);\\n } catch (Exception e) {\\n log.error(\\"{} \uc791\uc5c5 \uc2e4\ud589 \uc911 \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\\", jobId);\\n scheduleTaskJdbcRepository.updateById(entity.getId(), JobStatus.ERROR);\\n tasks.add(schedulingEvent);\\n }\\n }\\n\\n private boolean isJobInProgressOrDone(String jobId) {\\n Optional taskOptional = scheduleTaskRepository.findById(jobId);\\n if (taskOptional.isPresent()) {\\n ScheduleTask scheduleTask = taskOptional.get();\\n return scheduleTask.getStatus() == JobStatus.RUNNING || scheduleTask.getStatus() == JobStatus.DONE;\\n }\\n return false;\\n }\\n}\\n```\\n\uc774 \ubd80\ubd84\uc740 \uac04\ub2e8\ud558\uac8c \uad6c\ud604\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc704\uc640 \uac19\uc774 \uc791\uc5c5\uc758 \uc2e4\ud589 \uc2dc\uac04\uacfc, job\uc758 \uc774\ub984\uc73c\ub85c \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \uc870\ud68c\ud558\uace0, \uc5c6\ub2e4\uba74 \uc791\uc5c5\uc744 \uc2e4\ud589\ud558\uace0\\n\uc788\ub2e4\uba74 \uc791\uc5c5\uc774 ERROR \uc778\uc9c0 \ud655\uc778\ud558\uc5ec \uc791\uc5c5\uc744 \uc2e4\ud589\ud574\uc8fc\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n![complete](https://github.com/drunkenhw/comments/assets/106640954/3ff855db-ff8e-4aa4-8b47-ed5b2ff6dd64)\\n\\n\uc704\uc640 \uac19\uc774 \ub450 \uac1c\uc758 \uc11c\ubc84\ub97c \ub3d9\uc2dc\uc5d0 \ub744\uc6e0\uc744 \ub54c\uc5d0\ub3c4 \uc2a4\ucf00\uc904\uc774 \uc798 \uc791\ub3d9\ud558\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\uc2a4\ucf00\uc904\uc744 \uc774\ub807\uac8c \uad6c\ud604\ud560 \uc218\ub3c4 \uc788\uc9c0\ub9cc \ud658\uacbd\uc774 \ub41c\ub2e4\uba74 Message Queue\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc5b4\ub5a8\uae4c\uc694?\\n\\n\\n\ud639\uc2dc \ud2c0\ub9b0 \ubd80\ubd84\uc774 \uc788\ub2e4\uba74 \uc9c0\uc801 \ubd80\ud0c1\ub4dc\ub9bd\ub2c8\ub2e4."},{"id":"34","metadata":{"permalink":"/34","source":"@site/blog/2023-09-17-caching.mdx","title":"\uce90\uc2dc\uc640 \uc774\ubd84 \ud0d0\uc0c9\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","description":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4","date":"2023-09-17T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 17\uc77c","tags":[{"label":"java","permalink":"/tags/java"}],"readingTime":12.495,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"34","title":"\uce90\uc2dc\uc640 \uc774\ubd84 \ud0d0\uc0c9\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","authors":["boxster"],"tags":["java"]},"prevItem":{"title":"Scale-out \uc2dc Scheduling \uc911\ubcf5 \uc2e4\ud589 \ub9c9\uae30","permalink":"/35"},"nextItem":{"title":"\ud63c\uc7a1\ub3c4 \uc870\ud68c \uc18d\ub3c4\ub97c \ud30c\ud2f0\uc154\ub2dd\uacfc \uc778\ub371\uc2a4\ub85c \uac1c\uc120\ud574\ubcf4\uae30","permalink":"/33"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4\\n\\n## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\uc774\uc804 \uae00\uc5d0\uc11c\ub3c4 \uacc4\uc18d \uc124\uba85\ud588\ub4ef\uc774 \uc870\ud68c \uc131\ub2a5\uc744 \ucd5c\ub300\ud55c \ube60\ub974\uac8c \ud558\ub294 \uac83\uc774 \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \ud575\uc2ec\uc774\ub77c\uace0 \uc0dd\uac01\ud558\uae30 \ub54c\ubb38\uc5d0 \uc9c0\uae08\ub3c4 \uc608\uc804\uc5d0 \ube44\ud574 \ube68\ub77c\uc84c\uc9c0\ub9cc \ub2e4\ub978 \uac1c\uc120\uc810\uc774 \ubcf4\uc5ec \uac1c\uc120\uc744 \ud558\uace0\uc790\ud569\ub2c8\ub2e4.\\n\\n[\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30 1 (\uc778\ub371\uc2a4)](https://car-ffeine.github.io/31)\\n\\n[\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30 2 (\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ubcf5\uc81c)](https://car-ffeine.github.io/32)\\n## \uacb0\ub860\\n\uacb0\ub860\ubd80\ud130 \ub9d0\uc500\ub4dc\ub9ac\uba74 \ub85c\uceec\uc5d0\uc11c \uce90\uc2f1\uc744 \uc801\uc6a9\ud55c \ud6c4 100\uba85\uc758 \uc0ac\uc6a9\uc790\uac00 \uc9c0\ub3c4\uc758 \ub370\uc774\ud130\ub97c \uc870\ud68c\ud560 \ub54c\ub97c \uae30\uc900\uc73c\ub85c\\n\\n**TPS** 78 -> 128\\n\\n**Response Time** 1236 ms -> 751 ms\\n\\n\uc57d **64%** \uc131\ub2a5\uc774 \uac1c\uc120 \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n*(\uc800\ubc88 \uc131\ub2a5 \ud14c\uc2a4\ud2b8\uc758 \uacb0\uacfc\uac00 \ub2e4\ub978 \uc774\uc720\ub294 \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1\uc774 \ubcc0\uacbd\ub418\uc5b4 \uc870\ud68c \ubc29\uc2dd\uc774 \ubc14\ub00c\uc5c8\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4. \uadf8\ub798\uc11c \uce90\uc2f1\uc744 \uc801\uc6a9\ud558\uae30\uc804, \ud55c \ud6c4 \ub97c \ube44\uad50\ud588\uc2b5\ub2c8\ub2e4.)*\\n\\n## Caching\\n\\n>In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.\\n\\n\uce90\uc2f1\uc740 \uc704\ud0a4 \ubc31\uacfc\uc5d0\uc11c \uc704\uc640 \uac19\uc774 \uc124\uba85\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uc989 \uba54\ubaa8\ub9ac\uc5d0 \ub370\uc774\ud130\ub97c \ubcf5\uc0ac\ubcf8\uc744 \uc62c\ub824 \uc880 \ub354 \ube60\ub974\uac8c \ub370\uc774\ud130\uc5d0 \uc811\uadfc\ud558\ub294 \ubc29\uc2dd\uc785\ub2c8\ub2e4.\\n\\n\uce90\uc2f1\uc758 \ub2e8\uc810\uc740 \uc218\uc815, \uc0bd\uc785, \uc0ad\uc81c\uac00 \ub418\uc5c8\uc744 \ub54c, \uad00\ub9ac \ud3ec\uc778\ud2b8\uac00 \ub450 \uad70\ub370\uac00 \ub41c\ub2e4\ub294 \uc810\uc785\ub2c8\ub2e4. \ub9cc\uc57d \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\ub9cc \uc0c8\ub85c\uc6b4 \uc815\ubcf4\ub97c \uc800\uc7a5\ud558\uace0, \uce90\uc2dc\uc5d0\ub294 \uc800\uc7a5\ud574\uc8fc\uc9c0 \uc54a\ub294\ub2e4\uba74 \uc0ac\uc6a9\uc790\ub294 \uadf8 \uc815\ubcf4\ub97c \ubcfc \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \uc801\uc6a9\ud55c \uc774\uc720\ub294 \ucda9\uc804\uae30\uc758 \ucda9\uc804 \uc0c1\ud0dc (\ucda9\uc804 \uc911, \ub300\uae30\uc911, \uace0\uc7a5)\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ucd5c\uc2e0\ud654\uac00 \ub418\uc5b4\uc57c\ud558\uc9c0\ub9cc, \ucda9\uc804\uc18c\uc758 \uc774\ub984\uc774\ub77c\ub358\uc9c0, \uc704\uce58, \ub2e4\ub978 \uc815\ubcf4\ub4e4\uc740 \uc27d\uac8c \ubcc0\ud558\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \ud574\ub2f9 \uc815\ubcf4\ub97c \uce90\uc2f1\ud55c\ub2e4\uba74 \uc88b\uc744 \uac83 \uac19\uc558\uc2b5\ub2c8\ub2e4.\\n\\n## \uce90\uc2f1 \uc801\uc6a9\ud558\uae30\\n\\n\uba3c\uc800 \uce90\uc2f1\uc744 \uc5b4\ub514\uc5d0\uc11c \ud558\ub294\uc9c0\ub3c4 \uc911\uc694\ud569\ub2c8\ub2e4. \ud06c\uac8c **\ub85c\uceec \uce90\uc2dc**\uc640 **\uae00\ub85c\ubc8c \uce90\uc2dc**\ub85c \ub098\ub20c \uc218 \uc788\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\uae00\ub85c\ubc8c \uce90\uc2dc\uc758 \uc7a5\uc810\uc740 \uc2a4\ucf00\uc77c \uc544\uc6c3\uc744 \ud588\uc744 \ub54c, \ubaa8\ub4e0 \uc11c\ubc84\uac00 \ub2e4 \uac19\uc740 \ub370\uc774\ud130\ub97c \ubc14\ub77c\ubcf4\uae30 \ub54c\ubb38\uc5d0 \ub370\uc774\ud130 \uc815\ud569\uc131\uc774 \uc88b\uc544\uc9d1\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc800\ud76c \uc11c\ube44\uc2a4\ub294 \ub2e8\uc77c \uc11c\ubc84\ub85c \uad6c\uc131\ub418\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0, \ub85c\uceec \uce90\uc2dc\ub97c \ud574\ub3c4 \ubb38\uc81c\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uae00\ub85c\ubc8c \uce90\uc2dc\ub97c \uc801\uc6a9\ud558\uae30 \uc704\ud574\uc11c\ub294 Redis\ub098 Memcached \uac19\uc740 \ub3c4\uad6c\ub97c \ubaa8\ub4e0 \ud300\uc6d0\uc774 \uc54c\uc544\uc57c\ud558\uc9c0\ub9cc \ub85c\uceec \uce90\uc2dc\ub294 \uadf8\ub807\uac8c \ud558\uc9c0 \uc54a\ub354\ub77c\ub3c4 \ud3b8\ud558\uac8c \uc801\uc6a9\ud560 \uc218 \uc788\ub2e4\ub294 \uc810\uc5d0\uc11c \ub85c\uceec\uc5d0 \uce90\uc2f1\ud558\ub294 \ubc29\ubc95\uc744 \uc801\uc6a9\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n### \uce90\uc2f1\ud560 \uc815\ubcf4 \uac00\uc838\uc624\uae30\\n\\n\uce90\uc2f1\uc744 \ud558\uae30 \uc704\ud574\uc11c\ub294 \uba3c\uc800 \uce90\uc2f1\ud560 \ub370\uc774\ud130\ub97c \uac00\uc838\uc640\uc57c\ud569\ub2c8\ub2e4. \uc800\ud76c \uc11c\ube44\uc2a4\ub294 \ucd9c\uc7a5 \ud639\uc740 \uc5ec\ud589\uc744 \uac00\ub294 \uc804\uae30\ucc28 \uc624\ub108\uac00 \ud575\uc2ec \ud398\ub974\uc18c\ub098\uc774\uae30 \ub54c\ubb38\uc5d0 \uc0ac\uc6a9\uc790\ub4e4\uc774 \ucc3e\ub294 \uc815\ubcf4\uc758 \uc704\uce58\ub294 \ubd88\ud2b9\uc815\ud569\ub2c8\ub2e4. \uc11c\uc6b8\uc5d0\uc11c \ub2e4\ub978 \uc9c0\ubc29\uc73c\ub85c \ucd9c\uc7a5\uc744 \uac00\ub294 \uacbd\uc6b0\ub3c4 \uc788\uc744 \uac83\uc774\uace0, \uc9c0\ubc29\uc5d0\uc11c \uc11c\uc6b8\uc5d0 \uac00\ub294 \uacbd\uc6b0\ub3c4 \uc788\uae30 \ub54c\ubb38\uc5d0, \ubaa8\ub4e0 \ub370\uc774\ud130\ub97c \uce90\uc2f1\ud574\uc57c\ud560 \uac83\uc774\ub77c \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158 \uc2e4\ud589 \uc2dc\uc5d0 \ubaa8\ub4e0 \ucda9\uc804\uc18c\ub97c \uce90\uc2f1\ud558\uae30\ub85c \uc120\ud0dd\ud588\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Configuration\\npublic class InitialStationCache implements ApplicationRunner {\\n\\n private final StationCacheRepository stationCacheRepository;\\n private final StationQueryRepository stationQueryRepository;\\n\\n @Override\\n public void run(ApplicationArguments args) {\\n log.info(\\"Initialize station cache\\");\\n List stations = stationQueryRepository.findAll();\\n stationCacheRepository.initialize(stations);\\n log.info(\\"Station cache initialized\\");\\n log.info(\\"Station cache size: {}\\", stations.size());\\n }\\n}\\n\\n```\\n\\n\uc704\uc640 \uac19\uc774 ApplicationRunner\ub97c \uad6c\ud604\ud558\uc5ec \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158 \uc2e4\ud589 \uc2dc \ubaa8\ub4e0 \ucda9\uc804\uc18c\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc624\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uc5ec\uae30\uc11c Entity\uc778 Station\uc744 \uac00\uc838\uc624\uc9c0 \uc54a\uc740 \uc774\uc720\ub294 \ud06c\uac8c \ub450\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n1. \uc9c0\ub3c4\ub85c \uc870\ud68c\ud558\ub294 \ubd80\ubd84\uc758 \uc131\ub2a5\uc744 \uac1c\uc120\ud558\uace0\uc790 \ud588\uc9c0\ub9cc, Entity\uc5d0\ub294 \uc9c0\ub3c4\ub97c \uc870\ud68c\ud560 \ub54c \ubd88\ud544\uc694\ud55c \uc815\ubcf4\ub3c4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uba54\ubaa8\ub9ac\uc0c1\uc758 \ub0ad\ube44\uac00 \uc0dd\uae38 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n2. Entity\ub97c \uce90\uc2f1\ud558\uac8c \ub41c\ub2e4\uba74 hibernate 1\ucc28 \uce90\uc2dc\uc5d0\ub3c4 \uc801\uc7ac\ub418\uace0, \ud799 \uba54\ubaa8\ub9ac\uc5d0\ub3c4 \uc801\uc7ac\ub418\ub294 \uc77c\uc774 \ubc1c\uc0dd\ud558\uc5ec \uba54\ubaa8\ub9ac\uc0c1 \ub0ad\ube44\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n### \ubc94\uc704 \uac80\uc0c9\ud558\uae30\\n\\n\ucda9\uc804\uc18c\uc758 \ub370\uc774\ud130\ub97c \uc870\ud68c\ud558\ub294 \uc870\uac74\uc740 \uc704\ub3c4, \uacbd\ub3c4\uc758 \ucd5c\uc18c, \ucd5c\ub300\uac12\uc744 \uae30\uc900\uc73c\ub85c \ub9cc\uc871\ud558\ub294 \ub370\uc774\ud130\ub97c \ubcf4\uc5ec\uc90d\ub2c8\ub2e4.\\n\uc544\ub798\uc640 \uac19\uc774 \uac04\ub2e8\ud788 \uc870\uac74\uc744 stream()\uc758 filter()\ub97c \uc0ac\uc6a9\ud574\uc11c \uad6c\ud604\ud588\uc2b5\ub2c8\ub2e4.\\n```java\\npublic class StationCacheRepository {\\n\\n private final List cachedStations;\\n\\n public List findByCoordinate(\\n BigDecimal minLatitude,\\n BigDecimal maxLatitude,\\n BigDecimal minLongitude,\\n BigDecimal maxLongitude\\n ) {\\n return cachedStations.stream()\\n .filter(it -> it.latitude().compareTo(minLatitude) >= 0 && it.latitude().compareTo(maxLatitude) <= 0)\\n .filter(it -> it.longitude().compareTo(minLongitude) >= 0 && it.longitude().compareTo(maxLongitude) <= 0)\\n .toList();\\n }\\n}\\n```\\n\ud558\uc9c0\ub9cc \ud574\ub2f9 \ubc29\ubc95\uc73c\ub85c \ub85c\uceec\uc5d0\uc11c \uc870\ud68c\ub97c \ud14c\uc2a4\ud2b8 \ud588\uc744 \ub54c \uce90\uc2dc\ub97c \uc801\uc6a9\ud55c \uac83\ubcf4\ub2e4 \ub354 \ub290\ub824\uc9c4 \uacb0\uacfc\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\uce90\uc2f1\uc744 \ud574\uc11c \ub370\uc774\ud130\ubca0\uc774\uc2a4\uae4c\uc9c0 \uc694\uccad\uc744 \ubcf4\ub0b4\uc9c0 \uc54a\ub294\ub370 \uc65c \ub354 \ub290\ub824\uc9c4 \uac83\uc77c\uae4c\uc694?\\n\\n\ub2f5\uc740 **\uc778\ub371\uc2a4** \uc600\uc2b5\ub2c8\ub2e4. Mysql \uc5d0\uc11c \uc778\ub371\uc2a4\ub294 B Tree\ub85c \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c\ub294 \uc704\ub3c4, \uacbd\ub3c4\ub85c \ubcf5\ud569 \uc778\ub371\uc2a4\uac00 \uc124\uc815\ub418\uc5b4 \uc788\uc5c8\uc9c0\ub9cc, \ud604\uc7ac \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158 \ub85c\uc9c1\uc5d0\ub294 \ud574\ub2f9 \ubd80\ubd84\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c filter\ub85c \uc21c\ud68c\ud558\ub294 \uc2dc\uac04\ubcf5\uc7a1\ub3c4\uac00 O(n)\uc774\uace0, \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c\ub294 O(log n)\uc774\uae30 \ub54c\ubb38\uc5d0 \ub354 \ub290\ub824\uc9c4 \uac83\uc785\ub2c8\ub2e4. \uadf8\ub807\ub2e4\uace0 \uc81c\uac00 \uc9c1\uc811 B tree \uc790\ub8cc\uad6c\uc870\ub97c \uc9c1\uc811 \uad6c\ud604\ud574\uc57c\ud560\uae4c\uc694?\\n\\n\ud604\uc7ac \ud574\ub2f9 \uc870\ud68c API\ub294 \uc704\ub3c4 \uacbd\ub3c4\ub85c \ubc94\uc704 \ud0d0\uc0c9\uc744 \ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uacb0\uad6d\uc5d4 station\uc758 \uc815\ubcf4\ub4e4\uc774 \uc704\ub3c4, \uacbd\ub3c4\ub85c \uc815\ub82c\ub9cc \ub418\uc5b4 \uc788\ub2e4\uba74 B tree\ub97c \uc9c1\uc811 \uad6c\ud604\ud558\uc9c0 \uc54a\ub354\ub77c\ub3c4 \uac19\uc740 \uc2dc\uac04\ubcf5\uc7a1\ub3c4 O(log n)\uc73c\ub85c \ud0d0\uc0c9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\ubb3c\ub860 B tree\uc640 \ub2e4\ub978 \ubd80\ubd84\uc740 \ud574\ub2f9 \ucda9\uc804\uc18c\uc758 \uc815\ud655\ud55c \uc704\ub3c4, \uacbd\ub3c4\ub85c \ub2e8\uc77c \uce7c\ub7fc\uc744 \uc870\ud68c\ud560 \ub54c\ub294 O(n)\uc774\uae30 \ub54c\ubb38\uc5d0 \uc774\ub7f0 \ubc29\ubc95\uc774 \ubb38\uc81c\uac00 \ub420 \uc218 \uc788\uc9c0\ub9cc, \ud574\ub2f9 \uce90\uc2dc \ub370\uc774\ud130\ub85c\ub294 \ubb34\uc870\uac74 \ubc94\uc704 \ud0d0\uc0c9\uc744 \ud558\uae30 \ub54c\ubb38\uc5d0, B tree\ub97c \uad6c\ud604\ud558\uc9c0 \uc54a\uace0 \uc774\ubd84 \ud0d0\uc0c9\uc73c\ub85c \uc870\ud68c\ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \ubcc0\uacbd\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n public void initialize(List stations) {\\n cachedStations.addAll(stations);\\n cachedStations.sort((o1, o2) -> {\\n int latitudeCompare = o1.latitude().compareTo(o2.latitude());\\n if (latitudeCompare == 0) {\\n return o1.longitude().compareTo(o2.longitude());\\n }\\n return latitudeCompare;\\n });\\n }\\n\\n private List findStations(BigDecimal minLatitude, BigDecimal maxLatitude, BigDecimal minLongitude, BigDecimal maxLongitude) {\\n int lowerBound = binarySearch(minLatitude, START_INDEX);\\n int upperBound = binarySearch(maxLatitude, lowerBound);\\n if (lowerBound == -1 || upperBound == -1) {\\n return Collections.emptyList();\\n }\\n return cachedStations.stream()\\n .skip(lowerBound)\\n .limit(upperBound - lowerBound)\\n .filter(station -> station.longitude().compareTo(minLongitude) >= 0 && station.longitude().compareTo(maxLongitude) <= 0)\\n .toList();\\n }\\n\\n private int binarySearch(BigDecimal latitude, int startIndex) {\\n int left = startIndex;\\n int right = cachedStations.size() - 1;\\n int result = -1;\\n while (left <= right) {\\n int middle = left + (right - left) / 2;\\n StationInfo middleStation = cachedStations.get(middle);\\n if (middleStation.latitude().compareTo(latitude) >= 0) {\\n result = middle;\\n right = middle - 1;\\n } else {\\n left = middle + 1;\\n }\\n }\\n return result;\\n }\\n\\n```\\n\\n\uba3c\uc800 \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc774 \uc2e4\ud589\ub420 \ub54c cache \ub370\uc774\ud130\ub97c \ucc3e\uc544 \uc800\uc7a5\ud558\ub294 \uac83 \ubfd0\ub9cc \uc544\ub2c8\ub77c, \uc704\ub3c4(Latitude)\ub97c \uae30\uc900\uc73c\ub85c \uc815\ub82c\ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc704\ub3c4\uc758 \ucd5c\uc18c, \ucd5c\ub300\uac12\uc758 \uc778\ub371\uc2a4\ub97c \uac00\uc7a5 \ud6a8\uc728\uc801\uc73c\ub85c \ucc3e\uc544\uc62c \uc218 \uc788\ub3c4\ub85d binary search\ub97c \ud558\ub294 \uba54\uc11c\ub4dc\ub97c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4. \uc774\ub807\uac8c \ud55c\ub2e4\uba74 O(log n) \uc73c\ub85c \uc704\ub3c4\uc758 \ucd5c\ub300 \ucd5c\uc18c \uc870\uac74\uc5d0 \ud3ec\ud568\ub418\ub294 \ubaa8\ub4e0 station\uc758 \uac12\uc744 \uc870\ud68c\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc870\ud68c\ud55c \ub370\uc774\ud130\ub4e4\uc758 \uac1c\uc218\ub9cc\ud07c filter\ub97c \ud1b5\ud574 \uacbd\ub3c4(longitude) \uac00 \ud3ec\ud568\ub418\ub294\uc9c0 \ud655\uc778\ud569\ub2c8\ub2e4. \ud574\ub2f9 \ubc29\uc2dd\uc758 \uad6c\ud604\uc740 B tree\uac00 \uc791\ub3d9\ud558\ub294 \ubc29\uc2dd\uacfc \uc720\uc0ac\ud560 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc774\ubd84 \ud0d0\uc0c9\uc744 \uc801\uc6a9\ud55c \uacb0\uacfc \ub85c\uceec\uc5d0\uc11c \uc751\ub2f5 \uc18d\ub3c4\uac00 120 ms -> 50 ~ 70 ms\ub85c \uc57d 2\ubc30 \ube68\ub77c\uc9c4 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uc2e4\uc2dc\uac04\uc774 \uc911\uc694\ud55c \ub370\uc774\ud130\ub294?\\n\\n\uc55e\uc11c \ub9d0\uc500\ub4dc\ub838\ub2e4\uc2dc\ud53c \uc9c0\ub3c4\ub85c \ucda9\uc804\uc18c\ub97c \uc870\ud68c\ud560 \ub54c, \ucda9\uc804\uc18c\uc758 \uc815\ubcf4\ub4e4\uc5d0\ub294 \ubc14\ub00c\uc9c0 \uc54a\ub294 \uc815\ubcf4\ubfd0\ub9cc \uc544\ub2c8\ub77c, \ucd5c\uc2e0\ud654\ud574\uc57c\ud558\ub294 \ucda9\uc804\uae30\uc758 \ud604\uc7ac \uc0c1\ud0dc \uc815\ubcf4\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub7ec\ud55c \uc815\ubcf4\ub4e4\uc740 \uce90\uc2f1\ud574\ub458 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ud558\ub354\ub77c\ub3c4, \uad00\ub9ac \ud3ec\uc778\ud2b8\uac00 \ub298\uc5b4\ub098\uae30 \ub54c\ubb38\uc5d0 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \uce90\uc2f1\ud574\ub454 \ucda9\uc804\uae30 id\ub85c \ucda9\uc804\uae30\uc758 \uc0c1\ud0dc\ub97c \ucc3e\uc544\uc640\uc11c \uc815\ubcf4\ub97c \ud569\uccd0 \ubc18\ud658\ud558\ub294 \uc2dd\uc73c\ub85c \ub9cc\ub4e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n```sql\\n select cs.station_id,\\n sum(case\\n when cs.charger_condition = \'STANDBY\' then 1\\n else 0\\n end)\\n from charger_status cs\\n where cs.station_id in (?, ?, ?, ?, ?, ?, ?)\\n group by cs.station_id\\n```\\n\uc704\uc640 \uac19\uc740 \ucffc\ub9ac\ub85c \ud574\ub2f9 \ucda9\uc804\uc18c\uc758 \ucd5c\uc2e0\ud654\ub41c \ucda9\uc804\uae30 \uc0c1\ud0dc\ub97c \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uce90\uc2f1\uc744 \ud558\uae30\uc804\uc5d0 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c \uc774\uc6a9\ud574 \ub370\uc774\ud130\ub97c \uac00\uc838\uc62c \ub54c\uc758 \ucffc\ub9ac\ub294 \uc544\ub798\uc640 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n```sql\\n select\\n distinct s.station_id\\n from\\n charge_station s\\n inner join\\n charger c\\n on (\\n c.station_id=s.station_id\\n )\\n where\\n s.latitude>=?\\n and s.latitude<=?\\n and s.longitude>=?\\n and s.longitude<=?\\n -------------------------------------------------\\n select\\n s.station_id,\\n s.station_name,\\n s.latitude,\\n s.longitude,\\n s.is_parking_free,\\n s.is_private,\\n sum(case\\n when cs.charger_condition=\'STANDBY\' then 1\\n else 0\\n end),\\n sum(case\\n when c.capacity>=50 then 1\\n else 0\\n end)\\n from\\n charge_station s\\n inner join\\n charger c\\n on (\\n c.station_id=s.station_id\\n )\\n inner join\\n charger_status cs\\n on (\\n c.station_id=cs.station_id\\n and c.charger_id=cs.charger_id\\n )\\n where\\n s.station_id in (\\n ?,?,?,?\\n )\\n group by\\n s.station_id\\n```\\n\uc6d0\ub798\ub294 \uc704\uc640 \uac19\uc774 \uc5ec\ub7ec\ubc88\uc758 Join\uc744 \ud558\uace0, 2\ubc88\uc758 \ucffc\ub9ac\uac00 \ub098\uac14\ub358 \ubc18\uba74 \uc9c0\uae08\uc740 join\uc744 \ud558\uc9c0\uc54a\ub294 \ud55c\ubc88\uc758 \uae54\ub054\ud55c \ucffc\ub9ac\ub85c \uac1c\uc120\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 **station \ud14c\uc774\ube14\uc758 \uc704\ub3c4, \uacbd\ub3c4\ub85c \ubc94\uc704 \ud0d0\uc0c9\uc744 \uc704\ud574 \uc0dd\uc131\ud588\ub358 index\ub3c4 \uc81c\uac70**\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4!\\n\\n## \uacb0\ub860\\n1. \uce90\uc2f1\ud560 \uc218 \uc788\ub294 \ubd80\ubd84\uc740 \ud558\ub294 \uac83\ub3c4 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4\\n2. \uc2dc\uac04 \ubcf5\uc7a1\ub3c4\ub97c \uacc4\uc0b0\ud574\ubd05\uc2dc\ub2e4.\\n3. \uc131\ub2a5 \uac1c\uc120 \uc7ac\ubc0c\uc2b5\ub2c8\ub2e4."},{"id":"33","metadata":{"permalink":"/33","source":"@site/blog/2023-09-11-congestion_speed_up.mdx","title":"\ud63c\uc7a1\ub3c4 \uc870\ud68c \uc18d\ub3c4\ub97c \ud30c\ud2f0\uc154\ub2dd\uacfc \uc778\ub371\uc2a4\ub85c \uac1c\uc120\ud574\ubcf4\uae30","description":"\uc548\ub155\ud558\uc138\uc694.","date":"2023-09-11T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 11\uc77c","tags":[{"label":"mysql","permalink":"/tags/mysql"}],"readingTime":6.19,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"33","title":"\ud63c\uc7a1\ub3c4 \uc870\ud68c \uc18d\ub3c4\ub97c \ud30c\ud2f0\uc154\ub2dd\uacfc \uc778\ub371\uc2a4\ub85c \uac1c\uc120\ud574\ubcf4\uae30","authors":["jay"],"tags":["mysql"]},"prevItem":{"title":"\uce90\uc2dc\uc640 \uc774\ubd84 \ud0d0\uc0c9\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/34"},"nextItem":{"title":"\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/32"}},"content":"\uc548\ub155\ud558\uc138\uc694.\\n\uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \ucda9\uc804\uc18c\uc758 \uc694\uc77c\uacfc \uc2dc\uac04\ub300 \ubcc4\ub85c \ucda9\uc804\uc18c \ud63c\uc7a1\ub3c4 \uc815\ubcf4\ub97c \uc81c\uacf5\uc744 \ucc28\ubcc4\uc801\uc778 \uae30\ub2a5\uc73c\ub85c \uc81c\uacf5\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \uad6c\ud604\ud558\uae30 \uc704\ud574\uc11c \uacf5\uacf5 \ub370\uc774\ud130\uc5d0\uc11c \uc815\ubcf4\ub97c \uc218\uc9d1\ud558\uace0\uc788\uc2b5\ub2c8\ub2e4.\\n\ud63c\uc7a1\ub3c4\ub97c \uc870\ud68c\ud558\uae30 \uc704\ud574\uc11c\ub294 \uc57d 23\ub9cc \uac74\uc758 \ucda9\uc804\uc18c * 7\uc77c * 24\uc2dc\uac04 = \uc57d 4000\ub9cc \uac74\uc758 \ub370\uc774\ud130 \uc911\uc5d0\uc11c \uc870\ud68c\ub97c \ud558\ub294 \ud615\uc2dd\uc73c\ub85c \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub108\ubb34 \ub9ce\uc740 \ub370\uc774\ud130\uac00 \uc788\ub2e4\ubcf4\ub2c8 \uc870\ud68c \uc18d\ub3c4\uac00 \ub9ce\uc774 \ub290\ub9b0\ub370\uc694.\\n\uc624\ub298\uc740 \uc774\ub97c \uc5b4\ub5bb\uac8c \uac1c\uc120\ud588\ub294\uc9c0 \uc791\uc131\ud574\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\ucc38\uace0\ub85c \ud574\ub2f9 \uae00\uc758 \uc131\ub2a5 \uce21\uc815\uc5d0 \uc774\uc6a9\ud55c \ub370\uc774\ud130\uc758 \uc218\ub294 \uc57d 20\ub9cc \uac74\uc785\ub2c8\ub2e4.\\n\\n---\\n\\n## \ubb38\uc81c \ud655\uc778\\n\uae30\uc874\uc758 \uc800\ud76c\ub294 \ub9ce\uc740 \uc591\uc758 \ub370\uc774\ud130\ub97c \uac10\ub2f9\ud558\uae30 \ud798\ub4e4\uc5b4\uc11c [\uc624\uc804, \uc624\ud6c4] \uc774\ub807\uac8c \ub450 \ubd80\ubd84\uc73c\ub85c \ub098\ub220\uc11c \ud63c\uc7a1\ub3c4\ub97c \uc870\ud68c\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc2e4\uc81c \ubc30\ud3ec\ub97c \ud558\uae30 \uc704\ud574\uc11c \ub354\uc774\uc0c1\uc740 \uc624\uc804 \uc624\ud6c4\ub85c \ub098\ub20c \uc218\uac00 \uc5c6\uc5c8\ub294\ub370\uc694.\\n\\n\uc815\uc0c1\uc801\uc778 \ub370\uc774\ud130\ub97c \uc81c\uacf5\ud558\uae30 \uc704\ud574\uc11c \uba3c\uc800 24\uc2dc\uac04 \uae30\uc900\uc73c\ub85c \ud63c\uc7a1\ub3c4\ub97c \uac31\uc2e0\ud558\ub3c4\ub85d \ub85c\uc9c1\ubd80\ud130 \ubc14\uafb8\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc704\uc640 \uac19\uc774 \ucf54\ub4dc\ub97c \ubc14\uafb8\ub2c8 \ubc14\ub85c \uc131\ub2a5\uc5d0 \ubb38\uc81c\uac00 \uc0dd\uacbc\uc2b5\ub2c8\ub2e4.\\n![img](https://postfiles.pstatic.net/MjAyMzA5MTFfMTA4/MDAxNjk0NDIwNjEzOTU3.Q1_sK5nRvBVbJ9w4bdYkofc0zX00TQmJUQPIqRQiofwg.FRujZOroDjWC4znh0pueWi84EAh9-LVKk17z2ojLi1Ig.PNG.sosow0212/image.png?type=w773)\\n\\n\uc704\uc758 \uc0ac\uc9c4\uacfc \uac19\uc774 slow-query\ub97c \ubd84\uc11d\ud574\ubcf4\uc558\uc2b5\ub2c8\ub2e4.\\n\ud63c\uc7a1\ub3c4 \uc5c5\ub370\uc774\ud2b8\uc5d0\ub3c4 \uc2dc\uac04\uc774 \uac78\ub9ac\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc9c0\ub9cc, \uc870\ud68c \uc2dc\uac04\uc740 \ucd5c\uc545\uc758 \uacbd\uc6b0 \uc57d 12\ubd84 \uc815\ub3c4\ub85c \uc0ac\uc6a9\uc790\ub4e4\uc774 \ubcfc \uc218\ub3c4 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud55c \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\uba3c\uc800 \uac00\uc7a5 \ud070 \ubb38\uc81c\ub294 \ub370\uc774\ud130\uac00 \ub9ce\uae30 \ub54c\ubb38\uc774\uace0, \ub450 \ubc88\uc9f8\ub85c\ub294 \ube44\ud6a8\uc728\uc801\uc778 API\ub85c \uc778\ud55c \ubb38\uc81c\uc785\ub2c8\ub2e4.\\n\\n\ud604\uc7ac \ud63c\uc7a1\ub3c4 \uc870\ud68c\uc2dc 0~23\uc2dc\uae4c\uc9c0 \ubaa8\ub4e0 \uc694\uc77c\uc758 \uae09\uc18d\uacfc \uc644\uc18d \ucda9\uc804\uae30\uc5d0 \ub300\ud55c \ud63c\uc7a1\ub3c4\ub97c \uac00\uc838\uc635\ub2c8\ub2e4.\\n\uad73\uc774 \uc774\ub7f4 \ud544\uc694 \uc5c6\uc774 \uc120\ud0dd\ud55c \uc694\uc77c\uc5d0\ub9cc \ud63c\uc7a1\ub3c4\ub97c \uac00\uc838\uc628\ub2e4\uba74 \ubd88\ud544\uc694\ud55c \uc870\ud68c\ub294 \uc5c6\uc744 \uac70\ub77c\uace0 \uc0dd\uac01\ud574\uc11c \uc77c\ubd80\ubd84 \ub9ac\ud329\ud1a0\ub9c1\uc744 \ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ucd94\uac00\uc801\uc73c\ub85c \ubc15\uc2a4\ud130\uac00 DB Replication\uc744 \uc801\uc6a9\ud574\uc11c, Update\ub85c \uc778\ud55c \uc18d\ub3c4 \uc800\ud558 \ud604\uc0c1\ub3c4 \ub9ce\uc774 \uc904\uc5b4\ub4e4 \uac83\uc744 \uae30\ub300\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n## \ubb38\uc81c \ud574\uacb0 \uacfc\uc815\\n\\n- \uba3c\uc800 \uae30\uc874 \ucf54\ub4dc\ub85c \uc870\ud68c\uc2dc\uc5d0 \uc18d\ub3c4\uac00 \uc5bc\ub9c8\ub098 \ub098\uc624\ub294\uc9c0 \ud655\uc778\uc744 \ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n![img](https://blogfiles.pstatic.net/MjAyMzA5MTFfMTgz/MDAxNjk0NDIwODU1MDg0.d2ig3CCgdHDwkz_7d4qyhVKM0PQ4MV8BcUwm9LjqAcAg.LdVGDSqRuArzM32ZD1tHbxsD2xG5pt8xrOrDwhR25wcg.PNG.sosow0212/image.png)\\n\uae30\uc874\uc758 \ubaa8\ub4e0 \ud63c\uc7a1\ub3c4\ub97c \ub4e4\uace0\uc624\ub294 \uacbd\uc6b0 \uc704\uc640 \uac19\uc774 536ms\uc758 \uc2dc\uac04\uc774 \uc18c\ubaa8\ub418\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://blogfiles.pstatic.net/MjAyMzA5MTFfMjA5/MDAxNjk0NDIwODk3NDE4.N3tGXL52LYr5Koc1Lwk0Tfhe3Apkao9BEI8waHIkwNgg.AUEcIoBUg8AtXMiZCc2P13Vb_DCeWnsoXH2-6acaClIg.PNG.sosow0212/image.png)\\n\uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 `day_of_week` \uc989 \ud63c\uc7a1\ub3c4\ub97c \ud655\uc778\ud558\uace0 \uc2f6\uc740 \uc694\uc77c\uc744 \ucd94\uac00\uc801\uc73c\ub85c \uc870\uac74\uc5d0 \uba85\uc2dc\ud574\uc8fc\ub2c8\\n148ms\ub85c \uc904\uc740 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n148ms\ub294 \uc544\uc9c1 \ud55c\ucc38 \ub290\ub9bd\ub2c8\ub2e4.\\n\\n\uba3c\uc800 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574\uc11c `DB Partitioning`\uc744 \uc801\uc6a9\ud588\uc2b5\ub2c8\ub2e4.\\n\\nDB Partitioning\uc5d0 \ub300\ud574 \uac04\ub2e8\ud558\uac8c \uc124\uba85\ud558\uc790\uba74 \ud070 \ud14c\uc774\ube14\uc744 \uc791\uc740 \ub2e8\uc704\ub85c \uad00\ub9ac\ud558\ub294 \uae30\ubc95\uc785\ub2c8\ub2e4.\\n\ud558\ub098\uc758 \ud14c\uc774\ube14\ub85c \ubcf4\uc774\uc9c0\ub9cc \uc774\ub97c \uc801\uc6a9\ud558\uba74 \uc2e4\uc81c\ub85c \uc5ec\ub7ec \uac1c\uc758 \ud14c\uc774\ube14\ub85c \ubd84\ub9ac\ud574\uc11c \uad00\ub9ac\ud558\ub294 \uae30\ubc95\uc774\uace0, \uc774\ub97c \ud1b5\ud574\uc11c \uc870\ud68c \ubc0f \uc5c5\ub370\uc774\ud2b8 \ucffc\ub9ac \uc131\ub2a5\uc774 \uac1c\uc120\ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c \ud300\uc740 List partitioning\uc744 \uc801\uc6a9\ud574\uc11c `day_of_week(\uc694\uc77c)`\uc744 \uae30\uc900\uc73c\ub85c \ud30c\ud2f0\uc154\ub2dd\uc744 \ud588\uc2b5\ub2c8\ub2e4.\\n![img](https://blogfiles.pstatic.net/MjAyMzA5MTFfMTE0/MDAxNjk0NDIxMzg1NTMx.Q4VBItbFdityCKdRFYqpC1qVtoi81RRqcmysYMh-9xog.d8MIYW-tatGXoaxCJ-o6vS5wydEk1yQVQTtmmZvooFIg.PNG.sosow0212/image.png)\\n\uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 day_of_week\ub97c \uae30\uc900\uc73c\ub85c \ud30c\ud2f0\uc154\ub2dd\uc744 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://blogfiles.pstatic.net/MjAyMzA5MTFfMjA5/MDAxNjk0NDIxNDM3MTI2.QXclZKmnwVTcYrkR95yPJV3vxCCzcaisaWj29WGxFucg.CO0SafuQLRmWPzAs9-9ForUnT1fjcqxXBRmX1UmB-b8g.PNG.sosow0212/image.png)\\nList Partitioning\uc744 \uc801\uc6a9\ud558\uace0 \uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 \uc870\ud68c \ucffc\ub9ac\ub97c \ub2e4\uc2dc \ub0a0\ub824\ubcf4\uba74, `partitions = p_friday` \ub85c \uc798 \ub098\ub258\uc5b4\uc9c4 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud30c\ud2f0\uc154\ub2dd \uc791\uc5c5\uc774 \uc798 \ub418\uc5c8\uc73c\ub2c8 \uc774\uc81c API\uc5d0\uc11c \uc694\uc77c \ubcc4 \ud63c\uc7a1\ub3c4 \uc870\ud68c\ub85c \ubc14\uafd4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\uba3c\uc800 \ucffc\ub9ac\ub97c \ubcc0\uacbd\ud558\uace0 \ucffc\ub9ac\ub97c \ud655\uc778\ud574\ubcf4\ub2c8 \ub2e4\uc74c\uacfc \uac19\uc774 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://postfiles.pstatic.net/MjAyMzA5MTFfMjQ5/MDAxNjk0NDIxNTcwOTg3.mgx-mdBa6J6k8erhiksOOkzrMzOMmCLX7iuRPZf7RNEg.ALwxez4qUVHB1wlIr9zsZovCBlxoIsmgCa4wNv-7t_4g.PNG.sosow0212/image.png?type=w773)\\n\uc704\uc640 \uac19\uc740 \uc870\ud68c \ucffc\ub9ac\uac00 \ub098\uc654\uc73c\ubbc0\ub85c \uc778\ub371\uc2a4\ub97c \uc544\ub798\uc640 \uac19\uc774 `station_id, day_of_week`\uc5d0 \uac78\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://postfiles.pstatic.net/MjAyMzA5MTFfMjgy/MDAxNjk0NDIxNjI2NDAz.XqGsab-JR_fQaIZhCYMiKy5r3cn85wFLwUNlmCo1Gqwg.a26_N5lnwzXX6z0JRqn35u8pGcj1TAa2nDamgRyOYjUg.PNG.sosow0212/image.png?type=w773)\\n\uc704 \uc2e4\ud589 \uc18d\ub3c4\uc5d0\uc11c execution time\uc744 \ud655\uc778\ud574\ubcf4\uba74 \uc778\ub371\uc2a4\ub97c \uac78\uace0 `134ms -> 5ms`\ub85c \uc131\ub2a5\uc774 \ub9ce\uc774 \uac1c\uc120 \ub418\uc5c8\uc74c\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://postfiles.pstatic.net/MjAyMzA5MTFfMjI3/MDAxNjk0NDIxNjc5NDIw.kbfBLWKeY70QFeByKN5xVX1WhFSpFZbJnFkx9l0zyrQg.Wv0QU9W6Fqjfr8eyyLT2MyttjDKzN2cdrItGH7CDLPYg.PNG.sosow0212/image.png?type=w773)\\n\uc2e4\ud589 \uacc4\ud68d\ub3c4 \uc758\ub3c4\ud55c\ub300\ub85c \uc798 \ub098\uc624\ub294 \uac83\uc744 \ubcf4\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n## \uc815\ub9ac\\n\\n1. DB Partitioning - (day_of_week : \uc694\uc77c)\uc744 \uae30\uc900\uc73c\ub85c \ud30c\ud2f0\uc154\ub2dd\\n2. \uc870\ud68c \ucffc\ub9ac\uc5d0 \ub9de\uac8c \uc778\ub371\uc2a4 \uc124\uc815\\n3. API \uc218\uc815 (\ubaa8\ub4e0 \uc694\uc77c\uc758 \ud63c\uc7a1\ub3c4 \uc870\ud68c -> \ud574\ub2f9 \uc694\uc77c\uc758 \ud63c\uc7a1\ub3c4 \uc870\ud68c)\\n\\n\uacb0\uacfc\uc801\uc73c\ub85c \uae30\uc874 \ud63c\uc7a1\ub3c4 \uc870\ud68c\uc2dc 511ms\uac00 \ub098\uc654\uc73c\ub098, \uc694\uc77c \ubcc4 \uc870\ud68c \ubc0f \ud30c\ud2f0\uc154\ub2dd & \uc778\ub371\uc2a4\ub97c \uc801\uc6a9\ud558\uace0 execution time = 5ms\ub85c \uac1c\uc120"},{"id":"32","metadata":{"permalink":"/32","source":"@site/blog/2023-09-11-database-replication.mdx","title":"\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","description":"\uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720","date":"2023-09-11T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 11\uc77c","tags":[{"label":"mysql","permalink":"/tags/mysql"}],"readingTime":23.75,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"32","title":"\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","authors":["boxster"],"tags":["mysql"]},"prevItem":{"title":"\ud63c\uc7a1\ub3c4 \uc870\ud68c \uc18d\ub3c4\ub97c \ud30c\ud2f0\uc154\ub2dd\uacfc \uc778\ub371\uc2a4\ub85c \uac1c\uc120\ud574\ubcf4\uae30","permalink":"/33"},"nextItem":{"title":"\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/31"}},"content":"## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\\n\uba3c\uc800 \uc774 \uae00\uc744 \uc4f0\uac8c \ub41c \uacc4\uae30\ub97c \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4. \uc9c0\ub09c \uae00\uc5d0\uc11c \uc124\uba85\ud588\ub4ef\uc774 \uc800\ud76c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c\ub294 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uac00 \uc2e4\ud589\ub418\uace0 \uc788\ub294 \uc11c\ubc84\uc758 cpu \uc0ac\uc6a9\ub960\uc774 100%\uac00 \ub418\ub294 \ubb38\uc81c\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uc774 \ubd80\ubd84\uc5d0 \ub300\ud574\uc11c\ub294 \uc870\ud68c \uc131\ub2a5\uc744 \ub192\ud600 \uc5b4\ub290\uc815\ub3c4 \ud574\uacb0\ud558\uace0\uc790 \ud588\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc870\ud68c\uac00 \uc544\ub2cc \ub9ce\uc740 \ub370\uc774\ud130\ub97c \uc77c\uc815\ud55c \uc8fc\uae30\ub85c \uc5c5\ub370\uc774\ud2b8 \ud574\uc918\uc57c\ud558\ub294 \ub85c\uc9c1\ub3c4 \ud3ec\ud568\ub418\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc5c5\ub370\uc774\ud2b8\ub97c \ud560 \ub54c \uc870\ud68c\ub97c \ud558\uac8c \ub41c\ub2e4\uba74 cpu \uc0ac\uc6a9\ub960\uc740 \ube44\uc2b7\ud560 \uac83\uc785\ub2c8\ub2e4. \uc774 \ubd80\ubd84\uc744 \ud574\uacb0\ud558\uace0\uc790 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\uacb0\ub860\ubd80\ud130 \ub9d0\uc500\ub4dc\ub9ac\uba74 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc801\uc6a9\ud55c \ud6c4 \uc131\ub2a5\uc774 \ub208\uc5d0 \ub744\uac8c \uc88b\uc544\uc84c\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ubd80\ubd84\uc740 \ub2e4\uc74c \ud3ec\uc2a4\ud305\uc5d0 \uc791\uc131\ud558\uaca0\uc2b5\ub2c8\ub2e4\\n100\uba85\uc758 \uc0ac\uc6a9\uc790\uac00 \uc9c0\ub3c4\uc758 \ub370\uc774\ud130\ub97c \uc870\ud68c\ud560 \ub54c\ub97c \uae30\uc900\uc73c\ub85c\\n\\n**TPS** 179 -> 366\\n\\n**Response** Time 550 ms -> 271 ms\\n\\n\uc57d 2\ubc30 \uac00\ub7c9 \uc131\ub2a5\uc774 \ud5a5\uc0c1\ub41c \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n# \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc774\ub780?\\n\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc774\ub780 \ud558\ub098\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \ub2e4\ub978 \ud558\ub098 \uc774\uc0c1\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub85c \ub370\uc774\ud130\uc758 \ubcf5\uc81c \ub610\ub294 \ubcf5\uc0ac\ub97c \uc218\ud589\ud558\ub294 \ud504\ub85c\uc138\uc2a4 \ub610\ub294 \uae30\uc220\uc785\ub2c8\ub2e4. \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc740 \uc8fc\ub85c \ub2e4\uc74c\uacfc \uac19\uc740 \ubaa9\uc801\uc73c\ub85c \uc0ac\uc6a9\ub429\ub2c8\ub2e4\\n\\n1. **\uace0\uac00\uc6a9\uc131**:\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc11c\ubc84\uc758 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c, \ub808\ud50c\ub9ac\uce74 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2dc\uc2a4\ud15c\uc744 \uacc4\uc18d \uc6b4\uc601\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub807\uac8c \ud558\uba74 \uc11c\ube44\uc2a4 \uc911\ub2e8 \uc2dc\uac04\uc744 \ucd5c\uc18c\ud654\ud558\uace0 \ube44\uc988\ub2c8\uc2a4 \uc5f0\uc18d\uc131\uc744 \uc720\uc9c0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n2. **\uc131\ub2a5 \ud5a5\uc0c1** :\\n\ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc0ac\uc6a9\ud558\uba74 \uc77d\uae30 \uc791\uc5c5\uc744 \ubd84\uc0b0\uc2dc\ud0ac \uc218 \uc788\uc73c\ubbc0\ub85c \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc11c\ubc84\uc758 \uc77d\uae30 \ubd80\ud558\ub97c \uc904\uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub97c \ud1b5\ud574 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc131\ub2a5\uc744 \ud5a5\uc0c1\uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n3. **\uc9c0\uc5ed\uc801 \ubd84\uc0b0** :\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \ud1b5\ud574 \ub370\uc774\ud130\ub97c \uc9c0\ub9ac\uc801\uc73c\ub85c \ubd84\uc0b0\uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub807\uac8c \ud558\uba74 \uc9c0\uc5ed\uc801\uc778 \uc0ac\uc6a9\uc790 \ub610\ub294 \uc751\uc6a9 \ud504\ub85c\uadf8\ub7a8\uc5d0 \ube60\ub974\uac8c \ub370\uc774\ud130\ub97c \uc81c\uacf5\ud560 \uc218 \uc788\uc73c\uba70, \uc9c0\uc5ed\uc801\uc778 \uaddc\uc815 \uc900\uc218 \uc694\uad6c\uc0ac\ud56d\uc744 \ucda9\uc871\uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n4. **\ubc31\uc5c5\uacfc \ubcf5\uad6c** :\\n\ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc8fc \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \ubc31\uc5c5\uc744 \uc0dd\uc131\ud558\uace0, \uc774\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc7a5\uc560 \ubcf5\uad6c\ub97c \uc218\ud589\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc8fc \ub370\uc774\ud130\ubca0\uc774\uc2a4\uac00 \uc190\uc0c1\ub418\uc5c8\uc744 \ub54c \ubc31\uc5c5 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2dc\uc2a4\ud15c\uc744 \ube60\ub974\uac8c \ubcf5\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n5. **\ub370\uc774\ud130 \ubd84\uc11d \ubc0f \ubcf4\uace0** :\\n\ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub370\uc774\ud130\ub97c \ub2e4\ub978 \ubd84\uc11d \ub610\ub294 \ubcf4\uace0 \ub3c4\uad6c\ub85c \ubcf5\uc0ac\ud558\uc5ec \ub370\uc774\ud130 \uc6e8\uc5b4\ud558\uc6b0\uc2a4 \ub610\ub294 \ubd84\uc11d \uc2dc\uc2a4\ud15c\uc5d0\uc11c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c \ud300\uc5d0\uc11c \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc801\uc6a9\ud55c \uac00\uc7a5 \ud070 \uc774\uc720\ub294 \uc131\ub2a5 \ud5a5\uc0c1\uc785\ub2c8\ub2e4. \uc544\ubb34\ub798\ub3c4 \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \uc77d\uae30 \uc791\uc5c5\uacfc \uc4f0\uae30 \uc791\uc5c5\uc774 \ub458 \ub2e4 \ube48\ubc88\ud558\uac8c \uc77c\uc5b4\ub098\uace0, \ud2b9\ud788 \uc4f0\uae30 \uc791\uc5c5\uc5d0 \ub9ce\uc740 \uc5f0\uc0b0\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc0ac\uc6a9\uc790\uc5d0\uac8c \ucd5c\uc2e0\uc758 \ub370\uc774\ud130\ub97c \uc81c\uacf5\ud558\uace0\uc790 \uc4f0\uae30 \uc791\uc5c5\uc744 \uc790\uc8fc\ud558\uc5ec \ub370\uc774\ud130\ub97c \ucd5c\uc2e0\ud654\ud558\ub354\ub77c\ub3c4, \uc77d\uae30 \uc791\uc5c5\uc774 \ub290\ub824\uc9c0\uba74 \uc544\ubb34\ub3c4 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc744 \uac83\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc774\ub807\uac8c \uc11c\ubc84\ub97c \uc5ec\ub7ec \ub300 \ub450\uc5b4 \ud558\ub098\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc11c\ubc84\uac00 \ubc1b\ub294 \ubd80\ud558\ub97c \ubd84\uc0b0\uc2dc\ud0a8\ub2e4\uba74 \uc131\ub2a5\uc774 \ud5a5\uc0c1 \ub420 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ub450\ubc88\uc9f8\ub85c\ub294 \uace0\uac00\uc6a9\uc131\uc785\ub2c8\ub2e4. \ud604\uc7ac \uc800\ud76c\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub294 \ud558\ub098\uc758 \uc11c\ubc84\ub85c SPOF \ubb38\uc81c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc801\uc6a9\ud558\uc5ec \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c \ubd84\uc0b0\ud55c\ub2e4\uba74 \ud558\ub098\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uac00 \uc7a5\uc560\uac00 \uc0dd\uaca8 \uc911\uc9c0\uac00 \ub418\ub354\ub77c\ub3c4, \ub2e4\ub978 \uc11c\ubc84\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub85c \uc11c\ube44\uc2a4\ub97c \uc774\uc5b4\ub098\uac08 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n# \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ubcf5\uc81c \ubc29\uc2dd\\n\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ubcf5\uc81c \ubc29\uc2dd\uc740 \ud06c\uac8c \ub450\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4. **Binary Log\ub85c \ubcf5\uc81c\ud558\ub294 \ubc29\uc2dd**\uacfc **GTID(Global Transaction Id)\ub97c \ud1b5\ud574 \ubcf5\uc81c\ub97c \ud558\ub294 \ubc29\uc2dd**\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## Binary log \ubcf5\uc81c \ubc29\uc2dd\\n\uba3c\uc800 Binary Log \ub294 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \uc218\ud589\ud55c \ucffc\ub9ac (\uc0ac\uc6a9\uc790 \ucd94\uac00, \uc778\ub371\uc2a4 \ucd94\uac00, Update, Insert, Delete \ub4f1 ) \ubaa8\ub4e0 \uc815\ubcf4\ub97c Binary Log\uc5d0 \uae30\ub85d\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ud574\ub2f9 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\uc5d0\ub294 \uc774\ubca4\ud2b8\ub9c8\ub2e4 Mysql \uc11c\ubc84\uc758 \uace0\uc720\ud55c Server id\ub97c \uac00\uc9c0\uace0 \uc788\ub294\ub370, \ud574\ub2f9 Id\uac00 \uac19\uc740 \uc11c\ubc84\uc5d0\uc11c\ub294 \ud574\ub2f9 \uc774\ubca4\ud2b8\ub97c \uc790\uc2e0\uc774 \ubc1c\uc0dd\uc2dc\ud0a8 \uc774\ubca4\ud2b8\ub85c \uac04\uc8fc\ud558\uace0 \uc801\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uadf8\ub7ec\ubbc0\ub85c \uac01\uac01\uc758 \uace0\uc720\ud55c server id\ub97c \uc124\uc815\ud574\uc918\uc57c \ud569\ub2c8\ub2e4.\\n\uc774 **\ubc14\uc774\ub108\ub9ac \ub85c\uadf8 \ud30c\uc77c\uc758 \uc704\uce58\uc640 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8 \ud30c\uc77c\uba85**\uc744 \ud1b5\ud574 Replica \uc11c\ubc84\ub294 Source \uc11c\ubc84\uc758 \uc774\ubca4\ud2b8\ub97c \uc801\uc6a9\ud569\ub2c8\ub2e4\\n## GTID \ubcf5\uc81c \ubc29\uc2dd\\nMysql 5.5 \ubc84\uc804 \uc774\uc0c1\ubd80\ud130\ub294 GTID \uae30\ubc18 \ubcf5\uc81c\ub3c4 \uac00\ub2a5\ud558\uac8c \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4 GTID\ub294 source id\uc640 transaction id\uac00 \uc870\ud569\ub41c \ubc29\uc2dd\uc73c\ub85c \uc0dd\uc131\ub429\ub2c8\ub2e4. source id\ub294 \ud2b8\ub79c\uc7ad\uc158\uc774 \ubc1c\uc0dd\ud55c \uc18c\uc2a4 \uc11c\ubc84\ub97c \uc2dd\ubcc4\ud558\uae30 \uc704\ud55c \uac12\uc73c\ub85c server\uc758 uuid \uc785\ub2c8\ub2e4.\\n```sql\\n+--------------------------------------+\\n| source_uuid |\\n+--------------------------------------+\\n| c3a2296b-31a2-11ee-b887-02a8cf0173ac |\\n+--------------------------------------+\\n```\\n\uc774\ub7ec\ud55c GTID\ub97c \uae30\ubc18\uc73c\ub85c Source \uc11c\ubc84\ub97c \uad6c\ubd84\ud558\uace0 Binary Log \ud30c\uc77c\uc5d0 \uae30\ub85d\ub41c GTID\ub97c \ud655\uc778\ud558\uc5ec \ub9c8\uc9c0\ub9c9\uc5d0 \uc801\uc6a9\ud55c \uc774\ubca4\ud2b8\ub97c \ud655\uc778\ud558\uace0, \uc801\uc6a9\ud558\uc9c0 \uc54a\uc740 \uc774\ubca4\ud2b8\ub97c \uc21c\ucc28\ub300\ub85c \uc2e4\ud589\uc2dc\ucf1c \ubcf5\uc81c\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \ub450\uac00\uc9c0 \ubc29\ubc95 \uc911 \uc800\ud76c\ub294 GTID \ubc29\uc2dd\uc758 \ubcf5\uc81c\ub97c \uc120\ud0dd\ud588\uc2b5\ub2c8\ub2e4. \uc774\uc720\ub294 \uac04\ub2e8\ud569\ub2c8\ub2e4.\\n```mermaid\\ngraph TD\\n Source[Source Server: BinaryLog10] --\x3e Replica1[Replica1: BinaryLog10]\\n Source[Source Server: BinaryLog10] --\x3e Replica2[Replica2: BinaryLog9]\\n```\\n\uc774\ub7f0 \ubc29\uc2dd\uc73c\ub85c \ud1a0\ud3f4\ub85c\uc9c0\ub97c \uad6c\uc131\ud588\ub2e4\uace0 \uac00\uc815\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4. Source \uc11c\ubc84\uc5d0\uc11c\ub294 Binary Log 10\ubc88 \ud30c\uc77c\uae4c\uc9c0 \uc774\ubca4\ud2b8\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 Replica1 \uc5d0\uc11c\ub294 Source \uc11c\ubc84\uc758 \uc774\ubca4\ud2b8\uac00 \ucd5c\uc2e0\ud654 \ub418\uc5b4 \uc788\uc9c0\ub9cc, Replica2 \uc11c\ubc84\ub294 \uc544\uc9c1 \ucd5c\uc2e0\ud654\uac00 \ub418\uc9c0 \uc54a\uc740 \uc0c1\ud669\uc785\ub2c8\ub2e4. \uc774 \uc0c1\ud669\uc5d0\uc11c Source Server\uc5d0 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud558\uc5ec \uc11c\ubc84\uac00 \uc911\ub2e8 \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 Replica1 \uc11c\ubc84\ub97c Source \uc11c\ubc84\ub85c \uc2b9\uaca9\ud569\ub2c8\ub2e4. \uc774\ub807\uac8c \ub41c\ub2e4\uba74 Replica1 \uc11c\ubc84\uc5d0\uc11c \ubaa8\ub4e0 \ucffc\ub9ac\uc758 \uc694\uccad\uc774 \ub4e4\uc5b4\uc624\uac8c \ub429\ub2c8\ub2e4. BinaryLog10\uc774\ub77c\ub294 \ud30c\uc77c\uc758 \uc704\uce58\uc640 \ud30c\uc77c\uc744 \ucc3e\uc744 \ubc29\ubc95\uc774 \uc5c6\uae30 \ub54c\ubb38\uc5d0 Source\uc11c\ubc84\uac00 \ubcf5\uad6c\ub418\uc9c0 \uc54a\ub294 \uc774\uc0c1 \ud639\uc740 Replica 1 \uc11c\ubc84\uc758 Relay Log\uac00 \ub0a8\uc544\uc788\uc9c0 \uc54a\ub294 \uc774\uc0c1 Replica2 \uc11c\ubc84\ub294 \uc808\ub300 \ucd5c\uc2e0\ud654\ub420 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774\ub7f0 \uc2dd\uc758 \ubc29\uc2dd\uc774\ub77c\uba74 Source \uc11c\ubc84\uac00 \uc911\ub2e8\ub418\uc5c8\uc744 \ub54c \ub2e4\ub978 \uc11c\ubc84\uac00 \ub3d9\uc791\ud558\uae30 \ub54c\ubb38\uc5d0 \uace0\uac00\uc6a9\uc131 \ubb38\uc81c\ub294 \ud574\uacb0\ub41c \uac83 \uac19\uc9c0\ub9cc, Replica2 \uc11c\ubc84\ub294 \uc544\ubb34 \uc77c\ub3c4 \ud558\uc9c0\uc54a\uace0 \ub0a8\uc544\uc788\ub294 \uc11c\ubc84, \uc989 Source \uc11c\ubc84 \ud558\ub098\uac00 \uc911\ub2e8\ub418\uc5c8\uc73c\ub098 **2\ub300\uc758 \uc11c\ubc84\uac00 \uc911\ub2e8\ub41c \uac83**\uacfc \ub9c8\ucc2c\uac00\uc9c0\uc785\ub2c8\ub2e4.\\n\\n\uc774\ub7ec\ud55c \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574 GTID\uac00 \ub4f1\uc7a5\ud588\uc2b5\ub2c8\ub2e4. GTID \ubc29\uc2dd\uc740 Binary Log\uc758 \uc704\uce58\uc640 \ud30c\uc77c\uba85\uc774 \ud544\uc694\ud55c \uac83\uc774 \uc544\ub2cc \ub2e4\uc74c \uc774\ubca4\ud2b8\uc758 GTID\ub9cc \uc788\ub2e4\uba74 \ud574\ub2f9 \uc774\ubca4\ud2b8\ub97c \ubc14\ub85c \uc801\uc6a9\ud560 \uc218 \uc788\ub2e4\ub294 \uc810\uc785\ub2c8\ub2e4. Source \uc11c\ubc84\ub85c \uc2b9\uaca9\ub41c Replica1 \uc11c\ubc84\uc5d0\uc11c **GTID\ub97c \ubc1b\uc544 \uc801\uc6a9\ud558\uc5ec \ucd5c\uc2e0\ud654**\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uc800\ud76c \ud300\uc758 \ubcf5\uc81c \ubc29\uc2dd\\n\uc774\ub7ec\ud55c \uc7a5\uc810\uc73c\ub85c GTID\uae30\ubc18 \ubcf5\uc81c \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n# \ubcf5\uc81c \ub3d9\uae30\ud654 \ubc29\uc2dd\\n\\n\ubcf5\uc81c \ubc29\uc2dd\uc5d0\ub294 \ud06c\uac8c \ub450\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4. **\ube44\ub3d9\uae30 \ubcf5\uc81c**\uc640 **\ubc18\ub3d9\uae30 \ubcf5\uc81c**\uc785\ub2c8\ub2e4.\\n\\n## \ube44\ub3d9\uae30 \ubcf5\uc81c\\n\ube44\ub3d9\uae30 \ubcf5\uc81c\ub294 \ub9d0\uadf8\ub300\ub85c \ube44\ub3d9\uae30\ub85c \ubcf5\uc81c\ud558\ub294 \uac83\uc785\ub2c8\ub2e4. \uc544\uc8fc \uac04\ub2e8\ud569\ub2c8\ub2e4. Source \uc11c\ubc84\uc5d0\uc11c \uc5b4\ub5a0\ud55c \uc774\ubca4\ud2b8\uac00 \ubc1c\uc0dd\ud560 \ub54c Replica \uc11c\ubc84\uc758 \ubc18\uc601\uacfc \uc0c1\uad00\uc5c6\uc774 \ub3d9\uc791\ud558\ub294 \uac83\uc785\ub2c8\ub2e4.\\n\uc18c\uc2a4 \uc11c\ubc84\uc5d0\uc11c \ucee4\ubc0b\ub41c \ud2b8\ub79c\uc7ad\uc158\uc740 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\uc5d0 \uae30\ub85d\ub418\uace0, \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc5d0\uc11c\ub294 \uc8fc\uae30\uc801\uc73c\ub85c \uc0c8\ub85c\uc6b4 \ud2b8\ub79c\uc7ad\uc158\uc5d0 \ub300\ud55c \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\ub97c \uc694\uccad\ud569\ub2c8\ub2e4. \uc774\ub7ec\ud55c \ubc29\uc2dd\uc740 \uc18c\uc2a4 \uc11c\ubc84\ub294 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \uc81c\ub300\ub85c \ubcc0\uacbd \ub418\uc5c8\ub294\uc9c0 \uc54c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc989 \ub370\uc774\ud130 \uc815\ud569\uc131\uc5d0 \ubb38\uc81c\uac00 \uc0dd\uae34\ub2e4\ub294 \ub2e8\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc774\ub7ec\ud55c \ubc29\uc2dd\uc740 \uc18c\uc2a4 \uc11c\ubc84\uac00 \uac01 \ud2b8\ub79c\uc7ad\uc158\uc5d0 \ub300\ud574 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\ub85c \uc804\uc1a1\ub418\ub294 \ubd80\ubd84\uc744 \uace0\ub824\ud558\uc9c0 \uc54a\ub294\ub2e4\ub294 \uc810\uc774 \uc18d\ub3c4 \uce21\uba74\uc5d0\uc11c \ube60\ub974\uace0, \ub610 \uc5ec\ub7ec \ub300\uc758 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\ub97c \uad6c\uc131\ud558\ub354\ub77c\ub3c4 \ud070 \uc131\ub2a5 \uc800\ud558\uac00 \uc5c6\ub2e4\ub294 \uc810\uc774\uc11c \uc7a5\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \ubc18\ub3d9\uae30 \ubcf5\uc81c\\n\ubc18\ub3d9\uae30 \ubcf5\uc81c\ub294 \ube44\ub3d9\uae30 \ubcf5\uc81c\ubcf4\ub2e4 \uc880 \ub354 \ub370\uc774\ud130 \uc815\ud569\uc131\uc774 \uc62c\ub77c\uac11\ub2c8\ub2e4. \uc18c\uc2a4 \uc11c\ubc84\ub294 \ubcc0\uacbd\ub41c \ud2b8\ub79c\uc7ad\uc158\uc774 \uc788\uc744 \ub54c \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \ub2e4 \uc804\uc1a1\uc774 \ub418\uc5c8\ub2e4\ub294 ACK \uc2e0\ud638\ub97c \ubc1b\uae30 \ub54c\ubb38\uc5d0 \ud655\uc2e4\ud788 \uc54c \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc804\uc1a1\uc5ec\ubd80\ub9cc \ud655\uc778\ud558\uae30 \ub54c\ubb38\uc5d0 \ud2b8\ub79c\uc7ad\uc158\uc774 \ubc18\uc601\uc774 \ub418\uc5c8\ub2e4\ub294 \ubcf4\uc7a5\uc740 \uc5c6\uc2b5\ub2c8\ub2e4. \ubc18\ub3d9\uae30 \ubcf5\uc81c \ubc29\uc2dd\uc740 2\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1. **After sync**: After Sync \ubc29\uc2dd\uc740 \uc18c\uc2a4 \uc11c\ubc84\uc5d0\uc11c \ud2b8\ub79c\uc7ad\uc158\uc744 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\uc5d0 \uae30\ub85d \ud6c4 Storage Engine\uc5d0 \ubc14\ub85c \ucee4\ubc0b\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\uc5d0 \uae30\ub85d \ud6c4 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc758 ACK \uc751\ub2f5\uc744 \uae30\ub2e4\ub9bd\ub2c8\ub2e4. \uadf8\ub9ac\uace0 ACK \uc751\ub2f5\uc774 \ub3c4\ucc29\ud558\uba74 \uadf8\uc81c\uc11c\uc57c \uc2a4\ud1a0\ub9ac\uc9c0 \uc5d4\uc9c4\uc744 \ucee4\ubc0b\ud558\uc5ec \ud2b8\ub79c\uc7ad\uc158\uc744 \ucc98\ub9ac\ud558\uace0 \uacb0\uacfc\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4.\\n2. **After commit**: After commit\uc740 \uc774\ub984 \uadf8\ub300\ub85c \ucee4\ubc0b\uc744 \uba3c\uc800 \ud558\ub294 \uac83\uc785\ub2c8\ub2e4. \ud2b8\ub79c\uc7ad\uc158\uc774 \uc0dd\uae30\uba74 \uba3c\uc800 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\uc5d0 \uae30\ub85d \ud6c4 \uc18c\uc2a4 \uc11c\ubc84 \uc2a4\ud1a0\ub9ac\uc9c0 \uc5d4\uc9c4\uc5d0 \ucee4\ubc0b\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc758 ACK \uc751\ub2f5\uc774 \ub0b4\ub824\uc624\uba74 \ud074\ub77c\uc774\uc5b8\ud2b8\ub294 \ucc98\ub9ac \uacb0\uacfc\ub97c \uc5bb\uace0 \ub2e4\uc74c \ucffc\ub9ac\ub97c \uc218\ud589\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 after commit \ubc29\uc2dd\uc740 \uc18c\uc2a4 \uc11c\ubc84\uc5d0 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c \ud32c\ud140 \ub9ac\ub4dc\uac00 \ubc1c\uc0dd\ud558\uac8c \ub429\ub2c8\ub2e4. \ud2b8\ub79c\uc7ad\uc158\uc774 \uc2a4\ud1a0\ub9ac\uc9c0 \uc5d4\uc9c4 \ucee4\ubc0b\uae4c\uc9c0\ub41c \ud6c4 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc758 \uc751\ub2f5\uc744 \uae30\ub2e4\ub9bd\ub2c8\ub2e4. \uc774\ucc98\ub7fc \uc2a4\ud1a0\ub9ac\uc9c0 \uc5d4\uc9c4 \ucee4\ubc0b\uae4c\uc9c0 \uc644\ub8cc\ub41c \ub370\uc774\ud130\ub294 \ub2e4\ub978 \uc138\uc158\uc5d0\uc11c\ub3c4 \uc870\ud68c\uac00 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud2b8\ub79c\uc7ad\uc158\uc774 \ucee4\ubc0b\ub418\uc5c8\uace0, \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\ub85c \uc544\uc9c1 \uc751\ub2f5\uc744 \uae30\ub2e4\ub9b4 \ub54c, \uc18c\uc2a4 \uc11c\ubc84\uc5d0 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud55c\ub2e4\uba74 \uc0c8\ub85c\uc6b4 \uc18c\uc2a4 \uc11c\ubc84\ub85c \uc2b9\uaca9\ub41c \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc5d0\uc11c \ub370\uc774\ud130\ub97c \uc870\ud68c\ud560 \ub54c \uc790\uc2e0\uc774 \uc774\uc804 \uc18c\uc2a4 \uc11c\ubc84\uc5d0\uc11c \uc870\ud68c\ud588\ub358 \ub370\uc774\ud130\ub97c \ubcf4\uc9c0 \ubabb\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc774\ucc98\ub7fc \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \uc2b9\uaca9\ub41c \uc0c1\ud669\uc5d0 \uc18c\uc2a4 \uc11c\ubc84\uc758 \uc7a5\uc560\uac00 \ubcf5\uad6c\ub418\uc5b4 \uc7ac\uc0ac\uc6a9\ud560 \uacbd\uc6b0 \uc774\ubbf8 \ucee4\ubc0b\ub41c \uadf8 \ud2b8\ub79c\uc7ad\uc158\uc744 \uc218\ub3d9\uc73c\ub85c \ub864\ubc31 \uc2dc\ucf1c\uc57c\ub9cc \ub370\uc774\ud130\uac00 \ub9de\ub294 \uc0c1\ud669\uc774 \uc0dd\uae41\ub2c8\ub2e4.\\n\\n### \uc800\ud76c \ud300\uc758 \ubcf5\uc81c \ub3d9\uae30\ud654 \ubc29\uc2dd\\n\uc774\ub7ec\ud55c \uc7a5\ub2e8\uc810\uc73c\ub85c \uc800\ud76c \ud300\uc740 \ub370\uc774\ud130 \ubb34\uacb0\uc131\uc774 \uc911\uc694\ud558\ub2e4 \ud310\ub2e8\ub418\uc5b4 \ubc18\ub3d9\uae30 \ubcf5\uc81c \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud558\uace0, After Sync \ubc29\uc2dd\uc744 \uc801\uc6a9\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n# \ubcf5\uc81c \ud1a0\ud3f4\ub9ac\uc9c0\\n\\n\ubcf5\uc81c \ud1a0\ud3f4\ub9ac\uc9c0\ub294 \uc5ec\ub7ec\uac00\uc9c0 \ubc29\uc2dd \uc911 \uc790\uc2e0\uc758 \uc0c1\ud669\uacfc \uac00\uc7a5 \ub9de\ub294 \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud558\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4. \uc800\ud76c \ud300\uc774 \uace0\ub824\ud574\uc57c\ud560 \ubb38\uc81c\ub294 \uba3c\uc800 \uc131\ub2a5\uc744 \uc62c\ub824\uc57c \ud588\uace0, \ub2e8\uc77c \uc7a5\uc560\ud3ec\uc778\ud2b8\ub97c \uac1c\uc120\ud574\uc57c\ud588\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uc11c\ubc84\ub294 2\ub300 \ubfd0\uc774\uc600\uc2b5\ub2c8\ub2e4. \uc774\ub7ec\ud55c \uc0c1\ud669\uc5d0\uc11c \uc5b4\ub5a4 \ubc29\uc2dd\uc744 \ud0dd\ud560 \uc218 \uc788\uc744\uae4c\uc694?\\n\\n## \uc2f1\uae00 \ub808\ud50c\ub9ac\uce74\\n```mermaid\\ngraph LR\\n A[Application Server] -- Read + Write --\x3e S[Source]\\n A -- Read --\x3e R[Replica]\\n S--\x3e R\\n```\\n\uac00\uc7a5 \uae30\ubcf8\uc801\uc774\uba70 \uac00\uc7a5 \ub9ce\uc774 \uc4f0\uc774\ub294 \ud615\ud0dc\uc785\ub2c8\ub2e4. \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc5d0\uc11c \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc5d0 \uc77d\uae30 \uc694\uccad\uc744 \uc804\ub2ec\ud558\uba74, \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc5d0 \ubb38\uc81c\uac00 \uc0dd\uacbc\uc744 \ub54c, \uc11c\ube44\uc2a4 \uc7a5\uc560 \uc0c1\ud669\uc774 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub7ec\ubbc0\ub85c \uc18c\uc2a4 \uc11c\ubc84\uc5d0\uc11c Read, Write\ub97c \ub458 \ub2e4 \ud558\uace0, \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\ub294 failover\ub97c \uc704\ud574 \ub300\uae30\ud558\ub294 \uc608\ube44\uc6a9 \uc11c\ubc84\ub85c \uad6c\uc131\ud569\ub2c8\ub2e4.\\n\uc18c\uc2a4 \uc11c\ubc84\uc5d0 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c \uc18c\uc2a4 \uc11c\ubc84\ub97c \ub300\uccb4\ud558\uac70\ub098 \ub370\uc774\ud130\ub97c \ubc31\uc5c5\ud558\ub294 \uc6a9\ub3c4\ub85c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\\n\\n## \uba40\ud2f0 \ub808\ud50c\ub9ac\uce74\\n\\n```mermaid\\ngraph LR\\n A[Application Server] -- Read + Write --\x3e S[Source]\\n A -- Read --\x3e R1[Replica1]\\n S --\x3e R1\\n S --\x3e R2[Replica2]\\n```\\n\uc2f1\uae00 \ub808\ud50c\ub9ac\uce74\uc640 \ube44\uc2b7\ud55c \uad6c\uc131\uc774\uc9c0\ub9cc \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \ud55c \ub300 \ub354 \ucd94\uac00\ub41c \uad6c\uc131\uc785\ub2c8\ub2e4. \ud574\ub2f9 \ubc29\uc2dd\uc740 SPOF \ubb38\uc81c\uac00 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84 \ud558\ub098\ub97c \uc77d\uae30 \uc804\uc6a9 \uc11c\ubc84\ub85c \ub458 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc77d\uae30 \uc791\uc5c5\uc744 \ubd84\uc0b0\ud568\uc73c\ub85c \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc758 \uc131\ub2a5\uc744 \ud5a5\uc0c1 \uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc544\uae4c \ub9d0\ud588\ub358 \uc7a5\uc560 \uc0c1\ud669\uc774 \ubc1c\uc0dd\ud558\uba74 \uc608\ube44\uc6a9 \uc11c\ubc84\uc778 Replica2 \uc11c\ubc84\ub97c Source \uc11c\ubc84 \ud639\uc740 Replica1(\uc77d\uae30 \uc804\uc6a9) \uc11c\ubc84\ub85c \ub300\uccb4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uccb4\uc778 \ubcf5\uc81c\\n\\n```mermaid\\ngraph LR\\n A[Application Server] -- Read + Write --\x3e S[Source1]\\n A -- Read --\x3e R1-1[Replica1-1]\\n S --\x3e R1-1\\n S --\x3e R1-2[Replica1-2]\\n S --\x3e R1-3[Replica1-3 / Source2]\\n R1-3 --\x3e R2-1[Replica2-1]\\n R1-3 --\x3e R2-2[Replica2-2]\\n B[Batch Server] --Read--\x3e R2-2\\n\\n```\\n\ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \ub9ce\uc544\uc838 \uc18c\uc2a4 \uc11c\ubc84\uc758 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\ub97c \uc77d\ub294 \ubd80\ud558\uac00 \ub9ce\uc544\uc9c8 \ub54c \ud560 \uc218 \uc788\ub294 \uad6c\uc131\uc785\ub2c8\ub2e4. \uc880 \uc804\uc5d0 \uc124\uba85\ub4dc\ub838\ub358 \uba40\ud2f0 \ub808\ud50c\ub9ac\uce74 \ubc29\uc2dd\uc5d0\uc11c \ub611\uac19\uc740 \uad6c\uc131\uc744 \ucd94\uac00\ud55c \ubc29\uc2dd\uc73c\ub85c \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. Source 1 \uc758 \uc815\ubcf4\ub97c \ubcf5\uc81c\ud55c Replica 1-1, 1-2 \uc11c\ubc84\ub294 \ube60\ub974\uac8c \ub370\uc774\ud130\uac00 \ubc18\uc601\ub418\uc9c0\ub9cc, Source1\uc758 \uc774\ubca4\ud2b8\ub97c \ubcf5\uc81c\ud55c Source2\ub97c \ubcf5\uc81c\ud55c Replica 2-1, 2-2 \uc11c\ubc84\ub294 \ub2f9\uc5f0\ud788 \ub2a6\uac8c \ubc18\uc601\ub418\uae30 \ub54c\ubb38\uc5d0 \ud574\ub2f9 \uadf8\ub8f9\uc740 \uc608\ube44\uc6a9\uc73c\ub85c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\\n\\n## \ub4c0\uc5bc \uc18c\uc2a4 \ubcf5\uc81c\\n\\n```mermaid\\ngraph LR\\n A[Application Server] -- Read + Write --\x3e S1[Source/Replica 1]\\n A -- Read + Write --\x3e S2[Source/Replica 2]\\n S1 <-- Replication --\x3e S2\\n```\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub458 \ub2e4 \uc18c\uc2a4 \uc11c\ubc84\uc774\uba74\uc11c \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc778 \uacbd\uc6b0\uc785\ub2c8\ub2e4. \uc774 \uacbd\uc6b0\ub294 **Active-Active**\uad6c\uc131\uacfc **Active-Passive** \uad6c\uc131\uc73c\ub85c \ub098\ub269\ub2c8\ub2e4\\n\\nActive-Active\ub294 \uc11c\ubc84 \ub458 \ub2e4 \uc77d\uae30\uc640 \uc4f0\uae30\uac00 \uac00\ub2a5\ud55c \ud615\ud0dc\uc785\ub2c8\ub2e4. \uc989 \ubd80\ud558\ub97c \ubd84\uc0b0\uc2dc\ud0a4\uae30 \uc704\ud574 \uc11c\ubc84 \ubaa8\ub450 \uc77d\uace0 \uc4f0\ub294 \uc791\uc5c5\uc744 \ud558\ub294 \uac83\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc774\ub7ec\ud55c \ubc29\uc2dd\uc740 \ubed4\ud55c \ub2e8\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uc11c\ub85c\uc758 \uc774\ubca4\ud2b8\uac00 \ub3d9\uae30\ud654 \ub418\uae30 \uc804\uc5d0\ub294 \uc815\ud569\uc131\uc774 \uae68\uc9c8 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub610 \ub3d9\uc2dc\uc5d0 \uac19\uc740 \ub370\uc774\ud130\uc5d0 \ub300\ud574 \uc4f0\uae30 \uc791\uc5c5\uc744 \uc218\ud589\ud560 \ub54c, \ud558\ub098\uc758 \uc11c\ubc84\uc5d0\uc11c \uc4f0\uae30\uac00 \uc644\ub8cc\ub418\uc5c8\ub354\ub77c\ub3c4, \ub2e4\ub978 \ud558\ub098\uc758 \uc11c\ubc84\uc5d0 \ub2a6\uac8c \ub05d\ub09c \uc4f0\uae30\uac00 \uc788\ub2e4\uba74 \ub9c8\uc9c0\ub9c9 \ud2b8\ub79c\uc7ad\uc158\uc778 \ub2a6\uac8c \ub05d\ub09c \uc4f0\uae30 \uc791\uc5c5\uc774 \ubc18\uc601\ub418\uc5b4 \uc608\uc0c1\ud558\uc9c0 \ubabb\ud55c \uacb0\uacfc\uac00 \ub098\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub610 \ub2e4\ub978 \ubb38\uc81c\ub85c\ub294 Auto Increment\ub97c \uc0ac\uc6a9\ud560 \ub54c\uc785\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \ub370\uc774\ud130\uac00 \ub3d9\uc2dc\uc5d0 \uc0dd\uc131\ub420 \ub54c Auto Increment\uac00 \uc911\ubcf5\ub418\ub294 \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uae30 \ub54c\ubb38\uc5d0 \ud574\ub2f9 \ud1a0\ud3f4\ub85c\uc9c0\uc5d0\uc11c\ub294 ID\ub97c DB\uc5d0 \uc758\uc874\ud558\uc9c0 \uc54a\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4.\\n\\nActive-Passive \ubc29\uc2dd\uc740 \ud558\ub098\uc758 \uc11c\ubc84\ub9cc \uc77d\uae30\uc640 \uc4f0\uae30 \uc694\uccad\uc774 \ub418\uc9c0\ub9cc, \ub098\uba38\uc9c0 \uc11c\ubc84\ub294 \ub300\uae30\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \ub450 \uc11c\ubc84 \ubaa8\ub450 \uc5b8\uc81c\ub098 \uc4f0\uae30 \uc791\uc5c5\uc774 \uac00\ub2a5\ud55c \ud615\ud0dc\uc774\uae30 \ub54c\ubb38\uc5d0 \uc7a5\uc560 \ubc1c\uc0dd \uc2dc \ube60\ub974\uac8c Faliover\ud560 \uc218 \uc788\ub2e4\ub294 \uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uba40\ud2f0 \uc18c\uc2a4 \ubcf5\uc81c\\n\\n\\n```mermaid\\ngraph LR\\n A[Application Server] -- Read + Write --\x3e S1[Source 1]\\n A[Application Server] -- Read + Write --\x3e S2[Source 2]\\n A[Application Server] -- Read + Write --\x3e S3[Source 3]\\n A[Application Server] -- Read + Write --\x3e S4[Source 4]\\n S1 --\x3e R[Replica]\\n S2 --\x3e R[Replica]\\n S3 --\x3e R[Replica]\\n S4 --\x3e R[Replica]\\n```\\n\ud558\ub098\uc758 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \ub2e4\uc218\uc758 \uc18c\uc2a4 \uc11c\ubc84\ub97c \uac16\ub294 \uad6c\uc131\uc785\ub2c8\ub2e4. \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc0e4\ub529\uc744 \ud574\ub480\ub294\ub370, \ub2e4\uc2dc \ud558\ub098\uc758 \uc11c\ubc84\ub85c \ud1b5\ud569\ud558\uace0 \uc2f6\uc744 \ub54c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud639\uc740 \uc11c\ub85c \ub2e4\ub978 \ub370\uc774\ud130\ub97c \ud55c \uacf3\uc5d0 \ubc31\uc5c5\uc744 \ud560 \ub54c\ub3c4 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uc800\ud76c \ud300\uc758 \ud1a0\ud3f4\ub85c\uc9c0 \ubc29\uc2dd\\n\uadf8\ub7fc \uc774\ub807\uac8c\ub098 \ub9ce\uc740 \uad6c\uc131 \uc911\uc5d0 \uc800\ud76c \ud300\uc5d0\uc11c \ud0dd\ud560 \uc218 \uc788\ub294 \ud1a0\ud3f4\ub85c\uc9c0 \ubc29\uc2dd\uc740 \uc2f1\uae00 \ub808\ud50c\ub9ac\uce74 \ubc29\uc2dd\uacfc \ub4c0\uc5bc \uc18c\uc2a4 \ubcf5\uc81c \ubc29\uc2dd \ubc16\uc5d0 \uc5c6\uc2b5\ub2c8\ub2e4. \uc65c\ub0d0\ud558\uba74 \uc8fc\uc5b4\uc9c4 \uc11c\ubc84\uac00 2\ub300\ubfd0\uc774\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \ub4c0\uc5bc \uc18c\uc2a4 \ubc29\uc2dd\uc740 \uc801\uc6a9\ud558\ub294\ub370 \ubb34\ub9ac\uac00 \uc788\ub294 \ubd80\ubd84\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uc77c\ub2e8 \uc800\ud76c\uac00 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc801\uc6a9\ud558\ub824\ub294 \uac00\uc7a5 \ud070 \uc774\uc720\ub294 **\uc131\ub2a5** \uc774\uae30 \ub54c\ubb38\uc5d0 \uc131\ub2a5\uc774 \ubcc0\ud558\uc9c0 \uc54a\ub294 \ub4c0\uc5bc \uc18c\uc2a4\uc758 Active-Passive \ubc29\uc2dd\uc740 \uc81c\uc678\ud558\uaca0\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 Active-Active \ubc29\uc2dd\uc740 \ubd80\ud558\ub97c \ubd84\uc0b0\uc2dc\ud0ac \uc218 \uc788\ub2e4\ub294 \uc7a5\uc810\uc774 \uc788\uc9c0\ub9cc, \ub2e8\uc810\uc73c\ub85c\ub294 Auto Increment\ub97c \uc0ac\uc6a9\ud558\ub294\ub370\uc5d0 \uc704\ud5d8\uc774 \uc788\ub2e4\ub294 \uc810\uacfc, \ub370\uc774\ud130\uc758 \uc815\ud569\uc131 \ubb38\uc81c\uac00 \uc0dd\uae38 \uc218 \uc788\ub2e4\ub294 \uc810\uc5d0\uc11c \ub4c0\uc5bc \uc18c\uc2a4 \ubc29\uc2dd\uc740 \uc81c\uc678\ud558\ub3c4\ub85d \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \uc2f1\uae00 \ub808\ud50c\ub9ac\uce74 \ubc29\uc2dd\uc744 \uc801\uc6a9\ud560 \uc218 \ubc16\uc5d0 \uc5c6\ub294\ub370\uc694. \uc2f1\uae00 \ub808\ud50c\ub9ac\uce74\uc758 \ubc29\uc2dd\uc740 \uac00\uc6a9\uc131 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574 \ub9cc\ub4e4\uc5b4\uc9c4 \ubc29\uc2dd\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc800\ud76c \uc11c\ube44\uc2a4\ub294 \ud604\uc7ac \uac00\uc6a9\uc131\ubcf4\ub2e4 \uc131\ub2a5\uc744 \ub354 \uc2e0\uacbd\uc368\uc57c\ud558\ub294 \uc0c1\ud669\uc774\uae30\ub54c\ubb38\uc5d0 \uc2f1\uae00 \ub808\ud50c\ub9ac\uce74 \ud1a0\ud3f4\ub85c\uc9c0\ub97c \uad6c\uc131\ud558\uc9c0\ub9cc \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\ub97c \uc608\ube44\uc6a9\uc774 \uc544\ub2cc \uc77d\uae30 \uc804\uc6a9 \ubc29\uc2dd\uc73c\ub85c \uc0ac\uc6a9\ud558\ub3c4\ub85d \ud558\uace0, \uac00\uc6a9\uc131 \ubd80\ubd84\uc744 \ud3ec\uae30\ud558\uae30\ub85c \uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n# \ucf54\ub4dc\uc5d0 \uc801\uc6a9\ud558\uae30\\n[replication-datasource](https://github.com/kwon37xi/replication-datasource) Github \uc18c\uc2a4 \ucf54\ub4dc\ub97c \ucc38\uace0\ud558\uc2dc\uac70\ub098, [DB \ubcf5\uc81c, @Transactional\uc5d0 \ub530\ub77c \uc694\uccad \ubd84\ub9ac\ud574\ubcf4\uae30](https://greeng00se.github.io/db-replication) \uae00\uc744 \ucc38\uace0\ud558\uc5ec \ub530\ub77c\ud558\uba74 \uae08\ubc29\ud558\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4!\\n\\n## \uacb0\ub860\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158 \uc0dd\uac01\ubcf4\ub2e4 \uc5b4\ub835\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc7ac\ubc0c\uc2b5\ub2c8\ub2e4. \uc778\ud504\ub77c\ub3c4 \uc7ac\ubc0c\uc2b5\ub2c8\ub2e4.\\n\\n## \ucc38\uace0\\nReal Mysql 8.0"},{"id":"31","metadata":{"permalink":"/31","source":"@site/blog/2023-09-03-improved-query-performance.mdx","title":"\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","description":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4","date":"2023-09-03T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 3\uc77c","tags":[{"label":"mysql","permalink":"/tags/mysql"}],"readingTime":13.275,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"31","title":"\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","authors":["boxster"],"tags":["mysql"]},"prevItem":{"title":"\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/32"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \ud611\uc5c5 \uc77c\ud654","permalink":"/30"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4\\n## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\\n\uba3c\uc800 \uc774 \uae00\uc744 \uc4f0\uac8c \ub41c \uacc4\uae30\ub97c \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4. \uce74\ud398\uc778 \ud300 \ud504\ub85c\uc81d\ud2b8\uc5d0\ub294 \uc0ac\uc6a9\uc790\uac00 \ubcf4\uace0\uc788\ub294 \uc9c0\ub3c4\uc5d0 \ucda9\uc804\uc18c\ub97c \ubcf4\uc5ec\uc8fc\ub294 \uc870\ud68c \uae30\ub2a5\uc774 \uac00\uc7a5 \uc911\uc694\ud558\uace0, \uc81c\uc77c \uc694\uccad\uc774 \ub9ce\uc774 \ub4e4\uc5b4\uc635\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc870\ud68c \uc131\ub2a5\uc774 \uc88b\uc9c0 \uc54a\uc740 \uae4c\ub2ed\uc778\uc9c0 \uc5ec\ub7ec \uc0ac\uc6a9\uc790\uac00 \uc811\uc18d\ud558\uba74 \uc544\ub798\uc640 \uac19\uc774 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uac00 \uc2e4\ud589\ub418\uace0 \uc788\ub294 \uc11c\ubc84\uc758 cpu \uc0ac\uc6a9\ub960\uc774 100%\uac00 \ub418\ub294 \ubb38\uc81c\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n![cpu](https://github.com/drunkenhw/drunkenhw.github.io/assets/106640954/2330435f-17b4-4d38-b16b-c72fd7017969)\\n\\n## \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30\\n\\n\uba3c\uc800 \uc81c\uac00 \uac1c\uc120\ud558\uae30 \uc704\ud574 \uc0ac\uc6a9\ud588\ub358 \ubc29\ubc95\ub4e4\uc5d0 \ub300\ud574 \uc801\uc5b4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n### DTO \uc774\uc6a9\ud558\uae30\\n\\n\ud604\uc7ac \uad6c\uc870\ub294 \uc544\ub798\uc758 JPA\ub97c \uc774\uc6a9\ud574 \uc544\ub798\uc640 \uac19\uc740 \ucffc\ub9ac\ub85c entity\ub85c \ub370\uc774\ud130\ub97c \uc870\ud68c\ud569\ub2c8\ub2e4.\\n\\n```sql\\n select distinct station.station_id,\\n charger.charger_id,\\n charger.station_id,\\n chargerStatus.charger_id,\\n chargerStatus.station_id,\\n station.created_at,\\n station.updated_at,\\n station.address,\\n station.company_name,\\n station.contact,\\n station.detail_location,\\n station.is_parking_free,\\n station.is_private,\\n station.latitude,\\n station.longitude,\\n station.operating_time,\\n station.private_reason,\\n station.station_name,\\n station.station_state,\\n charger.created_at,\\n charger.updated_at,\\n charger.capacity,\\n charger.method,\\n charger.price,\\n charger.type,\\n charger.station_id,\\n charger.charger_id,\\n chargerStatus.created_at,\\n chargerStatus.updated_at,\\n chargerStatus.charger_condition,\\n chargerStatus.latest_update_time\\n from charge_station station\\n inner join\\n charger charger on station.station_id = charger.station_id\\n inner join\\n charger_status chargerStatus on charger.charger_id = chargerStatus.charger_id\\n and charger.station_id = chargerStatus.station_id\\n where station.latitude >= 37.5019194727953082567\\n and station.latitude <= 37.5092305272047217433\\n and station.longitude >= 127.044542269049714936\\n and station.longitude <= 127.058071330950285064\\n\\n```\\n\\nJPA\ub97c \ud1b5\ud574 \uc774\ub7ec\ud55c \ubc29\uc2dd\uc73c\ub85c \uc870\ud68c\ud55c\ub2e4\uba74 \uc544\uc8fc \ud3b8\ud558\uac8c \uac12\uc744 \uac00\uc838\uc624\uace0, fetch join\uc744 \ud1b5\ud574 \ud558\uc704\uc758 entity\ub4e4\uc758 \uc815\ubcf4\ub3c4 \uae54\ub054\ud558\uac8c \uac00\uc838\uc635\ub2c8\ub2e4.\\n\\n\uac00\uc838\uc628 \uac12\uc73c\ub85c \ud544\uc694\ud55c \uc815\ubcf4\ub4e4\uc744 \ub9e4\ud551\ud558\uace0 \uac00\uacf5\ud558\uc5ec \uc751\ub2f5\uc744 \ub0b4\ub824\uc92c\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc870\ud68c\ub9cc\uc744 \uc704\ud574 JPA\uc758 entity\ub97c \uc870\ud68c\ud55c\ub2e4\ub294 \uac83\uc740 \uc5ec\ub7ec \ub2e8\uc810\uc774 \uc874\uc7ac\ud569\ub2c8\ub2e4.\\n\\n\uc81c\uc77c \uba3c\uc800 \uc751\ub2f5\uc744 \ub0b4\ub824\uc904 \ub54c \ubd88\ud544\uc694\ud55c \ub370\uc774\ud130\uae4c\uc9c0 \ubaa8\ub450 \uc870\ud68c\ub97c \ud55c\ub2e4\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4.\\n\uc774\ub807\uac8c \ub9ce\uc740 \ud544\ub4dc\ub4e4\uc774 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc751\ub2f5\uc5d0\uc11c\ub294 \ub300\ubd80\ubd84\uc758 \uacbd\uc6b0 \ubaa8\ub4e0 \uc815\ubcf4\uac00 \ud544\uc694\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ubaa8\ub4e0 \uc815\ubcf4\ub97c \ub2e4 \ubcf4\ub0b4\uc8fc\ub294 \uac83\ub3c4 \uc88b\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c \uc870\ud68c\ud560 \ub54c\uc758 \uc131\ub2a5\uc774 \uc544\uc8fc \ub098\ube60\uc9d1\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \ud544\uc694\ud55c \uce7c\ub7fc\ub9cc \uc870\ud68c\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ub610 \ub2e4\ub978 \ub2e8\uc810\uc73c\ub85c\ub294 JPA\ub85c entity\ub97c \uc870\ud68c\ud560 \ub54c Hibernate \uce90\uc2dc\uc5d0 \uc800\uc7a5\ud55c\ub2e4\ub358\uac00, One To One \uc5d0\uc11c N+1 \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud558\uae30 \ub54c\ubb38\uc5d0 \uc131\ub2a5\uc801\uc778 \uc774\uc288\uac00 \uc5ec\ub7ec\uac00\uc9c0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc870\ud68c\ub9cc \ud558\ub294 api\ub77c\uba74 DTO Projection\uc73c\ub85c \ud558\ub294 \uac83\uc774 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \uc544\ub798\uc640 \uac19\uc774 \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n```sql\\nSELECT s.station_id,\\n s.station_name,\\n s.latitude,\\n s.longitude,\\n s.is_parking_free,\\n s.is_private,\\n sum(case\\n when cs.charger_condition = \'STANDBY\' then 1\\n else 0\\n end),\\n sum(case\\n when c.capacity >= 50 then 1\\n else 0\\n end)\\nFROM charge_station s\\n inner join charger c on (c.station_id = s.station_id)\\n inner join charger_status cs on (c.charger_id = cs.charger_id and c.station_id = cs.station_id)\\nwhere s.station_id in (?, ?)\\ngroup by s.station_id;\\n```\\n\\n\uc774\ub807\uac8c \ud544\uc694\ud55c \uce7c\ub7fc\ub9cc \uc870\ud68c\ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \ubcc0\uacbd\ud558\uc5ec, \uc120\ub989\uc5ed \uadfc\ucc98\ub97c \uc870\ud68c\ud558\ub294 \uae30\uc900\uc73c\ub85c \uc57d 450ms -> 350ms\ub85c \uac1c\uc120\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc544\uc9c1\ub3c4 \ub108\ubb34 \ub290\ub9b0 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub798\uc11c \uc2e4\ud589 \uacc4\ud68d\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n### \uc2e4\ud589 \uacc4\ud68d \ud655\uc778\ud558\uae30\\n\\nsql\uc758 \uc2e4\ud589 \uacc4\ud68d\uc740 \uc544\uc8fc \uc911\uc694\ud558\uace0 \uc131\ub2a5\uc744 \uac1c\uc120\ud560 \ub54c \uc544\uc8fc \uc720\uc6a9\ud569\ub2c8\ub2e4.\\n\\n\uc2e4\ud589 \uacc4\ud68d\uc5d0\ub294 \uc5ec\ub7ec\uac00\uc9c0 \uc815\ubcf4\ub4e4\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n1. **ID**: \uc2e4\ud589 \uacc4\ud68d \ub0b4\uc5d0\uc11c \uac01 \uc791\uc5c5 \ub610\ub294 \ub2e8\uacc4\ub97c \uc2dd\ubcc4\ud558\ub294 \uc77c\ub828\ubc88\ud638\uc785\ub2c8\ub2e4. \uc2e4\ud589 \uacc4\ud68d\uc740 \uc5ec\ub7ec \ub2e8\uacc4\ub85c \ub098\ub258\uba70, ID\ub97c \ud1b5\ud574 \uc774\ub7ec\ud55c \ub2e8\uacc4\ub97c \uc2dd\ubcc4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n2. **Select Type**: \ucffc\ub9ac\uc758 \uac01 \ub2e8\uacc4(\uc608: SIMPLE, PRIMARY, SUBQUERY)\uc5d0 \ub300\ud55c \uc2e4\ud589 \uc720\ud615\uc744 \ub098\ud0c0\ub0c5\ub2c8\ub2e4. \uc774\ub294 MySQL\uc774 \ub370\uc774\ud130\ub97c \uc120\ud0dd\ud558\uace0 \ucc98\ub9ac\ud558\ub294 \ubc29\uc2dd\uc744 \ub098\ud0c0\ub0c5\ub2c8\ub2e4.\\n\\n3. **Table**: \uc2e4\ud589 \uacc4\ud68d\uc5d0 \ud3ec\ud568\ub41c \ud14c\uc774\ube14\uc758 \uc774\ub984 \ub610\ub294 \ubcc4\uce6d\uc785\ub2c8\ub2e4. \uc5b4\ub5a4 \ud14c\uc774\ube14\uc774 \uc0ac\uc6a9\ub418\ub294\uc9c0\ub97c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n4. **Type**: \ud14c\uc774\ube14 \uc811\uadfc \ubc29\uc2dd\uc744 \ub098\ud0c0\ub0c5\ub2c8\ub2e4. \uc774 \uac12\uc740 \uc778\ub371\uc2a4 \uc2a4\uce94, \ud480 \ud14c\uc774\ube14 \uc2a4\uce94 \ub4f1\uacfc \uac19\uc740 \uac12\uc77c \uc218 \uc788\uc73c\uba70, \uc131\ub2a5\uc5d0 \ud070 \uc601\ud5a5\uc744 \ubbf8\uce69\ub2c8\ub2e4.\\n\\n5. **Possible Keys**: \uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc778\ub371\uc2a4\ub97c \ub098\ud0c0\ub0c5\ub2c8\ub2e4. MySQL\uc774 \uc5b4\ub5a4 \uc778\ub371\uc2a4\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294\uc9c0 \uc54c\ub824\uc90d\ub2c8\ub2e4.\\n\\n6. **Key**: \uc2e4\uc81c\ub85c \uc120\ud0dd\ub41c \uc778\ub371\uc2a4\uc785\ub2c8\ub2e4. \uc774 \uac12\uc740 \uac00\ub2a5\ud55c \uc778\ub371\uc2a4 \uc911\uc5d0\uc11c \uc2e4\uc81c\ub85c \uc0ac\uc6a9\ub418\ub294 \uc778\ub371\uc2a4\ub97c \ub098\ud0c0\ub0c5\ub2c8\ub2e4.\\n\\n7. **Key Len**: \uc0ac\uc6a9\ub41c \uc778\ub371\uc2a4\uc758 \uae38\uc774\ub97c \ub098\ud0c0\ub0c5\ub2c8\ub2e4.\\n\\n8. **Ref**: \uc778\ub371\uc2a4\ub97c \uc0ac\uc6a9\ud558\uc5ec \ud14c\uc774\ube14 \uac04\uc758 \uc5f0\uacb0\uc744 \ub098\ud0c0\ub0b4\ub294 \uc5f4\uc785\ub2c8\ub2e4.\\n\\n9. **Rows**: \uac01 \ub2e8\uacc4\uc5d0\uc11c \uc608\uc0c1\ub418\ub294 \ud589\uc758 \uc218\uc785\ub2c8\ub2e4. \uc774 \uac12\uc740 \uc131\ub2a5 \ud3c9\uac00\uc5d0 \uc911\uc694\ud55c \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\n10. **Extra**: \uae30\ud0c0 \uc815\ubcf4\ub97c \uc81c\uacf5\ud569\ub2c8\ub2e4. \uc774 \uce7c\ub7fc\uc5d0\ub294 \ucd94\uac00 \uc815\ubcf4 \ubc0f \ud78c\ud2b8\uac00 \ud3ec\ud568\ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub807\uac8c \uc5ec\ub7ec \uce7c\ub7fc\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uadf8 \uc911 \uc131\ub2a5\uc5d0 \ud070 \uc601\ud5a5\uc744 \ubbf8\uce58\ub294 \uce7c\ub7fc \ub450 \uac00\uc9c0\ub9cc \uc790\uc138\ud788 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n### Type\\n1. **const** : \ucffc\ub9ac\uc5d0 Primary key \ud639\uc740 unique key \uce7c\ub7fc\uc744 \uc774\uc6a9\ud558\ub294 where \uc870\uac74\uc808\uc744 \uac00\uc9c0\uace0 \uc788\uace0, \ubc18\ub4dc\uc2dc \ud558\ub098\uc758 \ub370\uc774\ud130\ub97c \ubc18\ud658\ud558\ub294 \ubc29\uc2dd\uc774\ub2e4. (\uc635\ud2f0\ub9c8\uc774\uc800\uac00 \ud574\ub2f9 \ubd80\ubd84\uc740 \uc0c1\uc218\ub85c \ucc98\ub9ac\ud558\uae30 \ub54c\ubb38\uc5d0 const\ub77c\uace0 \ud55c\ub2e4.)\\n2. **eq_ref** : \uc870\uc778\uc5d0\uc11c Primary key \ud639\uc740 unique key \uce7c\ub7fc\uc744 \uc774\uc6a9\ud558\ub294 where \uc870\uac74\uc808\uc744 \uac00\uc9c0\uace0 \uc788\uace0, \ubc18\ub4dc\uc2dc \ud558\ub098\uc758 \ub370\uc774\ud130\ub97c \ubc18\ud658\ud558\ub294 \ubc29\uc2dd\uc774\ub2e4. (const\uc640 \ub2e4\ub978 \uc810\uc740 eq_ref\ub294 \uc870\uc778\uc5d0\uc11c \uc0ac\uc6a9\ub41c\ub2e4\ub294 \uc810\uc774\ub2e4.)\\n3. **ref** : eq_ref\uc640 \ub2e4\ub974\uac8c join\uc758 \uc21c\uc11c\uc640 \uad00\uacc4\uc5c6\uc774 \uc0ac\uc6a9\ub41c\ub2e4. \uadf8\ub9ac\uace0 primary key, unique key\ub3c4 \uad00\uacc4\uc5c6\ub2e4. \uadf8\ub0e5 \uc778\ub371\uc2a4\uc758 \uc885\ub958\uc640 \uad00\uacc4\uc5c6\uc774 `=` \uc870\uac74\uc73c\ub85c \uac80\uc0c9\ud560 \ub54c \uc0ac\uc6a9\ub41c\ub2e4\\n4. **fulltext**: mysql \uc804\ubb38 \uac80\uc0c9 \uc778\ub371\uc2a4\ub97c \uc0ac\uc6a9\ud574\uc11c \ub808\ucf54\ub4dc\uc5d0 \uc811\uadfc\ud558\ub294 \ubc29\ubc95, \uc804\ubb38 \uac80\uc0c9\ud560 \uceec\ub7fc\uc5d0 \uc778\ub371\uc2a4\uac00 \uc788\uc5b4\uc57c \ud55c\ub2e4. \\"MATCH ... AGAINST ...\\" \uad6c\ubb38\uc744 \uc0ac\uc6a9\ud574\uc11c \uc2e4\ud589\ub41c\ub2e4\\n5. **range**: \uc778\ub371\uc2a4\ub97c \uc774\uc6a9\ud574\uc11c \uac80\uc0c9\ud558\ub294\ub370, \uac80\uc0c9 \uc870\uac74\uc774 `>, >=, <, <=, BETWEEN, IN()` \ub4f1\uc758 \uc5f0\uc0b0\uc790\ub97c \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0\uc774\ub2e4. \ubcf4\ud1b5\uc758 \uc778\ub371\uc2a4 \uc2a4\uce94\uc774\ub77c\uace0 \ud558\uba74 range, const, ref\ub97c \uce6d\ud55c\ub2e4\\n6. **index**: \uc778\ub371\uc2a4 \ud480 \uc2a4\uce94\uc774\ub2e4. \uc778\ub371\uc2a4\ub97c \uc774\uc6a9\ud574\uc11c \ud14c\uc774\ube14\uc758 \ubaa8\ub4e0 \ub808\ucf54\ub4dc\ub97c \uc77d\ub294\ub2e4. \uc778\ub371\uc2a4\ub97c \uc774\uc6a9\ud574\uc11c \ud14c\uc774\ube14\uc744 \uc77d\ub294 \uac83\uc774\uae30 \ub54c\ubb38\uc5d0 all\ubcf4\ub2e4\ub294 \ube60\ub974\ub2e4.\\n7. **all**: \ud14c\uc774\ube14 \ud480 \uc2a4\uce94\uc774\ub2e4. \ud14c\uc774\ube14\uc758 \ubaa8\ub4e0 \ub808\ucf54\ub4dc\ub97c \uc77d\ub294\ub2e4. \uac00\uc7a5 \ub290\ub9b0 \ubc29\ubc95\uc774\ub2e4.\\n\\n\uc2e4\ud589 \uacc4\ud68d\uc5d0\uc11c \uc790\uc8fc \ubcf4\uc774\ub294 type\ub4e4\ub9cc **\uc131\ub2a5\uc774 \uc88b\uc740 \uc21c**\uc73c\ub85c \uc815\ub9ac\ud574\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n### Extra\\n1. **using filesort**: \uc815\ub82c\uc744 \uc704\ud574 \ubcc4\ub3c4\uc758 \ud30c\uc77c \uc815\ub82c\uc744 \uc218\ud589\ud55c\ub2e4. \uc774\ub294 \uc778\ub371\uc2a4\ub97c \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0 \uc815\ub82c\uc744 \uc218\ud589\ud55c\ub2e4\ub294 \uc758\ubbf8\uc774\ub2e4. \uc774\ub294 \uc131\ub2a5\uc5d0 \uc88b\uc9c0 \uc54a\ub2e4.\\n2. **using index**: \uc778\ub371\uc2a4\ub9cc\uc73c\ub85c \ucffc\ub9ac\ub97c \ucc98\ub9ac\ud55c\ub2e4. \uc774\ub294 \uc778\ub371\uc2a4\ub9cc\uc73c\ub85c \ucffc\ub9ac\ub97c \ucc98\ub9ac\ud558\uae30 \ub54c\ubb38\uc5d0 \uc131\ub2a5\uc774 \uc88b\ub2e4.\\n3. **using join** buffer: join\uc774 \ub418\ub294 \uce7c\ub7fc\uc740 \uc778\ub371\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4. \ud558\uc9c0\ub9cc driven table\uc5d0 \uc801\uc808\ud55c \uc778\ub371\uc2a4\uac00 \uc5c6\ub2e4\uba74 driving table\uc5d0 \uc788\ub294 \ubaa8\ub4e0 \ub808\ucf54\ub4dc\ub97c \uc77d\uc5b4\uc11c join\uc744 \uc218\ud589\ud55c\ub2e4. \uadf8\ub798\uc11c \uc774\uac78 \ubcf4\uc644\ud558\uae30 \uc704\ud574 driving table\uc5d0 \uc77d\uc740 \ub808\ucf54\ub4dc\ub97c \uc784\uc2dc \uacf5\uac04\uc5d0 \uc800\uc7a5\ud558\ub294\ub370 \uadf8 \uacf3\uc774 join buffer\uc774\ub2e4.\\n4. **using temporary**: \ucffc\ub9ac\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud574 \uc784\uc2dc \ud14c\uc774\ube14\uc744 \uc0dd\uc131\ud55c\ub2e4. \uc778\ub371\uc2a4\ub97c \uc0ac\uc6a9\ud558\uc9c0 \ubabb\ud558\ub294 group by \ucffc\ub9ac\uac00 \ub300\ud45c\uc801\uc778 \uc608\uc774\ub2e4.\\n5. **using where**: mysql \uc5d4\uc9c4\uc774 \ubcc4\ub3c4\uc758 \uac00\uacf5, \ud544\ud130\ub9c1 \uc791\uc5c5\uc744 \ucc98\ub9ac\ud55c \uacbd\uc6b0\uc77c \ub54c\ub9cc \ub098\ud0c0\ub09c\ub2e4. \ubc94\uc704 \uc870\uac74\uc740 \uc2a4\ud1a0\ub9ac\uc9c0 \uc5d4\uc9c4\uc5d0\uc11c \ucc98\ub9ac\ub418\uc5b4 \ub808\ucf54\ub4dc\ub97c \ub9ac\ud134\ud574\uc8fc\uc9c0\ub9cc, \uccb4\ud06c \uc870\uac74\uc740 mysql \uc5d4\uc9c4\uc5d0\uc11c \ucc98\ub9ac\ub41c\ub2e4.\\n\\n\\ntype\ubfd0\ub9cc \uc544\ub2c8\ub77c extra\ub3c4 \ucffc\ub9ac\uc758 \ubb38\uc81c\ub97c \ud30c\uc545\ud558\ub294\ub370 \uc544\uc8fc \ud070 \ub3c4\uc6c0\uc744 \uc90d\ub2c8\ub2e4. \uadf8 \uc911 \uc790\uc8fc \ubcf4\uc774\ub294 \uac83\ub4e4\uc5d0 \ub300\ud574\uc11c\ub9cc \uc815\ub9ac\ud574\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \uc544\uae4c \uc0dd\uc131\ud55c \ucffc\ub9ac\uc758 \uc2e4\ud589 \uacc4\ud68d\uc744 \ud655\uc778\ud574\ubd05\uc2dc\ub2e4.\\n```\\n+----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+\\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\\n+----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+\\n| 1 | SIMPLE | station | NULL | range | PRIMARY,idx_station_coordination | PRIMARY | 1022 | NULL | 2 | 100.00 | Using where; Using temporary |\\n| 1 | SIMPLE | charger | NULL | ALL | PRIMARY | NULL | NULL | NULL | 240340 | 10.00 | Using where; Using join buffer (hash join) |\\n| 1 | SIMPLE | chargersta | NULL | eq_ref | PRIMARY | PRIMARY | 2044 | charge.charger1_.charger_id,charge.station0_.station_id | 1 | 100.00 | NULL |\\n+----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+\\n```\\n\\nstation \ud14c\uc774\ube14\uc5d0 \ub300\ud574\uc11c\ub294 range \uc2a4\uce94, \uc784\uc2dc \ud14c\uc774\ube14\uc744 \uc0dd\uc131\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4, \uadf8\ub9ac\uace0 charger\uc5d0\uc11c\ub294 \ud14c\uc774\ube14 \ud480 \uc2a4\uce94, join buffer\uae4c\uc9c0 \uc0dd\uc131\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \ub2e4\ud589\ud788\ub3c4 chargersta \ud14c\uc774\ube14\uc5d0\uc11c\ub294 \uc801\ub2f9\ud55c \uc870\uac74\uc744 \uc0dd\uc131\ud55c \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e4\uc2dc \ud55c\ubc88 \ucffc\ub9ac\ub97c \ubcf4\uace0 \uc2e4\ud589 \uacc4\ud68d\uc774 \uc774\ub807\uac8c \ub098\uc628 \uc774\uc720\ub97c \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n```sql\\nSELECT\\n ...\\n FROM charge_station s\\n inner join charger c on (c.station_id = s.station_id)\\n inner join charger_status cs on (c.charger_id = cs.charger_id and c.station_id = cs.station_id)\\nwhere s.station_id in (?, ?)\\ngroup by s.station_id;\\n```\\n\\n\uc544\uae4c \uc598\uae30\ud588\ub358, using temporary\uc640 using join buffer\uac00 \ubc1c\uc0dd\ud558\ub294 \uc774\uc720\uc758 \uacf5\ud1b5\uc810\uc744 \ucc3e\uc544\ubcf4\uba74, \uc778\ub371\uc2a4\uac00 \ubb38\uc81c\uc778 \uac83\uc744 \uc720\ucd94\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\nstation\uacfc charger\ub97c join\ud560 \ub54c, driven table \uc989, charger \ud14c\uc774\ube14\uc5d0 \uc801\uc808\ud55c \uc778\ub371\uc2a4\uac00 \uc5c6\uc5b4 \uc131\ub2a5\uc774 \ub098\ube60\uc9c4 \uac83\uc774\ub77c \uc758\uc2ec\ud558\uc5ec, \uc778\ub371\uc2a4\ub97c \uc0dd\uc131\ud558\uace0 \ub2e4\uc2dc \ud55c\ubc88 \uc2e4\ud589 \uacc4\ud68d\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n```\\n+----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+\\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\\n+----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+\\n| 1 | SIMPLE | station | NULL | range | PRIMARY,idx_station_coordination | PRIMARY | 1022 | NULL | 2 | 100.00 | Using where |\\n| 1 | SIMPLE | charger | NULL | ref | PRIMARY,idx_station_id | idx_station_id | 1022 | charge.s.station_id | 3 | 100.00 | NULL |\\n| 1 | SIMPLE | chargersta | NULL | eq_ref | PRIMARY | PRIMARY | 2044 | charge.charger1_.charger_id,charge.station0_.station_id | 1 | 100.00 | NULL |\\n+----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+\\n```\\n\\n\uc774\ub807\uac8c charger \ud14c\uc774\ube14\uc5d0 \uc778\ub371\uc2a4\ub97c \uc0dd\uc131\ud55c \uac83\ub9cc\uc73c\ub85c\ub3c4 \uc2e4\ud589 \uacc4\ud68d\uc744 \uae54\ub054\ud558\uac8c \uac1c\uc120\ud588\uc2b5\ub2c8\ub2e4.\\n\\n### \uacb0\uacfc\\n\uc544\ub798\ub294 \uc778\ub371\uc2a4\ub97c \uc0dd\uc131\ud558\uae30 \uc804 \uc2e4\ud589 \uc18d\ub3c4\uc785\ub2c8\ub2e4.\\n\\n![\uac1c\uc120_\uc804](https://github.com/woowacourse-teams/2023-car-ffeine/assets/106640954/1130eee6-c2b9-4846-b294-73de78b0f070)\\n\\n\uc544\ub798\ub294 \uc778\ub371\uc2a4\ub97c \uc0dd\uc131\ud55c \ud6c4 \uc2e4\ud589 \uc18d\ub3c4\uc785\ub2c8\ub2e4.\\n\\n![\uac1c\uc120_\ud6c4](https://github.com/woowacourse-teams/2023-car-ffeine/assets/106640954/d024330a-c233-4e75-a28b-1b01b6ae3245)\\n\\n315ms -> 24ms \ub85c \uc57d 13\ubc30 \ube68\ub77c\uc9c4 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\\n**\uc2e4\ud589 \uacc4\ud68d \ud655\uc778\uc740 \ud544\uc218\uc785\ub2c8\ub2e4!**\\n\\n### \ucc38\uace0\\nreal mysql \ucc45"},{"id":"30","metadata":{"permalink":"/30","source":"@site/blog/2023-08-31-love-my-team.mdx","title":"\uce74\ud398\uc778 \ud300\uc758 \ud611\uc5c5 \uc77c\ud654","description":"\ub808\ubca83 \ub54c \ud504\ub85c\uc81d\ud2b8\ub97c \uc9c4\ud589\ud558\uba74\uc11c, \uc800\ud76c \ud300\uc740 \ub9ce\uc740 \ud611\uc5c5\uc744 \uc9c4\ud589\ud588\uc2b5\ub2c8\ub2e4.","date":"2023-08-31T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 31\uc77c","tags":[],"readingTime":2.895,"hasTruncateMarker":false,"authors":[],"frontMatter":{"slug":"30","title":"\uce74\ud398\uc778 \ud300\uc758 \ud611\uc5c5 \uc77c\ud654","authors":[],"tags":[]},"prevItem":{"title":"\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/31"},"nextItem":{"title":"useSyncExternalStore\ub85c \ub9cc\ub4e4\uc5b4\ubcf4\ub294 \uc804\uc5ed\uc0c1\ud0dc\uad00\ub9ac \ub3c4\uad6c","permalink":"/29"}},"content":"\ub808\ubca83 \ub54c \ud504\ub85c\uc81d\ud2b8\ub97c \uc9c4\ud589\ud558\uba74\uc11c, \uc800\ud76c \ud300\uc740 \ub9ce\uc740 \ud611\uc5c5\uc744 \uc9c4\ud589\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ucc98\uc74c\uc5d0\ub294 \ud504\ub860\ud2b8\uc5d4\ub4dc, \ubc31\uc5d4\ub4dc \uc11c\ub85c \uac01\uac01\uc758 \ubd84\uc57c\ub9cc \uac1c\ubc1c\uc744 \ud574\uc654\uace0 \ud611\uc5c5\uc774 \uc775\uc219\ud558\uc9c0 \uc54a\uc544\uc11c \ub9ce\uc740 \ubd80\ubd84\uc5d0\uc11c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uace4 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uacfc\uc815\uc5d0\uc11c \uc800\ud76c \ud300\uc740 \uc5b4\ub5bb\uac8c \ub300\ucc98\ub97c \ud588\uc744\uae4c\uc694?\\n\\n\ud55c \uac00\uc9c0 \uc77c\ud654\ub85c \uc800\ud76c \ud300\uc758 \uc81c\uc774\uc640 \uc13c\ud2b8\uc758 \ud544\ud130 \uc801\uc6a9 \ubd80\ubd84\uc744 \uc124\uba85 \ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc870\ud68c \uc2dc\uc5d0 \ud544\ud130 \uc801\uc6a9 \ubd80\ubd84\uc744 \ub9cc\ub4e4 \ub54c \uae30\uc874\uc5d0 \uc791\uc131\ud574\ub454 API \uba85\uc138\ub300\ub85c \uc11c\ub85c \uc791\uc5c5\uc744 \uc9c4\ud589\ud558\uace0, \uc911\uac04\uc5d0 \uc0dd\uac01\ud558\uc9c0 \ubabb\ud55c \ubd80\ubd84\uc5d0 \ub300\ud574\uc11c\ub294 \uc11c\ub85c \ub300\ud654\ub97c \ub9ce\uc774 \ud588\uc2b5\ub2c8\ub2e4.\\n\ub300\ud654\ub97c \ud558\uba74\uc11c \uc9c4\ud589\uc744 \ud588\uc9c0\ub9cc \ubc1c\uacac\ud558\uc9c0 \ubabb\ud55c \ubb38\uc81c\uc810\uc774 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ubc14\ub85c \ucda9\uc804\uc18c \ud68c\uc0ac \uba85\uc5d0\uc11c key \uac12\uc744 \uc5b4\ub5bb\uac8c \ud558\ub0d0\uc5d0 \ubb38\uc81c\uc600\uc2b5\ub2c8\ub2e4.\\n\uc608\ub97c \ub4e4\uba74 \ucda9\uc804\uc18c \ud68c\uc0ac \uba85\uc5d0\uc11c `\uad11\uc8fc\uc2dc`\ub77c\ub294 \uc774\ub984\uc774 \uc788\uc5c8\ub294\ub370, \uc774 \ud544\ud130\ub294 \uc2e4\uc81c\ub85c \ub450 \uac00\uc9c0\uac00 \uc874\uc7ac\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\ub098\ub294 \uacbd\uae30\ub3c4 \uad11\uc8fc, \ud558\ub098\ub294 \uc804\ub77c\ub3c4 \uad11\uc8fc\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \ubd80\ubd84\uc5d0\uc11c \ubd88\ud544\uc694\ud55c \uc9c0\uc5ed\uc758 \ud544\ud130\uae4c\uc9c0 \uac78\ub9ac\uac8c \ub418\ub294 \ubb38\uc81c\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\ud611\uc5c5\ud558\ub294 \uacfc\uc815\uc5d0\uc11c \uc774\ub97c \ubc1c\uacac\ud588\uace0, \uc989\uac01 \uc870\uce58\ub97c \ucde8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc870\uce58\ub97c \ucde8\ud560 \ub54c \uc11c\ub85c\uc5d0\uac8c \uac01\uc790 \ud3b8\ud55c \ubc29\ubc95\uc774 \uc788\uc5c8\uc9c0\ub9cc,\\n\ub2e8\uc21c\ud788 \uc11c\ub85c\uc5d0\uac8c \ud3b8\ud55c \uc791\uc5c5\uc744 \ud558\uc9c0 \uc54a\uc558\uace0, \ud300\uc6d0\uacfc \uc0c1\uc758\ud558\uba74\uc11c \ucd94\ud6c4 \uc9c4\ud589\uc5d0 \ubb38\uc81c \uc5c6\ub294 \ubc29\ud5a5\uc744 \ucc3e\uace0 \uc9c4\ud589\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc9c0\uae08 \uc0dd\uac01\ud574\ubcf4\uba74 \ub9cc\uc57d \uac01\uc790\uc5d0\uac8c \ud3b8\ud55c \ubc29\uc2dd\uc73c\ub85c \ubb38\uc81c\ub97c \uc218\uc815\ud588\ub2e4\uba74, \ub2e4\ub978 \ud300\uc6d0\uc774 \ub2e4\ub978 \uc791\uc5c5\uc744 \ud560 \ub54c \uc9c0\uc7a5\uc774 \uac14\uc744 \uc218\ub3c4 \uc788\uace0 \ubd88\ud544\uc694\ud55c \uc791\uc5c5\uc744 \ud588\uc744 \uc218\ub3c4 \uc788\uc5c8\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uc2dc\uc810\uc744 \uacc4\uae30\ub85c \uc800\ud76c \ud300\ub07c\ub9ac \uc608\uc0c1\ud558\uc9c0 \ubabb\ud55c \ubb38\uc81c\ub97c \uc791\uc5c5 \uc911\uc5d0 \ubc1c\uacac\ud558\ub354\ub77c\ub3c4 \ub2e4\ub978 \ud300\uc6d0\uc5d0\uac8c \uacf5\uc720\ud558\uace0 \uc11c\ub85c \uc9e7\uc740 \ud68c\uc758\ub97c \ud1b5\ud574 \ubb38\uc81c \ud574\uacb0 \ubc29\uc548\uc744 \uac19\uc774 \ucc3e\ub294 \uac83\uc774 \uc790\uc5f0\uc2a4\ub7fd\uac8c \ud300\ubb38\ud654\ub85c \uc790\ub9ac \uc7a1\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4."},{"id":"29","metadata":{"permalink":"/29","source":"@site/blog/2023-08-25-external-state/index.mdx","title":"useSyncExternalStore\ub85c \ub9cc\ub4e4\uc5b4\ubcf4\ub294 \uc804\uc5ed\uc0c1\ud0dc\uad00\ub9ac \ub3c4\uad6c","description":"\uc800\ud76c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc9c0\ub3c4\uc640 React\ub97c \uacb0\ud569\uc744 \ud574\uc57c\ud588\uc2b5\ub2c8\ub2e4.","date":"2023-08-25T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 25\uc77c","tags":[{"label":"useSyncExternalStore","permalink":"/tags/use-sync-external-store"},{"label":"\uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac","permalink":"/tags/\uc804\uc5ed-\uc0c1\ud0dc-\uad00\ub9ac"},{"label":"\uc804\uc5ed\uc0c1\ud0dc","permalink":"/tags/\uc804\uc5ed\uc0c1\ud0dc"}],"readingTime":10.165,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"29","title":"useSyncExternalStore\ub85c \ub9cc\ub4e4\uc5b4\ubcf4\ub294 \uc804\uc5ed\uc0c1\ud0dc\uad00\ub9ac \ub3c4\uad6c","authors":["gabriel"],"tags":["useSyncExternalStore","\uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac","\uc804\uc5ed\uc0c1\ud0dc"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \ud611\uc5c5 \uc77c\ud654","permalink":"/30"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \uad00\ud558\uc5ec","permalink":"/28"}},"content":"\uc800\ud76c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc9c0\ub3c4\uc640 React\ub97c \uacb0\ud569\uc744 \ud574\uc57c\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud504\ub85c\uc81d\ud2b8 \ucd08\uae30\uc5d0\ub294 Google Maps API\ub97c React DOM\uc774 \uc544\ub2cc, \ubc14\ub2d0\ub77c JS\uc758 \uc601\uc5ed\uc5d0\uc11c \ub2e4\ub8e8\uae30\ub97c \ud76c\ub9dd\ud558\uc600\uace0, \uc5ec\ub7ec \ud14c\uc2a4\ud2b8 \uacb0\uacfc \ub450 \uc601\uc5ed\uc744 \ubd84\ub9ac\ud558\ub294 \uac83\uc740 \uc131\uacf5\uc801\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\nReact\ub294 \uadf8\uc800 \ubd80\ucc29 \ub2f9\ud560 DOM\uc744 \uc678\ubd80(Google Maps API)\ub85c \ub0b4\uc5b4\uc8fc\ub294 \uae30\ub2a5\uc5d0 \ubd88\uacfc\ud558\uc600\uace0, \uc9c0\ub3c4\uc640 React\uac00 \uc11c\ub85c \ud611\ub825 \ud574\uc57c\ud560 \ub54c\ub9cc \uc5f0\ub77d\uc744 \ud558\ub294 \uad6c\uc870\ub97c \ucde8\ud558\uace0\uc790 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uba74, React UI\ub294 UI\ub300\ub85c \ub3d9\uc791\ud558\uace0, \uc9c0\ub3c4\ub294 \uc9c0\ub3c4 \ub300\ub85c \ub3d9\uc791\ud558\ub2e4\uac00 \uc5b4\ub290 \uc21c\uac04\uc5d0\ub9cc \uc11c\ub85c\uac00 \uc11c\ub85c\ub97c \uc870\uc791\ud560 \uc218 \uc788\uc73c\uba74 \ub410\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \uac00\ub2a5\ud558\uac8c \ud558\ub294 \uae30\uc220\ub85c useSyncExternalStore\ub97c \uc120\uc815\ud558\uac8c \ub410\uc2b5\ub2c8\ub2e4. \uc774 \ud6c5\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\uc81c \ube14\ub85c\uadf8](https://leirbag.tistory.com/144)\ub098 [\uacf5\uc2dd\ubb38\uc11c](https://react.dev/reference/react/useSyncExternalStore)\uc5d0 \ub098\uc640\uc788\uc73c\ubbc0\ub85c \uc124\uba85\uc744 \uac04\ub7b5\ud788 \ud558\uc790\uba74 useSyncExternalStore\ub294 React DOM \ub0b4\ubd80\uac00 \uc544\ub2cc \uc678\ubd80 \uc800\uc7a5\uc18c(JS)\uc5d0\uc11c React DOM\uc744 \uc870\uc791\ud560 \uc218 \uc788\ub3c4\ub85d \ud558\ub294 \ucee4\uc2a4\ud140 \ud6c5\uc785\ub2c8\ub2e4.\\n\\n![no offset](./0-1.png)\\n\\n\uc774 \ud6c5\uc740 React 18\uc5d0 \ucd9c\uc2dc\ub418\uc5c8\uc73c\uba70, \uc678\ubd80 \uc800\uc7a5\uc18c\uc640 React\uc758 \uc18c\ud1b5\uc744 \uc6d0\ud65c\ud558\uac8c \ub3d5\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \ud65c\uc6a9\ud558\uae30 \uc801\uc808\ud558\ub2e4\uace0 \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4. \uc774 \uae30\ub2a5\uc744 \uc5b4\ub5bb\uac8c \ud558\uba74 \ub354 \ud6a8\uc728\uc801\uc778 \ubc29\ubc95\uc73c\ub85c \uc7ac\uc0ac\uc6a9\ud560 \uc218 \uc788\uc744\uc9c0 \uace0\ubbfc\ud558\uc600\uace0, \uc5ec\ub7ec \ucd94\uc0c1\ud654 \ub2e8\uacc4\ub97c \uac70\uccd0 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc218\uc900\uc73c\ub85c \uc81c\uc791\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774\ud6c4\uc5d0 TanStack Query\ub97c \ub3c4\uc785\ud558\ub294 \uacfc\uc815\uc5d0\uc11c \uac01\uc885 \uae30\ub2a5\uc774 React Component \ub0b4\uc5d0\uc11c\ub9cc \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud558\ub3c4\ub85d \uac15\uc81c\ub418\uc5c8\uace0, \ub530\ub77c\uc11c \ub354\uc774\uc0c1 \uc9c0\ub3c4 API\ub97c \ubc14\ub2d0\ub77cJS \uc601\uc5ed\uc5d0\uc11c \ub2e4\ub8f0 \uc218 \uc5c6\uc5b4 React DOM\uc73c\ub85c \uc774\uc2dd \ud558\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./0-2.png)\\n\\n\uc774\ubbf8 \ub9cc\ub4e4\uc5b4 \ub454 \uae30\ub2a5\uc774 \ubd95 \ub5a0\ubc84\ub9b0 \uc0c1\ud669\uc774\uc5c8\uc9c0\ub9cc \uc5b4\ucc0c \ub410\ub4e0 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\uc5d0 \uc9c0\ub3c4 \uc778\uc2a4\ud134\uc2a4\ub97c \ub123\uc5b4\uc57c \ud558\ub294 \uc0c1\ud669\uc774\ub77c useSyncExternalStore\ub97c \ud504\ub85c\uc81d\ud2b8 \ub05d\uae4c\uc9c0 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc \uad00\ub9ac \ub3c4\uad6c\ub85c\uc368 \uc0ac\uc6a9\ud558\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc0c1\ud0dc \uad00\ub9ac \ud6c5\uc758 \ucd94\uc0c1\ud654 \uacfc\uc815\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n## **use-external-state \uad6c\uc131 \ubc0f \ub3d9\uc791 \uc6d0\ub9ac**\\n\\n**Store\ub294 \uc0c1\ud0dc \uad00\ub9ac \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4**\\n\\n\ubc14\uae65\uc5d0\uc11c \uc8fc\uc5b4\uc9c4 \ucd08\uae30 \uc0c1\ud0dc \uac12\uc740 StateManager\ub77c\ub294 \ud074\ub798\uc2a4\uc5d0 \uc804\ub2ec\ub429\ub2c8\ub2e4.\\n\\n![no offset](./1.png)\\n\\n```typescript\\nexport const store = (initialState: T) => {\\n const stateManager = new StateManager(initialState);\\n return stateManager;\\n};\\n```\\n\\n\ucd08\uae30 \uc0c1\ud0dc \uac12\uc744 \uc804\ub2ec\ubc1b\uc740 store \ud568\uc218\ub294 StateManager\ub77c\ub294 \uc5b4\ub5a4 \uc0c1\ud0dc \uad00\ub9ac \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.\\n\uc0dd\uc131\ub41c StateManager \uc778\uc2a4\ud134\uc2a4\uac00 \ubc18\ud658\ub418\uc5b4 store\uac00 \uace7 \ucd08\uae30 \uac12\uc744 \uac00\uc9c0\ub294 StateManager\uac00 \ub429\ub2c8\ub2e4.\\n\\n![no offset](./2.png)\\n\\n\uc608\ub97c \ub4e4\uc5b4, \ub2e4\uc74c\uacfc \uac19\uc740 \ucf54\ub4dc\uac00 \uc788\ub2e4\uace0 \ud560 \ub54c\\n\\n```typescript\\nexport const countStore = store(0);\\n```\\n\\ncountStore\ub294 \uace7 0\uc744 \ucd08\uae30\uac12\uc73c\ub85c \uac00\uc9c0\ub294 StateManager \uc778\uc2a4\ud134\uc2a4\uc774\uae30\ub3c4 \ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 StateManager\uc5d0 \ub300\ud574\uc11c \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n### StateManager\ub294 react \ubc14\uae65\uc5d0 \uc788\ub294 \uc5b4\ub5a4 \uc800\uc7a5\uc18c\uc774\ub2e4.\\n\\n(\uadfc\ub370 \uc774\uac8c \uadf8\ub0e5 \uc800\uc7a5\uc18c\ub294 \uc544\ub2c8\uace0 \uc880 \ud2b9\ubcc4\ud55c \uc800\uc7a5\uc18c\ub2e4.)\\n\\n```typescript\\nexport type SetStateCallbackType = (prevState: T) => T;\\n\\nexport interface DataObserver {\\n setState: (param: SetStateCallbackType | T) => void;\\n getState: () => T;\\n subscribe: (listener: () => void) => () => void;\\n emitChange: () => void;\\n}\\n\\nclass StateManager implements DataObserver {\\n public state: T;\\n private listeners: Array<() => void> = [];\\n\\n constructor(initialState: T) {\\n this.state = initialState;\\n }\\n\\n setState = (param: SetStateCallbackType | T) => {\\n if (param instanceof Function) {\\n const newState = param(this.state);\\n this.state = newState;\\n } else {\\n this.state = param;\\n }\\n\\n this.emitChange();\\n };\\n\\n getState = () => {\\n return this.state;\\n };\\n\\n subscribe = (listener: () => void) => {\\n this.listeners = [...this.listeners, listener];\\n\\n return () => {\\n this.listeners = this.listeners.filter((l) => l !== listener);\\n };\\n };\\n\\n emitChange = () => {\\n for (const listener of this.listeners) {\\n listener();\\n }\\n };\\n}\\n\\nexport default StateManager;\\n```\\n\\nStateManager \ud074\ub798\uc2a4\ub294 \uc678\ubd80\uc5d0\uc11c \ubc1b\uc544\uc628 \ucd08\uae30\uac12\uc744 \uc0c1\ud0dc\ub85c \uac00\uc9d1\ub2c8\ub2e4.\\nsetState, getState, subscribe, emitChange\ub97c \uba54\uc11c\ub4dc\ub85c \uac00\uc9d1\ub2c8\ub2e4.\\n\uc5ec\uae30\uc11c \uc791\uc131\ub41c \ucf54\ub4dc\ub4e4\uc740 react\uc5d0\uc11c \uc678\ubd80 \uc800\uc7a5\uc18c\uc640 \uc18c\ud1b5\ud558\uae30 \uc704\ud55c [\ucd5c\uc18c\ud55c\uc758 \uaddc\uaca9](https://react.dev/reference/react/useSyncExternalStore#subscribing-to-an-external-store)\uc785\ub2c8\ub2e4.\\n\\n- subscribe: \ub2e8\uc77c \ucf5c\ubc31 \uc778\uc218\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2a4\ud1a0\uc5b4\uc5d0 \uad6c\ub3c5\ud558\ub294 \ud568\uc218\uc785\ub2c8\ub2e4. \uc2a4\ud1a0\uc5b4\uac00 \ubcc0\uacbd\ub418\uba74 \uc81c\uacf5\ub41c \ucf5c\ubc31\uc744 \ud638\ucd9c\ud574\uc57c \ud569\ub2c8\ub2e4. \uadf8\ub7ec\uba74 \uad6c\uc131 \uc694\uc18c\uac00 \ub2e4\uc2dc \ub80c\ub354\ub9c1 \ub429\ub2c8\ub2e4. \uad6c\ub3c5 \uae30\ub2a5\uc740 \uad6c\ub3c5\uc744 \uc815\ub9ac\ud558\ub294 \uae30\ub2a5\uc744 \ubc18\ud658\ud574\uc57c \ud569\ub2c8\ub2e4. (\uad6c\ub3c5\uc5d0 \uad00\ub828\ub41c \ub370\uc774\ud130\ub294 \ub9ac\uc2a4\ub108 \ubc30\uc5f4 \ud544\ub4dc\uc5d0 \ub123\uc5b4\uc11c \uad00\ub9ac\ud569\ub2c8\ub2e4.)\\n\\n- emitChange: \ub9ac\uc2a4\ub108 \ubc30\uc5f4 \ud544\ub4dc\uc5d0 \ub2f4\uaca8\uc788\ub294 \ubaa8\ub4e0 \ub9ac\uc2a4\ub108\ub97c \uc2e4\ud589\ud569\ub2c8\ub2e4. \uc989, \uad6c\ub3c5\ub41c \uc5b4\ub5a4 \uac83\uc744 \uc21c\ucc28\uc801\uc73c\ub85c \uc2e4\ud589\ud558\uac8c \ud569\ub2c8\ub2e4. \uc774\ub294 \ub9ac\uc561\ud2b8 DOM\uc744 \uac15\uc81c\ub85c \uc77c\uae68\uc6cc\uc8fc\ub294 \uc635\uc800\ubc84 \ud328\ud134\uc758 \uc5ed\ud560\uc744 \ud558\uac8c \ub429\ub2c8\ub2e4. \uc774 \uacfc\uc815 \ub54c\ubb38\uc5d0 react DOM\uc774 \uc815\ud655\ud55c \uc7ac \ub80c\ub354\ub9c1 \uc9c0\uc810\uc744 \ud30c\uc545\ud560 \uc218 \uc788\uac8c\ub429\ub2c8\ub2e4. (\ucd5c\uc801\ud654 \ubb38\uc81c\uc5d0\uc11c \uc790\uc720\ub85c\uc6cc\uc9d0)\\n\\n- setState: \uc0c1\ud0dc\ub97c \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4. \ub2e4\ub9cc \uc0c1\ud0dc\uac00 \uc5c5\ub370\uc774\ud2b8 \ub410\uc74c\uc744 \uc54c\ub824\uc57c \ud558\ubbc0\ub85c emitChange\ub97c \uc2e4\ud589\uc2dc\ucf1c react DOM\uc744 \uac15\uc81c\ub85c \ub3d9\uae30\ud654\uc2dc\ud0b5\ub2c8\ub2e4.\\n\\n- getState: \ud638\ucd9c\ub418\ub294 \uc21c\uac04 \ud604\uc7ac \uc0c1\ud0dc \uac12\uc744 \uc77d\uc2b5\ub2c8\ub2e4.\\n\\n\uc880 \uc5b4\ub835\uc9c0\ub9cc \ub9ac\uc561\ud2b8\uc5d0\uc11c \uc774\ub7f0 \uaddc\uaca9\uc744 \uac00\uc838\uc57c useSyncExternalStore\ud6c5\uc744 \uc4f8 \uc218 \uc788\uac8c \ud574 \uc90d\ub2c8\ub2e4.\\n\uae30\uc874 \uc608\uc81c\uc5d0\uc11c\ub294 \ub2e8\uc21c\ud55c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8 \uac1d\uccb4\ub85c \uc9dc\uc5ec\uc788\uc5c8\uc9c0\ub9cc \uc778\uc2a4\ud134\uc2a4\ub97c \uc790\uc720\ub86d\uac8c \ucc0d\uc5b4\ub0bc \uc218 \uc788\ub294 class \uad6c\uc870\ub85c \uac1c\uc120\ud558\uace0 \ucd94\uc0c1\ud654\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc2e4 \uc5ec\uae30\uae4c\uc9c0\ub9cc \uad6c\ud604\ud574\ub3c4 useSyncExternalStore\ub97c \uc0ac\uc6a9\ud558\ub294\ub370 \uc9c0\uc7a5\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\uc55e\uc11c \uc120\uc5b8\ud55c store\uac1d\uccb4\uc5d0\uc11c subscribe\uc640 getState\ub97c \uaebc\ub0b4\uc11c \uc9c1\uc811 \uc804\ub2ec\ud574 \uc8fc\uba74 \uadf8\ub9cc\uc774\uae30 \ub54c\ubb38\uc774\uc8e0.\\n\\n\ud558\uc9c0\ub9cc \uacb0\uad6d \uc774 \uacfc\uc815 \uc790\uccb4\uac00 \ubc18\ubcf5\ub41c \uc791\uc5c5\uc744 \uc694\uad6c\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n### \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \uc27d\uac8c \uc811\uadfc\ud558\ub3c4\ub85d \ucd9c\uad6c\ub97c \uc5f4\uc5b4\uc8fc\uc790!\\n\\n\ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c\ub294 \ubc14\ub2d0\ub77c JS\ub85c \uc0c1\ud0dc\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\ub294 \uac83\ubcf4\ub2e4\ub294 useState\uc640 \ube44\uc2b7\ud55c \ud615\ud0dc\ub85c \ud6c5\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \ud6e8\uc52c \ubcf4\uae30 \uae54\ub054\ud560 \uac83\uc785\ub2c8\ub2e4.\\n\ub9e4\ubc88 \uc2a4\ud1a0\uc5b4\uc5d0\uc11c \ubb34\uc5b8\uac00\ub97c \uc9c1\uc811 \uaebc\ub0b4\uc9c0 \uc54a\ub3c4\ub85d \ud558\ub294 \uc911\uac04 \ucee4\uc2a4\ud140 \ud6c5\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n```typescript\\nexport const useExternalState = (\\n store: DataObserver\\n): [T, (param: SetStateCallbackType | T) => void] => {\\n const { subscribe, getState, setState } = store;\\n const state = useSyncExternalStore(subscribe, getState);\\n\\n return [state, setState];\\n};\\n```\\n\\n\uc774 \ud6c5\uc740, \ubc14\uae65\uc5d0\uc11c \ubc1b\uc544\uc628 store\ub97c \ud65c\uc6a9\ud558\uc5ec \uad6c\ub3c5/\uc5c5\ub370\uc774\ud2b8 \uae30\ub2a5\uc744 \ubc30\uc5f4\ub85c \ubc18\ud658\ud569\ub2c8\ub2e4.\\n\ubaa8\uc2dd\ub3c4\ub97c \uadf8\ub824\ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./3.png)\\n\\nReact \ucef4\ud3ec\ub10c\ud2b8\ub294 \uc5b4\ub514\uc120\uac00 \uc0dd\uc131\ub41c store() \uac1d\uccb4\ub97c useExternalStore\uc5d0 \ub118\uaca8\uc8fc\uace0, \\\\[\uc0c1\ud0dc, \uc0c1\ud0dc\uc5c5\ub370\uc774\ud2b8\ud568\uc218\\\\]\ub97c \ubc1b\uac8c \ub429\ub2c8\ub2e4.\\n\ub9c8\uce58 \uae30\uc874\uc758 useState\ub098 useRecoilState\ucc98\ub7fc \ub9d0\uc774\uc8e0.\\n\\n\uc815\ub9ac\ud558\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\ud478\ub978 \uc601\uc5ed\uc740 React DOM\\n\ub179\uc0c9 \uc601\uc5ed\uc740 \uc9c1\uc811 \ud638\ucd9c\ud574\uc57c \ud558\ub294 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc758 \uc601\uc5ed (\ud558\uc9c0\ub9cc \ucd5c\ub300\ud55c \ub2e8\uc21c\ud55c \ud615\ud0dc\ub85c \uad6c\uc131\ud574\uc11c \uac1c\ubc1c\uc790\uc758 \ubd80\ub2f4\uc744 \ub35c\uc5b4\uc8fc\ub294 \ud615\ud0dc)\\n\ube68\uac04\uc0c9\uc740 \uac1c\ubc1c\uc790\uac00 \uc9c1\uc811 \uac74\ub4e4\uc9c0 \ubabb\ud558\uc9c0\ub9cc \uac04\uc811\uc801\uc73c\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uc601\uc5ed\\n\ub178\ub780\uc0c9\uc740 React 18 \uc5d4\uc9c4\uc758 \uc601\uc5ed\uc785\ub2c8\ub2e4.\\n\\n\uc774\uc678\uc5d0 \uc81c\uacf5\ub418\ub294 \ub2e4\ub978 \ucee4\uc2a4\ud140 \ud6c5\ub4e4\ub3c4 \uac70\uc758 \ube44\uc2b7\ud55c \uad6c\uc870\ub97c \ub744\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```typescript\\n// \ucd94\uac00\ub85c \uad6c\ud604\ud560 \uc218 \uc788\ub294 \ud568\uc218\ub4e4\\n\\nexport const useSetExternalState = (store: DataObserver) => {\\n const { setState } = store;\\n\\n return setState;\\n};\\n\\nexport const useExternalValue = (store: DataObserver) => {\\n const { subscribe, getState } = store;\\n const state = useSyncExternalStore(subscribe, getState);\\n\\n return state;\\n};\\n\\n// \ubc14\ub2d0\ub77cJS \uc601\uc5ed\uc5d0\uc11c \uc790\uc5f0\uc2a4\ub7ec\uc6b4 \uc77d\uae30\ub97c \uc9c0\uc6d0\ud558\ub294 \ud568\uc218\\n\\nexport const getStoreSnapshot = (store: DataObserver) => {\\n return store.getState();\\n};\\n```\\n\\n\ub354 \ub2e4\uc591\ud55c \uc608\uc81c\ub294 [\uc5ec\uae30\uc5d0\uc11c \ud655\uc778](https://github.com/gabrielyoon7/external-state/tree/main/src/examples)\ud560 \uc218 \uc788\uace0\\n\uc791\uc131\ud55c \ub77c\uc774\ube0c\ub7ec\ub9ac \ucf54\ub4dc \uc804\ubb38\uc740 [\uc5ec\uae30\uc5d0\uc11c \ud655\uc778](https://github.com/gabrielyoon7/external-state/tree/main/src/lib/external-state)\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uaca8\uc6b0 \ud30c\uc77c \uc218\uc2ed \uc904\ub85c \ub9cc\ub4e0 \ucd08\uacbd\ub7c9 \uc0c1\ud0dc\uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\uc600\uc2b5\ub2c8\ub2e4"},{"id":"28","metadata":{"permalink":"/28","source":"@site/blog/2023-08-23-about-the-map-system-used-by-carffeine/index.mdx","title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \uad00\ud558\uc5ec","description":"\uc548\ub155\ud558\uc138\uc694? \uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \ub300\ud574\uc11c \uc18c\uac1c\ud558\ub824\uace0 \ud569\ub2c8\ub2e4.","date":"2023-08-23T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 23\uc77c","tags":[{"label":"google maps api","permalink":"/tags/google-maps-api"},{"label":"\uad6c\uae00 \uc9c0\ub3c4","permalink":"/tags/\uad6c\uae00-\uc9c0\ub3c4"}],"readingTime":17.43,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"28","title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \uad00\ud558\uc5ec","authors":["gabriel"],"tags":["google maps api","\uad6c\uae00 \uc9c0\ub3c4"]},"prevItem":{"title":"useSyncExternalStore\ub85c \ub9cc\ub4e4\uc5b4\ubcf4\ub294 \uc804\uc5ed\uc0c1\ud0dc\uad00\ub9ac \ub3c4\uad6c","permalink":"/29"},"nextItem":{"title":"EC2 \uc11c\ubc84 \ucd94\uac00\uc640 \ub3d9\uc2dc\uc5d0 Dev, Prod \ud658\uacbd \ubd84\ub9ac\ud558\uae30","permalink":"/27"}},"content":"\uc548\ub155\ud558\uc138\uc694? \uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \ub300\ud574\uc11c \uc18c\uac1c\ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\uc9c0\ub3c4 \uae30\ub2a5\uc5d0\uc11c \uac00\uc7a5 \ud575\uc2ec\uc778 \uae30\ub2a5 \ub450 \uac00\uc9c0\ub97c \ubf51\uc790\uba74, \uc9c0\ub3c4 \uadf8 \uc790\uccb4\uc640 \uc9c0\ub3c4 \uc704\uc5d0 \uadf8\ub824\uc9c0\ub294 \ub9c8\ucee4\ub97c \ubf51\uc744 \uc218 \uc788\uc744 \uac83\uc785\ub2c8\ub2e4. \uc9c0\ub3c4 \uc704\uc5d0 \ub9c8\ucee4\ub97c \uadf8\ub9ac\ub294 \uc77c\uc740 \uadf8\ub2e4\uc9c0 \uc5b4\ub835\uc9c0 \uc54a\uace0, documents \uc5d0 \uc788\ub294 \uc608\uc81c\ub4e4\uc744 \uc798 \ub530\ub77c\ud558\uba74 \ub204\uad6c\ub098 \ucda9\ubd84\ud788 \uad6c\ud604\ud560 \uc218 \uc788\uc744 \uac83\uc785\ub2c8\ub2e4.\\n\\n![no offset](./markers-on-map.png)\\n\\n\ud558\uc9c0\ub9cc \ub9c8\ucee4\uc758 \uac2f\uc218\uac00 \uacfc\ub3c4\ud558\uac8c \ub9ce\ub2e4\uba74 \uc5b4\ub5a4 \uc804\ub7b5\uc744 \uc138\uc6b8 \uc218 \uc788\uc744\uae4c\uc694?\\n\\n### \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294\uc694 ...\\n\\n\\n\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc5d0\uc11c \uc9c0\ub3c4\ub294 \uad49\uc7a5\ud788 \uc911\uc694\ud55c \uc694\uc18c \uc911 \ud558\ub098\uc600\uc2b5\ub2c8\ub2e4. \uc0ac\uc6a9\uc790\ub4e4\uc774 \uad81\uae08\ud55c \uc7a5\uc18c\uc758 \uc8fc\ubcc0\uc5d0 \uc788\ub294 \ucda9\uc804\uc18c\ub97c \uc2dc\uac01\uc801\uc73c\ub85c \uc81c\uacf5\ud574\uc8fc\uae30 \uc704\ud574\uc11c\ub294 \uc9c0\ub3c4\ub97c \uc798 \uc81c\uc5b4\ud560 \uc218 \uc788\uc5b4\uc57c \ud588\uc2b5\ub2c8\ub2e4. \ud2b9\ud788 \uc804\uad6d\uc5d0 \uc774\ubbf8 `\uc218\ub9cc \ub300\uc758 \ucda9\uc804\uc18c`\uac00 \ubcf4\uae09\uc774 \ub41c \uc0c1\ud669\uc5d0\uc11c \ucda9\uc804\uc18c \ub9c8\ucee4\ub97c \ubaa8\ub450 \uadf8\ub824\uc8fc\uae30 \uc704\ud574\uc11c\ub294 \ub9ce\uc740 \uc81c\uc57d\uc774 \uc788\uc5c8\uace0, \ub9c8\ucee4\ub97c \uc801\ub2f9\ud55c \uc218\uc900\uc73c\ub85c \ub80c\ub354\ub9c1 \ud558\ub824\uba74 \ud074\ub77c\uc774\uc5b8\ud2b8\uc640 \uc11c\ubc84 \uac04\uc5d0 \ud2b9\ubcc4\ud55c \uc791\uc5c5\uc774 \ud544\uc694\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc5b4\ub5a4 \uc804\ub7b5\uc744 \ud3bc\ucce4\ub294\uc9c0 \uc18c\uac1c\ud558\uae30\uc5d0 \uc55e\uc11c \ubbf8\ub9ac \ub9d0\uc500\ub4dc\ub9ac\uc9c0\ub9cc, \uc800\ud76c \ud300\uc5d0\uc11c \ucde8\ud55c \uc9c0\ub3c4 \uad00\ub9ac \uc804\ub7b5\uc740 \ubaa8\ub4e0 \ud504\ub85c\uc81d\ud2b8\uc5d0 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc744 \uac83\uc785\ub2c8\ub2e4. \uc9c0\ub3c4 \uc704\uc5d0 \ud55c\ubc88\uc5d0 \ud45c\ud604\ud560 \ub9c8\ucee4\uc758 \uac2f\uc218\uac00 \uc218\ubc31 \uac1c \uc774\ud558\ub77c\uba74, \uc11c\ubc84\uc5d0 \ub370\uc774\ud130\uac00 \uacfc\ub3c4\ud558\uac8c \ub9ce\uc740 \uac83\uc774 \uc544\ub2c8\ub77c\uba74 \uc624\ud788\ub824 \uc774\ub7ec\ud55c \uc804\ub7b5\uc774 \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc744 \ud574\uce60 \uc218 \uc788\uc744 \uac83\uc785\ub2c8\ub2e4. (\ud658\uacbd\uc774 \uc6d0\ud65c\ud558\ub2e4\uba74 \ub370\uc774\ud130\ub97c \uac00\ub2a5\ud55c \ub9ce\uc774 \ubcf4\uc5ec\uc8fc\ub294 \uac83\uc774 \uc88b\uc744\ud14c\ub2c8\uae50\uc694.)\\n\\n\ub610, \uc774 \uae00\uc5d0\uc11c\ub294 Google Maps API\ub97c \uae30\uc900\uc73c\ub85c \uc124\uba85\ud558\uace0 \uc788\uc9c0\ub9cc, \uc9c0\uc6d0\ud558\ub294 \uae30\ub2a5\uc774 \uc77c\ubd80 \ub2e4\ub974\ub354\ub77c\ub3c4 \ub300\ubd80\ubd84\uc758 \uc9c0\ub3c4 API\uc5d0\uc11c \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud55c \uc804\ub7b5\uc77c \uac83\uc785\ub2c8\ub2e4. \ucc38\uace0\ub85c \uac1c\uc778\uc801\uc73c\ub85c \uc0ac\uc6a9 \ud574\ubcf8 \uc5ec\ub7ec \ubca4\ub354 \uc0ac\uc758 \uc9c0\ub3c4 API\ub4e4\uc740 \ubaa8\ub450 \uc774\uc640 \uc720\uc0ac\ud55c \uae30\ub2a5\uc744 \uc81c\uacf5\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\\n### \uc88c\ud45c\ub780 \ubb34\uc5c7\uc77c\uae4c?\\n\\n\uc544\ub9c8 \uc5b4\ub9b0 \uc2dc\uc808\ubd80\ud130 \uc6b0\ub9ac\ub098\ub77c\uc5d0\ub294 \ud2b9\ubcc4\ud788 38\uc120\uc774\ub77c\ub294 \uac83\uc774 \uc874\uc7ac\ud55c\ub2e4\ub294 \uc0ac\uc2e4\uc744 \uad50\uc721\ubc1b\uae30\uc5d0 `\uc88c\ud45c\uacc4\ub77c\ub294 \uac83\uc774 \uc788\ub2e4\ub294 \uc0ac\uc2e4`\uc740 \ub204\uad6c\ub098 \uc54c \uac83\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \ub2f9\uc7a5 \uc704\ub3c4\uc640 \uacbd\ub3c4\ub97c \uad6c\ubd84\uc9c0\uc73c\ub77c\uace0 \ud558\uba74 \uc5b4\ub5a4 \uc120\uc774 \uc704\uc120\uc774\uace0 \uacbd\uc120\uc778\uc9c0 \ud5f7\uac08\ub9ac\uae30\uc5d0 \ucc0d\uc5b4\uc57c \ud560 \uac83\uc785\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc774 \uc120\uc774 \uc5b4\ub5a4 \uc120\uc778\uc9c0, \uc5b4\ub5a4 \uac12\uc744 \uc598\uae30\ud558\ub824\ub294 \uac83\uc778\uc9c0 \uc0ac\uc9c4\uacfc \ud568\uaed8 \uac04\ub2e8\ud788 \uc124\uba85\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./latlng.jpeg)\\n\\n\uc0ac\uc9c4\uc744 \ubcf4\uc2dc\uba74 \uc544\uc2dc\uaca0\uc9c0\ub9cc \uc704\ub3c4\ub780, \ub0a8\ubd81\uc758 \uc704\uce58\ub97c \ub098\ud0c0\ub0b4\ub294 \ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \uacbd\ub3c4\ub294 \ub3d9\uc11c\uc758 \uc704\uce58\ub97c \ub098\ud0c0\ub0b4\ub294 \ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \ub300\ubd80\ubd84\uc758 \uacf5\uc2dd \ubb38\uc11c\uac00 \uc601\uc5b4\ub85c \uc791\uc131\ub418\uc5b4\uc788\uace0, \ucf54\ub4dc\uc5d0\uc11c\ub3c4 \uc774\ub97c \ub098\ud0c0\ub0b4\ub294 \uac83\uc774 \uc911\uc694\ud558\uae30\uc5d0 \uc601\ubb38 \ud45c\uae30\ubc95\uae4c\uc9c0 \uc18c\uac1c\ub97c \ud558\uc790\uba74 \uc704\ub3c4\ub294 Latitude, \uacbd\ub3c4\ub294 Longitude\ub85c \ud45c\uae30\ud569\ub2c8\ub2e4. \uc774\uc720\ub294 \ubaa8\ub974\uaca0\uc9c0\ub9cc \uc81c\uacf5\ub418\ub294 \ubcc0\uc218\ub098 \uba54\uc11c\ub4dc \uba85\uc73c\ub85c lat, lng\ub77c\uace0 \uc904\uc5ec\uc11c \ud45c\uae30\ud558\uae30\ub3c4 \ud569\ub2c8\ub2e4.\\n\\n![no offset](./latlngeng.gif)\\n\\n\uc704\ub3c4\uc640 \uacbd\ub3c4\ub9cc \uc54c\uba74, \uc9c0\uad6c \uc704\uc758 \uc5b4\ub5a4 \uc704\uce58\ub97c \ub098\ud0c0\ub0bc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c, \uc5b4\ub5a4 \ub9c8\ucee4\ub97c \uc5b4\ub5a4 \uc704\uce58\uc5d0 \ucc0d\uc744 \uac83\uc778\uc9c0\ub294 \uc704\ub3c4\uc640 \uacbd\ub3c4 \uac12\uc73c\ub85c \uacb0\uc815\ud560 \uc218 \uc788\uac8c \ub418\uaca0\uc8e0?\\n\\n### \uc0ac\uc6a9\uc790\uac00 \uc5b4\ub51c \ubcf4\uace0 \uc788\uc744\uae4c?\\n\\n\uc9c0\ub3c4 api\uc5d0\uc11c \uc81c\uacf5\ud574\uc8fc\ub294 \uba54\uc11c\ub4dc\ub97c \ud65c\uc6a9\ud558\uba74 \uc0ac\uc6a9\uc790\uc758 \ub514\ubc14\uc774\uc2a4\uac00 \uc5b4\ub290 \uc704\uce58\ub97c \ubcf4\uace0 \uc788\ub294\uc9c0 \uc54c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```typescript\\nlet map = /* \uc5b4\ub514\uc120\uac00 \uc0dd\uc131\ub41c \uad6c\uae00 \ub9f5 \uac1d\uccb4 */\\nconst center = map.getCenter();\\nconsole.log(center.lng()); // \ub514\ubc14\uc774\uc2a4 \uc911\uc2ec\uc758 longitude\\nconsole.log(center.lat()); // \ub514\ubc14\uc774\uc2a4 \uc911\uc2ec\uc758 latitude\\n```\\n\\n\uc9c0\ub3c4 \uac1d\uccb4\ub85c \ubd80\ud130 \uc911\uc2ec\uc810\uc744 \uc54c\uac8c\ub418\uba74 \ud574\ub2f9 \ub514\ubc14\uc774\uc2a4\uc758 \uc911\uc2ec\uc758 \uc88c\ud45c\ub97c \uc54c\uc544\ub0bc \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\\n![no offset](./get-center.png)\\n\\n### \uc0ac\uc6a9\uc790\uc758 \ub514\ubc14\uc774\uc2a4\ub294 \uc5bc\ub9c8\ub098 \ub113\uac8c \ubcf4\uace0 \uc788\uc744\uae4c?\\n\\n\uc9c0\ub3c4 api\uc5d0\uc11c \uc81c\uacf5\ud574\uc8fc\ub294 \uba54\uc11c\ub4dc\ub97c \ud65c\uc6a9\ud558\uba74 \uc0ac\uc6a9\uc790\uc758 \ub514\ubc14\uc774\uc2a4\uac00 \uc5b4\ub5a4 \uc601\uc5ed\uc744 \ubcf4\uace0 \uc788\ub294\uc9c0\ub3c4 \uc54c\uac8c \ub429\ub2c8\ub2e4. \uc9c0\ub3c4 api \ub9c8\ub2e4 \uc81c\uacf5\ud558\ub294 \uc2a4\ud399\uc774 \ub2e4\ub974\uc9c0\ub9cc, \ub300\ubd80\ubd84\uc740 \uc5b4\ub5a4 \uc2dd\uc73c\ub85c\ub4e0 \uc54c\ub824\uc90d\ub2c8\ub2e4.\\n\\ngoogle maps API\uc5d0\uc11c\ub294 \ub514\uc2a4\ud50c\ub808\uc774\uc758 \ubd81\ub3d9\ucabd \ub05d \uc810\uc758 \uc88c\ud45c\uc640, \ub0a8\uc11c\ucabd \ub05d \uc810\uc758 \uc88c\ud45c\ub97c \uc81c\uacf5\ud574\uc90d\ub2c8\ub2e4.\\n\\n```typescript\\nconst map = /* \uc5b4\ub514\uc120\uac00 \uc0dd\uc131\ub41c \uad6c\uae00 \ub9f5 \uac1d\uccb4 */\\nconst bounds = map.getBounds();\\nconsole.log(bounds.getNorthEast().lng(), bounds.getNorthEast().lat()); // \ub514\ubc14\uc774\uc2a4 1\uc0ac\ubd84\uba74 \ub05d \uc810\uc758 longitude\uc640 latitude\\nconsole.log(bounds.getSouthWest().lng(), bounds.getSouthWest().lat()); // \ub514\ubc14\uc774\uc2a4 3\uc0ac\ubd84\uba74 \ub05d \uc810\uc758 longitude\uc640 latitude\\n```\\n\\n![no offset](./get-bounds.png)\\n\\n\ud3b8\uc758\uc0c1 \uc88c\ud45c\ub97c \ub2e4\uc74c\uacfc \uac19\uc774 \uc815\uc758\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n- \uc911\uc2ec \uc810 p0: (x0, y0)\\n- \ub514\ubc14\uc774\uc2a4\uc758 \uc81c 1\uc0ac\ubd84\uba74 \ub05d\uc810 p2: (x2, y2)\\n- \ub514\ubc14\uc774\uc2a4\uc758 \uc81c 3\uc0ac\ubd84\uba74 \ub05d\uc810 p1: (x1, y1)\\n\\n```\\n\uc704 \uc815\uc758\ub294 \uc544\ub798\uc5d0\uc11c\ub3c4 \uacc4\uc18d \uc124\uba85 \ub420 \uc810\uacfc \uc88c\ud45c \uc785\ub2c8\ub2e4.\\n```\\n\\n\uc774\ub807\uac8c \uc54c\uc544\ub0b8 \uac12\uc73c\ub85c \uc0ac\uc6a9\uc790 \ub514\ubc14\uc774\uc2a4\uc758 \uc601\uc5ed\uc744 \uc54c\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc774 \uac12\uc744 \uc880 \ub354 \ud6a8\uc728\uc801\uc73c\ub85c \ub2e4\ub8e8\uae30 \uc704\ud574 delta \uac1c\ub150\uc744 \ub3c4\uc785\ud588\uc2b5\ub2c8\ub2e4.\\n\\n### \ud654\uba74\uc5d0\uc11c \ubcf4\uace0 \uc788\ub294 \uc601\uc5ed\uc744 \ud655\ub300/\ucd95\uc18c \ud558\uba74 \uc5b4\ub5a4 \ud2b9\uc9d5\uc744 \ubcf4\uc77c\uae4c?\\n\\ndelta \uc124\uba85\uc744 \uc55e\uc11c, \uc0ac\uc6a9\uc790\uc758 \ub514\ubc14\uc774\uc2a4 \uc601\uc5ed\uacfc \ud655\ub300 \uc218\uc900\uc5d0 \ub530\ub978 \uc2e4\uc81c \uc88c\ud45c\uc5d0 \ub300\ud574 \uc54c\uc544\ubcf4\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790\uac00 \ud654\uba74\uc744 \uc5bc\ub9c8\ub098 \ub113\uac8c \ubcf4\uace0 \uc788\ub294\uc9c0\ub97c \uc27d\uac8c \uc54c\uae30 \uc704\ud574\uc11c\ub294 \ub05d\uc810\ub4e4\uc758 \uc218\uce58\ub97c \uacc4\uc0b0\ud574\uc904 \ud544\uc694\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc9c4\uc740 \uc0ac\uc6a9\uc790\uac00 \ub514\ubc14\uc774\uc2a4\ub97c \ud1b5\ud574 \ubc14\ub77c \ubcf4\uace0 \uc788\ub294 \uc911\uc2ec \uc88c\ud45c\uc640 \uadf8 \ub05d \uc810\uc744 \uc758\ubbf8\ud569\ub2c8\ub2e4.\\n\\n![no offset](./map-with-different-size.png)\\n\\n\\n\uc608\ub97c \ub4e4\uc5b4 \uc0ac\uc6a9\uc790\uac00 \uc9c0\ub3c4\ub97c \ub9ce\uc774 \ucd95\uc18c\ud55c \uacbd\uc6b0\uc5d0\ub294 \uc911\uc2ec \uc810 p0\uc740 \uadf8\ub300\ub85c\uc9c0\ub9cc \uc591 \ub05d\uc810 p1, p2\uc758 \uc704\uce58\uac00 \uc810\uc810 \uc911\uc2ec \uc810 p0\uc73c\ub85c \ubd80\ud130 \uba40\uc5b4\uc9c8 \uac83\uc785\ub2c8\ub2e4.\\n\\n\ubc18\uba74\uc5d0 \uc0ac\uc6a9\uc790\uac00 \uc9c0\ub3c4\ub97c \ub9ce\uc774 \ud655\ub300\ud55c \uacbd\uc6b0\uc5d0\ub294 \uc911\uc2ec \uc810 p0\uc740 \uadf8\ub300\ub85c\uc9c0\ub9cc \uc591 \ub05d\uc810 p1, p2\uc758 \uc704\uce58\uac00 \uc810\uc810 \uc911\uc2ec\uc810\uacfc \uac00\uae4c\uc6cc\uc9c8 \uac83\uc785\ub2c8\ub2e4.\\n\\n![no offset](./map-with-different-zoom.png)\\n\\n\uc591 \uc0ac\uc9c4 \ubaa8\ub450 \uc911\uc2ec \uc810 p0\ub294 \uadf8\ub300\ub85c\uc9c0\ub9cc, \ub514\ubc14\uc774\uc2a4\uc758 \ud655\ub300 \uc218\uc900\uc73c\ub85c \uc778\ud574 \uc591 \ub05d\uc810\uc778 p1\uacfc p2\uac00 \ub2ec\ub77c\uc9c4 \ubaa8\uc2b5\uc744 \ubcf4\uc778 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc989, \uc774\ub7f0 \uacb0\ub860\uc744 \ub0b4\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc591 \ub05d\uc810 p1, p2\uac00 \uc911\uc2ec \uc810 p0\uc73c\ub85c \ubd80\ud130 \uba40\uc5b4\uc9c8 \uc218\ub85d \uc9c0\ub3c4\ub97c \ucd95\uc18c\ud55c \uac83\uc774\ub2e4.\\n2. \uc591 \ub05d\uc810 p1, p2\uac00 \uc911\uc2ec \uc810 p0\uc73c\ub85c \ubd80\ud130 \uac00\uae4c\uc6cc \uc218\ub85d \uc9c0\ub3c4\ub97c \ud655\ub300\ud55c \uac83\uc774\ub2e4.\\n\\n\uc774 \ub54c \ub514\ubc14\uc774\uc2a4\uc758 \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc704\ub3c4 \uacbd\ub3c4 \uc0c1\uc73c\ub85c \uc5bc\ub9c8\ub098 \uba40\uc5b4\uc838\uc788\ub294\uc9c0\ub97c \uc218\uce58\ud654\ud558\uba74 \ud3b8\ud558\uac8c \ub2e4\ub8f0 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \ud655\ub300 \uc218\uc900\uc744 \uc218\uce58\ud654 \ud560 \uc218 \uc5c6\uc744\uae4c?\\n\\n\uc0ac\uc6a9\uc790\uc758 \ub514\uc2a4\ud50c\ub808\uc774\uc758 \uc911\uc2ec \uc810 p0\uc744 \uae30\uc900\uc73c\ub85c \ud558\uc5ec \uc591 \ub05d\uc810 p1, p2\uc774 \uc5bc\ub9c8\ub098 \uba40\uc5b4\uc838\uc788\ub294\uc9c0\uc5d0 \ub530\ub77c \uc9c0\ub3c4\uc758 \uc601\uc5ed \ubfd0\ub9cc \uc544\ub2c8\ub77c \uc5bc\ub9c8\ub098 \ub9ce\uc774 \ud655\ub300 \ub418\uc5c8\ub294\uc9c0 \uc5ec\ubd80\ub97c \uc54c\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub807\ub2e4\uba74 \uc774\ub97c \uc880 \ub354 \ud6a8\uc728\uc801\uc778 \ubc29\ubc95\uc73c\ub85c \ub098\ud0c0\ub0b4\ub824\uba74 \uc5b4\ub5a4 \uc804\ub7b5\uc744 \ucde8\ud560 \uc218 \uc788\uc744\uae4c\uc694?\\n\\n\uc0ac\uc6a9\uc790 \ub514\uc2a4\ud50c\ub808\uc774\ub97c \uc870\uae08 \ub354 \uc790\uc138\ud788 \uc0b4\ud3b4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./map-points.png)\\n\\n\uc911\ud559\uad50 \uc2dc\uc808 \ubc30\uc6e0\ub358 \uc88c\ud45c \ud3c9\uba74\uacc4\ub97c \ub5a0\uc62c\ub824\ubcf4\uba74 \ud654\uba74\uc5d0\uc11c \uc5bb\uc744 \uc218 \uc788\ub294 \uc88c\ud45c\ub4e4\uc740 \uc704\uc640 \uac19\uc2b5\ub2c8\ub2e4. \uc5ec\uae30\uc5d0\uc11c \uac01 \uc810\uc758 \uc218\uc9c1/\uc218\ud3c9\uc758 \ubcc0\ud654\ub7c9\uc778 delta\ub97c \uc54c\uc544\ubcf4\uba74 \uc5b4\ub5a8\uae4c\uc694?\\n\\n#### \uacbd\ub3c4 \ub378\ud0c0 (longitudeDelta)\\n\\np2\uc640 p0\uc758 \uacbd\ub3c4 \uac70\ub9ac, \uadf8\ub9ac\uace0 p1\uacfc p0\uc758 \uacbd\ub3c4 \uac70\ub9ac\ub294 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc989, `x2 - x0 === x0 - x1` \uc774\ub77c\ub294 \uacb0\ub860\uc744 \uc5bb\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c longitudeDelta\ub85c \uc815\uc758\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n#### \uc704\ub3c4 \ub378\ud0c0 (latitudeDelta)\\n\\np2\uc640 p0\uc758 \uc704\ub3c4 \uac70\ub9ac, \uadf8\ub9ac\uace0 p1\uacfc p0\uc758 \uc704\ub3c4 \uac70\ub9ac\ub294 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc989, `y2 - y0 === y0 - y1` \uc774\ub77c\ub294 \uacb0\ub860\uc744 \uc5bb\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c latitudeDelta\ub85c \uc815\uc758\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\\n![no offset](./delta.png)\\n\\n\ucf54\ub4dc\ub85c \uc54c\uc544\ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n```typescript\\nconst map = /* \uc5b4\ub514\uc120\uac00 \uc0dd\uc131\ub41c \uad6c\uae00 \ub9f5 \uac1d\uccb4 */\\nconst bounds = map.getBounds();\\nconst longitudeDelta = (bounds.getNorthEast().lng() - bounds.getSouthWest().lng()) / 2; // \uacbd\ub3c4 \ubcc0\ud654\ub7c9\\nconst latitudeDelta = (bounds.getNorthEast().lat() - bounds.getSouthWest().lat()) / 2; // \uc704\ub3c4 \ubcc0\ud654\ub7c9\\n```\\n\\n\ub4dc\ub514\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0\uc11c \ub378\ud0c0 \uac12\uc744 \uc0dd\uc131\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub807\ub2e4\uba74 \uc65c \uc774\ub807\uac8c \uad73\uc774 \ub378\ud0c0 \uac12\uc744 \uc0dd\uc131\ud55c \uac83\uc77c\uae4c\uc694?\\n\\n### delta\uc758 \uc720\uc6a9\ud55c \uc810 1: \uc6d0\ub798 \uc758\ub3c4\ud55c \uac12\uc744 \ubcf5\uc6d0\ud558\uae30 \uc27d\ub2e4.\\n\\n\uc11c\ubc84\uc758 \uc785\uc7a5\uc5d0\uc11c\ub294 \uc911\uc2ec \uc88c\ud45c\uc640 \ub378\ud0c0 \uac12\ub9cc \uc54c\uba74 \uc815\ud655\ud55c \uc601\uc5ed\ub9cc\ud07c \ub370\uc774\ud130\ub97c \ud638\ucd9c\ud560 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0\uc11c \uc11c\ubc84\ub85c \ub2e4\uc74c\uacfc \uac19\uc740 \ud30c\ub77c\ubbf8\ud130\ub97c \ub118\uaca8\uc92c\ub2e4\uace0 \uac00\uc815\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```json\\n{\\n \\"longitude\\": 127,\\n \\"latitude\\": 37,\\n \\"longitudeDelta\\": 0.1,\\n \\"longitudeDelta\\": 0.2,\\n}\\n```\\n\\n\uadf8\ub807\ub2e4\uba74 \uc11c\ubc84\uc5d0\uc11c\ub294 \ub2e4\uc74c\uacfc \uac19\uc774 \ud574\uc11d\ud560 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n```javascript\\nconst maxLongitude = longitude + longitudeDelta;\\nconst minLongitude = longitude - longitudeDelta;\\nconst maxLatitude = latitude + latitudeDelta;\\nconst minLatitude = latitude - latitudeDelta;\\n```\\n(javascript \uae30\uc900\uc73c\ub85c \uc791\uc131\ud588\uc2b5\ub2c8\ub2e4.)\\n\\n\uc774\ub807\uac8c \uc54c\uc544\ub0b8 \uacbd\uacc4 \uac12\uc744 \uac00\uc9c0\uace0 \ub2e4\uc74c\uacfc \uac19\uc740 sql\ubb38\uc744 \uc791\uc131\ud560 \uc218 \uc788\uac8c \ub420 \uac83\uc785\ub2c8\ub2e4.\\n\\n```sql\\nSELECT * FROM stations WHERE latitude >= :minLatitude AND latitude <= :maxLatitude AND longitude >= :minLongitude AND longitude <= :maxLongitude;\\n```\\n\\n![no offset](./find-within-range.png)\\n\\n\uc989, \uc704 \uadf8\ub9bc\ucc98\ub7fc, \uc6d0\ud558\ub294 \uc601\uc5ed\ub9cc\ud07c\ub9cc \uc815\ud655\ud558\uac8c \ub370\uc774\ud130\ub97c \ud638\ucd9c\ud560 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\\n### delta\uc758 \uc720\uc6a9\ud55c \uc810 2: \ub378\ud0c0\uac00 \ubb34\ubd84\ubcc4\ud558\uac8c \ucee4\uc9c0\ub294 \uac83\uc744 \ub9c9\uae30 \uc27d\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uc5b4 \uc0ac\uc6a9\uc790\uac00 \uc9c0\ub3c4\ub97c \ucd95\uc18c\ud558\uc5ec \ud55c\ubc18\ub3c4\ub97c \ub514\uc2a4\ud50c\ub808\uc774\uc5d0 \uac00\ub4dd \ucc44\uc6b4\ub2e4\uba74 \uc11c\ubc84\uac00 \uc5b4\ub5bb\uac8c \ub420\uae4c\uc694?\\n\\n\uc774\ub7ec\ud55c \ud589\uc704\ub97c \ub9c9\ub294 \uac00\uc7a5 \uc26c\uc6b4 \ubc29\ubc95\uc740 \uc9c0\ub3c4 api\uc5d0\uc11c \uc9c0\uc6d0\ud558\ub294 \uc90c \ub808\ubca8\uc744 \uc81c\ud55c \ud558\ub294 \uac83\uc785\ub2c8\ub2e4. \ud6c4\uc220\ud558\uaca0\uc9c0\ub9cc _\uc90c \ub808\ubca8\uc740 \ub514\uc2a4\ud50c\ub808\uc774\uc758 \ud574\uc0c1\ub3c4\ub97c \uace0\ub824\ud558\uc9c0 \ubabb\ud569\ub2c8\ub2e4._\\n\\n\ub530\ub77c\uc11c \uadfc\ubcf8\uc801\uc73c\ub85c \ub378\ud0c0\uac00 \uc77c\uc815 \uac12 \uc774\uc0c1 \uc694\uccad\ub418\uc9c0 \ubabb\ud558\ub3c4\ub85d, \ud639\uc740 \uc5f0\uc0b0\ub418\uc9c0 \ubabb\ud558\ub3c4\ub85d \ub9c9\uac8c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ubb3c\ub860 \ub378\ud0c0\uac00 \uc5c6\ub354\ub77c\ub3c4 \ub378\ud0c0 \uac12\uc744 \ucd94\uc815\ud558\uc5ec \uc5f0\uc0b0\ud560 \uc218 \uc788\uaca0\uc9c0\ub9cc, \uc774\ub97c _\uc218\uce58\ud654 \ud574\uc11c \uad00\ub9ac\ud55c\ub2e4\uba74 \ud074\ub77c\uc774\uc5b8\ud2b8\uc640 \uc11c\ubc84 \ubaa8\ub450 \uc9c0\ub3c4\ub97c \uc190\uc27d\uac8c \ud1b5\uc81c\ud558\ub294 \uac83\uc774 \uac00\ub2a5_\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uc5b4 \ub2e4\uc74c\uacfc \uac19\uc774 \ub378\ud0c0 \uac12\uc744 \uace0\uc815\ud558\uc5ec \uc694\uccad \uc601\uc5ed\uc744 \uc81c\ud55c\ud560(\uc694\uccad\uc744 \ubcf4\ub0b4\uc9c0 \uc54a\uac70\ub098 \uace0\uc815\ub41c \uc0ac\uc774\uc988\ub85c\ub9cc \uc694\uccad\uc744 \ubcf4\ub0bc) \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```json\\n{\\n longitude,\\n latitude,\\n longitudeDelta: longitudeDelta < 0.008 ? longitudeDelta : 0.008,\\n latitudeDelta: latitudeDelta < 0.004 ? latitudeDelta : 0.004,\\n}\\n```\\n\\n\ud2b9\uc815 \uc218\uce58\ub97c \ub118\uae30\uc9c0 \ubabb\ud558\uac8c \ucc98\ub9ac\ud560 \ub54c \ub208\uc5d0 \ubcf4\uc774\ub294 \ubcc0\uc218\ub85c \ucde8\uae09\ud558\uae30 \uc27d\uc2b5\ub2c8\ub2e4. (\uc989, \ub9e4\ubc88 \uacc4\uc0b0\ud558\uc9c0 \uc54a\uc544\ub3c4 \ub429\ub2c8\ub2e4.)\\n\\n\ub514\ubc14\uc774\uc2a4 \ud06c\uae30 \uad00\ub828 \ubb38\uc81c\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ubd84\uba85\ud788 \uac19\uc740 \uc90c \ub808\ubca8\uc774\uc9c0\ub9cc, \ub514\ubc14\uc774\uc2a4\uc758 \ud06c\uae30\ub098 \ud574\uc0c1\ub3c4\uc5d0 \ub530\ub77c \uc9c0\ub3c4\uac00 \ubcf4\uc5ec\uc9c0\ub294 \uc815\ub3c4\uac00 \ub2e4\ub985\ub2c8\ub2e4.\\n\\n![no offset](./different-device-size.png)\\n\\n\uc704 \uc0ac\uc9c4\uc740 \uad6c\uae00\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 zoom \ub808\ubca8\uc744 \ub3d9\uc77c\ud558\uac8c \ub9de\ucd98 \ud6c4, \uc5ec\ub7ec \ub514\ubc14\uc774\uc2a4\uc5d0\uc11c \ud638\ucd9c\ud55c \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc90c \ub808\ubca8\uc744 \ud1b5\ud574\uc11c \uc694\uccad\uc744 \uc81c\ud55c\ud558\ub2e4\ubcf4\uba74 \uc5ec\ub7ec \ud574\uc0c1\ub3c4\ub97c \uc81c\uc5b4\ud558\uae30 \uc5b4\ub835\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./too-big-screen.png)\\n\\n\uc2e4\uc81c\ub85c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uace0\ud574\uc0c1\ub3c4 \ubaa8\ub2c8\ud130\ub97c \ub300\uc751\ud558\uae30 \uc704\ud574 \ub378\ud0c0 \uac12\uc774 \ub108\ubb34 \ud06c\uac8c \ub418\uba74 \uc694\uccad\uc758 \uc81c\ud55c\uc744 \ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uc0ac\uc9c4\uc5d0\uc11c \ubcf4\uc2dc\ub2e4\uc2dc\ud53c \uace0\ud574\uc0c1\ub3c4 \ubaa8\ub2c8\ud130\uc758 \uacbd\uc6b0, \ub108\ubb34 \ub113\uc740 \ubc94\uc704\ub97c \uc694\uccad\ud55c\ub2e4 \uc2f6\uc73c\uba74 \uc911\uc2ec\uc810\uc73c\ub85c \ubd80\ud130 \uc77c\uc815 \uac70\ub9ac\ub9cc \ubcf4\uc5ec\uc8fc\ub3c4\ub85d \ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n(\ucc38\uace0\ub85c \uc90c \ub808\ubca8\uc5d0 \ub530\ub978 \uc694\uccad\ub3c4 \ub364\uc73c\ub85c \uc81c\ud55c\ud558\uace0 \uc788\uc5b4\uc11c \uba40\ub9ac\uc11c \ud638\ucd9c\ud558\ub294 \ud589\uc704\ub3c4 \uae08\uc9c0\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.)\\n\\n### delta\uc758 \uc720\uc6a9\ud55c \uc810 3: \uc801\ub2f9\ud55c \ubc94\uc704\ub97c \uc815\ud574\uc8fc\uae30 \ud3b8\ud558\ub2e4\\n\\n\uc704 \uc608\uc81c\uc5d0\uc11c\ub294 \uc815\ud655\ud55c \ubc94\uc704\ub9cc\ud07c \uc694\uccad\ud558\ub294 \uac83\uc744 \uc608\uc81c\ub85c \ud558\uc9c0\ub9cc, \ud504\ub85c\uc81d\ud2b8\uc5d0 \ub530\ub77c\uc11c \uc870\uae08 \ub354 \ub113\uc740 \uc601\uc5ed\uc744 \ud638\ucd9c\ud558\uace0 \uc2f6\uc744 \ub54c\uac00 \uc788\uc744 \uac83\uc785\ub2c8\ub2e4.\\n\\n![no offset](./bigger-than-delta.png)\\n\\n\uc608\ub97c \ub4e4\uc5b4 \ud604\uc7ac \uc0ac\uc6a9\uc790\uc758 \ub514\ubc14\uc774\uc2a4 \ud06c\uae30\ubcf4\ub2e4 \uc0b4\uc9dd \ud070 \ubc94\uc704\uc758 \ub370\uc774\ud130\ub97c \ubbf8\ub9ac \ub85c\ub4dc\ud574 \ub193\uc73c\uba74 \uc0ac\uc6a9\uc790\uac00 \uc881\uc740 \uc6c0\uc9c1\uc784\uc744 \ubcf4\uc77c \ub54c \ubd88\ud544\uc694\ud55c \uc7ac \ub80c\ub354\ub9c1\uc744 \uc904\uc5ec\uc11c \ub354 \ube60\ub978 \ub80c\ub354\ub9c1\uc774 \uac00\ub2a5\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uc0ac\uc2e4 \uc774 \uae30\ubc95\uc740 \ud504\ub85c\uc81d\ud2b8\ub9c8\ub2e4 \ub2e4\ub974\uaca0\uc9c0\ub9cc, \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ud55c\ubc88 \ubd88\ub7ec\uc628 \ub9c8\ucee4\ub97c \ub9e4\ubc88 \ud574\uc81c \ud558\uc9c0 \uc54a\uace0 **\uc774\uc804 \uc694\uccad \ub370\uc774\ud130\uc640 \ub2e4\uc74c \uc694\uccad \ub370\uc774\ud130\ub97c \ube44\uad50\ud558\uc5ec \ub2ec\ub77c\uc9c4 \ub9c8\ucee4\ub9cc\uc744 \uc815\ud655\ud558\uac8c \ud0c8\ubd80\ucc29\ud558\ub294 \uc791\uc5c5\uc744 \uc9c4\ud589**\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uae30\ubc95\uc744 \ud65c\uc6a9\ud558\uba74 \uc0ac\uc6a9\uc790\uac00 \uc881\uc740 \ubc94\uc704\uc5d0\uc11c \uc6c0\uc9c1\uc784\uc744 \ubcf4\uc600\uc744 \ub54c, \uae30\uc874\uc5d0 \ubd88\ub7ec\uc628 \ub9c8\ucee4\ub97c \uba54\ubaa8\ub9ac\uc5d0\uc11c \ud0c8\ub77d\uc2dc\ud0a4\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc744 \uac1c\uc120\ud560 \uc218\ub3c4 \uc788\uc744 \uac83\uc785\ub2c8\ub2e4.\\n\\n\ub9c8\ucee4\ub97c \uc0c1\ud0dc\uc5d0 \uc5f0\ub3d9\ud558\uc5ec \uc815\ud655\ud558\uac8c \uba54\ubaa8\ub9ac\uc5d0\uc11c \ud0c8\ubd80\ucc29 \uc2dc\ud0a4\ub294 \uc804\ub7b5\uc5d0 \ub300\ud55c \uae00\uc740 \uc774\ud6c4\uc5d0 \uc791\uc131\ud560 \uc608\uc815\uc785\ub2c8\ub2e4.\\n\\n\uae34 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4."},{"id":"27","metadata":{"permalink":"/27","source":"@site/blog/2023-08-17-given-ec2-prod-dev-sep.mdx","title":"EC2 \uc11c\ubc84 \ucd94\uac00\uc640 \ub3d9\uc2dc\uc5d0 Dev, Prod \ud658\uacbd \ubd84\ub9ac\ud558\uae30","description":"\uc548\ub155\ud558\uc138\uc694.","date":"2023-08-17T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 17\uc77c","tags":[{"label":"ec2","permalink":"/tags/ec-2"},{"label":"prod","permalink":"/tags/prod"},{"label":"dev","permalink":"/tags/dev"}],"readingTime":3,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"27","title":"EC2 \uc11c\ubc84 \ucd94\uac00\uc640 \ub3d9\uc2dc\uc5d0 Dev, Prod \ud658\uacbd \ubd84\ub9ac\ud558\uae30","authors":["jay"],"tags":["ec2","prod","dev"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \uad00\ud558\uc5ec","permalink":"/28"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300 \ud074\ub77c\uc774\uc5b8\ud2b8\uc758 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654","permalink":"/26"}},"content":"\uc548\ub155\ud558\uc138\uc694.\\n\uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\uc624\ub298\uc740 \uc800\ud76c\uac00 EC2 \uc778\uc2a4\ud134\uc2a4\ub97c \ubc1b\uc73c\uba74\uc11c, \uc5b4\ub5bb\uac8c dev, prod \ubc30\ud3ec \ud658\uacbd\uc744 \ubd84\ub9ac\ud588\ub294\uc9c0 \uc801\uc5b4\ubcf4\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\uae30\uc874 \uce74\ud398\uc778 \ud300\uc758 EC2 \uad6c\uc870\ub294 [\uc5ec\uae30\uc11c](https://blog.naver.com/sosow0212/223163203356) \ubcf4\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n## \uae30\uc874 \uc0c1\ud669\uacfc \ubb38\uc81c\uc810\\n\\n\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uae30\uc874\uc5d0 3\ub300\uc758 EC2 \uc778\uc2a4\ud134\uc2a4\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uac01\uac01 `infra, dev, db` \uc5ed\ud560\uc744 \ud558\ub294 \uc778\uc2a4\ud134\uc2a4\ub85c \uc874\uc7ac\ud558\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 release \ube0c\ub79c\uce58\ub97c \ud1b5\ud574 dev\uc11c\ubc84\uc5d0 \ubc30\ud3ec\ub97c \ud55c \ud6c4 \uac80\uc99d\uc774 \ub41c\ub2e4\uba74, \uc2e4\uc81c \uc0ac\uc6a9\uc790\ub4e4\uc774 \uc0ac\uc6a9\ud558\ub294 prod \uc11c\ubc84\uc5d0 \ubc30\ud3ec\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ubb38\uc81c\ub294 \uae30\uc874\uc758 3\ub300\uc758 \uc778\uc2a4\ud134\uc2a4 \uc911\uc5d0\uc11c dev \uc11c\ubc84\uc5d0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uae30\uc874 dev \uc11c\ubc84\ub294 \ucd1d 4\uac1c\uc758 \uc11c\ubc84\ub97c \ubc30\ud3ec\ud558\uace0 \uc788\uc5c8\uace0 \ubc30\ud3ec\ud558\ub294 \uc11c\ubc84\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4. `prod-BE, prod-FE, dev-BE, dev-FE`\\n\\n\uadf8\ub9ac\uace0, \uae30\uc874 dev \uc11c\ubc84\uc5d0\uc11c\ub294 \ud658\uacbd\uc744 \ubd84\ub9ac\ud574\uc8fc\uae30 \uc704\ud574\uc11c Nginx\ub97c \ud1b5\ud574\uc11c \ud3ec\ud2b8 \ud3ec\uc6cc\ub529\uc740 \ub2e4\uc74c\uacfc \uac19\uc774 \ud574\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n- prod-BE = 8080\\n- prod-FE = 3031\\n- dev-BE = 8081\\n- dev-FE = 3031\\n\\n\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 dev, prod \ud658\uacbd\uc774 \ubd84\ub9ac\ub418\uc9c0 \uc54a\uc544\uc11c \uc778\uc2a4\ud134\uc2a4\uc758 \uc0ac\uc6a9\ub7c9\uc774 \ub192\uc558\uace0, \uc774\uc5d0 \ub530\ub77c \ucd94\uac00\uc801\uc778 EC2 \uc778\uc2a4\ud134\uc2a4\uac00 \ud544\uc694\ud588\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n## \ubb38\uc81c \ud574\uacb0\\n\ub2e4\ud589\ud788\ub3c4 \uce74\ud398\uc778 \ud300\uc5d0\uc11c \ucd94\uac00\uc801\uc778 EC2 \uc778\uc2a4\ud134\uc2a4\ub97c \ubc1b\uc558\uace0, \uc800\ud76c\ub294 \ubc30\ud3ec \ud658\uacbd\uc744 \ubd84\ub9ac\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![dev-prod-server](https://github.com/car-ffeine/car-ffeine.github.io/assets/63213487/52942893-3d8c-4c72-9972-278afa810d1d)\\n\\n\uc774\uc640 \uac19\uc774 \uae30\uc874 dev \uc11c\ubc84 \ud55c \uac1c\uac00 infra \uc11c\ubc84\uc640 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc5c8\ub294\ub370, \ub450 \uac08\ub798\ub85c \ub098\ub25c \uac83\uc744 \ud655\uc778\ud558\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 \ubc30\ud3ec\ub294 \ub2e4\uc74c\uacfc \uac19\uc774 \uc9c4\ud589\ub429\ub2c8\ub2e4.\\n\\n`release branch`\uc5d0 push\uac00 \uc77c\uc5b4\ub098\uba74 `dev\uc11c\ubc84\uc5d0 \ubc30\ud3ec \uc791\uc5c5`\uc774 \uc774\ub904\uc9d1\ub2c8\ub2e4.\\n`prod branch`\uc5d0 push\uac00 \uc77c\uc5b4\ub098\uba74 `prod\uc11c\ubc84\uc5d0 \ubc30\ud3ec \uc791\uc5c5`\uc774 \uc774\ub904\uc9d1\ub2c8\ub2e4.\\n\\n\ub610\ud55c \uae30\uc874 dev \uc11c\ubc84\uc5d0\uc11c 4\uac1c\uc758 \ud3ec\ud2b8\ud3ec\uc6cc\ub529 \ub610\ud55c \uad73\uc774 \uadf8\ub7f4 \ud544\uc694\uac00 \uc5c6\uc5b4\uc84c\uc2b5\ub2c8\ub2e4.\\n\uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \ucd94\uac00\ub428\uc5d0 \ub530\ub77c dev, prod \uc11c\ubc84 \uac01\uac01 Nginx\uc5d0\uc11c \ud3ec\ud2b8\ud3ec\uc6cc\ub529\uc744 \ub3d9\uc77c\ud558\uac8c `FE:3000, BE:8080` \uc73c\ub85c \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub807\uac8c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 dev, prod \ud658\uacbd\uc744 \ubd84\ub9ac\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uac10\uc0ac\ud569\ub2c8\ub2e4!"},{"id":"26","metadata":{"permalink":"/26","source":"@site/blog/2023-08-16-how-fe-test.mdx","title":"\uce74\ud398\uc778 \ud300 \ud074\ub77c\uc774\uc5b8\ud2b8\uc758 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654","description":"\uc548\ub155\ud558\uc138\uc694, \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ud14c\uc2a4\ud2b8\ub97c \uc5b4\ub5bb\uac8c \ud558\uace0 \uc788\uc744\uae4c\uc694?","date":"2023-08-16T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 16\uc77c","tags":[{"label":"\ud14c\uc2a4\ud2b8","permalink":"/tags/\ud14c\uc2a4\ud2b8"},{"label":"test","permalink":"/tags/test"}],"readingTime":7.65,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"26","title":"\uce74\ud398\uc778 \ud300 \ud074\ub77c\uc774\uc5b8\ud2b8\uc758 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654","authors":["gabriel"],"tags":["\ud14c\uc2a4\ud2b8","test"]},"prevItem":{"title":"EC2 \uc11c\ubc84 \ucd94\uac00\uc640 \ub3d9\uc2dc\uc5d0 Dev, Prod \ud658\uacbd \ubd84\ub9ac\ud558\uae30","permalink":"/27"},"nextItem":{"title":"flyway\ub97c \uc774\uc81c\uc11c\uc57c \uc801\uc6a9\ud558\ub294 \uc774\uc720","permalink":"/25"}},"content":"\uc548\ub155\ud558\uc138\uc694, \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ud14c\uc2a4\ud2b8\ub97c \uc5b4\ub5bb\uac8c \ud558\uace0 \uc788\uc744\uae4c\uc694?\\n\\n\uc77c\ubc18\uc801\uc73c\ub85c \uc18c\ud504\ud2b8\uc6e8\uc5b4 \ud14c\uc2a4\ud2b8\ub780 \ubc31\uc5d4\ub4dc\uc5d0\uc11c \uadf8 \uc911\uc694\uc131\uc774 \uac15\uc870\ub418\uace4 \ud558\uc9c0\ub9cc, \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0\uc11c\ub3c4 \uadf8\uc5d0 \ubabb\uc9c0 \uc54a\uac8c \uc911\uc694\ud55c \ubd80\ubd84\uc744 \ucc28\uc9c0\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc218\ub9ce\uc740 \ud234 \uc911\uc5d0\uc11c \uc5b4\ub5a4 \ud14c\uc2a4\ud2b8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub294\uc9c0 \uc18c\uac1c\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ub2e4\uc74c\uacfc \uac19\uc740 \ud504\ub860\ud2b8\uc5d4\ub4dc \ud14c\uc2a4\ud2b8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### Jest\\nJest\ub294 JavaScript\uc758 \ud14c\uc2a4\ud2b8\ub97c \uc704\ud55c \ub300\ud45c\uc801\uc778 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.\\n\uae30\ubcf8 \uc124\uc815\uc774 \uac04\ud3b8\ud558\uace0, \ube60\ub974\uac8c \ud14c\uc2a4\ud2b8\ub97c \uc2e4\ud589\ud560 \ub54c \uad49\uc7a5\ud788 \uc720\uc6a9\ud569\ub2c8\ub2e4.\\n\ud568\uc218\ub97c mocking\ud558\uc5ec \uc758\uc874\uc131\uc774 \uac15\ud55c \ud568\uc218\ub97c \uc81c\uac70\ud558\uc5ec \uc6d0\ud558\ub294 \ud14c\uc2a4\ud2b8\ub97c \uc27d\uac8c \uad6c\uc131\ud560 \uc218 \uc788\ub2e4\ub294 \ud2b9\uc9d5\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\\n### React Testing Library\\nReact Testing Library\ub294 \ub9ac\uc561\ud2b8 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc758 UI\ub97c \ud14c\uc2a4\ud2b8\ud558\uae30 \uc704\ud55c \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.\\nReact \ucef4\ud3ec\ub10c\ud2b8\ub97c \ud638\ucd9c\ud558\uc5ec, \uc0ac\uc6a9\uc790\uc758 \uc758\ub3c4\ub300\ub85c \uc870\uc791\ud560 \uc218 \uc788\ub294 \ud589\uc704\ub97c \uc815\uc758\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\uc0ac\uc6a9\uc790 \uc785\uc7a5\uc5d0\uc11c \uc0c1\ud638\uc791\uc6a9 \ud560 \uc218 \uc788\ub294 \ubd80\ubd84\uc744 \uc2a4\ud06c\ub9bd\ud2b8\ub85c \uc791\uc131\ud558\uc5ec \ucef4\ud3ec\ub10c\ud2b8\uac00 \uc5b4\ub5bb\uac8c \ubcc0\ud654\ud558\ub294\uc9c0\ub97c \ud14c\uc2a4\ud2b8 \ud560 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\uac00\ub839, \uc5b4\ub5a4 \uc0ac\uc6a9\uc790\uac00 \uc5b4\ub5a4 \ud3fc\uc5d0 \uc5b4\ub5a4 \uac12\uc744 \uc785\ub825\ud588\uc744 \ub54c\uc758 \uc608\uc0c1\ub418\ub294 \uacb0\uacfc\ub97c \uc791\uc131\ud574\ub450\uba74 \uc774\ud6c4\uc5d0 \ucf54\ub4dc \uc791\uc5c5 \uc911 \ubc84\uadf8\uac00 \ubc1c\uc0dd\ud55c\ub2e4\uba74 \ud574\ub2f9 \uc704\uce58\uc5d0\uc11c \ud14c\uc2a4\ud2b8\uac00 \uc2e4\ud328\ud560 \uac83\uc785\ub2c8\ub2e4.\\n\\n### Storybook\\nStorybook\uc740 UI\ub97c \ucef4\ud3ec\ub10c\ud2b8 \ub2e8\uc704\ub85c \uac1c\ubc1c\ud558\uace0 \uadf8 \uc989\uc2dc \uc2dc\uac01\ud654 \ud560 \uc218 \uc788\ub3c4\ub85d \ub3d5\ub294 \ud14c\uc2a4\ud305 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.\\n\ucef4\ud3ec\ub10c\ud2b8\ub97c \ub208 \uc55e\uc5d0 \ubc14\ub85c \ubcf4\uc5ec\uc8fc\uace0 \uc2e4\uc81c \ub9ac\uc561\ud2b8\uc5d0\uc11c \ub3d9\uc791\ud558\ub294 \uac83 \ucc98\ub7fc \ucef4\ud3ec\ub10c\ud2b8 \ub2e8\uc704\ub85c \uac1c\ubc1c\uc744 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. CDD\ub97c \uc9c0\ud5a5\ud55c\ub2e4\uba74 \uad49\uc7a5\ud788 \uc720\uc6a9\ud55c \uae30\ub2a5\uc774\uba70, \uac1c\ubc1c\uc790\uac00 \uc544\ub2cc \ud611\uc5c5\uc790\uc5d0\uac8c\ub3c4 \uc6d0\ud65c\ud55c \ucee4\ubba4\ub2c8\ucf00\uc774\uc158\uc744 \ub3c4\uc640\uc90d\ub2c8\ub2e4.\\n\ucef4\ud3ec\ub10c\ud2b8 \ub2e8\uc704\ub85c \uac1c\ubc1c\ud558\uae30 \ub54c\ubb38\uc5d0 \uac1c\ubcc4 \ucef4\ud3ec\ub10c\ud2b8\uac00 \uc5b4\ub5bb\uac8c \ub3d9\uc791\ud558\ub294\uc9c0 \ud655\uc778\ud560 \uc218 \uc788\ub2e4\ub294 \uac83 \uc790\uccb4\uac00 \uad49\uc7a5\ud55c \uc774\uc810\uc73c\ub85c \uc791\uc6a9\ud569\ub2c8\ub2e4.\\n\uc608\ub97c \ub4e4\uc5b4 \uc5b4\ub5a4 \ucef4\ud3ec\ub10c\ud2b8\uac00 \ud2b9\uc815 \uba54\ub274 \uc548\uc5d0 \uc874\uc7ac\ud574\uc57c \ud55c\ub2e4\uba74, \uc774\uac83\uc744 \ud655\uc778\ud558\uae30 \uc704\ud574 \ud574\ub2f9 \uba54\ub274\uae4c\uc9c0 \uc811\uadfc\ud574\uc57c \ud560 \uac83\uc785\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc Storybook\uc744 \uc774\uc6a9\ud558\uba74 \ud2b9\uc815 \ucef4\ud3ec\ub10c\ud2b8\ub97c Storybook \uc704\uc5d0 \uc62c\ub824\ub193\uace0 \ud14c\uc2a4\ud2b8\ub97c \ud560 \uc218 \uc788\uc5b4 \ube60\ub974\uac8c \uc791\uc5c5\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n\uc778\ud130\ub809\uc158\uc774\ub098 \uc6f9\uc811\uadfc\uc131\uc744 \ud655\uc778\ud574\uc8fc\ub294 \ud50c\ub7ec\uadf8\uc778\ub3c4 \uc874\uc7ac\ud558\uc5ec \ud504\ub860\ud2b8\uc5d4\ub4dc \uac1c\ubc1c\uc5d0\uc11c \uad49\uc7a5\ud788 \uc911\uc694\ud55c \uc5ed\ud560\ub85c \ubd80\uc0c1\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uc800\ud76c \ud300\uc740 \uc774\uc678\uc5d0 Cypress\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\ub3c4 \uace0\ub824\ud558\uc600\uc73c\ub098, \uc9c0\ub3c4\uc640 \uacb0\ud569\ub41c \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \ud14c\uc2a4\ud2b8\ud558\uae30\uc5d0 \ub2e4\uc18c \uc5b4\ub824\uc6c0\uc774 \uc788\uc5b4 \uc704 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc744 \uac1c\ubc1c\uc5d0 \ud65c\uc6a9\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uc800\ud76c\ub294 \uc704 \ud14c\uc2a4\ud305 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc744 \uc6d0\ud65c\ud788 \ud65c\uc6a9\ud558\uae30 \uc704\ud574 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654\ub97c \uad6c\ucd95\ud588\uc2b5\ub2c8\ub2e4.\\n\\n## Jest\uc640 React Testing Library \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654\\n\\n```yaml\\nname: frontend-test\\n\\non:\\n pull_request:\\n branches:\\n - main\\n - develop\\n paths:\\n - frontend/**\\n - .github/**\\n\\npermissions:\\n contents: read\\n\\njobs:\\n test:\\n name: test-when-pull-request\\n runs-on: ubuntu-latest\\n environment: test\\n defaults:\\n run:\\n working-directory: ./frontend\\n steps:\\n - name: Checkout PR\\n uses: actions/checkout@v2\\n - name: Install dependencies\\n run: npm install\\n - name: Test\\n run: npm run test\\n```\\n\\n\\n#### \uc774\ubca4\ud2b8 \ud2b8\ub9ac\uac70 \uc124\uc815\\npull_request \uc774\ubca4\ud2b8\uac00 \ubc1c\uc0dd\ud558\uc600\uc744 \ub54c, \ud574\ub2f9 \uc774\ubca4\ud2b8\uac00 main \ube0c\ub79c\uce58\uc640 develop \ube0c\ub79c\uce58\uc5d0\uc11c\ub9cc \ub3d9\uc791\ud569\ub2c8\ub2e4.\\n\\n#### \ubcc0\uacbd \uc0ac\ud56d \uacbd\ub85c \uc81c\ud55c\\n\ud14c\uc2a4\ud2b8\ub97c \uc2e4\ud589\ud560 \ub54c\ub294 frontend \ub514\ub809\ud1a0\ub9ac\uc640 .github \ub514\ub809\ud1a0\ub9ac \ub0b4\uc758 \ud30c\uc77c\ub4e4\uc744 \uace0\ub824\ud558\ub3c4\ub85d \ud588\uc2b5\ub2c8\ub2e4. \ubc31\uc5d4\ub4dc\uc640\uc758 \ud658\uacbd \ubd84\ub9ac\ub97c \uc704\ud574 \uc774\ub7ec\ud55c \uc811\uadfc \uc81c\ud55c\uc744 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n#### \uad8c\ud55c \uc124\uc815\\npermissions\uc740 \uc77d\uae30 \uad8c\ud55c\ub9cc \uc124\uc815\ub418\uc5b4 \uc788\uc5b4 \ucf54\ub4dc\ub098 \ud30c\uc77c\uc744 \ubcc0\uacbd\uc744 \ubc29\uc9c0\ud569\ub2c8\ub2e4.\\n\\n#### \uc791\uc5c5(Job) \uc124\uc815\\ntest\ub77c\ub294 \uc774\ub984\uc758 \uc791\uc5c5\uc744 \uc815\uc758\ud558\uc600\uace0, \uc774 \uc791\uc5c5\uc5d0\uc11c\ub294 Ubuntu \ud658\uacbd\uc5d0\uc11c \ud14c\uc2a4\ud2b8\ub97c \uc2e4\ud589\ud569\ub2c8\ub2e4. test\ub77c\ub294 \uc774\ub984\uc758 \ud658\uacbd \ubcc0\uc218\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4. \ud14c\uc2a4\ud2b8\ub294 (\uce74\ud398\uc778 \ud300 \ub808\ud3ec\uc9c0\ud1a0\ub9ac\uc758) frontend \ub514\ub809\ud1a0\ub9ac\uc5d0\uc11c \uc791\uc5c5\ud558\ub3c4\ub85d \ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n#### \uc2a4\ud15d(Step) \uc124\uc815\\n\ucf54\ub4dc\ub97c \uccb4\ud06c\uc544\uc6c3\ud558\uace0, \uc758\uc874\uc131\uc744 \uc124\uce58\ud558\uba70, \ud14c\uc2a4\ud2b8\ub97c \uc2e4\ud589\ud558\ub294 \uc138 \uac00\uc9c0 \ub2e8\uacc4\ub85c \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\\n\\n\uc774\ub7ec\ud55c \uc124\uc815\uc744 \ud1b5\ud574 PR\uc5d0 \ucf54\ub4dc\uac00 \uc62c\ub77c\uc62c \ub54c \uc790\ub3d9\uc73c\ub85c \ud504\ub860\ud2b8\uc5d4\ub4dc \ud14c\uc2a4\ud2b8\uac00 \uc2e4\ud589\ub429\ub2c8\ub2e4.\\n\\n\uc774\ub7ec\ud55c \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654 \uc804\ub7b5\uc740 \ud504\ub860\ud2b8\uc5d4\ub4dc \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc548\uc815\uc801\uc774\uac8c \uac1c\ubc1c\ud558\uace0 \uc720\uc9c0\ud560 \uc218 \uc788\ub3c4\ub85d \ub3c4\uc640\uc90d\ub2c8\ub2e4.\\n\\n## Storybook\uc758 \ube4c\ub4dc \uc790\ub3d9\ud654\\n\\n```yaml\\nname: storybook-deploy\\n\\non:\\n pull_request:\\n branches:\\n - develop\\n paths:\\n - frontend/**\\n - .github/**\\n\\njobs:\\n build:\\n runs-on: ubuntu-22.04\\n defaults:\\n run:\\n working-directory: ./frontend\\n steps:\\n - name: Setup Repository\\n uses: actions/checkout@v3\\n\\n - name: Set up Node\\n uses: actions/setup-node@v3\\n with:\\n node-version: 18.16.0\\n\\n - name: Install dependencies\\n run: npm install\\n\\n - name: Cache node_modules\\n id: cache\\n uses: actions/cache@v3\\n with:\\n path: \'**/node_modules\'\\n key: ${{ runner.os }}-node-${{ hashFiles(\'**/package-lock.json\') }}\\n restore-keys: |\\n ${{ runner.os }}-node-\\n\\n - name: storybook build\\n run: npm run build-storybook\\n\\n - name: Upload storybook build files to temp artifact\\n uses: actions/upload-artifact@v3\\n with:\\n name: Storybook\\n path: frontend/storybook-static\\n deploy:\\n needs: build\\n runs-on: self-hosted\\n steps:\\n - name: Remove previous version app\\n working-directory: .\\n run: rm -rf dist\\n\\n - name: Download the built file to AWS\\n uses: actions/download-artifact@v3\\n with:\\n name: Storybook\\n path: frontend/dev/dist\\n\\n - name: Move folder\\n working-directory: frontend/dev/\\n run: |\\n rm -rf /home/ubuntu/dist/*\\n cp -r ./dist /home/ubuntu\\n\\n - name: comment PR\\n uses: thollander/actions-comment-pull-request@v1\\n env:\\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\\n with:\\n message: \'\ud83d\ude80storybook: https://storybook.carffe.in/\'\\n```\\n\\n\ube44\uc2b7\ud55c \ucf54\ub4dc\uc774\uc9c0\ub9cc, \ub9e4\ubc88 PR\uc774 \uc5f4\ub9b4 \ub54c \ub9c8\ub2e4 \uc2a4\ud1a0\ub9ac\ubd81\uc774 \uc790\ub3d9\uc73c\ub85c \ube4c\ub4dc \ubc0f \ubc30\ud3ec\ub429\ub2c8\ub2e4.\\n\ubc30\ud3ec\uac00 \uc644\ub8cc\ub418\uba74 \ubc30\ud3ec\ub41c URL\uc744 \uc54c\ub824 \ucf54\ub4dc \ub9ac\ubdf0\ud560 \ub54c \ucc38\uace0\ud560 \uc218 \uc788\ub3c4\ub85d \ub3d5\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc0c1 \uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud558\uace0 \uc788\ub294 \ud14c\uc2a4\ud305 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc640 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654 \ubc29\ubc95\uc744 \uc54c\uc544\ubd24\uc2b5\ub2c8\ub2e4."},{"id":"25","metadata":{"permalink":"/25","source":"@site/blog/2023-08-15-flyway.mdx","title":"flyway\ub97c \uc774\uc81c\uc11c\uc57c \uc801\uc6a9\ud558\ub294 \uc774\uc720","description":"\uc548\ub155\ud558\uc138\uc694","date":"2023-08-15T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 15\uc77c","tags":[{"label":"hello","permalink":"/tags/hello"},{"label":"world","permalink":"/tags/world"}],"readingTime":7.585,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"25","title":"flyway\ub97c \uc774\uc81c\uc11c\uc57c \uc801\uc6a9\ud558\ub294 \uc774\uc720","authors":["boxster"],"tags":["hello","world"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300 \ud074\ub77c\uc774\uc5b8\ud2b8\uc758 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654","permalink":"/26"},"nextItem":{"title":"Out of memory trouble shooting","permalink":"/24"}},"content":"\uc548\ub155\ud558\uc138\uc694\\n\\n## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\\n\uc800\ud76c \ud300\uc740 flyway\ub97c \uc801\uc6a9\ud588\uc2b5\ub2c8\ub2e4. \uac00\uc7a5 \ud070 \uc774\uc720\ub294 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \ub370\uc774\ud130\ub97c drop \ud560 \uc218 \uc5c6\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c drop\ud558\ub294 \uac83\uacfc flyway\uac00 \ubb34\uc2a8 \uc0c1\uad00\uc774 \uc788\uae38\ub798 \uc801\uc6a9\ud560\uae4c\uc694.\\n\\n### \uc608\uc2dc \uc0c1\ud669\\n\\n\uc81c\uac00 \uc544\ub798\uc640 \uac19\uc774 Member\ub77c\ub294 entity\ub97c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n```java\\nclass Member {\\n\\n private Long id;\\n private String name;\\n}\\n```\\n\uc9c0\uae08\uc758 entity\ub294 \ub450\uac1c\uc758 \ud544\ub4dc \ubc16\uc5d0 \uc5c6\uc2b5\ub2c8\ub2e4. \uc5b4\ub290 \ub0a0\ubd80\ud130 Member\uc5d0 email\uc774\ub77c\ub294 \uc815\ubcf4\uac00 \uc788\uc5b4\uc57c\ud55c\ub2e4\ub294 \uc694\uad6c\uc0ac\ud56d\uc774 \uc0dd\uae41\ub2c8\ub2e4.\\n\uadf8\ub798\uc11c \uc800\ud76c\ub294 \uc544\ub798\uc640 \uac19\uc774 email\uc744 \ucd94\uac00\ud569\ub2c8\ub2e4.\\n```java\\nclass Member {\\n\\n private Long id;\\n private String name;\\n private String email;\\n}\\n```\\n\uadf8\ub9ac\uace0 \ub2e4\uc2dc jpa\uc758 ddl-auto \uc18d\uc131 \uc911 create\ub97c \uc0ac\uc6a9\ud574\uc11c \uc0c8\ub85c\uc6b4 \ud14c\uc774\ube14\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4. \uae30\uc874\uc758 \ud14c\uc774\ube14\uc744 \ub2e4 \ub0a0\ub9ac\uba74\uc11c\uc694.\\n\\n\ud558\uc9c0\ub9cc \uc800\ud76c\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \ub370\uc774\ud130\ub4e4\uc744 \uadf8\ub0e5 drop\ud574\ub3c4 \ub418\ub294 \uac83\uc77c\uae4c\uc694?\\n\uac1c\ubc1c \uc11c\ubc84\ub77c\ub3c4 \ud798\ub4e4\uac8c \uc313\uc740 \ub370\uc774\ud130\ub4e4\uc744 \ud14c\uc774\ube14\uc774 \uc870\uae08 \ubcc0\uacbd\ub418\uc5c8\ub2e4\uace0 \ub0a0\ub824\ubc84\ub9ac\ub294 \uac83\uc740 \ubc14\ubcf4\uac19\uc740 \uc77c\uc774\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub7ec\uba74 ddl-auto\uc758 \ub2e4\ub978 \uc870\uac74\uc778 update\ub97c \uc0ac\uc6a9\ud558\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4. \uadf8\ub7ac\ub354\ub2c8 jpa\uac00 \uc544\ub798\uc640 \uac19\uc774 \ucffc\ub9ac\ub97c \uc774\uc058\uac8c \ub9cc\ub4e4\uc5b4 \uc92c\uc2b5\ub2c8\ub2e4.\\n```sql\\nALTER TABLE member\\n ADD COLUMN email varchar(255);\\n```\\nupdate\ub97c \uc0ac\uc6a9\ud558\ub2c8 \uc544\uc8fc \ud3b8\ud558\uac8c \uce7c\ub7fc\uc774 \ucd94\uac00\ub418\ub294 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc5ec\uae30\uc11c \ub610 \uc544\ub798\uc640 \uac19\uc740 \uc694\uad6c\uc0ac\ud56d\uc774 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\nemail\uc758 \uc81c\uc57d\uc870\uac74\uc73c\ub85c null\uc774 \ub418\uba74 \uc548\ub418\uace0, \uae38\uc774\ub294 20\uc790\uac00 \ub418\uc5b4\uc57c\ud569\ub2c8\ub2e4.\\n\uadf8\ub7ec\uba74 \uc5b4\ub178\ud14c\uc774\uc158\uc744 \uc0ac\uc6a9\ud558\uc5ec \ubcc0\uacbd\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n```java\\nclass Member {\\n\\n private Long id;\\n private String name;\\n @Column(nullable= false, length = 20)\\n private String email;\\n}\\n```\\n\uc774\ub807\uac8c \ud558\uace0 \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc7ac\uc2dc\uc791 \ud588\uc2b5\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uc544\ubb34\ub7f0 ddl\uc774 \ubc1c\uc0dd\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc65c\ub0d0\uba74 Jpa\uc758 ddl-auto: update\uc758 \uc18d\uc131\uc740 \uc81c\uc57d\uc870\uac74\uc774 \ubcc0\uacbd\ub41c \uac83\uc740 \ubc18\uc601\ud574\uc8fc\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ub9cc\uc57d \uc774 \uc804\uc758 \ud68c\uc6d0\ub4e4\uc758 email\uc774 null\uc778 row\ub3c4 \uc788\ub2e4\uba74 \uc5b4\ub5bb\uac8c \ub420\uae4c\uc694? \uc81c\uc57d\uc870\uac74\uc744 \ubc18\uc601\ud560 \uc218 \uc5c6\uc744 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uc2dd\uc73c\ub85c \uc6b4\uc601 \ub3c4\uc911 table\uc758 \uce7c\ub7fc\ub4e4\uc774 \ucd94\uac00\ub418\uac70\ub098, \uc0ad\uc81c\ub418\uac70\ub098, \ud639\uc740 \uc81c\uc57d\uc870\uac74\uc774 \ubcc0\uacbd\ub420 \ub54c update \uc18d\uc131\ub9cc\uc73c\ub85c\ub294 \ubc18\uc601\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n## flyway\\n\\n\uadf8\ub798\uc11c flyway\ub97c \uc0ac\uc6a9\ud588\uc2b5\ub2c8\ub2e4.\\n\ubb3c\ub860 flyway \uc5c6\uc774\ub3c4 \uc774\ub7f0 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc29\ubc95\uc740 \uac04\ub2e8\ud569\ub2c8\ub2e4. \ub370\uc774\ud130\ubca0\uc774\uc2a4\uac00 \uc788\ub294 \uc11c\ubc84\uc5d0 \uc9c1\uc811 \uc811\uc18d\ud558\uc5ec ddl\uc744 \uc9c1\uc811 \ud558\ub098 \ud558\ub098 \ub2e4 \uc791\uc131\ud558\uba74 \ub429\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774\ub7f0 \ubc29\uc2dd\uc5d0\ub294 \ub2e8\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4. \ud558\ub098 \ud558\ub098 \uc9c1\uc811 \uc785\ub825\ud558\ub2e4\ubcf4\ub2c8 \ud734\uba3c \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud560 \uc218\ub3c4 \uc788\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ub9e4\ubc88 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc11c\ubc84\uc5d0 \uc811\uc18d\ud574\uc57c\ud55c\ub2e4\ub294 \ub2e8\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\uc774\ub807\uac8c \ub9e4\ubc88 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \uc811\uc18d\uc744 \ud574\uc57c\ud55c\ub2e4\uba74 cd\ub97c \ud558\ub294 \uc774\uc720\uac00 \uc788\uc744\uae4c\uc694?\\n\\n\ud558\uc9c0\ub9cc flyway\ub97c \uc0ac\uc6a9\ud558\uba74 \ud3b8\ud558\uac8c \ubcc0\uacbd\ub41c schema\ub97c \uad00\ub9ac\ud560 \uc218 \uc788\uace0 \uc5b8\uc81c \ubc14\ub00c\uc5c8\ub294\uc9c0 \uc5b4\ub5bb\uac8c \ubc14\ub00c\uc5c8\ub294\uc9c0 \ud655\uc778\ub3c4 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uae00\ub85c\ub294 \uc798 \uc640\ub2ff\uc9c0 \uc54a\uc744 \uc218\ub3c4 \uc788\uc73c\ub2c8 \uc0ac\uc6a9\ubc95\uc744 \ud655\uc778\ud558\uba74\uc11c \uc5b4\ub5a4 \uc7a5\uc810\uc774 \uc788\ub294\uc9c0 \ud655\uc778\ud574\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 flyway \uc758\uc874\uc131\uc744 \ucd94\uac00\ud558\uace0 `resources/db/migration` \ud328\ud0a4\uc9c0\ub97c \ub9cc\ub4ed\ub2c8\ub2e4.\\n\uac70\uae30\uc5d0 file\uc744 \ub9cc\ub4ed\ub2c8\ub2e4. \ud30c\uc77c \uc774\ub984\uc774 \uc911\uc694\ud55c\ub370\uc694 `V1__init.sql` \uc774\ub7ec\ud55c \ubc29\uc2dd\uc73c\ub85c `V{version \uc22b\uc790}__{\uc5b4\ub5a0\ud55c \ud30c\uc77c\uc778\uc9c0\uc5d0 \ub300\ud55c \uc774\ub984}.sql` \uc5b8\ub354\uc2a4\ucf54\uc5b4 2\uac1c\ub294 \ud544\uc218\ub85c \uc791\uc131\ud574\uc57c\ud569\ub2c8\ub2e4.\\n\\n```sql\\ncreate table member(\\n id bigint auto_increment primary key,\\n name varchar(255) null,\\n);\\n```\\n\uc774\ub807\uac8c `V1__init.sql`\uc5d0 \ub300\ud55c \ud30c\uc77c\uc744 \uc791\uc131\ud588\uc2b5\ub2c8\ub2e4. \uc774\uc81c\ub294 email\uc744 \ucd94\uac00\ud55c\ub2e4\ub294 \uc694\uad6c\uc0ac\ud56d\uc744 \ubc18\uc601\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```sql\\nALTER TABLE member\\n ADD COLUMN email varchar(255);\\n```\\n\uc774\ub807\uac8c \uc0c8\ub85c\uc6b4 \ud30c\uc77c\uc744 \ub9cc\ub4e4\uc5b4\uc11c \ud574\ub2f9 \uc2a4\ud06c\ub9bd\ud2b8\ub97c \uc791\uc131\ud588\uc2b5\ub2c8\ub2e4. \ud30c\uc77c\uba85\uc774 \uc911\uc694\ud55c\ub370\uc694, \uc774\uc804 \ud30c\uc77c\uc758 \uc22b\uc790\ubcf4\ub2e4 +1 \uc774 \ub418\ub294 \uc22b\uc790\ub97c V \ub4a4\uc5d0 \ubd99\uc785\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc774\ubc88 \ud30c\uc77c\uc740 `V2__add_column_email.sql` \uc774\ub77c\uace0 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \uc774\uc81c \ub610 \uc2dc\uac04\uc774 \uc9c0\ub098 \ud68c\uc6d0\uc774 \ub9ce\uc544\uc84c\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc email\uc774 \uc5c6\ub294 \uc0ac\uc6a9\uc790\ub3c4 \ub9ce\uc2b5\ub2c8\ub2e4. \uc774 \uc0c1\ud669\uc5d0\uc11c email\uc744 not null\ub85c \ubcc0\uacbd\ud574\uc57c\ud55c\ub2e4\ub294 \uc694\uad6c\uc0ac\ud56d\uc774 \uc0dd\uacbc\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 \uc544\ub798\uc640 \uac19\uc774 \ubc18\uc601\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n```sql\\nALTER TABLE member\\n MODIFY email VARCHAR(20) NOT NULL default \'default\'\\n```\\n\uc774\ub807\uac8c `V3__add_constraints.sql` \ud30c\uc77c\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4. \uadf8\ub7ec\uba74 null\uc774 \uc788\ub358 row\ub4e4\uc740 email\uc774 default\uac00 \ub418\uace0 not null \uc81c\uc57d\uc870\uac74\uc774 \ud65c\uc131\ud654 \ub41c \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 \uc8fc\uc5b4\uc9c4 \uc694\uad6c\uc0ac\ud56d\uc740 \ubaa8\ub450 \ub9cc\uc871\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uac70\uae30\uc5d0\ub2e4 v1, v2, v3 \uac00 \ub098\ub258\uc5b4\uc838\uc788\uc5b4\uc11c \uc5b4\ub290 \ucee4\ubc0b\ubd80\ud130 \ud574\ub2f9 sql\uc774 \ucd94\uac00\ub418\uc5c8\ub294\uc9c0\ub3c4 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 ddl-auto update\ub97c \uc0ac\uc6a9\ud558\uba74 \ubc18\uc601\ub418\uc9c0 \uc54a\uc558\ub358 \uc81c\uc57d\uc870\uac74\uc758 \ucd94\uac00\ub3c4 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub7ec\uba74 ddl-auto\uc758 \uc18d\uc131\uc744 validate\ub85c \ubcc0\uacbd\ud558\uc5ec, db schema\uc640 entity\uc758 \ud544\ub4dc\uac00 \ub2e4\ub974\uba74\\n\uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc774 \uc2e4\ud589\ub418\uc9c0 \uc54a\ub3c4\ub85d \ud574\uc11c \uc880 \ub354 \uc548\uc804\ud55c \uac1c\ubc1c\uc744 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\\nflyway\ub294 roll back\uc744 \ud558\ub294 \uac83\uc774 \uc720\ub8cc\ub77c\uc11c, production \uc11c\ubc84\uc5d0\uc11c \ud639\uc740 \ub864\ubc31\uc744 \ud574\uc57c\ud558\ub294 \uc77c\uc774 \uc788\ub294 \uc11c\ubc84\uc5d0\uc11c\ub294 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc9c0 \uc54a\uc9c0\ub9cc,\\n\uc774\uc640 \uac19\uc774 \ub370\uc774\ud130\ub97c drop \ud560 \uc218 \uc5c6\ub294 \uc0c1\ud669\uc774\ub77c\uba74, \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc744 \uc774\uc720\uac00 \uc5c6\uc5b4\ubcf4\uc774\ub294 \uc88b\uc740 \ub3c4\uad6c\uc785\ub2c8\ub2e4.\\n\\n\uc9e7\uc740 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4."},{"id":"24","metadata":{"permalink":"/24","source":"@site/blog/2023-08-06-out-of-memory-trouble-shooting/index.mdx","title":"Out of memory trouble shooting","description":"\uc548\ub155\ud558\uc138\uc694 \ubd80\ub989\ubd80\ub989 \ud5c8\ub9ac\ucf00\uc778 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4.","date":"2023-08-06T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 6\uc77c","tags":[{"label":"OOM","permalink":"/tags/oom"},{"label":"java","permalink":"/tags/java"},{"label":"trouble-shooting","permalink":"/tags/trouble-shooting"}],"readingTime":15.43,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"24","title":"Out of memory trouble shooting","authors":["boxster"],"tags":["OOM","java","trouble-shooting"]},"prevItem":{"title":"flyway\ub97c \uc774\uc81c\uc11c\uc57c \uc801\uc6a9\ud558\ub294 \uc774\uc720","permalink":"/25"},"nextItem":{"title":"Deadlock trouble shooting","permalink":"/23"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubd80\ub989\ubd80\ub989 \ud5c8\ub9ac\ucf00\uc778 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4.\\n## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\uba3c\uc800 \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\ub294 \uc800\ud76c \uce74\ud398\uc778 \ud300\uc758 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub4e4\uc758 \uc0c8\ub85c\uc6b4 \uc815\ubcf4\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\uac70\ub098, \uc800\uc7a5\ud558\ub294 \ub85c\uc9c1\uc5d0\uc11c \uc544\ub798\uc640 \uac19\uc774 OOM(Out of memory)\uac00 \ubc1c\uc0dd\ud588\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n![error-log](./error-log.png)\\n\\n### \uc65c \ubc1c\uc0dd\ud588\uc744\uae4c\\n\\n\uba3c\uc800 \uac04\ub2e8\ud788 \uc800\ud76c\uac00 \ucc98\ud55c \uc0c1\ud669\uc5d0 \ub300\ud574 \uc124\uba85\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\ucc98\uc74c \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc2e4\ud589\ud558\uba74 \uacf5\uacf5 API\ub97c \ud638\ucd9c\ud558\uc5ec \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\uc5d0 \ub300\ud55c \ubaa8\ub4e0 \uc815\ubcf4\ub4e4\uc744 \uac00\uc838\uc640 \uc800\uc7a5\ud569\ub2c8\ub2e4. (\ucda9\uc804\uc18c \uc57d 6\ub9cc \uacf3 + \ucda9\uc804\uae30 \uc57d 23\ub9cc \uae30)\\n\\n\ud558\uc9c0\ub9cc \uc774\ub7ec\ud55c \uc815\ubcf4\ub4e4\uc740 \uc218\uc815\uc774 \ub420 \uc218 \uc788\uace0, \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\uac00 \ucd94\uac00\ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\ubbc0\ub85c \uc815\ud655\ud55c \uc815\ubcf4\uac00 \uc0ac\uc6a9\uc790\uc5d0\uac8c \uac00\uc7a5 \uc911\uc694\uc2dc\ub418\ub294 \uc11c\ube44\uc2a4\uc5d0\uc11c \uc774\ub7ec\ud55c \uc815\ubcf4\ub4e4\uc774 \ub2a6\uac8c \ubc18\uc601\uc774 \ub41c\ub2e4\uac70\ub098, \ubc18\uc601\uc774 \ub418\uc9c0 \uc54a\ub294\ub2e4\uba74 \uc800\ud76c \uc11c\ube44\uc2a4\ub97c \uc0ac\uc6a9\ud560 \uc0ac\uc6a9\uc790\uac00 \uc5c6\uc744 \uac83\uc774\ub77c \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \ud558\ub8e8\uc5d0 \ud55c \ubc88 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub4e4\uc758 \uc815\ubcf4\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\uace0, \ucd94\uac00\ub41c \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub97c \uc800\uc7a5\ud558\ub294 \ub85c\uc9c1\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub300\ub7b5\uc801\uc778 \ub85c\uc9c1\uc740 \uc544\ub798\uc640 \uac19\uc2b5\ub2c8\ub2e4.\\n```java\\n public void updatePeriodicStations() {\\n List stations = requestStations();\\n stationUpdateService.updateStations(stations);\\n }\\n\\n public void updateStations(List updatedStations) {\\n List stations = stationRepository.findAllFetch();\\n\\n Map savedStationsByStationId = stations.stream()\\n .collect(Collectors.toMap(Station::getStationId, Function.identity()));\\n\\n // \uc800\uc7a5\ub41c \uc815\ubcf4\uc640 \ube44\uad50\ud558\uc5ec \uc0c8\ub85c\uc6b4 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub97c \ucc3e\ub294 \ub85c\uc9c1\\n ...\\n\\n saveAllStations(toSaveStations);\\n updateAllStations(toUpdateStations);\\n\\n saveAllChargers(toSaveChargers);\\n updateAllChargers(toUpdateChargers);\\n }\\n\\n```\\n\uac04\ub2e8\ud558\uac8c \ub9d0\uc500\ub4dc\ub9ac\uba74 `requestStations()` \uba54\uc11c\ub4dc\ub294 \uacf5\uacf5 API\uc5d0\uc11c \ubaa8\ub4e0 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub97c \uc694\uccad\ud558\uace0 \ubc1b\uc544\uc624\ub294 \uba54\uc11c\ub4dc\uc785\ub2c8\ub2e4. 23\ub9cc + 6\ub9cc\uac1c\uc758 \uc815\ubcf4\ub97c \ubc1b\uc544\uc624\ub294 \uac83\uc785\ub2c8\ub2e4.\\n\uc774\ub807\uac8c \ub9ce\uc740 \uc815\ubcf4\ub97c \ubc1b\uc544\uc624\uace0 \uba54\ubaa8\ub9ac\uc5d0 \uc62c\ub9b0\ub2e4\ub294 \uac83\uc740 \ub204\uac00\ubd10\ub3c4 \ube44\ud6a8\uc728\uc801\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc774\ub7ec\ud55c \uc120\ud0dd\uc744 \ud55c \uc774\uc720\ub294 \uacf5\uacf5 API\ub294 \uc800\ud76c\uac00 \uc5b4\ub5a4 \ubc29\uc2dd\uc73c\ub85c \ubcf4\ub0b4\uc904 \uc9c0 \ubaa8\ub978\ub2e4\ub294 \uac83\uc774\uc600\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub798\uc11c \uc5b4\uca54 \uc218 \uc5c6\uc774 23\ub9cc\uac74\uc744 \ubaa8\ub450 \uc694\uccad\ud574\uc57c\ud55c\ub2e4\ub294 \ubd80\ubd84\uc740 \ubc14\uafc0 \uc218 \uc5c6\ub294 \ud55c\uacc4\uc785\ub2c8\ub2e4.\\n\\n\uadf8 \ub2e4\uc74c\uc73c\ub85c\ub294 \uc694\uccad\ud574\uc11c \ubc1b\uc544\uc628 \ub370\uc774\ud130\ub4e4\uacfc \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \uc800\uc7a5\ub418\uc5b4 \uc788\ub358 \ub370\uc774\ud130\ub4e4\uc744 `findAll()`\uc744 \ud1b5\ud574 \ube44\uad50\ud558\uace0 \uc0c8\ub85c\uc6b4 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub294 \uc800\uc7a5\ud558\uace0, \uc5c5\ub370\uc774\ud2b8\ub41c \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub294 \uc218\uc815\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \ub85c\uc9c1\uc740 \ucd1d (23 + 6) * 2 \ub9cc\uac74\uc758 \uac1d\uccb4 \uc57d 58\ub9cc\uac1c\ub97c Heap \uba54\ubaa8\ub9ac\uc5d0 \uc801\uc7ac\ud569\ub2c8\ub2e4. \ub9ce\ub2e4\uace0\ub294 \uc0dd\uac01\ud588\uc9c0\ub9cc, \uc77c\ub2e8 \uc81c \ub85c\uceec\ud658\uacbd\uc5d0\uc11c\ub294 \uc798 \uc791\ub3d9\ud588\uace0, \uae30\ub2a5 \uad6c\ud604\uc774 \uc6b0\uc120\uc774\uae30 \ub54c\ubb38\uc5d0 \ucd94\ud6c4\uc5d0 \uac1c\uc120\uc744 \ud558\uae30\ub85c \ud558\uace0 \ub118\uc5b4\uac14\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uac1c\ubc1c \uc11c\ubc84 \ubc30\ud3ec\ub97c \ud558\uace0 \ub2e4\uc74c\ub0a0 \uc11c\ubc84\uac00 \uc811\uc18d\uc774 \ub418\uc9c0 \uc54a\ub294 \uac83\uc744 \ud655\uc778\ud588\uace0, \ub85c\uadf8\ub97c \ubcf4\ub2c8 \uc704\uc758 \uc0ac\uc9c4\uacfc \uac19\uc774 OOM\uc774 \ubc1c\uc0dd\ud55c \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \ud574\uacb0 \ubc29\uc548\\n\\n\\n### Heap size \uc870\uc808\ud558\uae30\\n\uc77c\ub2e8 \uc784\uc2dc \ubc29\ud3b8\uc73c\ub85c Heap memory\uc758 \ucd5c\ub300 \ud06c\uae30\ub97c \ub298\ub9ac\ub294 \ubc95\uc774\uc600\uc2b5\ub2c8\ub2e4. JVM\uc740 \uc2e4\ud589\ub418\ub294 \ud658\uacbd\uc5d0 \ub530\ub77c \ud799 \uba54\ubaa8\ub9ac\uc758 \ucd5c\ub300 \uc0ac\uc774\uc988\ub97c \uc815\ud569\ub2c8\ub2e4. \ud799 \uba54\ubaa8\ub9ac\ub294 \uc124\uc815\ud558\uc9c0 \uc54a\uc73c\uba74 \ud574\ub2f9 \ud658\uacbd\uc758 \uba54\ubaa8\ub9ac 1/4\ub85c \uc124\uc815\ud569\ub2c8\ub2e4.\\n\uadf8\ub798\uc11c \uc800\ud76c EC2 \uc778\uc2a4\ud134\uc2a4\uc758 \uba54\ubaa8\ub9ac\ub294 \uc57d 2\uae30\uac00\ub85c, \uc57d 500MB\uac00 \ud560\ub2f9\ub418\uc5b4 \uc788\uc5c8\uc2b5\ub2c8\ub2e4. \uadf8\ub798\uc11c \uc800\ud76c\ub294 \uba54\ubaa8\ub9ac\ub97c \uc870\uae08\uc529 \ub298\ub824\uac00\uba70 \uc870\uc815\ud558\uc5ec \uc57d 1\uae30\uac00\ub85c \ud799 \uba54\ubaa8\ub9ac\uc758 \ucd5c\ub300 \uc0ac\uc774\uc988\ub97c \uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\ud799 \uba54\ubaa8\ub9ac\uc758 \uc124\uc815\uc744 \ud558\ub294 \ubc29\ubc95\uc740 \uac04\ub2e8\ud569\ub2c8\ub2e4.\\n```shell\\njava -Xms512m -Xmx1024m boxster.jar\\n```\\n\uc2e4\ud589\ud560 \ub54c \uc774\ub7ec\ud55c \ubc29\uc2dd\uc73c\ub85c \ud558\uba74 \ucd5c\uc18c \ud799 \uba54\ubaa8\ub9ac \uc0ac\uc774\uc988\ub294 512MB, \ucd5c\ub300 1024MB\ub85c \uc124\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \ud398\uc774\uc9d5\ud574\uc11c \uac00\uc838\uc624\uae30\\n\ud799 \uba54\ubaa8\ub9ac\uc758 \uc0ac\uc774\uc988\ub97c \uc870\uc808\ud574\uc11c \ud574\uacb0\ud55c\ub2e4\ub294 \ubd80\ubd84\uc740 \uc784\uc2dc \ubc29\ud3b8\uc774\uc9c0 \ub9cc\uc57d \uc800\ud76c EC2 \ud658\uacbd\uc774 \ub2e4\uc6b4\uadf8\ub808\uc774\ub4dc \ub418\uac70\ub098 \ud55c\ub2e4\uba74 \ub610 OOM\uc774 \ubc1c\uc0dd\ud560 \uac83\uc774 \ubed4\ud569\ub2c8\ub2e4. \uadf8\ub798\uc11c \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158 \ub808\ubca8\uc5d0\uc11c \uc880 \ub354 \ud574\uacb0\ud560 \ubc29\uc548\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n\\nAPI\uc758 \uc694\uccad\uc5d0 \ub300\ud55c \ubd80\ubd84\uc740 \uc694\uccad\ubcf4\ub0b4\ub294 \ud68c\uc0ac\uc758 \uc815\ucc45\uc774 \ubc14\ub00c\uc9c0 \uc54a\ub294 \uc774\uc0c1 \uc800\ud76c\ub294 23\ub9cc\uac74\uc744 \ubaa8\ub450 \ub85c\ub529\ud574\uc57c\ud55c\ub2e4\ub294 \uc810\uc740 \uc5b4\uca54 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uadf8\ub807\ub2e4\uba74 \uc800\ud76c\uac00 \uc81c\uc5b4\ud560 \uc218 \uc788\ub294 \uc720\uc77c\ud55c \ubd80\ubd84\uc740 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \ub370\uc774\ud130\ub97c \uaebc\ub0b4\uc624\ub294 \ubd80\ubd84 \ubc16\uc5d0 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub807\ub2e4\uba74 \uc774\uac83\uc744 \uc5b4\ub5bb\uac8c \uc870\uc808\ud560 \uc218 \uc788\uc744\uae4c\uc694.\\n\\n\uc5ec\ub7ec \ubc29\ubc95\uc744 \ucc3e\uc544\ubcf4\ub358 \uc911 `No Offset`\ubc29\uc2dd\uc73c\ub85c \ub370\uc774\ud130\ub97c \ud398\uc774\uc9d5\ud55c\ub2e4\ub294 \uae00\uc744 \uc77d\uc5c8\uc2b5\ub2c8\ub2e4. \ud398\uc774\uc9d5\uc744 \ud558\uae30\uc704\ud574\uc11c\ub294 \uc5b4\ub514\uc11c\ubd80\ud130 \uc2dc\uc791\ud558\uace0 \uc5b4\ub514\uae4c\uc9c0 \uac00\uc838\uc62c \uac83\uc778\uc9c0 \uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \uadf8 \uc911 \uba3c\uc800 \uc81c\uc77c \uc790\uc8fc \uc0ac\uc6a9\ub418\ub294 Offset \ubc29\uc2dd\uc5d0 \ub300\ud574 \uac04\ub2e8\ud788 \uc124\uba85\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\ud574\ub2f9 \ubc29\uc2dd\uc740\\n```sql\\nSELECT *\\nFROM station\\nORDER BY id DESC\\nOFFSET 20000\\nLIMIT 10000\\n```\\n\uc774\ub7ec\ud55c \ucffc\ub9ac\ub97c \ub9cc\ub4e4\uc5b4 \uc694\uccad\ud569\ub2c8\ub2e4. station \ud14c\uc774\ube14\uc758 20001\ubc88\uc9f8 \ub808\ucf54\ub4dc\ubd80\ud130 10000\uac1c\uc758 \ub370\uc774\ud130\ub97c \uc694\uccad\ud558\ub294 \ubc29\uc2dd\uc785\ub2c8\ub2e4. \uc774\ub7ec\ud55c \ucffc\ub9ac\ub3c4 \ub098\uc058\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\uc7a5\uc810\uc73c\ub85c\ub294 \uc5b8\uc81c\ub4e0 \ud574\ub2f9 \ud398\uc774\uc9c0\ub85c \uc774\ub3d9\ud560 \uc218 \uc788\ub2e4\ub294 \uc810\uc785\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774 \ucffc\ub9ac\uc5d0\ub294 \ub2e8\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4. \ub4a4\ub85c \uac08\uc218\ub85d \uc131\ub2a5\uc774 \ub098\ube60\uc9c4\ub2e4\ub294 \uc810\uc785\ub2c8\ub2e4. 20001\ubc88\uc9f8 \ub808\ucf54\ub4dc\ubd80\ud130 10000\uac1c\ub97c \uc694\uccad\ud55c\ub2e4\uba74 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub294 \uc5b4\uca54 \uc218 \uc5c6\uc774 20001\ubc88\uc9f8 \ub808\ucf54\ub4dc\ub97c \ucc3e\uae30 \uc704\ud574\\n\uc815\ub82c\uc744 \ud558\uace0, \uc815\ub82c\ud55c \ud6c4\uc5d0 20001\ubc88\uc9f8\uae4c\uc9c0 \uc138\uc5b4\uac00\uba70 \uc77d\uace0, \uac70\uae30\uc11c\ubd80\ud130 10000\uac1c\uc758 \ub808\ucf54\ub4dc\ub97c \ubc18\ud658\ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n![offset](./offset.png)\\n\\n\ud55c \ubb38\uc7a5\uc73c\ub85c \uc815\uc758\ud558\uba74, \uc21c\uc11c\ub97c \uc54c\uc544\uc57c\ud558\uae30 \ub54c\ubb38\uc5d0 \ub0b4\uac00 \ud544\uc694\ud558\uc9c0 \uc54a\ub294 \ub808\ucf54\ub4dc\ub3c4 \uc77d\uc5b4\uc57c \ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\\n#### No Offset\\n\uadf8\ub7fc No offset \ubc29\uc2dd\uc5d0 \ub300\ud574 \uc124\uba85\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc2e4 \uc774\ub984\ub9cc \ub4e4\uc73c\uba74 \uc5b4\ub824\uc6b8 \uac83 \uac19\uc9c0\ub9cc \uadf8\ub0e5 offset\uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0 \ud398\uc774\uc9d5\ud558\ub294 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc2a4\ud06c\ub864\uc744 \ub0b4\ub9ac\uba74\uc11c \uc790\ub3d9\uc73c\ub85c \ub9c8\uc9c0\ub9c9\uc758 \ub370\uc774\ud130\ub97c \uae30\uc900\uc73c\ub85c \ub2e4\uc74c \uba87\uac1c\uc758 \ub808\ucf54\ub4dc\ub97c \ubd88\ub7ec\uc624\ub294 \ubc29\uc2dd\uc774\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\ud574\ub2f9 \ubc29\uc2dd\uc740\\n```sql\\nselect *\\nfrom station\\nwhere id < \ub9c8\uc9c0\ub9c9\uc73c\ub85c \ubcf4\ub0b8 id\\norder by id desc\\nlimit 10000;\\n```\\n\uc774\ub7ec\ud55c \ucffc\ub9ac\ub85c \uc791\ub3d9\ud569\ub2c8\ub2e4. \uc544\uae4c\uc640\ub294 \ub2e4\ub978 \ubd80\ubd84\uc740 where \uc808\uc5d0 `\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ubcf4\ub0b8 id`\ub77c\ub294 \uc815\ubcf4\uac00 \ud544\uc694\ud558\ub2e4\ub294 \ubd80\ubd84\uacfc, offset\uc774 \uc0ac\ub77c\uc9c4 \ubd80\ubd84\uc785\ub2c8\ub2e4.\\n\\n\uac19\uc740 \uacb0\uacfc\ub97c \ub9cc\ub4e4\uc5b4\ub0b4\ub294 \ucffc\ub9ac\uc9c0\ub9cc, \ud558\ub098\uac00 \ucd94\uac00\ub418\uace0 \ud558\ub098\uac00 \uc0ac\ub77c\uc84c\ub2e4\ub294 \uac83\uc740 \ucd94\uac00\ub41c \ubd80\ubd84\uc774 \uc0ac\ub77c\uc9c4 \ubd80\ubd84\uc744 \ub300\uc2e0\ud55c\ub2e4\ub294 \ub73b\uc774\uaca0\uc8e0.\\n\\n\uc774 \uc774\ub7ec\ud55c \ubc29\uc2dd\uc758 \ub2e8\uc810\uc740 offset\uc744 \uc774\uc6a9\ud55c \ubc29\uc2dd\uacfc\ub294 \ub2e4\ub974\uac8c page\ub97c \uc9c0\uc815\ud574\uc11c \ub3cc\uc544\uac00\uae30\ub294 \ud798\ub4ed\ub2c8\ub2e4.\\n\\n![no offset](./no-offset.png)\\n\\n\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ubcf4\ub0b8 id\ub97c \ubc1b\uc544 \uc778\ub371\uc2a4\ub97c \uc774\uc6a9\ud574 \ud574\ub2f9 id\uc5d0\uc11c\ubd80\ud130 \ub808\ucf54\ub4dc\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4. \uad73\uc774 \ud544\uc694\uc5c6\ub294 \ub808\ucf54\ub4dc\ub97c \uc77d\uc744 \ud544\uc694 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \uc131\ub2a5\uc774 \uc88b\uc544\uc84c\uc744 \uac83\uc774\ub77c \uc608\uc0c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uc131\ub2a5 \ucc28\uc774\\n\\n\ubc14\ub85c \ud55c\ubc88 \ub450 \uac1c\uc758 \ucffc\ub9ac\ub97c \uc2e4\ud589\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n![test](./test.png)\\n\\n\uc704\uc758 \ucffc\ub9ac\ub294 no offset, \uc544\ub798\ub294 offset \ubc29\uc2dd\uc785\ub2c8\ub2e4. \ud604\uc7ac \ub370\uc774\ud130\uac00 6\ub9cc\uac74 \ub4e4\uc5b4\uc788\ub294 \ud14c\uc774\ube14\uc758 \uc870\ud68c \uae30\uc900\uc73c\ub85c \uc57d 10\ubc30 \uac00\ub7c9 \uc131\ub2a5\uc774 \ucc28\uc774\ub098\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \uc2e4\ud589 \uacc4\ud68d\ub3c4 \uac04\ub2e8\ud788 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 offset \ubc29\uc2dd\uc758 \uc2e4\ud589 \uacc4\ud68d\uc785\ub2c8\ub2e4.\\n![offset explain](./offset-explain.png)\\n\\n type \uce7c\ub7fc\uc744 \ubcf4\uc2dc\uba74 `index`\ub77c\uace0 \ub418\uc5b4 \uc788\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc5ec\uae30\uc11c index \uc811\uadfc \ubc29\ubc95\uc740\\n \uc778\ub371\uc2a4\ub97c \ud6a8\uc728\uc801\uc73c\ub85c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc544\ub2cc \uc778\ub371\uc2a4\ub97c \ucc98\uc74c\ubd80\ud130 \ub05d\uae4c\uc9c0 \uc77d\ub294 full scan\uc744 \ub73b\ud569\ub2c8\ub2e4. \uadf8\ub798\uc11c \uadf8\ub2e4\uc9c0 \ud6a8\uc728\uc801\uc774\uc9c0 \ubabb\ud55c \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 rows \uce7c\ub7fc\uc5d0\ub294 `40010`\uc774\ub77c\uace0 \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ubd80\ubd84\uc740 \uc81c\uac00 offset\uc744 40000, limit\uc744 10\uc73c\ub85c \ub450\uc5c8\uae30 \ub54c\ubb38\uc5d0 40010d\uc758 row\ub97c\\n\uc77d\uc5b4\uc57c\ud55c\ub2e4\uace0 \uc608\uc0c1 \uac12\uc744 \ub098\ud0c0\ub0b8 \uac83\uc785\ub2c8\ub2e4.\\n\\n\ub2e4\uc74c\uc740 no offset \ubc29\uc2dd\uc758 \uc2e4\ud589 \uacc4\ud68d\uc785\ub2c8\ub2e4.\\n![no offset explain](./no-offset-explain.png)\\n\uc544\uae4c\uc640\ub294 \ub2e4\ub974\uac8c type \uce7c\ub7fc\uc740 `range`\uc785\ub2c8\ub2e4. range \uc811\uadfc \ubc29\uc2dd\uc740 \uc778\ub371\uc2a4\ub97c \ud558\ub098\uc758 \uac12\uc774 \uc544\ub2c8\ub77c \ubc94\uc704\ub85c \uac80\uc0c9\ud558\ub294 \uacbd\uc6b0\ub97c \uc758\ubbf8\ud569\ub2c8\ub2e4.\\n\uc880 \uc804\uc758 index \uc811\uadfc \ubc29\uc2dd\uacfc\ub294 \ub2e4\ub974\uac8c \ud6e8\uc52c \ud6a8\uc728\uc801\uc778 \uc811\uadfc \ubc29\uc2dd\uc785\ub2c8\ub2e4. \uadf8\ub9ac\uace0 rows\ub3c4 \ub2ec\ub77c\uc9c4 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uc9c4\uc9dc \ud574\uacb0\ud558\uae30\\n\uc774\uc81c \uc5f4\uc2ec\ud788 \ud398\uc774\uc9d5 \ucc98\ub9ac\ub97c \ud588\uc73c\ub2c8 \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc5d0\uc11c \ud574\uacb0\uc744 \ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5b4\uc57c\ud569\ub2c8\ub2e4.\\n\\n\uc800\ud76c \ud300\uc740 \ub3d9\uc801 \ucffc\ub9ac \uc0dd\uc131\uc744 \ub3c4\uc640\uc8fc\ub294 Query DSL\uc744 \ub3c4\uc785\ud558\uc9c0 \uc54a\uc558\uace0 \uc544\uc9c1\uae4c\uc9c4 \uad73\uc774 \ud544\uc694\ud558\uc9c0 \uc54a\uc544\uc11c no offset \ubc29\uc2dd\uc744 jpa\uc758 jpql\uc744 \ud1b5\ud574 \uad6c\ud604\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 \uccab \ud398\uc774\uc9c0\ub294 id\uc758 \uad00\uacc4\uc5c6\uc774 \uc6d0\ud558\ub294 \uac2f\uc218\ub9cc\ud07c\ub9cc \uac00\uc838\uc624\uba74 \ub429\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ub450\ubc88\uc9f8 \ud398\uc774\uc9c0\ubd80\ud130\ub294 id\ub97c \ubc1b\uc544 \uadf8 \ub2e4\uc74c\ubd80\ud130 \ubc18\ud658\ud558\uba74 \ub429\ub2c8\ub2e4.\\n```java\\npublic interface StationRepository extends Repository {\\n\\n @Query(\\"SELECT s FROM Station s INNER JOIN FETCH s.chargers ORDER BY s.stationId\\")\\n List findAllByOrder(Pageable pageable);\\n\\n @Query(\\"SELECT s FROM Station s INNER JOIN FETCH s.chargers WHERE s.stationId > :stationId ORDER BY s.stationId\\")\\n List findAllByPaging(@Param(\\"stationId\\") String stationId, Pageable pageable);\\n}\\n```\\n\uadf8\ub7fc \uc544\uae4c update\ub97c \ud574\uc8fc\ub358 \uba54\uc11c\ub4dc\uc5d0\uc11c \uc870\uae08 \uc218\uc815\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n```java\\n public void updatePeriodicStations() {\\n List stations = getStations();\\n // \ucc98\uc74c\uc5d0\ub294 station\uc758 id\uac00 null\\n String lastStationId = null;\\n for (int i = 0; i < stations.size() / LIMIT + 1; i++) {\\n // \ub9c8\uc9c0\ub9c9 id\ub97c \uba54\uc11c\ub4dc \uc2e4\ud589\ud560 \ub54c\ub9c8\ub2e4 \ubcc0\uacbd\ud574\uc900\ub2e4.\\n lastStationId = stationUpdateService.updateStations(stations, lastStationId, LIMIT);\\n }\\n }\\n\\n public String updateStations(List updatedStations, String lastStationId, int limit) {\\n List savedStations = getStations(lastStationId, limit);\\n\\n Map savedStationsByStationId = stations.stream()\\n .collect(Collectors.toMap(Station::getStationId, Function.identity()));\\n\\n // \uc800\uc7a5\ub41c \uc815\ubcf4\uc640 \ube44\uad50\ud558\uc5ec \uc0c8\ub85c\uc6b4 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub97c \ucc3e\ub294 \ub85c\uc9c1\\n ...\\n\\n saveAllStations(toSaveStations);\\n updateAllStations(toUpdateStations);\\n\\n saveAllChargers(toSaveChargers);\\n updateAllChargers(toUpdateChargers);\\n // \uac00\uc838\uc628 list\uc5d0\uc11c \uc81c\uc77c \ub9c8\uc9c0\ub9c9 station\uc758 id\ub97c \ubc18\ud658\\n return getLastStationId(savedStations);\\n }\\n // \ud398\uc774\uc9d5 \ucc98\ub9ac\\n private List getStations(String stationId, int limit) {\\n // Id \uac00 null \uc774\ub77c\uba74 \uccab \ud398\uc774\uc9c0\uc774\uae30 \ub54c\ubb38\uc5d0 limit \uc0ac\uc774\uc988\ub9cc\ud07c select\\n if (stationId == null) {\\n return stationRepository.findAllByOrder(Pageable.ofSize(limit));\\n }\\n // \uc544\ub2c8\ub77c\uba74 station Id \ubd80\ud130 limit \ub9cc\ud07c\\n return stationRepository.findAllByPaging(stationId, Pageable.ofSize(limit));\\n }\\n```\\n\uc774\ub807\uac8c \ub418\uba74 \uc6d0\ub798 23\ub9cc\uac1c\ub97c \ud55c\uaebc\ubc88\uc5d0 \uac00\uc838\uc624\ub358 \ub85c\uc9c1\uc744 \ub098\ub20c \uc218 \uc788\uae30 \ub54c\ubb38\uc5d0 Heap \uba54\ubaa8\ub9ac\uc758 \uc5ec\uc720\uac00 \uc0dd\uae38 \uac83\uc785\ub2c8\ub2e4.\\n\\n### \uc9c4\uc9dc \ud655\uc778\ud574\ubcf4\uae30\\n\\n\ubb3c\ub860 GC\uc758 \ub3d9\uc791\uc774 \uc5b4\ub5a8\uc9c0 \ubaa8\ub974\uaca0\uc9c0\ub9cc 23\ub9cc\uac1c \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uac83\ubcf4\ub2e4 5000\uac1c \ud639\uc740 \ub354 \uc801\uac8c \uc0dd\uc131\ud558\ub294 \uac83\uc774 Heap \uba54\ubaa8\ub9ac\ub97c \uc801\uac8c \uc0ac\uc6a9\ud560 \uac83\uc784\uc744 \uc720\ucd94\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uc9c1\uc811 \ud655\uc778\ud574\ubcf4\uae30 \uc804\uae4c\uc9c0\ub294 \ud655\uc2e0\ud560 \uc218 \uc5c6\uc73c\ub2c8 \uac04\ub2e8\ud788 `Runtime` \ud074\ub798\uc2a4\uc5d0\uc11c \uc81c\uacf5\ud574\uc8fc\ub294 `totalMemory()`, `freeMemory()` \uba54\uc11c\ub4dc\ub97c \ud1b5\ud574 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n @Test\\n void \ud398\uc774\uc9d5\uc744_\uc0ac\uc6a9\ud55c_\uc870\ud68c() {\\n List stations = stationRepository.findAllByOrder(Pageable.ofSize(1000));\\n\\n long total = Runtime.getRuntime().totalMemory();\\n long free = Runtime.getRuntime().freeMemory();\\n System.out.println(\\"paging \uc0ac\uc6a9 \uc911\uc778 \uba54\ubaa8\ub9ac: \\" + ((total - free) / 1024 / 1024) + \\"MB\\");\\n }\\n\\n @Test\\n void \ud398\uc774\uc9d5\uc744_\uc0ac\uc6a9\ud558\uc9c0_\uc54a\uace0_\uc870\ud68c() {\\n List stations = stationRepository.findAllFetch();\\n\\n long total = Runtime.getRuntime().totalMemory();\\n long free = Runtime.getRuntime().freeMemory();\\n\\n System.out.println(\\"findAll() \uc0ac\uc6a9 \uc911\uc778 \uba54\ubaa8\ub9ac: \\" + ((total - free) / 1024 / 1024) + \\"MB\\");\\n }\\n```\\n\\n![findAll](./findAll.png)\\n![paging](./paging.png)\\n\ud655\uc5f0\ud788 \ucc28\uc774\uac00 \ub098\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ubb3c\ub860 \ud14c\uc2a4\ud2b8\ucf54\ub4dc\uc5d0\uc11c\ub294 23\ub9cc\uac74\uc758 API \uc694\uccad\uc740 \uac19\uc740 \uc870\uac74\uc774\ub2c8 \ubc30\uc81c\ud558\uace0 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub85c\uc368 \ud558\ub098\uc758 \ubb38\uc81c\uac00 \ub610 \ud574\uacb0\ub41c \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\uc9c1 \ubc30\uc6b0\ub294 \ub2e8\uacc4\ub77c \ud639\uc2dc \ud2c0\ub9b0 \uc810\uc774 \uc788\ub2e4\uba74 \uc9c0\uc801 \ubd80\ud0c1\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## Reference\\n\\n- \ub9ac\uc5bc \ub9c8\uc774 \uc5d0\uc2a4\ud050\uc5d8 8.0\\n- https://jojoldu.tistory.com/528"},{"id":"23","metadata":{"permalink":"/23","source":"@site/blog/2023-07-31-deadlokc-trouble-shooting.mdx","title":"Deadlock trouble shooting","description":"\uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720","date":"2023-07-31T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 31\uc77c","tags":[{"label":"deadlock","permalink":"/tags/deadlock"},{"label":"trouble-shooting","permalink":"/tags/trouble-shooting"}],"readingTime":12.565,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"23","title":"Deadlock trouble shooting","authors":["boxster"],"tags":["deadlock","trouble-shooting"]},"prevItem":{"title":"Out of memory trouble shooting","permalink":"/24"},"nextItem":{"title":"\ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604\uacfc \uc778\ub371\uc2a4 \uc774\uc6a9\ud55c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\ud558\uae30","permalink":"/22"}},"content":"## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\uba3c\uc800 \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\ub294 \uc800\ud76c \uce74\ud398\uc778 \ud300\uc758 \ud63c\uc7a1\ub3c4 \uc800\uc7a5 \ubc0f \ucda9\uc804\uae30\uc758 \uc0c1\ud0dc\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\ub294 \ub85c\uc9c1\uc5d0\uc11c dead Lock\uc774 \ubc1c\uc0dd\ud558\uc5ec mysql\uacfc connection\uc744 \uc783\ub294 \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\\n```shell\\n------------------------\\nLATEST DETECTED DEADLock\\n------------------------\\n2023-07-21 01:49:54 281472560787424\\n*** (1) TRANSACTION:\\nTRANSACTION 1000560, ACTIVE 373 sec inserting\\nmysql tables in use 1, Locked 1\\nLock WAIT 3 Lock struct(s), heap size 1128, 9 row Lock(s), undo log entries 328\\nMySQL thread id 860, OS thread handle 281472107409376, query id 2958720 update\\nINSERT INTO charger_status (station_id, charger_id, latest_update_time, charger_state) VALUES (\'ST414511\', \'01\', \'2023-07-21 08:27:43\', \'CHARGING_IN_PROGRESS\') ON DUPLICATE KEY UPDATE latest_update_time = \'2023-07-21 08:27:43\', charger_state = \'CHARGING_IN_PROGRESS\'\\n\\n*** (1) HOLDS THE Lock(S):\\nRECORD LockS space id 64 page no 742 n bits 424 index PRIMARY of table `charge`.`charger_status` trx id 1000560 Lock_mode X Locks rec but not gap\\n\\n*** (1) WAITING FOR THIS Lock TO BE GRANTED:\\nRECORD LockS space id 64 page no 718 n bits 280 index PRIMARY of table `charge`.`charger_status` trx id 1000560 Lock_mode X Locks rec but not gap waiting\\n\\n*** (2) TRANSACTION:\\nTRANSACTION 946331, ACTIVE 507 sec inserting\\nmysql tables in use 1, Locked 1\\nLock WAIT 4 Lock struct(s), heap size 1323, 12 row Lock(s), undo log entries 432\\nMySQL thread id 859, OS thread handle 281472629186528, query id 3017483 update\\nINSERT INTO charger_status (station_id, charger_id, latest_update_time, charger_state) VALUES (\'ST412801\', \'11\', \'2023-07-21 10:48:20\', \'CHARGING_IN_PROGRESS\') ON DUPLICATE KEY UPDATE latest_update_time = \'2023-07-21 10:48:20\', charger_state = \'CHARGING_IN_PROGRESS\'\\n\\n*** (2) HOLDS THE Lock(S):\\nRECORD LockS space id 64 page no 718 n bits 208 index PRIMARY of table `charge`.`charger_status` trx id 946331 Lock_mode X Locks rec but not gap\\n\\n*** (2) WAITING FOR THIS Lock TO BE GRANTED:\\nRECORD LockS space id 64 page no 742 n bits 424 index PRIMARY of table `charge`.`charger_status` trx id 946331 Lock_mode X Locks rec but not gap waiting\\n\\n\\n```\\n\uc2e4\uc81c **\uac1c\ubc1c \uc11c\ubc84**\uc5d0\uc11c \ubc1c\uc0dd\ud55c \ub370\ub4dc\ub77d\uc758 \ub85c\uadf8\uc785\ub2c8\ub2e4. \ud574\ub2f9 \ub85c\uadf8\ub294 charger_status\uc5d0 \uc800\uc7a5 \uc2dc \uc11c\ub85c XLock\uc744 \ud68d\ub4dd\ud558\uc9c0 \ubabb\ud558\uc5ec \uc0dd\uae30\ub294 \uc5d0\ub7ec\uc785\ub2c8\ub2e4.\\n\\n## Mysql Dead Lock\uc774\ub780\\n\\n\uadf8\ub7fc Dead Lock\uc740 \uc65c \uc0dd\uae30\uace0 \uc5b8\uc81c \uc0dd\uae38\uae4c\uc694?\\n\uc800\ub294 \uc774 Log\ub97c \uc9c1\uc811 \ub9c8\uc8fc\ud558\uae30 \uc804\uae4c\uc9c0\ub294 Dead Lock\uc774 \uadf8\ub0e5 Lock\uc758 \uc2dc\uac04\uc774 \uc624\ub798 \uac78\ub9b4 \ub54c \uc0dd\uae30\ub294 \uc904 \uc54c\uc558\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uadf8\ub807\uac8c \uac04\ub2e8\ud558\uac8c \ubc1c\uc0dd\ud558\ub294 \uac83\uc740 \uc544\ub2c8\uc600\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc0c1\ud638 \ubc30\uc81c(Mutual Exclusion): MySQL\uc740 \uae30\ubcf8\uc801\uc73c\ub85c \ud2b8\ub79c\uc7ad\uc158 \ub0b4\uc5d0\uc11c \uc7a0\uae08(Lock)\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub370\uc774\ud130\uc758 \uc0c1\ud638 \ubc30\uc81c\ub97c \uc81c\uc5b4\ud569\ub2c8\ub2e4. \ub530\ub77c\uc11c \ub450 \uac1c \uc774\uc0c1\uc758 \ud2b8\ub79c\uc7ad\uc158\uc774 \uac19\uc740 \ub370\uc774\ud130\ub97c \ub3d9\uc2dc\uc5d0 \ubcc0\uacbd\ud558\ub824\uace0 \ud560 \ub54c, \ud574\ub2f9 \ub370\uc774\ud130\uc5d0 \ub300\ud55c \uc7a0\uae08\uc774 \uc124\uc815\ub418\uc5b4 \uc0c1\ud638 \ubc30\uc81c \uc870\uac74\uc774 \ub9cc\uc871\ub429\ub2c8\ub2e4.\\n\\n2. \uc810\uc720\uc640 \ub300\uae30(Hold and Wait): \ud2b8\ub79c\uc7ad\uc158\uc774 \uc774\ubbf8 \ud558\ub098 \uc774\uc0c1\uc758 \ub370\uc774\ud130\ub97c \uc7a0\uadfc \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ub370\uc774\ud130\uc758 \uc7a0\uae08\uc744 \uc5bb\uae30 \uc704\ud574 \ub300\uae30\ud558\uace0 \uc788\ub294 \uacbd\uc6b0 \uc810\uc720\uc640 \ub300\uae30 \uc870\uac74\uc774 \ub9cc\uc871\ub429\ub2c8\ub2e4. \uc989, \ud2b8\ub79c\uc7ad\uc158\uc774 \uc790\uc2e0\uc774 \uc810\uc720\ud55c \ub370\uc774\ud130\ub97c \uc720\uc9c0\ud55c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ub370\uc774\ud130\uc5d0 \ub300\ud55c \uc7a0\uae08\uc744 \uae30\ub2e4\ub9ac\uace0 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n\\n3. \ube44\uc120\uc810(Non-Preemption): MySQL\uc5d0\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud2b8\ub79c\uc7ad\uc158\uc774 \ub2e4\ub978 \ud2b8\ub79c\uc7ad\uc158\uc774 \uc810\uc720\ud55c \ub370\uc774\ud130\uc758 \uc7a0\uae08\uc744 \uac15\uc81c\ub85c \ud574\uc81c\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \ube44\uc120\uc810 \uc870\uac74\uc774 \ub9cc\uc871\ub429\ub2c8\ub2e4.\\n\\n4. \uc21c\ud658 \ub300\uae30(Circular Wait): \ub450 \uac1c \uc774\uc0c1\uc758 \ud2b8\ub79c\uc7ad\uc158\uc774 \uac01\uac01 \uc11c\ub85c\uac00 \uae30\ub2e4\ub9ac\ub294 \ub370\uc774\ud130\uc758 \uc7a0\uae08\uc744 \ubcf4\uc720\ud574\uc57c \uc21c\ud658 \ub300\uae30 \uc870\uac74\uc774 \ub9cc\uc871\ub429\ub2c8\ub2e4. \uc608\ub97c \ub4e4\uba74, \ud2b8\ub79c\uc7ad\uc158 A\uac00 \ub370\uc774\ud130 X\uc758 \uc7a0\uae08\uc744 \uae30\ub2e4\ub9ac\uace0, \ud2b8\ub79c\uc7ad\uc158 B\ub294 \ub370\uc774\ud130 Y\uc758 \uc7a0\uae08\uc744 \uae30\ub2e4\ub9ac\uba70, \ud2b8\ub79c\uc7ad\uc158 C\ub294 \ub370\uc774\ud130 Z\uc758 \uc7a0\uae08\uc744 \uae30\ub2e4\ub9ac\ub294 \uc0c1\ud0dc\uac00 \ubc1c\uc0dd\ud55c\ub2e4\uba74 \uc21c\ud658 \ub300\uae30 \uc870\uac74\uc774 \uc131\ub9bd\ud569\ub2c8\ub2e4.\\n\\n\uc0ac\uc2e4 \uae30\ubcf8 \ucef4\ud4e8\ud130 \uc2dc\uc2a4\ud15c\uc758 dead Lock\uacfc \uc720\uc0ac\ud55c \uc870\uac74\uc785\ub2c8\ub2e4. \uc774 \ubd80\ubd84\uc744 \ubaa8\ub450 \ub9cc\uc871\ud574\uc57c \ub370\ub4dc\ub77d\uc774 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\ud558\ub098\uc529 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \uac1c\ubc1c \uc11c\ubc84\uc5d0\uc11c \ubc1c\uc0dd\ud55c \ub370\ub4dc\ub77d\uc73c\ub85c \uc0b4\ud3b4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n```shell\\n*** (1) TRANSACTION:\\nTRANSACTION 1000560, ACTIVE 373 sec inserting\\nmysql tables in use 1, Locked 1\\nLock WAIT 3 Lock struct(s), heap size 1128, 9 row Lock(s), undo log entries 328\\nMySQL thread id 860, OS thread handle 281472107409376, query id 2958720 update\\nINSERT INTO charger_status (station_id, charger_id, latest_update_time, charger_state) VALUES (\'ST414511\', \'01\', \'2023-07-21 08:27:43\', \'CHARGING_IN_PROGRESS\') ON DUPLICATE KEY UPDATE latest_update_time = \'2023-07-21 08:27:43\', charger_state = \'CHARGING_IN_PROGRESS\'\\n\\n*** (1) HOLDS THE Lock(S):\\nRECORD LockS space id 64 page no 742 n bits 424 index PRIMARY of table `charge`.`charger_status` trx id 1000560 Lock_mode X Locks rec but not gap\\n\\n\\n-------------------------------------------------------------------------\\n*** (2) TRANSACTION:\\nTRANSACTION 946331, ACTIVE 507 sec inserting\\nmysql tables in use 1, Locked 1\\nLock WAIT 4 Lock struct(s), heap size 1323, 12 row Lock(s), undo log entries 432\\nMySQL thread id 859, OS thread handle 281472629186528, query id 3017483 update\\nINSERT INTO charger_status (station_id, charger_id, latest_update_time, charger_state) VALUES (\'ST412801\', \'11\', \'2023-07-21 10:48:20\', \'CHARGING_IN_PROGRESS\') ON DUPLICATE KEY UPDATE latest_update_time = \'2023-07-21 10:48:20\', charger_state = \'CHARGING_IN_PROGRESS\'\\n\\n*** (2) HOLDS THE Lock(S):\\nRECORD LockS space id 64 page no 718 n bits 208 index PRIMARY of table `charge`.`charger_status` trx id 946331 Lock_mode X Locks rec but not gap\\n```\\n\\n1\ubc88 \ud2b8\ub79c\uc7ad\uc158 1000560\uc774 charge_status \ud14c\uc774\ube14\uc5d0 insert ~~~ on duplicate key update ~~~ \ucffc\ub9ac\ub97c \ubc1c\uc0dd\uc2dc\ud0a4\uae30 \uc704\ud574 `space id 64 page no 742 n bits 424 index PRIMARY of table` \uc5d0 X Lock\uc744 \uac00\uc9c0\uace0 \uc788\uc2b5\ub2c8\ub2e4\\n\uadf8\ub9ac\uace0 2\ubc88 \ud2b8\ub79c\uc7ad\uc158 946331 \ub3c4 \ub611\uac19\uc740 \ud14c\uc774\ube14\uc5d0 \ube44\uc2b7\ud55c \ucffc\ub9ac\ub97c \ubc1c\uc0dd\uc2dc\ud0a4\ub824\uace0 \ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ud574\ub2f9 \ud2b8\ub79c\uc7ad\uc158\ub3c4 X Lock\uc744 \uac00\uc9c0\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uc800\ud76c \ud300\uc5d0 \ub370\ub4dc\ub77d\uc774 \ubc1c\uc0dd\ud55c \uc774\uc720\\n\uba3c\uc800 \uc800\ud76c \ud300\uc740 \uacf5\uacf5 API\ub97c \ud1b5\ud574 \uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c cron\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \ucda9\uc804\uae30\uc758 \uc0c1\ud0dc\ub3c4 \uc8fc\uae30\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uac31\uc2e0\ud560 \uacbd\uc6b0 \uc0c8\ub85c \uc0dd\uae34 \ucda9\uc804\uc18c\uac00 \uc788\ub2e4\uba74 \ucd94\uac00\ud574\uc918\uc57c\ud558\uace0, \uc0c8\ub85c \uc0dd\uae34 \ucda9\uc804\uc18c\uac00 \uc788\ub2e4\uba74 \uc0c8\ub85c\uc6b4 \ucda9\uc804\uae30\uac00 \uc788\uc744\ud14c\uace0, \uadf8\uc5d0\ub530\ub978 \ucda9\uc804\uae30 \uc0c1\ud0dc\ub3c4 \ucd94\uac00\ud574\uc918\uc57c\ud569\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \uc6d0\ub798 \uc788\ub358 \ucda9\uc804\uc18c\uc758 \uc815\ubcf4\uac00 \ubc14\ub00c\ub294 \uac83\uc5d0 \ub300\ud574\uc11c\ub3c4 \uc5c5\ub370\uc774\ud2b8 \ud574\uc918\uc57c\ud569\ub2c8\ub2e4. \uc774\ub807\uac8c \ub41c\ub2e4\uba74 \uc5ec\ub7ec\ubc88\uc758 \ubd84\uae30 \ucc98\ub9ac\ub85c application \ub808\ubca8\uc5d0\uc11c \ud574\uacb0\ud558\ub294 \uac83\uc774 \ub098\uc744\uc9c0 \ud639\uc740 mysql\uc758 \ubb38\ubc95 \uc911 `INSERT ~~ ON DUPLICATE KEY UPDATE ~~`\uc744 \uc774\uc6a9\ud560\uae4c \uace0\ubbfc\uc744 \ud588\uc5c8\ub294\ub370\uc694.\\n\\n\uadf8 \uc911 \ud6c4\uc790\ub97c \ud0dd\ud55c \uc774\uc720\ub97c \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4. \ucda9\uc804\uae30\uc758 \uc815\ubcf4\ub294 \ud558\ub8e8\uc5d0 \uacf5\uacf5 API\ub97c \uc694\uccad\ud560 \uc218 \uc788\ub294 key \uc81c\ud55c\uc774 \uc788\uace0 \uc9c0\ub3c4 \uae30\ubc18\uc73c\ub85c \uac80\uc0c9\ud560 \uc218 \uc788\ub294 \uc870\uac74\uc774 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \uc2e4\uc2dc\uac04\uc73c\ub85c \uc694\uccad\ud560 \uc218 \uc5c6\ub294 \uad6c\uc870\uc785\ub2c8\ub2e4.\\n\uadf8\ub798\uc11c \uc694\uccad key \uc81c\ud55c\uc744 \ub118\uc9c0 \uc54a\ub294 \uc120\uc5d0\uc11c \uc790\uc8fc \uc694\uccad\uc744 \ud574\uc57c\ud569\ub2c8\ub2e4. \uadf8\ub807\uac8c\ud574\uc57c \uc0ac\uc6a9\uc790\uc5d0\uac8c \uc815\ubcf4\uc5d0 \ub300\ud55c \uc2e0\ub8b0\ub97c \uc904 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc790\uc8fc \uc694\uccad\ub418\ub294 \uc791\uc5c5\uc5d0 Application \ub808\ubca8\uc5d0\uc11c \uad6c\ud604\ud55c\ub2e4\uba74, findAll() \uba54\uc11c\ub4dc\ub97c \ud1b5\ud574 23\ub9cc\uac74\uc758 \ucda9\uc804\uae30 \uc0c1\ud0dc \uc815\ubcf4\ub97c \uba54\ubaa8\ub9ac\uc5d0 \ub85c\ub529\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 api\uc758 \uc694\uccad\uc744 \ud1b5\ud574 \uc5bb\uc740 23\ub9cc+n\uac74\uc758 \ucda9\uc804\uae30 \uc0c1\ud0dc \uc815\ubcf4\ub97c \uba54\ubaa8\ub9ac\uc5d0 \uc62c\ub9bd\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ud574\ub2f9 \uc815\ubcf4\uac00 \uc788\ub294\uc9c0 \ube44\uad50\ud569\ub2c8\ub2e4. \ud574\ub2f9 \uc815\ubcf4\uac00 findAll()\ub85c \ucc3e\uc544\uc628 list\uc5d0 \uc5c6\uc73c\uba74 Insert, \ud574\ub2f9 \uc815\ubcf4\uac00 \uc788\ub2e4\uba74 update\ub97c \ud1b5\ud574 \uac31\uc2e0\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub807\uac8c \ud55c\ub2e4\uba74 \ucd1d batch Insert, Update\ub97c \ud1b5\ud574 2\ubc88\uc758 \ucffc\ub9ac + 46\ub9cc n\uac74\uc758 \ucda9\uc804\uae30 \uc815\ubcf4\ub97c \uba54\ubaa8\ub9ac\uc5d0 \uc62c\ub824 \ube44\uad50\ud558\ub294 \uc5f0\uc0b0\uc774 \uc0dd\uae38 \uac83 \uc785\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc `INSERT ~~ ON DUPLICATE KEY UPDATE ~~` \ub97c \uc0ac\uc6a9\ud55c\ub2e4\uba74 batch insert\ub97c \ud1b5\ud574 1\ubc88\uc758 \ucffc\ub9ac\ub85c \ud574\uacb0 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uba54\ubaa8\ub9ac\uc5d0 \ub370\uc774\ud130\ub3c4 \uc801\uc7ac\ud558\uc9c0 \uc54a\uace0 \ub9d0\uc774\uc8e0.\\n\\n\ud558\uc9c0\ub9cc \ubcf4\uc168\ub358 \uac83\uacfc \uac19\uc774 \ub370\ub4dc\ub77d\uc774 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc774\uc720\ub294 `INSERT ~~ ON DUPLICATE KEY UPDATE ~~` \uc758 \ud2b9\uc218\ud55c Lock mechanism \ub54c\ubb38\uc785\ub2c8\ub2e4. \ud574\ub2f9 \ucffc\ub9ac\ub294\\n1. \uba3c\uc800 \uc0bd\uc785\ud558\ub824\ub294 \ud589\uc774 \ud14c\uc774\ube14\uc5d0 \uc874\uc7ac\ud558\ub294\uc9c0 \ud655\uc778\ud569\ub2c8\ub2e4.\\n2. \uadf8\ub9ac\uace0 \uc77d\uc740 record\uc5d0 \ub300\ud574 S Lock\uc744 \uc124\uc815\ud569\ub2c8\ub2e4.\\n3. \uadf8\ub9ac\uace0 \ud574\ub2f9 record\uac00 duplicate key\ub77c\ub294 \uc870\uac74\uc774\ub77c\uba74 X Lock\uc744 \uc124\uc815\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \ubc29\uc2dd\uc73c\ub85c \uc791\ub3d9\ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\uc774\ub7f0 \ubc29\uc2dd\uc774 \uc65c \ubb38\uc81c\uac00 \ub420 \uc218 \uc788\ub294\uc9c0 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\uba3c\uc800 \uc790\uc2e0\uc774 \uc77d\uc740 record\uc5d0 S Lock\uc744 \uac01\uac01\uc758 \ud2b8\ub79c\uc7ad\uc158\uc774 \uc124\uc815\ud569\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \uc5c5\ub370\uc774\ud2b8\ub97c \ud558\uae30\uc704\ud574 record\uc5d0 X Lock\uc744 \uc124\uc815\ud558\ub824\uace0 \ud569\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uac01\uac01\uc758 \ud2b8\ub79c\uc7ad\uc158\uc774 \uc11c\ub85c S Lock\uc744 \uc124\uc815\ud588\uae30 \ub54c\ubb38\uc5d0 S Lock\uc744 \ubc18\ub0a9\ud558\uace0 X Lock\uc744 \uc124\uc815\ud558\ub824\uace0 \ud574\ub3c4 \ub450 \ud2b8\ub79c\uc7ad\uc158 \ubaa8\ub450 \uae30\ub2e4\ub9ac\ub294 \ub370\ub4dc\ub77d \uc0c1\ud669\uc774 \ubc1c\uc0dd\ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4. \uc774\ub7f0 \uc0c1\ud669\uc774 \ub418\uba74 \uc544\uae4c \uc704\uc5d0\uc11c \ub9d0\uc500\ub4dc\ub838\ub358 \ub370\ub4dc\ub77d\uc758 \uc870\uac74 4\uac00\uc9c0\uac00 \ub2e4 \ub9cc\uc871\ud558\uace0 \ub370\ub4dc\ub77d\uc774 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\\n### \ud574\uacb0 \ubc29\uc548\\n\\n\ud574\uacb0 \ubc29\uc548\uc740 \uc5ec\ub7ec\uac00\uc9c0\uac00 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\uadf8 \uc911 \uc81c \uc218\uc900\uc5d0\uc11c \uc0dd\uac01\ud560 \uc218 \uc788\ub294 \ubc29\ubc95\uc740 2\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1. \ud2b8\ub79c\uc7ad\uc158\uc744 \uc791\uac8c \ubd84\ub9ac\\n\ud2b8\ub79c\uc7ad\uc158\uc744 \uc624\ub798 \uac00\uc9c0\uace0 \uc788\uc73c\uba74 Lock\uc744 \uac00\uc9c0\uace0 \uc788\ub294 \uc2dc\uac04\uc774 \uc624\ub798\uac78\ub9bd\ub2c8\ub2e4.\\n\uadf8\ub798\uc11c \ud2b8\ub79c\uc7ad\uc158\uc744 \uc791\uac8c \ubd84\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud398\uc774\uc9d5\uc744 \ud1b5\ud574 \ud2b8\ub79c\uc7ad\uc158\uc744 \uc791\uac8c \ubd84\ub9ac\ud558\ub2e4\ubcf4\uba74 \ucffc\ub9ac\uac00 \uc5ec\ub7ec\ubc88 \ub098\uac00 \uc131\ub2a5\uc0c1 \ubb38\uc81c\uac00 \uc0dd\uae38 \uc218 \uc788\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n2. `INSERT ~~ ON DUPLICATE KEY UPDATE ~~` \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uae30\\n\ud574\ub2f9 sql\uc774 \uc544\ub2cc `INSERT IGNORE`\uc744 \uc0ac\uc6a9\ud558\uc5ec \ucd94\uac00\ub41c \uc815\ubcf4\ub9cc \ub123\uace0, update\ub294 \ub2e4\ub978 \uc791\uc5c5\uc73c\ub85c \ubd84\ub9ac\ud558\uae30\\n\\n\uc774\ub7f0 \ubc29\ubc95\ub4e4\uc744 \uc0ac\uc6a9\ud558\uba74 \ub420 \uac83 \uac19\uc558\uc2b5\ub2c8\ub2e4. \uadf8 \uc911 \uc800\ub294 \ud604\uc7ac\ub294 \uac04\ub2e8\ud558\uac8c 2\ubc88\uc9f8 \ubc29\ubc95\uc774 \uc81c\uc77c \ub098\uc744 \uac83 \uac19\ub2e4\ub294 \uc0dd\uac01\uc5d0 \ucffc\ub9ac\ub97c \uc218\uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ubb38\uc81c\ub97c \ud574\uacb0\ud588\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uac8c \ub418\uc5b4 \uc880 \ub354 \uc7ac\ubc0c\ub294 \uac83\ub4e4\uc744 \uace0\ubbfc\ud558\uace0 \uacf5\ubd80\ud560 \uc218 \uc788\ub294 \uc800\ud76c \ud300\uc5d0\uac8c \uac10\uc0ac\ud558\uace0 \ubaa8\ub974\ub294 \ud0a4\uc6cc\ub4dc\ub97c \ub9ce\uc774 \uc54c\ub824\uc900 \ub204\ub204\uc5d0\uac8c \uac10\uc0ac\ud569\ub2c8\ub2e4.\\n\\n\uc544\uc9c1 \ubc30\uc6b0\ub294 \ub2e8\uacc4\ub77c \uc815\ud655\ud55c \uc815\ubcf4\uac00 \uc544\ub2d0 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubd80\uc871\ud55c \ubd80\ubd84\uc5d0 \ub300\ud574 \ub9ce\uc740 \uc9c0\uc801 \ubd80\ud0c1\ub4dc\ub9bd\ub2c8\ub2e4."},{"id":"22","metadata":{"permalink":"/22","source":"@site/blog/2023-07-27-filtering-and-index.mdx","title":"\ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604\uacfc \uc778\ub371\uc2a4 \uc774\uc6a9\ud55c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\ud558\uae30","description":"\uc548\ub155\ud558\uc138\uc694~","date":"2023-07-27T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 27\uc77c","tags":[{"label":"filter","permalink":"/tags/filter"},{"label":"index","permalink":"/tags/index"}],"readingTime":10.86,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"22","title":"\ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604\uacfc \uc778\ub371\uc2a4 \uc774\uc6a9\ud55c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\ud558\uae30","authors":["jay"],"tags":["filter","index"]},"prevItem":{"title":"Deadlock trouble shooting","permalink":"/23"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc774 styled-components\ub97c \uc120\ud0dd\ud55c \uc774\uc720","permalink":"/20"}},"content":"\uc548\ub155\ud558\uc138\uc694~\\n\\n\uc6b0\ud14c\ucf54 \uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\uc624\ub298\uc740 \ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604 \ubc0f \uc778\ub371\uc2a4\ub97c \uc774\uc6a9\ud55c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\ud558\ub294 \uc791\uc5c5\uc744 \uc9c4\ud589\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\\n## \uc694\uad6c \uc0ac\ud56d\uacfc \uae30\ub2a5 \uad6c\ud604 \ubaa9\ub85d\\n\uce74\ud398\uc778 \ud300\uc740 \uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc870\ud68c \ubc0f \ud1b5\uacc4 \ub370\uc774\ud130\ub97c \uc81c\uacf5\ud574\uc8fc\ub294 \uc11c\ube44\uc2a4\uc785\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790 \uc785\uc7a5\uc5d0\uc11c \uc804\uae30\ucc28 \ucda9\uc804\uc18c\ub97c \uc870\ud68c\ud560 \ub54c \ubcf8\uc778 \ucc28\uc5d0 \ub9de\ub294 \ucda9\uc804\uae30 \ud0c0\uc785\uacfc, \uc18d\ub3c4, \ub9c8\uc9c0\ub9c9\uc73c\ub85c \ucda9\uc804\uae30\ub97c \uc81c\uacf5\ud558\ub294 \ud68c\uc0ac\uba85 \uc694\uae08\uacfc \uad00\ub828\ub3c4 \ub418\uc5b4 \uc788\uc5b4\uc11c \uc911\uc694\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \ubb34\uc218\ud788 \ub9ce\uc740 \ucda9\uc804\uc18c\ub97c \ubcf4\ub294 \uac83\uc774 \uc544\ub2cc \uc790\uc2e0\uc5d0\uac8c \ud544\uc694\ud55c \uac83\ub9cc \ubcf4\ub294 \uac83\uc774 \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc5d0 \uc788\uc5b4\uc11c\ub294 \ub354 \uc911\uc694\ud55c\ub370\uc694.\\n\\n\uc800\ud76c \ud300\uc740 \uc774\ub97c \uc704\ud574 \ud544\ud130\ub9c1 \uae30\ub2a5\uc744 \ub3c4\uc785\ud558\uace0\uc790 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ub610\ud55c \uc870\ud68c\uac00 \ub9ce\uc740 \uc11c\ube44\uc2a4\uc778\ub9cc\ud07c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\uc744 \uc704\ud574 \uc778\ub371\uc2a4\ub97c \uc801\uc6a9\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud544\ud130\ub9c1 \ubfd0\ub9cc \uc544\ub2c8\ub77c \ud574\ub2f9 \uc791\uc5c5\uc744 \ud558\uba74\uc11c \uc5b4\ub5a4 \uace0\ubbfc\uc744 \ud588\uace0 \uc5b4\ub5a4 \uac83\uc744 \ud588\ub294\uc9c0 \uc801\uc5b4\ubcf4\uace0\uc790 \ud569\ub2c8\ub2e4.\\n\\n\\n## \ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604\ud558\uae30\\n\uc800\ud76c \ud300\uc740 \ube60\ub974\uac8c \uae30\ub2a5\uc744 \uad6c\ud604\ud558\ub294 \ub2e8\uacc4\uc5d0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc77c\ub2e8 3\uac1c\uc758 \ud544\ud130\ub9cc \ub3c4\uc785\ud588\uace0, \ud544\ud130\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4. [\ucda9\uc804\uc18c \uc6b4\uc601 \ud68c\uc0ac \uc774\ub984, \ucda9\uc804 \ud0c0\uc785, \ucda9\uc804 \uc18d\ub3c4]\\n\\n\uc0ac\uc6a9\uc790\ub294 \ud544\ud130\ub97c \ud074\ub9ad\ud558\uba74 \ud604\uc7ac \uc704\uce58\ub97c \uae30\uc900\uc73c\ub85c \uc8fc\ubcc0\uc5d0 \ud574\ub2f9 \ud544\ud130\uac00 \uc801\uc6a9\ub41c \ucda9\uc804\uc18c\ub97c \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n3\uac1c\uc758 \ud544\ud130 \uc911\uc5d0\uc11c \ubaa8\ub450 \uc801\uc6a9\ub420 \uc218\ub3c4 \uc788\uace0, \ubaa8\ub450 \uc801\uc6a9\ub418\uc9c0 \uc54a\uc744 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c 2^3 = 8\uac00\uc9c0\uc758 \uacbd\uc6b0\ub97c \uc0dd\uac01\ud574\uc57c \ud588\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uadf8\ub798\uc11c \ucc98\uc74c\uc5d0 \ud544\ud130\ub97c \uc801\uc6a9\ud558\uae30 \uc704\ud574\uc11c \ub2e4\uc74c\uacfc \uac19\uc740 \ubc29\ubc95\ub4e4\uc744 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n1. JPQL + \ud544\ud130\uc758 \uc870\ud569 (2^3)\ub9cc\ud07c if\ubb38 \uc0ac\uc6a9\ud558\uae30\\n\\n2. \uae30\uc874 \uc88c\ud45c\ub85c \uc870\ud68c\ud558\ub294 findAllByLatitudeBetweenAndLongitudeBetween() \uba54\uc11c\ub4dc\ub97c \uc0ac\uc6a9 \ud6c4 Stream\uc744 \uc774\uc6a9\ud574 \uc790\ubc14 \ucf54\ub4dc\ub85c \ud544\ud130\ub9c1\ud558\uae30\\n\\n\\n\uc774\ub807\uac8c \ub450 \uac00\uc9c0 \ubc29\ubc95\uc774 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n1\ubc88\uc758 \uacbd\uc6b0 \uc6b0\ud14c\ucf54 \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c Querydsl\uc744 \uc0ac\uc6a9\ud574\ub3c4 \ub418\ub294\uc9c0 \ud655\uc2e4\ud558\uc9c0 \uc54a\uc558\uace0 \uc815\ud655\ud55c \ud544\ud130 \uba85\uc138\uac00 \uc544\uc9c1\uc740 \uc5c6\uace0 3\uac00\uc9c0\ub9cc \uc77c\ub2e8 \ub3c4\uc785\ud558\uace0\uc790 \ud574\uc11c JPQL\uc744 \uc774\uc6a9\ud574\uc11c \uc0c1\ud669\ub9c8\ub2e4 if\ubb38\uc73c\ub85c \ud574\ub2f9 \uba54\uc11c\ub4dc\ub97c \uc2e4\ud589\uc2dc\ucf1c\uc8fc\ub294 \ubc29\ubc95\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n```java\\n// 1. fetch join + \ud68c\uc0ac \uc774\ub984\ub9cc \uc870\ud68c\\n @Query(\\"SELECT DISTINCT s FROM Station s \\" +\\n \\"LEFT JOIN FETCH s.chargers c \\" +\\n \\"LEFT JOIN FETCH c.chargerStatus \\" +\\n \\"WHERE s.latitude.value BETWEEN :minLatitude AND :maxLatitude \\" +\\n \\"AND s.longitude.value BETWEEN :minLongitude AND :maxLongitude \\" +\\n \\"AND s.companyName IN :companyNames\\")\\n List findAllByFilteringBeingCompanyNames(@Param(\\"minLatitude\\") BigDecimal minLatitude,\\n @Param(\\"maxLatitude\\") BigDecimal maxLatitude,\\n @Param(\\"minLongitude\\") BigDecimal minLongitude,\\n @Param(\\"maxLongitude\\") BigDecimal maxLongitude,\\n @Param(\\"companyNames\\") List companyNames);\\n\\n // 2. fetch join + \ucda9\uc804 \ud0c0\uc785\\n @Query(\\"SELECT DISTINCT s FROM Station s \\" +\\n \\"LEFT JOIN FETCH s.chargers c \\" +\\n \\"LEFT JOIN FETCH c.chargerStatus \\" +\\n \\"WHERE s.latitude.value BETWEEN :minLatitude AND :maxLatitude \\" +\\n \\"AND s.longitude.value BETWEEN :minLongitude AND :maxLongitude \\" +\\n \\"AND c.type IN :types\\")\\n List findAllByFilteringBeingTypes(@Param(\\"minLatitude\\") BigDecimal minLatitude,\\n @Param(\\"maxLatitude\\") BigDecimal maxLatitude,\\n @Param(\\"minLongitude\\") BigDecimal minLongitude,\\n @Param(\\"maxLongitude\\") BigDecimal maxLongitude,\\n @Param(\\"types\\") List types);\\n\\n```\\n\\n\uc9c4\ud589 \ud588\ub2e4\uba74 \uc774\ub7f0 \ub290\ub08c\uc774\uc5c8\uaca0\ub124\uc694!\\n\\n\\n2\ubc88\uc758 \uacbd\uc6b0 \ubaa8\ub450 \uc870\ud68c\ub97c\ud558\uace0 \uc790\ubc14 \ucf54\ub4dc\ub97c \uc774\uc6a9\ud574\uc11c \ud544\ud130\ub9c1 \ud574\uc8fc\ub294 \ubc29\ubc95\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ud604\uc7ac \uc800\ud76c \uc11c\ube44\uc2a4\ub294 \uc88c\ud45c\ub97c \uc911\uc2ec\uc73c\ub85c \uc8fc\ubcc0 \ucda9\uc804\uc18c\ub97c \uc870\ud68c\ud569\ub2c8\ub2e4.\\n\\n\uc5b4\ucc28\ud53c \uc0ac\uc6a9\uc790\uac00 \ud654\uba74\uc744 \ucd95\uc18c\ud574\uc11c \ud070 \ubc94\uc704\uc758 \uc9c0\ub3c4\ub97c \ubcf4\ub294 \uac83\uc740 \uc5b4\ucc28\ud53c \ub9c9\ud790\ud14c\ub2c8 \uc0ac\uc6a9\uc790\ub294 \uc791\uc740 \ubc94\uc704\uc5d0 \ub300\ud574\uc11c \uc870\ud68c\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \ud558\ub098\uc758 \ucffc\ub9ac\ub97c \uc774\uc6a9\ud574\uc11c \uc790\ubc14 \ucf54\ub4dc\ub85c \ud544\ud130\ub9c1 \ud574\uc8fc\ub294 \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\\n\\n\\n\uc774\ub807\uac8c\ub9cc \ubd24\uc744 \ub550 1\ubc88 \ubc29\uc2dd\uc778 \ud544\ud130 \ubcc4\ub85c \uc870\ud68c\ud574\uc8fc\ub294 \uac83\uc740 \uc870\ud68c \ud6a8\uc728\uc740 \ub354 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc 1\ubc88\uc758 \ubc29\ubc95\uc740 \'\ud604\uc7ac \uad6c\uc870\'\uc5d0\uc11c\ub294 \ub9ce\uc740 \ucffc\ub9ac\ubb38\uacfc \uba54\uc11c\ub4dc\ub97c \uc791\uc131\ud574\uc57c\ud558\uace0, if\ubb38 \ubc94\ubc85\uc73c\ub85c \ubcf4\uae30 \uc88b\uc9c0 \uc54a\uc740 \ucf54\ub4dc\uac00 \uc644\uc131 \ub410\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uacb0\uad6d 2\ubc88 \ubc29\uc2dd\uc778 [\uc804\uccb4 \uc870\ud68c + \ucf54\ub4dc\ub85c \ud544\ud130\ub9c1] \ubc29\uc2dd\uc744 \uc120\ud0dd\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc5b4\ucc28\ud53c \uc0ac\uc6a9\uc790\ub294 \uc791\uc740 \ubc94\uc704\uc5d0\uc11c \uc870\ud68c\ub97c \ud55c\ub2e4.\\n2. \uc778\ub371\uc2a4\ub97c \uac78\uc5c8\uc744 \ub54c \uac00\uc7a5 \ud6a8\uc728\uc801\uc774\ub2e4.\\n\\n1\ubc88\uc758 \uc774\uc720\ub294 \uc704\uc5d0\uc11c \ub9d0\ud588\uace0, 2\ubc88\uc5d0 \ub300\ud574 \uac04\ub2e8\ud558\uac8c \uc124\uba85 \ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uc800\ud76c \uc11c\ube44\uc2a4\ub294 \uc870\ud68c\uac00 \uad49\uc7a5\ud788 \ub9ce\uc9c0\ub9cc, \ucda9\uc804\uc18c\uc758 \uc8fc\uae30\uc801\uc778 \uc5c5\ub370\uc774\ud2b8\ub97c \uc704\ud574 \ub370\uc774\ud130 \uc5c5\ub370\uc774\ud2b8\uac00 \uad49\uc7a5\ud788 \ube48\ubc88\ud558\uac8c \uc77c\uc5b4\ub0a9\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c \ub9ce\uc9c0\ub294 \uc54a\uc9c0\ub9cc \ub370\uc774\ud130 \uc0bd\uc785\ub3c4 \ubc1c\uc0dd\ud558\uace0, \ub370\uc774\ud130 \uc5c5\ub370\uc774\ud2b8\ub3c4 \ub9ce\uc544\uc9d1\ub2c8\ub2e4.\\n\\nJPQL\ub85c \uc870\uac74\uc744 \ub098\ub220\uc11c \uc870\ud68c\ud574\uc900\ub2e4\uba74 \ud574\ub2f9\ud558\ub294 \ubaa8\ub4e0 \ud544\ud130\uc5d0 \uc778\ub371\uc2a4\ub97c \uac78\uc5b4\uc57c\ud560\uae4c\uc694?\\n\\n\uadf8\ub7f4 \uc21c \uc5c6\uc5c8\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uac00\uc7a5 \ud6a8\uc728\uc801\uc778 Column\uc5d0 \uc778\ub371\uc2a4\ub97c \uac78\uc5c8\uaca0\uc8e0, \uadf8\ub807\ub2e4\uba74 \uc870\ud68c\ub9c8\ub2e4 \uc18d\ub3c4\ub3c4 \ub2ec\ub77c\uc84c\uc744 \uac83\uc774\uace0 \uac00\ub839 \ud574\ub2f9\ud558\ub294 \ubaa8\ub4e0 Column\uc5d0 \uc778\ub371\uc2a4\ub97c \uc124\uc815\ud574\ub194\ub3c4 \uc5c5\ub370\uc774\ud2b8\uc640 \uc0bd\uc785\uc774 \ub290\ub824\uc84c\uc744 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc774\ub294 7\ubd84\ub9c8\ub2e4 \ub370\uc774\ud130\ub97c \uc5c5\ub370\uc774\ud2b8 \ud558\ub294 \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \uc801\uc808\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\ubc18\uba74\uc5d0 \ud55c \uac1c\uc758 \ucffc\ub9ac\ub85c \uc8fc\ubcc0\uc744 \ubaa8\ub450 \uc870\ud68c\ud558\uace0 \uc774\ub97c \uc790\ubc14 \ucf54\ub4dc\ub85c \ubc14\uafb8\ub294 \ubc29\ubc95\uc740 \ub354 \uc26c\uc6e0\uc2b5\ub2c8\ub2e4.\\n\\n\uc5b4\ucc28\ud53c \ub9ce\uc9c0 \uc54a\uc740 \uc591\uc758 \ub370\uc774\ud130\ub97c \uc870\ud68c\ud558\uace0 \ud544\ud130\ub9c1 \ud558\uae30 \ub54c\ubb38\uc5d0 \uc18d\ub3c4 \uba74\uc5d0\uc11c\ub3c4 \ud070 \ucc28\uc774\uac00 \ub098\uc9c0 \uc54a\uc558\uace0, \uc778\ub371\uc2a4 \uc124\uc815\uc5d0\ub3c4 \uc720\ub9ac\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc870\ud68c\uc2dc \uc774\uc6a9\ud558\ub294 latitude\uc640 longitude\ub9cc \uc124\uc815\ud574\uc8fc\uba74 \uc5b4\ub5a4 \uacbd\uc6b0\ub4e0 \ube60\ub974\uac8c \uc870\ud68c\ub97c \ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\n## \uc778\ub371\uc2a4 \uc801\uc6a9\uc73c\ub85c \uc870\ud68c \uc18d\ub3c4 \ud5a5\uc0c1\uc2dc\ud0a4\uae30\\n\\n\uba3c\uc800 \uc77c\ub2e8 \ud604\uc7ac \ucf54\ub4dc\uc5d0\uc11c \uc870\ud68c\uc2dc \ub2e4\uc74c\uacfc \uac19\uc740 \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n```sql\\nHibernate:\\n select\\n station0_.station_id as station_1_0_0_,\\n ...\\n ...\\n ...\\n chargersta2_.latest_update_time as latest_u4_2_2_\\n from\\n charge_station station0_\\n left outer join\\n charger chargers1_\\n on station0_.station_id=chargers1_.station_id\\n left outer join\\n charger_status chargersta2_\\n on chargers1_.charger_id=chargersta2_.charger_id\\n and chargers1_.station_id=chargersta2_.station_id\\n where\\n (\\n station0_.latitude between ? and ?\\n )\\n and (\\n station0_.longitude between ? and ?\\n )\\n```\\n\\nwhere \uc808\uc5d0\uc11c \uc704\ub3c4 \uacbd\ub3c4\ub97c \ubc14\ud0d5\uc73c\ub85c \uc8fc\ubcc0\ub9cc \uac00\uc838\uc624\uac8c \ub429\ub2c8\ub2e4. \uae30\uc874\uc5d0 N+1 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud574\uc11c EntityGraph\ub85c \ubc14\uafe8\uace0 \uc2e4\ud589\uc2dc \ucffc\ub9ac\uc785\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc544\ub798 \uae00\uc744 \uc77d\uace0 BETWEEN \ucffc\ub9ac\uc5d0\uc11c \ubd80\ub4f1\ud638\ub97c \uc774\uc6a9\ud558\ub294 \ucffc\ub9ac\ub85c \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n[Mysql Query Between \uacfc >=, <= \uc131\ub2a5 \ucc28\uc774 \ube44\uad50 ( \ub354\ubbf8\ub370\uc774\ud130 50\ub9cc )\\n](https://velog.io/@ggomjae/Mysql-Query-Between-%EA%B3%BC-%EC%84%B1%EB%8A%A5-%EC%B0%A8%EC%9D%B4-%EB%B9%84%EA%B5%90-%EB%8D%94%EB%AF%B8%EB%8D%B0%EC%9D%B4%ED%84%B0-50%EB%A7%8C)\\n\\n\\n```java\\n@Query(\\"SELECT DISTINCT s FROM Station s \\" +\\n \\"LEFT JOIN FETCH s.chargers c \\" +\\n \\"LEFT JOIN FETCH c.chargerStatus \\" +\\n \\"WHERE s.latitude.value >= :minLatitude AND s.latitude.value <= :maxLatitude \\" +\\n \\"AND s.longitude.value >= :minLongitude AND s.longitude.value <= :maxLongitude\\")\\n List findAllByLatitudeBetweenAndLongitudeBetweenWithFetch(@Param(\\"minLatitude\\") BigDecimal minLatitude,\\n @Param(\\"maxLatitude\\") BigDecimal maxLatitude,\\n @Param(\\"minLongitude\\") BigDecimal minLongitude,\\n @Param(\\"maxLongitude\\") BigDecimal maxLongitude);\\n\\n```\\n\uc704\uc640 \uac19\uc774 \uc870\ud68c\ud574\uc8fc\ub294 \ucffc\ub9ac\ub97c \ub9cc\ub4e4\uc5c8\uace0, \uc778\ub371\uc2a4\ub97c \ub9cc\ub4e4\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc778\ub371\uc2a4 \uc124\uc815 \uae30\uc900\uc740 [\uc778\ub371\uc2a4 \uc815\ub9ac \ubc0f \ud301](https://jojoldu.tistory.com/243)\\n\uc704\uc5d0 \ub9c1\ud06c\uc640 \uac19\uc774 \ub3d9\uc6b1\ub2d8\uc758 \ube14\ub85c\uadf8\ub97c \ucc38\uc870\ud574\uc11c \uae30\uc900\uc744 \uc138\uc6e0\uc2b5\ub2c8\ub2e4.\\n\\n\ubb34\uc870\uac74 \uce74\ub514\ub110\ub9ac\ud2f0\uac00 \ub192\uc740 \uac83\uc744 \uc124\uc815\ud560 \uc21c \uc5c6\uc5c8\uae30 \ub54c\ubb38\uc5d0 (\uc5c5\ub370\uc774\ud2b8\uc640 \uc0bd\uc785 \uc791\uc5c5\uc774 \ub9ce\uae30 \ub54c\ubb38\uc5d0) \ucffc\ub9ac\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 column\uacfc update \uc791\uc5c5\uc744 \uace0\ub824\ud558\uace0 \uc131\ub2a5\uc744 \ube44\uad50\ud574\uac00\uba74\uc11c \uac00\uc7a5 \ud6a8\uc728\uc801\uc778 \uac83\uc744 \uc124\uc815\ud574\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc18d\ub3c4\ub97c \ube44\uad50\ud574\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n
    \\n
    \\n\\n\u200b\\n\\n\uba3c\uc800 \uc18d\ub3c4 \ube44\uad50\ub97c \uc704\ud574\uc11c \ub370\uc774\ud130 \uc14b\uc740 \ub2e4\uc74c\uacfc \uac19\uc774 \uc9c4\ud589\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n- Charger (23\ub9cc \uac74)\\n- Station (6\ub9cc \uac74)\\n- ChargerStatus(23\ub9cc \uac74)\\n- \uc120\ub989\uc5ed \uadfc\ucc98 \uc870\ud68c\\n\\n\\n### Ver1. \uc778\ub371\uc2a4 \uc801\uc6a9\uc744 \ud558\uc9c0 \uc54a\uace0 \uc870\ud68c \ubc0f \ud544\ud130\ub9c1 \ud588\uc744 \ub54c \uc18d\ub3c4 (0.84\ucd08)\\n![\uc774\ubbf8\uc9c0](https://postfiles.pstatic.net/MjAyMzA3MjdfMTYy/MDAxNjkwNDQwMDA0ODEw.vaeA83AD9ycHa26YN58rqzPV3XdX2zTvIZgKM6YKXWEg.Qqkkdr_lEJeGbYPpWji0E-IusfGpqMpZHKWZM4AyRrUg.PNG.sosow0212/image.png?type=w773)\\n\ud3c9\uade0\uc801\uc73c\ub85c 0.84\ucd08\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\\n### Ver2. \uc778\ub371\uc2a4 \uc801\uc6a9 \ubc0f \uc870\ud68c \ubc0f \ud544\ud130\ub9c1 \ud588\uc744 \ub54c \uc18d\ub3c4 (0.63\ucd08)\\n![\uc774\ubbf8\uc9c0](https://postfiles.pstatic.net/MjAyMzA3MjdfNTUg/MDAxNjkwNDQwMTUyMDcx.F3sSiDgLp3O2Rn1waqh31vC6yv1Uk0zZkRzjyuDQEM4g.eziRKLCmUbzW88ueQRozZcYvhsH10C17w-IDRLh0cJ4g.PNG.sosow0212/SE-48b3f814-3306-4add-ab95-381186bab6ca.png?type=w773)\\n\ud3c9\uade0\uc801\uc73c\ub85c 0.63\ucd08\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\uc57d 25 ~ 30%\uc758 \uc870\ud68c \uc18d\ub3c4\uac00 \uac1c\uc120\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\uc9c1 \uc774 \ubd80\ubd84\uc740 \uac1c\uc120\uc774 \ub354 \ud544\uc694\ud574\ubcf4\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub798\ub3c4 \uac1c\uc120\uc774 \ub410\uace0, \uc0bd\uc785\uacfc \uac31\uc2e0\uc5d0\ub294 \ud070 \uc9c0\uc7a5\uc774 \uc5c6\uc5b4\uc11c \uc77c\ub2e8 \uc774\uc815\ub3c4\ub85c \ub9c8\ubb34\ub9ac \ud558\uace0, \ucd94\ud6c4\uc5d0 \uac1c\uc120\uc744 \ud574\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\\n![\uc774\ubbf8\uc9c0](https://postfiles.pstatic.net/MjAyMzA3MjdfNzMg/MDAxNjkwNDQwODA1NzAy.b5gZPjl_E1x3wbjSMNcmfQDKB-hB9p8FEbIJqs5Kl4Qg.ZBq0-GmXJruPO7ejA_zq7RfaBaC17doHJUT19wje1Qkg.PNG.sosow0212/SE-f5396915-60ef-4293-a457-e30e8f5a2794.png?type=w773)\\n\ucd94\uac00\uc801\uc73c\ub85c \ucda9\uc804\uae30 \uc870\ud68c\ub294 \uad49\uc7a5\ud788 \ube68\ub77c\uc84c\uc2b5\ub2c8\ub2e4!\\n\\n\\n\ubc30\uc6b0\ub294 \ub2e8\uacc4\uc774\ub2e4\ubcf4\ub2c8 \ubbf8\uc219\ud558\uace0 \ud2c0\ub9b0 \ubd80\ubd84\uc774 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uae34 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4 :)"},{"id":"20","metadata":{"permalink":"/20","source":"@site/blog/2023-07-26-why-styled-components.mdx","title":"\uce74\ud398\uc778 \ud300\uc774 styled-components\ub97c \uc120\ud0dd\ud55c \uc774\uc720","description":"\uc65c styled-components\uc778\uac00?","date":"2023-07-26T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 26\uc77c","tags":[{"label":"styled-components","permalink":"/tags/styled-components"},{"label":"css","permalink":"/tags/css"},{"label":"css in js","permalink":"/tags/css-in-js"}],"readingTime":1.495,"hasTruncateMarker":false,"authors":[{"name":"\uc57c\ubbf8","title":"Frontend","url":"https://github.com/feb-dain","imageURL":"https://github.com/feb-dain.png","key":"yummy"}],"frontMatter":{"slug":"20","title":"\uce74\ud398\uc778 \ud300\uc774 styled-components\ub97c \uc120\ud0dd\ud55c \uc774\uc720","authors":["yummy"],"tags":["styled-components","css","css in js"]},"prevItem":{"title":"\ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604\uacfc \uc778\ub371\uc2a4 \uc774\uc6a9\ud55c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\ud558\uae30","permalink":"/22"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \uc0c1\ud0dc\uad00\ub9ac \uc804\ub7b5 (\uc65c Tanstack Query\uc5ec\uc57c \ud558\ub294\uac00?)","permalink":"/21"}},"content":"## \uc65c styled-components\uc778\uac00?\\n\\n
    \\n\\n\uc5ec\ub7ec `CSS-in-JS` \uc911 styled-components\ub97c \uc120\ud0dd\ud55c \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n1. \ucef4\ud3ec\ub10c\ud2b8 \uc548\uc5d0 \uad00\ub828 CSS\ub97c \uc791\uc131\ud560 \uc218 \uc788\uc5b4 \ucef4\ud3ec\ub10c\ud2b8\ubcc4 \ub514\uc790\uc778 \ucf54\ub4dc \ud655\uc778 \ubc0f \uc218\uc815\uc774 \uc6a9\uc774\ud558\ub2e4.\\n\\n2. \ud639\uc790\ub294 \ucf54\ub4dc \uac00\ub3c5\uc131\uc774 \uc548 \uc88b\uc544\uc9c4\ub2e4\uace0\ub3c4 \ud558\uc9c0\ub9cc, \uac1c\uc778\uc801\uc73c\ub85c\ub294 \ud0dc\uadf8\ub97c \ub354 \uc2dc\ub9e8\ud2f1 \ud558\uac8c \uc791\uc131\ud560 \uc218 \uc788\uc5b4\uc11c \uc88b\ub2e4\uace0 \ub290\uaf08\ub2e4.\\n\\n3. \ud300\uc6d0\ub4e4 \ubaa8\ub450 styled-components\uac00 \uc775\uc219\ud558\ub2e4.\\n\\n4. \uc9c0\uae08\uae4c\uc9c0 \uc0ac\uc6a9\ud558\uba74\uc11c \ubd88\ud3b8\ud55c \uc810\uc744 \ubabb \ub290\uaf08\ub2e4.\\n\\n
    \\n\\nstyled-components\uc640 emotion\uc740 \uae30\ub2a5\ub3c4, \uc791\uc131\ubc95\ub3c4 \uc0c1\ub2f9\ud788 \uc720\uc0ac\ud558\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc774\ubc88\uc5d0\ub294 styled-components \ub300\uc2e0 emotion\uc744 \uc368\ubcfc\uae4c\ub3c4 \uc0dd\uac01\ud588\uc5c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc emotion\uc5d0\uc11c\ub9cc \uc0ac\uc6a9 \uac00\ub2a5\ud558\ub358 \\\\*CSS Props\ub77c\ub294 \ud3b8\ub9ac\ud55c \uae30\ub2a5\uc744\\n\\nstyled-components(v5.2.0 \uc774\uc0c1)\uc5d0\uc11c \uc4f8 \uc218 \uc788\uac8c \ub418\uae30\ub3c4 \ud588\uace0,\\n\\n\'\uc0c8\ub85c\uc6b4 \uae30\uc220 \uacf5\ubd80\ub97c \ud574\ubcf4\uba74 \uc88b\uc744 \uac83 \uac19\ub2e4\'\ub294 \uc774\uc720\ub97c \uc81c\uc678\ud558\uace0\ub294\\n\\n\ub531\ud788 emotion\uc744 \uc0ac\uc6a9\ud560 \ud544\uc694\uc131\uc744 \ubabb \ub290\uaef4 styled-components\ub97c \ucc44\ud0dd\ud588\ub2e4.\\n\\n```typescript\\n// *CSS Props \uc608\uc2dc\\n\\nconst buttonStyle = css`\\n font-size: 18px;\\n color: white;\\n background: black;\\n`;\\n\\nconst ClickButton = styled.button<{ css: CSSProp }>`\\n width: 100px;\\n\\n ${({ css }) => css}\\n`;\\n\\nClick me!;\\n```"},{"id":"21","metadata":{"permalink":"/21","source":"@site/blog/2023-07-26-why-tanstack-query-is-good.mdx","title":"\uce74\ud398\uc778 \ud300\uc758 \uc0c1\ud0dc\uad00\ub9ac \uc804\ub7b5 (\uc65c Tanstack Query\uc5ec\uc57c \ud558\ub294\uac00?)","description":"\uc548\ub155\ud558\uc138\uc694? \uce74\ud398\uc778 \ud300 FE\uc5d0\uc11c \uc0c1\ud0dc\uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc5b4\ub5bb\uac8c \ud574\uc57c\ud560 \uc9c0 \uace0\ubbfc \ub05d\uc5d0 \uc11c\ub4dc\ud30c\ud2f0 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \ud544\uc694\ud558\uac8c \ub418\uc5b4 \uae00\uc744 \uc791\uc131\ud558\uac8c\ub410\uc2b5\ub2c8\ub2e4.","date":"2023-07-26T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 26\uc77c","tags":[{"label":"tanstack query","permalink":"/tags/tanstack-query"},{"label":"react state management","permalink":"/tags/react-state-management"}],"readingTime":8.695,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"21","title":"\uce74\ud398\uc778 \ud300\uc758 \uc0c1\ud0dc\uad00\ub9ac \uc804\ub7b5 (\uc65c Tanstack Query\uc5ec\uc57c \ud558\ub294\uac00?)","authors":["gabriel"],"tags":["tanstack query","react state management"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc774 styled-components\ub97c \uc120\ud0dd\ud55c \uc774\uc720","permalink":"/20"},"nextItem":{"title":"OAuth 2.0\uc758 \ud750\ub984\uacfc \uc124\uc815 \ud574\ubcf4\uae30","permalink":"/19"}},"content":"\uc548\ub155\ud558\uc138\uc694? \uce74\ud398\uc778 \ud300 FE\uc5d0\uc11c \uc0c1\ud0dc\uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc5b4\ub5bb\uac8c \ud574\uc57c\ud560 \uc9c0 \uace0\ubbfc \ub05d\uc5d0 \uc11c\ub4dc\ud30c\ud2f0 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \ud544\uc694\ud558\uac8c \ub418\uc5b4 \uae00\uc744 \uc791\uc131\ud558\uac8c\ub410\uc2b5\ub2c8\ub2e4.\\n\\n# \uc11c\ubc84 \uc0c1\ud0dc\uc640 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\uc758 \uad6c\ubd84\\n\\n\uc11c\ubc84\uc0c1\ud0dc\uc640 UI\uc0c1\ud0dc\ub97c \uc774\ud574\ud558\ub294 \uac83\uc740 \uad49\uc7a5\ud788 \uc911\uc694\ud588\uc2b5\ub2c8\ub2e4. \ub370\uc774\ud130\ub97c \uc1a1\uc218\uc2e0\ud558\ub294 \uc791\uc5c5\uacfc \uc0c1\ud0dc\ub97c \uad00\ub9ac\ud558\ub294 \uc791\uc5c5\uc740 \uc720\uae30\uc801\uc73c\ub85c \ub3d9\uc791\ud574\uc57c\ud588\uc2b5\ub2c8\ub2e4. \uae30\uc874\uc5d0\ub294 `\uc0c1\ud0dc\uc640 \ub370\uc774\ud130 \uc1a1\uc218\uc2e0 \uacfc\uc815\uc744 \ubd84\ub9ac\ud574\uc11c \uc0dd\uac01`\ud588\ub2e4\uba74, \ud604\ub300\uc758 react \ud504\ub85c\uc81d\ud2b8\ub4e4\uc740 `\uc11c\ubc84\uc640 \ub3d9\uae30\ud654\ub97c \ud574\uc57c\ud560 \uc0c1\ud0dc`\uc640 `\uadf8\ub807\uc9c0 \uc54a\uc740 \uc0c1\ud0dc`\ub85c \ubd84\ub9ac\ud574\uc11c \uc0dd\uac01\ud574\uc57c \ud569\ub2c8\ub2e4.\\n\\n`React\uc5d0\uc11c \uc5b4\ub5a4 \ub370\uc774\ud130\ub97c \uc0c1\ud0dc\ub85c \ub2e4\ub904\uc57c \ud558\ub294\uac00`\uc5d0 \ub300\ud574\uc11c\ub294 \uc5ec\ub7ec \uc758\uacac\uc774 \ub098\uc62c \uc218 \uc788\ub2e4\uace0 \uc0dd\uac01\ud558\uc9c0\ub9cc `\uc0c1\ud0dc\uac00 \ud2b9\uc131\uc744 \uac00\uc9c0\uace0 \uc788\ub294\uac00`\uc5d0 \ub300\ud574\uc11c\ub294 \ub300\ubd80\ubd84 \ud2b9\uc131\uc774 \uc788\ub2e4\uace0 \ub3d9\uc758\ud560 \uac83\uc785\ub2c8\ub2e4. \uc774 \uae00\uc5d0\uc11c\ub294 React\uc758 \uc0c1\ud0dc\ub780 \ubb34\uc5c7\uc778\uac00?\uc5d0 \ub300\ud574\uc11c \ub2e4\ub8e8\uc9c0 \uc54a\uace0 React\uc758 \uc0c1\ud0dc\uc758 \ud2b9\uc131\uc5d0 \ub300\ud574\uc11c\ub9cc \uc5b8\uae09\uc744 \ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\uc0c1\ud0dc\uc758 \ud2b9\uc131\uc73c\ub85c\ub294 \ud06c\uac8c \ub450 \uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\\n\\n\ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\ub294 \ucef4\ud3ec\ub10c\ud2b8\ub4e4 \uac04\uc5d0 \uc5b4\ub5a4 \uac12\uc744 \uacf5\uc720\ud574\uc57c\ud558\uba74\uc11c `\uc624\ub85c\uc9c0 React DOM \ub0b4\ubd80\uc5d0\uc11c\ub9cc CRUD\uac00 \uc77c\uc5b4\ub098\ub294 \uc0c1\ud0dc\ub97c \uc758\ubbf8`\ud569\ub2c8\ub2e4. \uc774 \uc0c1\ud0dc\ub4e4\uc740 React DOM \uc678\ubd80 \uc138\uacc4\uc640 \ud06c\uac8c \uad00\ub828\uc774 \uc5c6\uc73c\uba70 `\ub3d9\uae30\uc801\uc73c\ub85c \ubc18\uc601`\ub429\ub2c8\ub2e4. \ub300\ud45c\uc801\uc73c\ub85c\ub294 UI\ub97c \uc870\uc791\ud558\ub294 \uc0c1\ud0dc\ub4e4\uc774 \ub420 \uac83\uc785\ub2c8\ub2e4. \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\ub4e4\uc740 \ub300\ubd80\ubd84 \uc7a5\uae30\uc801\uc73c\ub85c \uc720\uc9c0\ub420 \ud544\uc694\uac00 \uc5c6\uae30\uc5d0 \ud654\uba74\uc744 \ubc97\uc5b4\ub098\uac70\ub098 \uc138\uc158\uc774 \ub04a\uae30\ub294 \uacbd\uc6b0 \uc0ac\ub77c\uc838\ub3c4 \uad1c\ucc2e\uc740 \uacbd\uc6b0\uac00 \ub9ce\uc2b5\ub2c8\ub2e4.\\n\\n### \uc11c\ubc84 \uc0c1\ud0dc\\n\\n\uc11c\ubc84 \uc0c1\ud0dc\ub294 React\uc758 \ubc14\uae65 \uc138\uc0c1(\uc11c\ubc84)\uc5d0 \uc874\uc7ac\ud558\ub294 `\ub370\uc774\ud130\uac00 React\uc758 \uc0c1\ud0dc \uad00\ub9ac\uc640 \ube44\ub3d9\uae30\uc801\uc73c\ub85c \ub3d9\uae30\ud654 \ub41c \uac83`\uc744 \uc758\ubbf8\ud569\ub2c8\ub2e4. \uc5b4\ub5a4 \uc0c1\ud0dc\uac00 `\uc678\ubd80\uc5d0\uc11c \uad00\ub9ac\ub418\ub294 \ub370\uc774\ud130\uc640 \ubc18\ub4dc\uc2dc \uc5f0\ub3d9`\ub418\uc5b4\uc57c \ud55c\ub2e4\uba74 \uc774\ub294 \uace7 \uc11c\ubc84 \uc0c1\ud0dc\uc784\uc744 \uc758\ubbf8\ud569\ub2c8\ub2e4. React\uc758 \uc0c1\ud0dc\ub97c CRUD \ud558\ub294 \uac83 \ubfd0\ub9cc \uc544\ub2cc, \uc11c\ubc84\uc5d0\uc11c\ub3c4 \ud56d\uc0c1 \uac19\uc740 \uc77c\uc774 \uc77c\uc5b4\ub098\uc57c \ud569\ub2c8\ub2e4. \uc11c\ubc84 \uc0c1\ud0dc\ub294 \uc7a5\uae30\uc801\uc73c\ub85c \uc720\uc9c0\ub418\uc5b4\uc57c \ud558\uba70, \uc138\uc158\uc5d0\uc11c \ubc97\uc5b4\ub098\ub354\ub77c\ub3c4 \uc11c\ubc84\ub85c \ubd80\ud130 \ubcf5\uad6c\ub97c \ud574\uc57c \ud569\ub2c8\ub2e4.\\n\\n\uae30\uc874\uc758 \uc0c1\ud0dc \uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc740 \ub9ac\uc561\ud2b8\uc758 \uc804\uc5ed\uc5d0\uc11c \uc0c1\ud0dc\ub97c \uc870\uc791\ud558\ub294 \uac83\uc5d0 \ud2b9\ud654\ub418\uc5b4\uc788\uace0, \ube44\ub3d9\uae30\uc801\uc778 \uc0c1\ud0dc \uad00\ub9ac\ub3c4 \uc9c0\uc6d0\ud558\uc5ec \uc11c\ubc84\uc640\uc758 \ud1b5\uc2e0\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \ub300\ubd80\ubd84\uc758 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc740 `\ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\ub97c \uc870\uc791\ud558\ub294 \uac83\uc5d0 \ucd08\uc810`\uc774 \ub9de\ucdb0\uc838\uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub354\uad70\ub2e4\ub098 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\uc640 \uc11c\ubc84 \uc0c1\ud0dc\uac00 \ud558\ub294 \uc77c\uc774 \uba85\ud655\ud558\uac8c \ub2e4\ub978 \uc0c1\ud669\uc5d0\uc11c \uc774 \ub458\uc744 \ud55c \uacf3\uc5d0\uc11c \uad00\ub9ac\ud558\ub294 \uac83 \ubcf4\ub2e4\ub294 \uc644\ubcbd\ud558\uac8c \ubd84\ub9ac\ud558\ub294 \uac83\uc774 \ub354 \ub098\uc744 \uac83\uc785\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc11c\ubc84 \uc0c1\ud0dc\ub97c \uad00\ub9ac\ud558\ub294 \uac83\uc5d0 \uc911\uc810\uc744 \ub454 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc774 \ub4f1\uc7a5\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub300\ud45c\uc801\uc778 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c\ub294 RTK Query, Tanstack Query, SWR \ub4f1\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n# \uc65c Tanstack Query\uc600\ub098?\\n\\n### vs RTK Query\\n\\nRTK Query\ub294 RTK\ub97c \ubc18\ub4dc\uc2dc \uc0ac\uc6a9\ud574\uc57c \ud558\ub294 \uac83\uc740 \uc544\ub2c8\uc9c0\ub9cc RTK\ub97c \ud0c0\uac9f\uc73c\ub85c \ub098\uc628 \uc11c\ubc84 \uc0c1\ud0dc \uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4. `\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\ub97c \uad00\ub9ac\ud558\uae30 \uc704\ud574 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.` \ub354\uc6b1\uc774 Redux\uc758 \ubcf5\uc7a1\ud55c \ucf54\ub4dc \uad6c\uc131\uacfc \ubc29\ub300\ud55c \ubcf4\uc77c\ub7ec \ud50c\ub808\uc774\ud2b8\ub294 \ub9e4\ub825\uc801\uc774\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. tanstack query\uc5d0\uc11c\ub294 \ubb34\ud55c \ub370\uc774\ud130 \ud398\uce6d\uc744 \uc9c0\uc6d0\ud558\uae30 \uc704\ud574 **Infinite Queries**\uac00 \uc788\uc9c0\ub9cc RTK Query\ub294 \uadf8\ub807\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\\n### vs SWR\\n\\nSWR\ub3c4 \ud558\ub098\uc758 \uc88b\uc740 \uc120\ud0dd\uc9c0\uc600\uc9c0\ub9cc, \uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc774 \ubc94\uc6a9\uc801\uc73c\ub85c \uc9c0\uc6d0\ud558\ub294 \uc140\ub809\ud130 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ub610, \uac00\ube44\uc9c0 \uceec\ub809\ud130\uc758 \ubd80\uc7ac\ub3c4 \uc544\uc26c\uc6e0\uc2b5\ub2c8\ub2e4. \uc7ac\uc694\uccad\uc744 \ud558\uae30 \uc704\ud55c stale time \uc124\uc815\uc774\ub098 \ucffc\ub9ac \ucde8\uc18c \uae30\ub2a5\uc774 \uc5c6\ub294 \uc810\ub3c4 \ub9e4\ub825\uc801\uc774\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\\n# \uce74\ud398\uc778 \ud300\uc5d0\uc11c \ud558\ub824\ub294 \uc77c\uc740\uc694\u2026\\n\\n\uc800\ud76c \uce74\ud398\uc778 \ud300\uc758 \ud504\ub85c\uc81d\ud2b8\ub294 `\uc2e4\uc2dc\uac04 \uc804\uae30\uc790\ub3d9\ucc28 \ucda9\uc804\uc18c \uc9c0\ub3c4 \ubc0f \uc0ac\uc6a9 \ud1b5\uacc4 \uc870\ud68c \uc11c\ube44\uc2a4` \ub85c \uc9c0\ub3c4 \uae30\ubc18\uc758 \ud504\ub85c\uc81d\ud2b8\uc785\ub2c8\ub2e4. \uc11c\ubc84 \uc0c1\ud0dc\ub97c \uc801\uadf9\uc801\uc73c\ub85c \ub2e4\ub904\uc57c \ud558\ub294 \uc0c1\ud669\uc5d0\uc11c Tanstack Query\ub97c \uc11c\ubc84 \uc0c1\ud0dc \uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c \uc120\uc815\ud558\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n\uba54\uc778 \uae30\ub2a5 \uc911 Tanstack Query\uac00 \ud575\uc2ec\uc73c\ub85c \uc0ac\uc6a9\ub420 \uac83 \uac19\uc740 \uae30\ub2a5\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n- \uc9c0\ub3c4\uc5d0\uc11c \ucda9\uc804\uc18c \uc870\ud68c\\n - \ud604\uc7ac \uc811\uc18d\ud55c \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0 \ub80c\ub354\ub9c1 \ub41c \uc9c0\ub3c4 \ud654\uba74(\ub514\uc2a4\ud50c\ub808\uc774)\uc758 \ud06c\uae30\uc5d0 \ub530\ub978 GPS\uc88c\ud45c\ub97c \uc54c\uc544\ub0b4\uc5b4 \uc11c\ubc84\ub85c \ubd80\ud130 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uc218\uc2e0 \ubc1b\uc2b5\ub2c8\ub2e4. \uc989, \ud654\uba74\uc774 \uc774\ub3d9\ud558\uac8c \ub418\uba74 \uc0ac\uc6a9\uc790\uac00 \ubc14\ub77c\ubcf4\uace0 \uc788\ub294 \uc601\uc5ed\uc774 \ubcc0\ud558\ubbc0\ub85c \uc0c8\ub85c\uc6b4 \uc694\uccad\uc744 \ubcf4\ub0b4\uac8c \ub429\ub2c8\ub2e4.\\n - \uc11c\ubc84\uc5d0\uc11c \uc218\uc2e0\ud55c \ucda9\uc804\uc18c \uc815\ubcf4\ub294 \uc2e4\uc2dc\uac04 \uc0ac\uc6a9 \ud604\ud669\ub3c4 \ubc18\uc601\ub418\uc5b4\uc788\uc73c\ubbc0\ub85c `\uc8fc\uae30\uc801\uc778 \uc5c5\ub370\uc774\ud2b8`\ub3c4 \ud544\uc694\ud569\ub2c8\ub2e4.\\n - \ube48\ubc88\ud55c \ub370\uc774\ud130\uc758 \ubcc0\ud654\uac00 \ud544\uc694\ud558\uba70 \uadf8\ub9cc\ud07c \ud1b5\uc2e0 \uc2e4\ud328 \ub4f1 `\uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud560 \uac00\ub2a5\uc131\ub3c4 \ub9ce\uc544\uc9c0\uac8c` \ub429\ub2c8\ub2e4.\\n - \uc0ac\uc6a9\uc790\uc758 \ube60\ub978 \uc9c0\ub3c4 \uc774\ub3d9\uc774 \ubc1c\uc0dd\ud558\ub294 \uacbd\uc6b0\ub97c \ub300\uc751\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n- \uc804\uad6d \ucda9\uc804\uc18c \uac80\uc0c9\uae30\\n - \uc6d0\ud558\ub294 \ucda9\uc804\uc18c \uac80\uc0c9\uc744 \ud558\ub294 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud569\ub2c8\ub2e4. \uc804\uad6d \ub2e8\uc704\ub85c \uac80\uc0c9 \uacb0\uacfc\ub97c \uc218\uc2e0\ud558\ub294 \uae30\ub2a5\uc785\ub2c8\ub2e4.\\n - \ub124\uc774\ubc84\uc640 \uad6c\uae00 \uac80\uc0c9\ucc3d \ucc98\ub7fc \uc0ac\uc6a9\uc790\uac00 input \ucc3d\uc5d0 \uac80\uc0c9\uc5b4\ub97c \uc785\ub825\ud560 \ub54c \ub9c8\ub2e4 \uac80\uc0c9 \uacb0\uacfc\uac00 \ub3d9\uc801\uc73c\ub85c \ud45c\uc2dc\ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n - \ube48\ubc88\ud55c \ub370\uc774\ud130\uc758 \ubcc0\ud654\uac00 \ud544\uc694\ud558\uace0, `\uc0ac\uc6a9\uc790\uc758 \ube60\ub978 \ud0c0\uc774\ud551\uc73c\ub85c \uc778\ud574 \uc7a6\uc740 \uac80\uc0c9\uc774 \ubc1c\uc0dd\ud558\ub294 \uacbd\uc6b0\ub97c \ub300\uc751\ud560 \uc218 \uc788\uc5b4\uc57c` \ud569\ub2c8\ub2e4.\\n - \uc774\ub97c \uc704\ud574 \ub370\uc774\ud130\ub97c \uce90\uc2f1\ud560 \ud544\uc694\ub3c4 \uc788\ub2e4\uace0 \uc0dd\uac01\ud569\ub2c8\ub2e4.\\n\\n\ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c \ud074\ub77c\uc774\uc5b8\ud2b8\uc640 \uc11c\ubc84\uc640\uc758 \ud1b5\uc2e0\uc774 \uc5b4\uca4c\ub2e4 \ud55c\ubc88 \uc77c\uc5b4\ub09c\ub2e4\uba74 \uad73\uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \ud544\uc694\uac00 \uc5c6\uaca0\uc9c0\ub9cc, \uc11c\ubc84\uc758 \ub370\uc774\ud130 \uc804\uc801\uc73c\ub85c \uc758\uc874\ud574\uc57c \ud558\ub294 \uc800\ud76c \ud504\ub85c\uc81d\ud2b8 \ud2b9\uc131\uc0c1 Tanstack Query\uc758 \uc5ec\ub7ec \uae30\ub2a5\uc774 \uc0dd\uc0b0\uc131\uc5d0 \ub9ce\uc740 \ub3c4\uc6c0\uc774 \ub420 \uac83\uc73c\ub85c \uae30\ub300\ud569\ub2c8\ub2e4."},{"id":"19","metadata":{"permalink":"/19","source":"@site/blog/2023-07-23-oauth.mdx","title":"OAuth 2.0\uc758 \ud750\ub984\uacfc \uc124\uc815 \ud574\ubcf4\uae30","description":"OAuth 2.0 ?","date":"2023-07-23T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 23\uc77c","tags":[{"label":"oauth","permalink":"/tags/oauth"},{"label":"login","permalink":"/tags/login"}],"readingTime":12.57,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"19","title":"OAuth 2.0\uc758 \ud750\ub984\uacfc \uc124\uc815 \ud574\ubcf4\uae30","authors":["boxster"],"tags":["oauth","login"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \uc0c1\ud0dc\uad00\ub9ac \uc804\ub7b5 (\uc65c Tanstack Query\uc5ec\uc57c \ud558\ub294\uac00?)","permalink":"/21"},"nextItem":{"title":"private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\ub97c \uc678\ubd80\uc640 \uc5f0\uacb0\ud560 \ub54c, public ip? private ip?","permalink":"/18"}},"content":"## OAuth 2.0 ?\\n\\n\\n> OAuth(\\"Open Authorization\\")\ub294 \uc778\ud130\ub137 \uc0ac\uc6a9\uc790\ub4e4\uc774 \ube44\ubc00\ubc88\ud638\ub97c \uc81c\uacf5\ud558\uc9c0 \uc54a\uace0 \ub2e4\ub978 \uc6f9\uc0ac\uc774\ud2b8 \uc0c1\uc758 \uc790\uc2e0\ub4e4\uc758 \uc815\ubcf4\uc5d0 \ub300\ud574 \uc6f9\uc0ac\uc774\ud2b8\ub098 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc758 \uc811\uadfc \uad8c\ud55c\uc744 \ubd80\uc5ec\ud560 \uc218 \uc788\ub294 \uacf5\ud1b5\uc801\uc778 \uc218\ub2e8\\n\\n\uc704\ud0a4 \ubc31\uacfc\uc5d0\uc11c\ub294 \uc704\uc640 \uac19\uc774 \uc124\uba85\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uc6b0\ub9ac\uac00 google\uacfc \uac19\uc740 \uc6f9 \uc0ac\uc774\ud2b8\uc5d0 \ud68c\uc6d0\uac00\uc785\uc744 \ud558\uace0 \uc800\uc7a5\ud574\ub454 \uc774\ub984, \uc774\uba54\uc77c, \ud504\ub85c\ud544 \uc774\ubbf8\uc9c0 \uac19\uc740 \uc815\ubcf4\ub97c\\n\uad73\uc774 \ud55c\ubc88 \ub354 \uc785\ub825\ud558\uc9c0 \uc54a\uace0\ub3c4 \ub2e4\ub978 \uc6f9 \uc0ac\uc774\ud2b8\uc5d0\uc11c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uac83 \uc785\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ub2e4\ub978 \uc6f9 \uc0ac\uc774\ud2b8\ub97c \uc0ac\uc6a9\ud558\ub354\ub77c\ub3c4 google\uc5d0\uc11c \ub85c\uadf8\uc778\uc744 \ud558\ub294 \uacfc\uc815\uc744 \uac70\uce58\uae30 \ub54c\ubb38\uc5d0, \uc0ac\uc6a9\uc790\ub294\\n\ube44\ubc00\ubc88\ud638\ub098, critical\ud55c \uac1c\uc778\uc815\ubcf4 \uac19\uc740 \uac83\uc744 \ud55c \uacf3\uc5d0\uc11c \uad00\ub9ac\ud560 \uc218 \uc788\ub2e4\ub294 \uc7a5\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e4\uc2dc \ud55c\ubc88 \uc815\ub9ac\ud558\uc790\uba74 \uc6b0\ub9ac \uc6f9 \uc0ac\uc774\ud2b8\uc758 \uc0ac\uc6a9\uc790\uac00 \uc774\uc6a9\ud558\ub294 \ub2e4\ub978 \uc6f9 \uc0ac\uc774\ud2b8\uc758 \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\uac8c\ub054 \ub2e4\ub978 \uc6f9 \uc0ac\uc774\ud2b8\uc5d0\uc11c \uad8c\ud55c\uc744 \uc704\uc784 \ubc1b\ub294 \uac83 \uc785\ub2c8\ub2e4.\\n\\n### OAuth flow\\n\\n\\nOAuth Flow\ub97c \uc124\uba85\ud558\uae30 \uc804\uc5d0 \uc5ec\uae30\uc11c \ubaa8\ub974\ub294 \ub2e8\uc5b4\ub4e4\uc774 \ub9ce\uc2b5\ub2c8\ub2e4.\\n\ud574\ub2f9 [\ub9c1\ud06c](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-16#section-1.1)\uc5d0\uc11c \ub354 \uc790\uc138\ud558\uac8c \uc815\ub9ac \ub418\uc5b4\uc788\uc9c0\ub9cc \uc124\uba85\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n#### Resource Owner\\nResource Owner\ub294 \ub9d0 \uadf8\ub300\ub85c \ub9ac\uc18c\uc2a4 \uc18c\uc720\uc790\uc774\uace0, \uad6c\uae00\uacfc \uac19\uc740 \ud50c\ub7ab\ud3fc\uc5d0 \ud68c\uc6d0\uac00\uc785\uc774 \ub418\uc5b4\uc788\ub294, \uc989 \uad6c\uae00\uc5d0 \uc790\uc2e0\uc758 \uc815\ubcf4\ub4e4\uc774 \uc788\ub294 \uc0ac\uc6a9\uc790\uc785\ub2c8\ub2e4.\\n\\n#### Client\\nClient\ub3c4 \ub9d0 \uadf8\ub300\ub85c \uace0\uac1d\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc5b4\ub5a4 \uad00\uc810\uc5d0\uc11c \ubcf4\ub290\ub0d0 \uace0\uac1d\uc774\ub780 \ub73b\uc740 \ub2ec\ub77c\uc9d1\ub2c8\ub2e4. \uc5ec\uae30\uc11c\ub294 Google\uacfc \uac19\uc740 \ud50c\ub7ab\ud3fc\uc5d0\uc11c \uc81c\uacf5\ubc1b\uc740 \ub9ac\uc18c\uc2a4\ub97c \uc0ac\uc6a9\ud558\ub294 \uace0\uac1d\uc785\ub2c8\ub2e4.\\n\uc989 \uc6b0\ub9ac\uc758 \uc11c\ube44\uc2a4\uac00 Client\uac00 \ub418\ub294 \uac83\uc785\ub2c8\ub2e4. \uc65c\ub0d0\uba74 \uc6b0\ub9ac\ub294 \uad6c\uae00\uc5d0 \uc815\ubcf4\ub97c \uc694\uccad\ud558\uace0 \uc6b0\ub9ac\uc758 \uc11c\ube44\uc2a4\uc5d0\uc11c \uc0ac\uc6a9\ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\\n#### Authorization Server\\n\uc5ec\uae30\ub3c4 \ub9d0 \uadf8\ub300\ub85c \uc778\uc99d \uc11c\ubc84\uc785\ub2c8\ub2e4. Resource Owner\uac00 \uc62c\ubc14\ub978 \uc815\ubcf4\ub97c \uc785\ub825\ud588\ub294\uc9c0 \uac80\uc99d\ud558\uace0, \ubc1c\uae09 \ubc1b\uc740 Code\uc640 Token\uc774 \uc62c\ubc14\ub978 \uac83\uc778\uc9c0 \uac80\uc99d\ud569\ub2c8\ub2e4.\\n\\n#### Resource Server\\nResource Owner\uc758 \uc815\ubcf4\ub4e4\uc744 \uac00\uc9c0\uace0 \uc788\ub294 \uc11c\ubc84\uc785\ub2c8\ub2e4. \uc778\uc99d \uc11c\ubc84\uc5d0\uc11c \uc778\uc99d\uc744 \ub9c8\uce58\uace0 \ub09c \ub4a4 \uc6b0\ub9ac\ub294 Resource\ub97c \ubc1b\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc5ec\uae30\uc11c Authorization Server \uc640 Resource Server\uac00 \ub098\ub258\uc5b4\uc9c4 \uc774\uc720\ub294 \ub531\ud788 \uc5c6\uc2b5\ub2c8\ub2e4. **\ud574\ub2f9 \ud50c\ub7ab\ud3fc\uc758 \uc11c\ubc84 \uad6c\uc131\uc5d0 \ub530\ub77c \ub2e4\ub97c \uc218 \uc788\uc2b5\ub2c8\ub2e4.**\\n\\n\uc911\uc694\ud55c \uac83\uc740 **Authorization Server**\uc640 **Resource Server**\uac00 \uac19\uc740 \ubb36\uc74c\uc774\ub77c\ub294 \uac83 \uc785\ub2c8\ub2e4.\\n\\n```mermaid\\nsequenceDiagram\\n actor RO as Resource Owner (\ubc15\uc2a4\ud130)\\n participant C as Client (\uce74\ud398\uc778)\\n participant AS as Authorization Server (\uad6c\uae00)\\n participant RS as Resource Server (\uad6c\uae00)\\n\\n RO->>+C: 1. \ub85c\uadf8\uc778 \uc694\uccad\\n C--\x3e>-AS: 2. \ub85c\uadf8\uc778 \uc694\uccad\\n AS ->>+ RO: 3. \ub85c\uadf8\uc778 \ud398\uc774\uc9c0 \uc81c\uacf5\\n RO ->>+ AS: 4. ID/PW \uc785\ub825\\n AS ->> RO: 5. Authorization Code \ubc1c\uae09\\n RO ->> C: 6. Redirect URI\ub85c \uc774\ub3d9\\n C ->>+ AS: 7. \ubc1c\uae09 \ubc1b\uc740 Authorization Code\ub85c Token \uc694\uccad\\n AS ->> C: 8. Access Token \ubc1c\uae09\\n C ->> RO: 9. \ub85c\uadf8\uc778 \uc131\uacf5\\n RO ->> C: 10. \uc11c\ube44\uc2a4 \uc694\uccad\\n C ->> RS: 11. \ubc1c\uae09 \ubc1b\uc740 Token\uc73c\ub85c \uc815\ubcf4 \ud638\ucd9c\\n RS ->> C: 12. \uc815\ubcf4 \uc81c\uacf5\\n```\\n\\n\uac04\ub2e8\ud558\uac8c flow\ub97c \ub3c4\uc2dd\ud654 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n1. \uba3c\uc800 Resource Owner\ub294 \ub85c\uadf8\uc778\uc744 \ud558\uace0 \uc2f6\ub2e4\uba74 Client\uac00 \uc81c\uacf5\ud558\ub294 \ud574\ub2f9 Resource platform\uc758 URI\ub97c \ud074\ub9ad\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc778\uc99d\uc11c\ubc84\uc5d0\uc11c \ub85c\uadf8\uc778 \ud398\uc774\uc9c0\ub97c \uc81c\uacf5 \ubc1b\uc2b5\ub2c8\ub2e4.\\n2. \uadf8\ub9ac\uace0 Resource Owner\ub294 ID/PW\ub97c \uc785\ub825\ud558\uace0 Authorization Code\ub97c \ubc1c\uae09 \ubc1b\uc2b5\ub2c8\ub2e4. \ub3d9\uc2dc\uc5d0 Client\uc5d0\uc11c \ub4f1\ub85d\ud574\ub193\uc740 Redirect URI\ub85c code\uc640 \ud568\uaed8 \uc774\ub3d9\ud569\ub2c8\ub2e4.\\n3. Client\ub294 Resource Owner\uc5d0\uac8c \ubc1b\uc740 Code\ub97c \uac00\uc9c0\uace0 Authorization Server\uc5d0 \ud1a0\ud070\uc744 \uc694\uccad\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ubc1b\uc740 \ud1a0\ud070\uc744 \uc800\uc7a5\ud569\ub2c8\ub2e4.\\n4. \uadf8\ub9ac\uace0 Client\ub294 \ub85c\uadf8\uc778\uc744 \uc131\uacf5\ud558\uace0 \uc774\ud6c4 \ub2e4\ub978 platform\uc5d0\uc11c \uc815\ubcf4\ub97c \ud544\uc694\ud558\uac8c \ub41c\ub2e4\uba74 \uc800\uc7a5\ud55c Access Token\uc744 \ud1b5\ud574 Resource Server\uc5d0\uc11c \uc815\ubcf4\ub97c \uac00\uc838\uc635\ub2c8\ub2e4.\\n\\n\uadfc\ub370 \uc5ec\uae30\uc11c \uc774\uc0c1\ud55c \uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uc774\uc0c1\ud558\ub2e4\uae30\ubcf4\ub2e8 \uc65c \uc774\ub807\uac8c \ubcf5\uc7a1\ud55c\uac00 \ub77c\ub294 \uc758\ubb38\uc744 \uac00\uc9c8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uad73\uc774 Authorization code\ub97c \ubc1b\uc544 \ub2e4\uc2dc \ud55c\ubc88 \ub354 Access Token\uc744 \ubc1b\uc544\uc57c \ud55c\ub2e4\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4. \ubc14\ub85c **Client\uc5d0\uac8c Access Token\uc744 \uc900\ub2e4\uba74 \ud1b5\uc2e0\uc774 \ud55c\ubc88 \uc904\uc5b4\ub4e4 \uc218 \uc788\uc9c0 \uc54a\uc744\uae4c??**\\n\\n\ubcf4\uc548\ubb38\uc81c \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\ub9cc\uc57d \ubc14\ub85c Access Token\uc744 \uc900\ub2e4\uba74 \uadf8 Access Token\uc774 \ud0c8\ucde8 \ub2f9\ud558\uba74 \ud574\ub2f9 Resource Owner\uc758 \ubaa8\ub4e0 \uc815\ubcf4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\nCode\ub294 Secret Key\uc640 \uac19\uc774 \uc804\ub2ec\ud574\uc57c Access Token\uc744 \ubc1c\uae09 \ubc1b\uc744 \uc218 \uc788\uae30 \ub54c\ubb38\uc5d0 \ud0c8\ucde8\ub418\uc5b4\ub3c4 \ub354 \uc548\uc804\ud569\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \ub2e4\ub978 \ud50c\ub7ab\ud3fc\uc5d0\uc11c Code\ub098 Token\uc774\ub098 \ud574\ub2f9 \uc815\ubcf4\ub97c \uc804\ub2ec\ud560 \ubc29\ubc95\uc740 URI\uc5d0 \uc804\ub2ec\ud558\ub294 \ubc29\ubc95\ubfd0 \uc785\ub2c8\ub2e4.\\n\uadf8\ub807\uae30 \ub54c\ubb38\uc5d0 Redircet URI\uc5d0 Access Token\uc744 \ub2f4\ub294\ub2e4\uba74 \ud0c8\ucde8 \uac00\ub2a5\uc131\uc774 \ucee4\uc9c0\uae30 \ub54c\ubb38\uc5d0 \ubcf4\uc548\ubb38\uc81c\uac00 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\\n\\n### \ubc31\uc5d4\ub4dc\uc640 \ud504\ub860\ud2b8\uc5d4\ub4dc\uc758 flow\\n\\n```mermaid\\n\\nsequenceDiagram\\n actor RO as Resource Owner\\n participant F as Frontend\\n participant B as Backend\\n participant AS as Authorization Server\\n participant RS as Resource Server\\n\\n RO->>+F: 1. \ub85c\uadf8\uc778 \uc694\uccad\\n F--\x3e>-B: 2. \ub85c\uadf8\uc778 \uc694\uccad\\n B->>+F: 3. \ud574\ub2f9 \ud50c\ub7ab\ud3fc \ub85c\uadf8\uc778 URI \uc81c\uacf5\\n F--\x3e>-RO: 4. \ud574\ub2f9 \ud50c\ub7ab\ud3fc \ub85c\uadf8\uc778 URI \uc81c\uacf5\\n AS ->>+ RO: 5. \ub85c\uadf8\uc778 \ud398\uc774\uc9c0 \uc81c\uacf5\\n RO ->>+ AS: 6. ID/PW \uc785\ub825\\n AS ->> RO: 7. Authorization Code \ubc1c\uae09\\n RO ->> F: 8. Redirect URI\ub85c \uc774\ub3d9 (w. Authorization Code)\\n F ->>+ B: 9. \ubc1c\uae09 \ubc1b\uc740 Authorization Code \uc804\ub2ec\\n B ->>+ AS: 10. \uc804\ub2ec \ubc1b\uc740 Authorization Code\ub85c Token \uc694\uccad\\n AS ->> B: 11. Access Token \ubc1c\uae09\\n B ->>+ F: 12. \ub85c\uadf8\uc778 \uc131\uacf5\\n F --\x3e>- RO: 13. \ub85c\uadf8\uc778 \uc131\uacf5\\n RO ->>+ F: 14. \uc11c\ube44\uc2a4 \uc694\uccad\\n F --\x3e>- B: 15. \uc11c\ube44\uc2a4 \uc694\uccad\\n B ->> RS: 16. \ubc1c\uae09 \ubc1b\uc740 Token\uc73c\ub85c \uc815\ubcf4 \ud638\ucd9c\\n RS ->> B: 17. \uc815\ubcf4 \uc81c\uacf5\\n\\n```\\n\uc544\uae4c Client \ubd80\ubd84\uc744 \uc880 \ub354 Frontend, Backend\ub85c \uad6c\ubd84\uc9c0\uc5b4 \uc138\ubd84\ud654 \ud574\ubd24\uc2b5\ub2c8\ub2e4. \ubcf5\uc7a1\ud574\ubcf4\uc774\uc9c0\ub9cc, \uc804\ud600 \uc5b4\ub835\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc544\uae4c \uc124\uba85\ud588\ub358 \ud750\ub984\uacfc \ub2e4\ub978 \ubd80\ubd84\uc740 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\\n\ub610 \uc5ec\uae30\uc11c\ub294 \uad73\uc774 \uc81c\uac00 Authorization Server\uc5d0\uc11c Code\ub97c \ubc1b\uc544\uc62c \ub54c Redirect URI\ub97c \ubc31\uc5d4\ub4dc \uc11c\ubc84\ub85c \ud558\uc9c0\uc54a\uace0 \ud504\ub860\ud2b8\uc5d4\ub4dc \uc11c\ubc84\ub85c \ud558\ub824\ub294 \uc774\uc720\ub294 Resource Owner\uac00 \ub2e4\ub978 platform\uacfc \uc778\uc99d\ud558\ub294 \ubd80\ubd84\uc740 \ubc31\uc5d4\ub4dc\uc758 \uc5ed\ud560\uc774 \uc544\ub2c8\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \ubc31\uc5d4\ub4dc\ub294 Resource Owner\uac00 \uac00\uc838\uc628 code\ub97c \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0\uc11c \uc804\ub2ec \ubc1b\uc544 Resource Server\uc5d0 \uc815\ubcf4\ub97c \uc694\uccad\ud558\ub294 \uac83\uc774\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n***(\ubb3c\ub860 \uc81c \uac1c\uc778\uc801\uc778 \uc758\uacac\uc774\ub77c \uc815\ub2f5\uc740 \uc544\ub2d9\ub2c8\ub2e4.)***\\n\\n\\n## OAuth \uad6c\ud604\ud574\ubcf4\uae30\\n\\n\uac04\ub2e8\ud788 Spring Security \uc5c6\uc774 OAuth \uc778\uc99d\uc744 \uad6c\ud604\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc81c\uc77c \uba3c\uc800 \uad6c\uae00 \ud639\uc740 \ub2e4\ub978 \ud50c\ub7ab\ud3fc\uc5d0\uc11c \uc124\uc815\ud55c id, secret key \ub4f1\ub4f1\uc758 \uc815\ubcf4\ub97c yml\uc5d0 \uc791\uc131\ud588\uc2b5\ub2c8\ub2e4.\\n```yml title=\\"application-oauth.yml\\"\\noauth2:\\n provider:\\n google:\\n id: google-id\\n secret: google-secret-key\\n redirect-url: http://localhost:8080/login/oauth2/code/google\\n token-url: https://www.googleapis.com/oauth2/v4/token\\n info-url: https://www.googleapis.com/oauth2/v2/userinfo\\n```\\n\uadf8\ub9ac\uace0 OAuth\ub294 \uc5b4\ub290 \ud50c\ub7ab\ud3fc\uc774 \ub420 \uc9c0 \ubaa8\ub974\uace0, \ud655\uc7a5\uc131 \uc788\uac8c \uad6c\uc131\ud558\ub294 \uac83\uc774 \uc88b\uc744 \uac83 \uac19\uc544 \uc778\ud130\ud398\uc774\uc2a4\ub85c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n```java title=\\"OAuthMember.java\\"\\npublic interface OAuthMember {\\n String id();\\n String email();\\n String nickname();\\n String imageUrl();\\n}\\n```\\n\uc774\ub7ec\ud55c \ud074\ub798\uc2a4\ub4e4\uc744 \uad00\ub9ac\ud558\uae30 \uc27d\uac8c Enum\uc744 \ucd94\uac00\ud569\ub2c8\ub2e4.\\n\\n```java title=\\"Provider.java\\"\\npublic enum Provider {\\n\\n GOOGLE(\\"google\\", GoogleMember::new),\\n ;\\n\\n private final String providerName;\\n private final Function, OAuthMember> function;\\n\\n Provider(String providerName, Function, OAuthMember> function) {\\n this.providerName = providerName;\\n this.function = function;\\n }\\n\\n public static Provider from(String name) {\\n return Arrays.stream(values())\\n .filter(it -> it.providerName.equals(name))\\n .findFirst()\\n .orElseThrow(() -> new RuntimeExceptin());\\n }\\n\\n public OAuthMember getOAuthProvider(Map body) {\\n return function.apply(body);\\n }\\n}\\n```\\n\ud574\ub2f9 Enum\uc740 \ub450\uac1c\uc758 \ud544\ub4dc\ub97c \uac00\uc9c0\uace0 \uc788\uc2b5\ub2c8\ub2e4. \ud558\ub098\ub294 \ud574\ub2f9 \ud50c\ub7ab\ud3fc\uc758 \uc774\ub984, \uadf8\ub9ac\uace0 `Map`\ub97c \uc544\uae4c \ub9cc\ub4e4\uc5c8\ub358 \uc778\ud130\ud398\uc774\uc2a4\ub85c \ubc18\ud658\ud558\ub294 Function \uc5ec\uae30\uc11c\\n`Map`\ub85c \uc9c0\uc815\ud574\uc900 \uc774\uc720\ub294, \ud50c\ub7ab\ud3fc\ub9c8\ub2e4 \ubc18\ud658\ub418\ub294 JSON \ud0c0\uc785\uc774 \ub2e4\ub974\uae30 \ub54c\ubb38\uc5d0 \uadf8\ub7f0 \ubd80\ubd84\uc5d0 \ub300\ud574 \uc911\ubcf5\uc744 \uc81c\uac70\ud558\uae30 \uc704\ud574 \uc774\ub7ec\ud55c \ud615\ud0dc\ub85c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc544\uae4c yml\uc5d0 \uc791\uc131\ud588\ub358 \uc815\ubcf4\ub4e4\uc744 \uac00\uc838\uc640\uc57c\ud569\ub2c8\ub2e4. `@Value` \uc5b4\ub178\ud14c\uc774\uc158\uc73c\ub85c\ub3c4 \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n```java\\n @Value(\\"oauth.provider.google.id\\")\\n private String id;\\n @Value(\\"oauth.provider.google.secret\\")\\n private String secret;\\n\\n ...\\n```\\n\ud558\uc9c0\ub9cc \uc774\ub807\uac8c \uacc4\uc18d binding\uc744 \ud574\uc918\uc57c\ud55c\ub2e4\ub294 \uc810\uc774 \uc544\uc8fc \uadc0\ucc2e\uace0 \ubcf4\uae30\ub3c4 \uc548\uc88b\uc2b5\ub2c8\ub2e4.\\n```groovy title=\\"build.gradle\\"\\nannotationProcessor \\"org.springframework.boot:spring-boot-configuration-processor\\"\\n```\\n\ud558\uc9c0\ub9cc \uc704\uc758 \uc758\uc874\uc131\uc744 \ucd94\uac00\ud574\uc900\ub2e4\uba74 \uc544\uc8fc \ud3b8\ud558\uac8c property\ub97c \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```java title=\\"OAuthProviderProperties.java\\"\\n@Component\\n@ConfigurationProperties(prefix = \\"oauth2\\")\\npublic class OAuthProviderProperties {\\n // prefix oauth2 \uae30\uc900\uc73c\ub85c \uc54c\uc544\uc11c google\uc774 \uc774\ub984\uc778 Provider Enum\uc744 \ucc3e\uc544\uc11c Key\ub85c \ubc14\uc778\ub529\\n private final Map provider = new EnumMap<>(Provider.class);\\n\\n public OAuthProviderProperty getProviderProperties(Provider provider) {\\n return this.provider.get(provider);\\n }\\n\\n @Getter\\n @Setter\\n public static class OAuthProviderProperty {\\n // \uadf8\ub9ac\uace0 provider \ud558\uc704 \uc815\ubcf4\ub4e4\uc740 \uc544\ub798\uc758 \ud544\ub4dc\uc5d0 \ubc14\uc778\ub529\\n private String id;\\n private String secret;\\n private String redirectUrl;\\n private String tokenUrl;\\n private String infoUrl;\\n }\\n}\\n```\\n\uc774\ub807\uac8c \ub418\uba74 \uad6c\uc870\uc801\uc778 \uc900\ube44\ub294 \ub05d\ub0ac\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc81c\ub294 \ud574\ub2f9 \ud50c\ub7ab\ud3fc\uc5d0 \uc815\ubcf4\ub97c \uc694\uccad\ud558\ub294 \uc791\uc5c5\ub9cc \ud558\uba74 \ub429\ub2c8\ub2e4.\\n\uadf8\ub7fc \uc544\uae4c \ub9d0\uc500\ub4dc\ub838\ub358 \uc21c\uc11c\ub85c \uc694\uccad\uc744 \ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java title=\\"RestTemplateOAuthRequester.java\\"\\npublic class RestTemplateOAuthRequester implements OAuthRequester {\\n\\n @Override\\n public OAuthMember login(OAuthLoginRequest request) {\\n // frontend\uc5d0\uc11c \ubc1b\uc544\uc628 \ub85c\uadf8\uc778 platform\\n Provider provider = Provider.from(request.provider());\\n // \ud574\ub2f9 Platform\uc5d0 \ub9de\ub294 \uc815\ubcf4 \ucc3e\uc74c\\n OAuthProviderProperty property = oAuthProviderProperties.getProviderProperties(provider);\\n // frontend\uc5d0\uc11c \ubc1b\uc544\uc628 code\uc640 \ub4f1\ub85d\ud574\ub193\uc740 property\ub85c Access Token \uc694\uccad\\n OAuthTokenResponse token = requestAccessToken(property, requet.getCode());\\n // \ubc1b\uc544\uc628 Token\uc73c\ub85c \ud574\ub2f9 Resource Owner\uc758 \uc815\ubcf4 \uc694\uccad\\n Map userAttributes = getUserAttributes(property, token);\\n return provider.getOAuthProvider(userAttributes);\\n }\\n\\n private OAuthTokenResponse requestAccessToken(OAuthProviderProperty property, String code) {\\n HttpHeaders headers = new HttpHeaders();\\n headers.setBasicAuth(property.getId(), property.getSecret());\\n headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\\n\\n HttpEntity> request = new HttpEntity<>(headers);\\n URI tokenUri = getTokenUri(property, code);\\n return restTemplate.postForEntity(tokenUri, request, OAuthTokenResponse.class).getBody();\\n }\\n\\n private URI getTokenUri(OAuthProviderProperty property, String code) {\\n return UriComponentsBuilder.fromUriString(property.getTokenUrl())\\n .queryParam(CODE, URLDecoder.decode(code, StandardCharsets.UTF_8))\\n .queryParam(GRANT_TYPE, AUTHORIZATION_CODE)\\n .queryParam(REDIRECT_URI, property.getRedirectUrl())\\n .build()\\n .toUri();\\n }\\n\\n private Map getUserAttributes(OAuthProviderProperty property, OAuthTokenResponse tokenResponse) {\\n HttpHeaders headers = new HttpHeaders();\\n headers.setBearerAuth(tokenResponse.accessToken());\\n headers.setContentType(MediaType.APPLICATION_JSON);\\n URI uri = URI.create(property.getInfoUrl());\\n RequestEntity requestEntity = new RequestEntity<>(headers, HttpMethod.GET, uri);\\n ResponseEntity> responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<>() {\\n });\\n return responseEntity.getBody();\\n }\\n}\\n```\\n\\n\uc774\ub807\uac8c\ub9cc \ud55c\ub2e4\uba74 \uadf8 \uc5b4\ub824\uc6cc \ubcf4\uc774\ub358 OAuth \uc778\uc99d\ub3c4 \uac04\ub2e8\ud558\uac8c \ud574\uacb0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n***(\ubb3c\ub860 \uc81c \ucf54\ub4dc\uac00 \uc815\ub2f5\uc774 \uc544\ub2d9\ub2c8\ub2e4)***\\n\\n\\n### Reference\\nhttps://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-16\\n\\nhttps://developers.google.com/identity/protocols/oauth2?hl=ko"},{"id":"18","metadata":{"permalink":"/18","source":"@site/blog/2023-07-23-why-private-ip-is-required-for-instance.mdx","title":"private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\ub97c \uc678\ubd80\uc640 \uc5f0\uacb0\ud560 \ub54c, public ip? private ip?","description":"\uc5b4\ub5a4 \ubb38\uc81c\uac00 \uc788\uc5c8\ub098\uc694?","date":"2023-07-23T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 23\uc77c","tags":[{"label":"aws","permalink":"/tags/aws"},{"label":"vpc","permalink":"/tags/vpc"},{"label":"subnet","permalink":"/tags/subnet"},{"label":"ip","permalink":"/tags/ip"}],"readingTime":10.365,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"18","title":"private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\ub97c \uc678\ubd80\uc640 \uc5f0\uacb0\ud560 \ub54c, public ip? private ip?","authors":["nunu"],"tags":["aws","vpc","subnet","ip"]},"prevItem":{"title":"OAuth 2.0\uc758 \ud750\ub984\uacfc \uc124\uc815 \ud574\ubcf4\uae30","permalink":"/19"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 CI/CD","permalink":"/17"}},"content":"## \uc5b4\ub5a4 \ubb38\uc81c\uac00 \uc788\uc5c8\ub098\uc694?\\n\\n\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4\uc5d0\uc11c private \uc11c\ube0c\ub137\uc5d0 db \uc778\uc2a4\ud134\uc2a4\ub97c \ub450\uace0, \ubcf4\uc548\uc744 \uc704\ud574 \uc678\ubd80\uc5d0\uc11c \uc811\uc18d\uc744 \ucc28\ub2e8\ud558\ub824\uace0 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c \ucd1d 2\uac00\uc9c0\uc758 \ubb38\uc81c\uc810\uc774 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n1. private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\uac00 \uc778\ud130\ub137\uc5d0\uc11c mysql\uc744 \uc124\uce58\ud560 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n2. public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0\uc11c private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc774 \uc548\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \ubd80\ubd84\uc744 \uc5b4\ub5bb\uac8c \ud574\uacb0\ud588\ub294\uc9c0 \uc54c\uc544\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\ub798\uc758 \ubaa8\ub4e0 \uc124\uba85\uc740 AWS \ub97c \uae30\uc900\uc73c\ub85c \ud569\ub2c8\ub2e4.\\n\\n## private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\uac00 \uc778\ud130\ub137\uc5d0\uc11c mysql\uc744 \uc124\uce58\ud560 \uc218 \uc5c6\uc5c8\ub2e4.\\n\\n### \ud574\uacb0 \ubc29\ubc95\\n\\npublic ip \uc790\ub3d9\ud560\ub2f9\uc744 \ud574\uc8fc\uc9c0 \uc54a\uc544\uc11c, \uc778\ud130\ub137\uc5d0 \uc5f0\uacb0\uc774 \uc548 \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574 public ip \uc790\ub3d9\ud560\ub2f9\uc744 \ud574\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc65c public ip\ub97c \ud560\ub2f9\ud588\ub354\ub2c8 \ubb38\uc81c\uac00 \ud574\uacb0\ub418\uc5c8\uc744\uae4c\uc694?\\n\\n## private \uc11c\ube0c\ub137\uc774\ub780?\\n\\n\uc815\ub9d0 \uac04\ub2e8\ud558\uac8c \uc124\uba85\ud588\uc744 \ub54c\\n\\nprivate \uc11c\ube0c\ub137\uc740 \uc778\ud130\ub137\uc5d0 \uc5f0\uacb0\ub418\uc9c0 \uc54a\uc740 \uc11c\ube0c\ub137\uc785\ub2c8\ub2e4.\\n\\n\uc870\uae08 \uc790\uc138\ud558\uac8c \ub4e4\uc5b4\uac00 \ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\nprivate \uc11c\ube0c\ub137\uc740 \uc778\ud130\ub137 \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc5f0\uacb0\ub418\uc9c0 \uc54a\uc740 \uc11c\ube0c\ub137\uc785\ub2c8\ub2e4.\\n\\naws \uacf5\uc2dd\ubb38\uc11c\uc5d0\uc11c \uc0ac\uc9c4\uc744 \ud1b5\ud574 \ubcf4\uba74 \uc544\ub798\uc640 \uac19\uc774 \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4\\n\\n![private subnet](https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/images/internet-gateway-basics.png)\\n\\npublic \uc11c\ube0c\ub137\uc5d0\ub9cc \uc778\ud130\ub137 \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc5f0\uacb0\ub418\uc5b4 \uc788\uace0, private \uc11c\ube0c\ub137\uc5d0\ub294 \uc778\ud130\ub137 \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc5f0\uacb0\ub418\uc5b4\uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\nprivate \uc11c\ube0c\ub137\uc5d0 \uc778\ud130\ub137 \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc9c0 \uc54a\ub2e4\uace0 \ud588\uc744 \ub54c, \uae30\ubcf8\uc801\uc73c\ub85c \uc778\ud130\ub137\uc5d0 \uc811\uc18d\uc774 \uc548\ub429\ub2c8\ub2e4.\\n\\nmysql\uc744 \uc124\uce58\ud560 \ub54c\ub3c4, \uc778\ud130\ub137\uc5d0 \uc811\uc18d\uc744 \ud574\uc57c\ud558\ub294\ub370, \uc778\ud130\ub137\uc5d0 \uc811\uc18d\uc774 \uc548\ub418\ub2c8 \uc124\uce58\uac00 \uc548\ub418\ub294 \uac83\uc785\ub2c8\ub2e4.\\n\\n### \uc5b4? \uc778\ud130\ub137 \uc790\uccb4\uac00 \uc811\uadfc\uc774 \uc548\ub418\uba74 \uc5b4\ub5bb\uac8c \uc124\uce58\ud558\ub098\uc694?\\n\\n\uc815\ub9d0 \uc6d0\uc2dc\uc801\uc73c\ub85c \ud574\uacb0\ud558\uae30 \uc704\ud574\uc11c\ub294 public \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\ub97c \ud558\ub098 \ub354 \ub9cc\ub4e4\uc5b4\uc11c, mysql \uc744 \uc555\ucd95\ud574\uc11c scp\ub97c \ud1b5\ud574 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc804\uc1a1\ud558\uace0, \uc555\ucd95\uc744 \ud480\uc5b4\uc11c \uc124\uce58\ud558\ub294 \ubc29\ubc95\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774 \ubc29\ubc95\uc740 \ub108\ubb34 \uc6d0\uc2dc\uc801\uc774\uace0, \ube44\ud6a8\uc728\uc801\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc778\ud130\ub137\uc73c\ub85c \uc694\uccad\uc744 \ubcf4\ub0bc \uc218 \uc788\ub3c4\ub85d \ub9cc\ub4dc\ub294 \uacfc\uc815\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n### \uc778\ud130\ub137\uc73c\ub85c \uc694\uccad\uc744 \ubcf4\ub0bc \uc218 \uc788\ub3c4\ub85d \ub9cc\ub4dc\ub294 \uacfc\uc815\\n\\n\uc778\ud130\ub137\uc73c\ub85c \uc694\uccad\uc744 \ubcf4\ub0bc \uc218 \uc788\ub3c4\ub85d \ub9cc\ub4dc\ub294 \uacfc\uc815\uc740 \ud06c\uac8c 2\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### private \uc11c\ube0c\ub137\uc744 public \uc11c\ube0c\ub137\uc73c\ub85c \ubc14\uafb8\uae30\\n\\n\ubcf4\uc548\uc744 \uc704\ud574\uc11c private \uc11c\ube0c\ub137\uc5d0 \ub450\ub824\uace0 \ud588\ub358 \uac83\uc744 public \uc11c\ube0c\ub137\uc73c\ub85c \ubc14\uafbc\ub2e4\ub294 \ubd80\ubd84\uc740 \ub9e4\uc6b0 \uc704\ud5d8\ud569\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc774 \ubc29\ubc95\uc740 \ubcf4\ud1b5 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n### NAT \uc778\uc2a4\ud134\uc2a4(Gateway) \ub9cc\ub4e4\uae30\\n\\nNAT \uc778\uc2a4\ud134\uc2a4\ub294 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 \uc778\ud130\ub137\uc5d0 \uc811\uc18d\ud560 \uc218 \uc788\ub3c4\ub85d \ub9cc\ub4e4\uc5b4\uc8fc\ub294 \uc778\uc2a4\ud134\uc2a4\uc785\ub2c8\ub2e4.\\n\\n\uc778\ud130\ub137\uc5d0 \uc811\uc18d\uc744 \ud558\uae30 \uc704\ud574\uc11c\ub294 public ip \uac00 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c NAT \uc778\uc2a4\ud134\uc2a4, NAT \uac8c\uc774\ud2b8\uc6e8\uc774\ub294 public \uc11c\ube0c\ub137\uc5d0 \uc874\uc7ac\ud574\uc57c \ud569\ub2c8\ub2e4.\\n\\n\uc5b4? NAT \uc778\uc2a4\ud134\uc2a4\ub97c \ud1b5\ud574\uc11c \ubc14\ub85c \ud1b5\uc2e0\uc774 \uac00\ub2a5\ud558\uba74 \uc65c private \uc11c\ube0c\ub137\uc774 \ud544\uc694\ud55c\uac00\uc694? \uadf8\ub0e5 \ub2e4 public \uc11c\ube0c\ub137\uc5d0 \ub450\uba74 \ub418\uc9c0 \uc54a\ub098\uc694?\\n\\nNAT \uc778\uc2a4\ud134\uc2a4, NAT Gateway\ub294 \ub0b4\ubd80\uc5d0\uc11c \ucd9c\ubc1c\ud55c \ud2b8\ub798\ud53d\ub9cc \ud1b5\uacfc\ud560 \uc218 \uc788\ub3c4\ub85d \uc124\uc815\uc774 \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uba74 private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\ud574\uc11c \uc9c1\uc811 mysql download \uc694\uccad\uc744 \ud588\uc744 \ub54c\ub9cc \ud5c8\uc6a9\uc774 \ub429\ub2c8\ub2e4.\\n\\n\uc678\ubd80\uc5d0\uc11c \ubc14\ub85c private \uc778\uc2a4\ud134\uc2a4\ub85c \uc811\uadfc\ud560 \uc218\ub294 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\nNAT \uc778\uc2a4\ud134\uc2a4\ub9cc \uc124\uc815\uc744 \ud558\uba74 \ubc14\ub85c \uc5f0\uacb0\uc774 \ub418\ub098\uc694?\\n\\npublic ip\ub3c4 \uc790\ub3d9 \ud560\ub2f9\uc744 \ud574\uc918\uc57c \ud569\ub2c8\ub2e4\\n\\n### public ip \uac00 \ud544\uc694\ud55c \uc774\uc720\\n\\nNAT \uc778\uc2a4\ud134\uc2a4\ub97c \ud1b5\ud574\uc11c private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 \uc778\ud130\ub137\uc5d0 \uc811\uc18d\ud560 \uc218 \uc788\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\ub294\ub370, \uc65c public ip \uac00 \ud544\uc694\ud560\uae4c\uc694?\\n\\n\uc678\ubd80 \uc778\ud130\ub137\uacfc \ud1b5\uc2e0\uc744 \ud560 \ub54c public ip \uac00 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\nNAT \uc778\uc2a4\ud134\uc2a4 \ud639\uc740 NAT \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc778\ud130\ub137\uacfc \ud1b5\uc2e0\ud560 \ub54c, NAT \uc778\uc2a4\ud134\uc2a4\uc758 public ip + private ip\ub97c \ud1b5\ud574\uc11c \ud1b5\uc2e0\uc744 \ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\ub0b4\ubd80 \uc778\uc2a4\ud134\uc2a4\uc758 public ip \ub97c \ud1b5\ud574\uc11c \ud1b5\uc2e0\uc744 \ud558\uac8c \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c NAT \uc778\uc2a4\ud134\uc2a4\uc640 \ub0b4\ubd80 \uc778\uc2a4\ud134\uc2a4 \ubaa8\ub450 public ip \uac00 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c 1\ubc88 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc81c 2\ubc88\uc9f8 \ubb38\uc81c\ub97c \ud574\uacb0\ud574 \ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0\uc11c private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc774 \uc548 \ub418\ub294 \ubb38\uc81c\\n\\npublic \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc11c\ubc84\uac00 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc11c\ubc84\uc5d0 \uc811\uc18d\uc744 \ud558\ub824\uace0 \ud588\ub294\ub370, \uc811\uc18d\uc774 \uc548 \ub418\ub294 \ubb38\uc81c\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n### \ud574\uacb0 \ubc29\ubc95\\n\\n\ud574\uacb0 \ubc29\ubc95\uc5d0\ub294 2\uac00\uc9c0 \uacfc\uc815\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc5d0 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc744 \ucd94\uac00\ud574 \uc8fc\uae30\\n\\n\uae30\ubcf8\uc801\uc73c\ub85c public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc5d0\ub294 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc774 \ucd94\uac00\ub418\uc5b4\uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc5d0 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc744 \ucd94\uac00\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n\\n### private ip\ub97c \ud1b5\ud574\uc11c \uc811\uc18d\ud558\uae30\\n\\npublic \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\ud560 \ub54c, public ip \ub97c \ud1b5\ud574\uc11c \uc811\uc18d\uc744 \ud558\uba74 \uc548 \ub429\ub2c8\ub2e4.\\n\\npublic ip\ub97c \ud1b5\ud574\uc11c \uc811\uc18d\ud558\ub294 \uacfc\uc815\uc744 \uc790\uc138\ud558\uac8c \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n1. public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 public ip \ub97c \ud1b5\ud574\uc11c private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc744 \uc2dc\ub3c4\ud569\ub2c8\ub2e4.\\n2. \ub77c\uc6b0\ud305 \ud14c\uc774\ube14\uc5d0\uc11c public ip \uc77c \uacbd\uc6b0\uc5d0 \uc5b4\ub5bb\uac8c \ucc98\ub9ac\ud560\uc9c0\uc5d0 \ub300\ud55c \uc815\ubcf4\ub97c \ucc3e\uc2b5\ub2c8\ub2e4.\\n3. \ub77c\uc6b0\ud130\ub97c \ud1b5\ud574\uc11c \uc678\ubd80 \uc778\ud130\ub137\uc73c\ub85c \ub098\uac00\uac8c \ub429\ub2c8\ub2e4.\\n4. \ud2b8\ub798\ud53d\uc774 NAT \uc778\uc2a4\ud134\uc2a4\uc5d0 \ub3c4\ucc29\ud569\ub2c8\ub2e4.\\n5. NAT \uc778\uc2a4\ud134\uc2a4\ub294 \ub0b4\ubd80\uc5d0\uc11c \ucd9c\ubc1c\ud55c \ud2b8\ub798\ud53d\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0, \ud2b8\ub798\ud53d\uc744 \uac70\ubd80\ud569\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc774 \uc77c\uc5b4\ub098\uae30\uc5d0, public ip \ub97c \ud1b5\ud574\uc11c \uc811\uc18d\uc744 \ud558\uba74 \uc548 \ub429\ub2c8\ub2e4.\\n\\nprivate ip\ub97c \ud1b5\ud574\uc11c \uc811\uadfc\ud558\uba74 \uc5b4\ub5bb\uac8c \ub418\ub294\uc9c0 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4\\n\\n1. public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 private ip \ub97c \ud1b5\ud574\uc11c private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc744 \uc2dc\ub3c4\ud569\ub2c8\ub2e4.\\n2. \ub77c\uc6b0\ud305 \ud14c\uc774\ube14\uc5d0\uc11c private ip \uc77c \uacbd\uc6b0\uc5d0 \uc5b4\ub5bb\uac8c \ucc98\ub9ac\ud560\uc9c0\uc5d0 \ub300\ud55c \uc815\ubcf4\ub97c \ucc3e\uc2b5\ub2c8\ub2e4.\\n3. \ub77c\uc6b0\ud130\ub97c \uac70\uccd0\uc11c private \uc11c\ube0c\ub137\uc758 \ub77c\uc6b0\ud130\ub85c \uc774\ub3d9\ud569\ub2c8\ub2e4.\\n4. private \uc11c\ube0c\ub137\uc758 \ub77c\uc6b0\ud130\ub294 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0\uac8c \ud2b8\ub798\ud53d\uc744 \uc804\ub2ec\ud569\ub2c8\ub2e4.\\n5. private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub294 \ud2b8\ub798\ud53d\uc744 \ubc1b\uc544\uc11c \ucc98\ub9ac\ud569\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c 2\ubc88 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \uc694\uc57d\\n\\n1. private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 \uc778\ud130\ub137\uc5d0 \uc811\uc18d\uc744 \ud558\ub824\uba74 NAT \uc778\uc2a4\ud134\uc2a4 \ud639\uc740 NAT \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \ud544\uc694\ud569\ub2c8\ub2e4.\\n2. private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub3c4 public ip \uac00 \ud544\uc694\ud569\ub2c8\ub2e4.\\n3. public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc744 \ud558\ub824\uba74 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc744 \ucd94\uac00\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n4. public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc744 \ud560 \ub54c, private ip \ub97c \ud1b5\ud574\uc11c \uc811\uc18d\uc744 \ud574\uc57c \ud569\ub2c8\ub2e4."},{"id":"17","metadata":{"permalink":"/17","source":"@site/blog/2023-07-22-ci-cd.mdx","title":"\uce74\ud398\uc778 \ud300\uc758 CI/CD","description":"\uc548\ub155\ud558\uc138\uc694. \uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.","date":"2023-07-22T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 22\uc77c","tags":[{"label":"CI","permalink":"/tags/ci"},{"label":"CD","permalink":"/tags/cd"}],"readingTime":7.735,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"17","title":"\uce74\ud398\uc778 \ud300\uc758 CI/CD","authors":["jay"],"tags":["CI","CD"]},"prevItem":{"title":"private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\ub97c \uc678\ubd80\uc640 \uc5f0\uacb0\ud560 \ub54c, public ip? private ip?","permalink":"/18"},"nextItem":{"title":"JPA\uc5d0\uc11c ID\uac00 \uc788\ub294 Entity\uc5d0 \ub300\ud574 save \uc2dc\uc5d0 select \ucffc\ub9ac\uac00 \ub098\uac00\ub294 \uc774\uc720","permalink":"/16"}},"content":"\uc548\ub155\ud558\uc138\uc694. \uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\uc800\ud76c \ud300\uc5d0\uc11c CI/CD\ub294 \uc5b4\ub5bb\uac8c \uc9c4\ud589\ub418\ub294\uc9c0 \uc791\uc131\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## CI (\uc9c0\uc18d\uc801 \ud1b5\ud569)\\n![ci](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/ci.png?raw=true)\\n\\n\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc9c0\uc18d\uc801 \ud1b5\ud569 \uc989 CI\ub97c \uc9c4\ud589\ud558\uae30 \uc704\ud574\uc11c \uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 Github Actions\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\\n\\nmain, develop \ube0c\ub79c\uce58\uc5d0 Push, Pull Request \uc694\uccad\uc774 \ub4e4\uc5b4\uac04\ub2e4\uba74 \uc774\ubca4\ud2b8\uac00 \ubc1c\uc0dd\ud558\uace0, Github Actions\ub97c \ud1b5\ud574 \uc800\ud76c\uac00 \uc791\uc131\ud574\ub454 \uc2a4\ud06c\ub9bd\ud2b8\uac00 \uc2e4\ud589 \ub429\ub2c8\ub2e4.\\n\\n\uc774 \uc2a4\ud06c\ub9bd\ud2b8\uc5d0 \uc5ec\ub7ec\uac00\uc9c0\ub97c \ub4f1\ub85d\ud560 \uc21c \uc788\uc9c0\ub9cc, \uc800\ud76c\ub294 \uc790\ub3d9\uc73c\ub85c \ud14c\uc2a4\ud2b8\ub97c \uc9c4\ud589\ud558\ub3c4\ub85d \ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\uc790\ub3d9\uc73c\ub85c \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub9ac\uba74\uc11c \ud14c\uc2a4\ud2b8\uac00 \ud1b5\uacfc\ub97c \ud574\uc57c\uc9c0\ub9cc Merge\ub97c \uc9c4\ud589\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \ud1b5\ud574 \uac1c\ubc1c\uc790\uc758 \uc2e4\uc218\ub97c \uc904\uc77c \uc218 \uc788\uace0 \uc548\uc815\uc801\uc73c\ub85c \uc9c0\uc18d\uc801 \ud1b5\ud569\uc744 \uc774\ub8f0 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\\n\\n
    \\n\\n## CD (\uc9c0\uc18d\uc801 \ubc30\ud3ec)\\n![cd](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/cd.png?raw=true)\\n\\n\uc800\ud76c\uc758 \uc9c0\uc18d\uc801 \ubc30\ud3ec \uc544\ud0a4\ud14d\ucc98\uc785\ub2c8\ub2e4.\\n\\n\uc21c\uc11c\ub97c \uc694\uc57d\ud558\uc790\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n1. Release \ube0c\ub79c\uce58\uc5d0 Push\ub97c \ud55c\ub2e4.\\n2. Github Actions\ub97c \ud1b5\ud574 Docker Hub\uc5d0 \ub808\ud3ec\uc9c0\ud1a0\ub9ac\uc758 \uc18c\uc2a4\ucf54\ub4dc\ub97c Docker Image\ub85c \ube4c\ub4dc\ud574\uc11c Push \ud55c\ub2e4.\\n3. \uc778\ud504\ub77c \uc11c\ubc84\uc5d0\uc11c Self Hosted Runner\uac00 \uc791\ub3d9\ud55c\ub2e4.\\n4. \uc778\ud504\ub77c \uc11c\ubc84\uc5d0\uc11c \ubc30\ud3ec \uc11c\ubc84\ub85c \ub4e4\uc5b4\uac04\ub2e4.\\n5. \ubc30\ud3ec \uc11c\ubc84 \uc548\uc5d0\uc11c Docker Hub\uc5d0 \ubbf8\ub9ac \uc5c5\ub85c\ub4dc\ud55c Docker Image\ub97c Pull \ud574\uc628\ub2e4.\\n6. \ubc30\ud3ec \uc11c\ubc84 \uc548\uc5d0\uc11c Docker Image\ub97c \ucee8\ud14c\uc774\ub108\uc5d0 \ub744\uc6b4\ub2e4.\\n\\n\\n
    \\n\\n### \ubc30\ud3ec \uc790\ub3d9\ud654 \ud234 \uc120\ud0dd\ud558\uae30\\n\uba3c\uc800 \ubc30\ud3ec \uc790\ub3d9\ud654 \uacfc\uc815\uc744 \uad6c\ucd95\ud558\uae30 \uc704\ud574\uc11c \uc5ec\ub7ec\uac00\uc9c0 \ud234\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\nTravis, Jenkins, Github Actions \ub4f1\ub4f1 \uc5ec\ub7ec\uac00\uc9c0\uac00 \uc788\ub294\ub370\uc694.\\n\uc800\ud76c \ud300\uc740 `Github Actions`\ub97c \uc120\ud0dd\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \uc120\ud0dd\ud55c \uc5ec\ub7ec\uac00\uc9c0 \uc774\uc720\uac00 \uc788\uc5c8\uc9c0\ub9cc\\n\uc800\ud76c \ud300 \ub204\ub204\ub97c \uc81c\uc678\ud558\uace0 CI/CD \uacbd\ud5d8\uc774 \ubd80\uc871\ud574\uc11c \ube44\uad50\uc801 \uc27d\uace0 \uc124\uce58 \ubc0f \ud070 \uc138\ud305\uc774 \uc5c6\ub294 \uc810\uc774 \uc800\ud76c\ud55c\ud14c\ub294 \ub9e4\ub825\uc801\uc73c\ub85c \ub2e4\uac00\uc654\uc2b5\ub2c8\ub2e4.\\n\\n\ub610\ud55c Docker\ub97c \uc0ac\uc6a9\ud558\ub294\ub370, \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n1. JDK \ud639\uc740 Node \ubc84\uc804\uc744 \uad00\ub9ac\ud560 \uc218 \uc788\ub2e4.\\n2. Docker Image\ub97c \ube4c\ub4dc\ud55c \ud6c4 \ubc30\ud3ec\ud558\uae30 \ub54c\ubb38\uc5d0 \uc11c\ubc84 \ud658\uacbd \ucc28\uc774\ub85c \ubc1c\uc0dd\ud558\ub294 \ubb38\uc81c\ub97c \ucd5c\uc18c\ud654\ud560 \uc218 \uc788\ub2e4.\\n3. \ubc30\ud3ec \uc11c\ubc84\uc5d0\uc11c Docker\ub9cc \uc124\uce58\ud558\uace0 Image\ub97c \ubc1b\uace0 \uc2e4\ud589\uc2dc\ud0a4\uba74 \ub3fc\uc11c \ube60\ub974\uace0 \uc27d\uac8c \ubc30\ud3ec \ud658\uacbd\uc744 \uad6c\ucd95\ud560 \uc218 \uc788\ub2e4.\\n\\n
    \\n\\n### \uacfc\uc815\\n\ubcf8\uaca9\uc801\uc73c\ub85c \uc800\ud76c\uc758 \ubc30\ud3ec \uc790\ub3d9\ud654\ub97c \uad6c\ucd95\ud558\ub294 \uacfc\uc815\uc744 \uc124\uba85\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n
    \\n\\n1. Github Actions\uc5d0 Runners \ub4f1\ub85d\\n\\n![runner](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/selfHosted.png?raw=true)\\n\uba3c\uc800 Self Hosted Runner\ub97c \uc774\uc6a9\ud558\uae30 \ub54c\ubb38\uc5d0 \uc800\ud76c\ub294 \uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 Runners\ub97c \ub4f1\ub85d\uc744 \ud574\uc92c\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \ub4f1\ub85d\uc744 \ud560 \ub54c \uc81c\uacf5\ud574\uc8fc\ub294 \uc124\uc815 \ucf54\ub4dc\uac00 \ub098\uc624\ub294\ub370\uc694.\\n\uc774 \ucf54\ub4dc\ub4e4\uc744 infra \uc11c\ubc84\uc5d0 \ubaa8\ub450 \uc785\ub825\uc744 \ud574\uc8fc\uba74\uc11c \uc124\uc815\uc744 \ud574\uc8fc\uc2dc\uba74 \ub429\ub2c8\ub2e4.\\n\\n
    \\n\\n2. Github workflow \ub9cc\ub4e4\uae30\\n\ub2e4\uc74c\uc73c\ub85c\ub294 \uc800\ud76c\uac00 \uc218\ud589\ud558\uace0\uc790 \ud558\ub294 Task\ub97c \ub4f1\ub85d\ud574\uc8fc\uae30 \uc704\ud574\uc11c yml \ud30c\uc77c\uc744 \ub9cc\ub4e4\uc5b4\uc90d\ub2c8\ub2e4.\\n\\nyml \ud30c\uc77c\uc758 \uacbd\ub85c\ub294 `./github/workflows/` \uc548\uc5d0 \ub9cc\ub4e4\uc5b4\uc8fc\uba74 \ub429\ub2c8\ub2e4.\\n```yaml\\nname: deploy\\n\\n# release/backend push \ud560 \ub54c\\non:\\n push:\\n branches:\\n - release/backend\\n\\njobs:\\n # Docker\\n docker-build:\\n runs-on: ubuntu-latest\\n defaults:\\n run:\\n working-directory: ./backend\\n steps:\\n\\t\\t# Docker Hub \ub85c\uadf8\uc778\\n - name: Log in to Docker Hub\\n uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a\\n with:\\n username: ${{ secrets.DOCKERHUB_USERNAME }}\\n password: ${{ secrets.DOCKERHUB_PASSWORD }}\\n - uses: actions/checkout@v3\\n\\n - name: Set up JDK 17\\n uses: actions/setup-java@v3\\n with:\\n java-version: \'17\'\\n distribution: \'adopt\'\\n\\n - name: Gradle Caching\\n uses: actions/cache@v3\\n with:\\n path: |\\n ~/.gradle/caches\\n ~/.gradle/wrapper\\n key: ${{ runner.os }}-gradle-${{ hashFiles(\'**/*.gradle*\', \'**/gradle-wrapper.properties\') }}\\n restore-keys: |\\n ${{ runner.os }}-gradle-\\n\\n - name: Grant execute permission for gradlew\\n run: chmod +x gradlew\\n\\n - name: Build with Gradle\\n run: ./gradlew bootjar\\n\\n - name: Extract metadata (tags, labels) for Docker\\n id: meta\\n uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7\\n with:\\n images: Docker Hub \uc0ac\uc6a9\uc790\uba85/\uc774\ubbf8\uc9c0 \uc774\ub984\\n\\n\\t # Build \ubc0f Docker image\ub97c Docker Hub\uc5d0 push\\n - name: Build and push Docker image\\n uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671\\n with:\\n context: .\\n file: ./backend/Dockerfile\\n push: true\\n platforms: linux/arm64\\n tags: woowacarffeine/backend:latest\\n labels: ${{ steps.meta.outputs.labels }}\\n\\n deploy:\\n runs-on: self-hosted\\n if: ${{ needs.docker-build.result == \'success\' }}\\n needs: [ docker-build ]\\n steps:\\n\\t\\t# EC2 \ubc30\ud3ec \uc11c\ubc84\ub85c \uc811\uc18d\\n - name: Join EC2 dev server\\n uses: appleboy/ssh-action@master\\n env:\\n JASYPT_KEY: ${{ secrets.JASYPT_KEY }}\\n with:\\n host: ${{ secrets.SERVER_HOST }}\\n username: ${{ secrets.SERVER_USERNAME }}\\n key: ${{ secrets.SERVER_KEY }}\\n port: ${{ secrets.SERVER_PORT }}\\n envs: JASYPT_KEY\\n\\n # 1. \ub3c4\ucee4 \uc774\ubbf8\uc9c0 \ubc1b\uae30\\n # 2. \uae30\uc874\uc5d0 \ucf1c\uc9c4 \ubc31\uc5d4\ub4dc \uc11c\ubc84(\ub3c4\ucee4 \uc774\ubbf8\uc9c0) stop\\n # 3. \ucd5c\uc2e0 \ubc31\uc5d4\ub4dc \uc11c\ubc84 run\\n # 4. \uc0ac\uc6a9\ud558\uc9c0 \uc54a\ub294 \uc774\ubbf8\uc9c0\uc640 \ucee8\ud14c\uc774\ub108 \uc0ad\uc81c\\n script: |\\n sudo docker pull woowacarffeine/backend:latest\\n sudo docker stop backend || true\\n sudo docker run -d --rm -p 8080:8080 \\\\\\n -e \\"ENCRYPT_KEY=${{secrets.JASYPT_KEY}}\\" \\\\\\n --name backend \\\\\\n Docker Hub \uc0ac\uc6a9\uc790\uba85/\uc774\ubbf8\uc9c0 \uc774\ub984:latest\\n\\n sudo docker image prune -f\\n```\\n\\n\uc800\ud76c \ud300\uc740 \uc704\uc640 \uac19\uc774 backend-deploy.yml \ud30c\uc77c\uc744 \ub9cc\ub4e4\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc704\uc5d0 yml\uc5d0\uc11c \uc800\ud76c\ub294 \ud0a4\ub97c \uc228\uacbc\ub294\ub370\uc694.\\n\\n![img](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/selfHostedKeys.png?raw=true)\\n\uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 \uc124\uc815\uc744 \ud574\uc8fc\uc2dc\uba74 \ub429\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc774\ub97c yml\uc5d0\uc11c \uc0ac\uc6a9\ud558\uae30 \uc704\ud574\uc120 `secrets.Key\uc774\ub984`\uc73c\ub85c \uc0ac\uc6a9\ud574\uc8fc\uc2dc\uba74 \ub429\ub2c8\ub2e4.\\n\\n
    \\n\\n\uc774\uc81c \ub9c8\uc9c0\ub9c9\uc73c\ub85c `Dockerfile`\uc744 \ub9cc\ub4e4\uc5b4\uc90d\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 `/backend/` \uacbd\ub85c\uc5d0 \ub9cc\ub4e4\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n```Dockerfile\\nFROM amazoncorretto:17-alpine-jdk\\nARG JAR_FILE=./backend/build/libs/carffeine-0.0.1-SNAPSHOT.jar\\nCOPY ${JAR_FILE} app.jar\\nENTRYPOINT [\\"java\\", \\"-Dspring.profiles.active=dev\\", \\"-jar\\",\\"/app.jar\\"]\\n```\\n\\n\uc800\ud76c\ub294 \uc704\ucc98\ub7fc \uc808\ub300 \uacbd\ub85c\ub97c \uae30\uc900\uc73c\ub85c JAR_FILE \uc704\uce58\ub97c \uc9c0\uc815\ud558\uace0, profiles\ub294 dev\ub85c \uc124\uc815\ud574\uc11c \ub9cc\ub4e4\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n
    \\n\\n3. \ubc30\ud3ec\ud558\uae30\\n\\n\ud2b8\ub9ac\uac70\ub97c \uc791\ub3d9\uc2dc\ucf1c\uc11c \uc800\ud76c\uac00 yml \ud30c\uc77c\uc5d0\uc11c \uc9c0\uc815\ud574\uc900 \uac83\ub4e4\uc774 \uc798 \uc791\ub3d9\ud558\ub294\uc9c0 \ud655\uc778\ud569\ub2c8\ub2e4.\\n\\n![jobSuccess](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/jobsSuccess.png?raw=true)\\n\uc704\uc5d0 \uc0ac\uc9c4\ucc98\ub7fc \ubaa8\ub4e0 Job\uc774 \uc131\uacf5\uc801\uc73c\ub85c \ud1b5\uacfc\ud558\ub294 \uac83\uc744 \ubcf4\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![dockerPs](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/success.png?raw=true)\\n\uc774\ub807\uac8c \uc778\ud504\ub77c \uc11c\ubc84\uc5d0\uc11c \ubc30\ud3ec \uc11c\ubc84\ub85c \ub4e4\uc5b4\uac00\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uc11c\ubc84\ub97c \ub3c4\ucee4\ub85c \ub744\uc6b4 \uac83\uc744 \ubcf4\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\nEC2 \ubc30\ud3ec \uc11c\ubc84\uc5d0\uc11c `docker ps`\ub97c \uc785\ub825\ud588\uc744 \ub54c\uc5d0\ub3c4 \uc798 \uc2e4\ud589\uc774 \ub418\ub124\uc694!\\n\\n
    \\n\\n### CD \ubc30\ud3ec \uacfc\uc815 \uc694\uc57d\\n\uc9c0\uc18d\uc801 \ubc30\ud3ec \uacfc\uc815\uc744 \uc694\uc57d \ud558\uc790\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n1. Self Hosted Runner\ub97c EC2 \uc778\ud504\ub77c \uc11c\ubc84\uc5d0 \ub4f1\ub85d\ud574\uc900\ub2e4.\\n2. yml \ud30c\uc77c\uacfc Dockerfile\uc744 \ub9cc\ub4e4\uc5b4\uc900\ub2e4.\\n3. \ud2b8\ub9ac\uac70\ub97c \uc791\ub3d9\uc2dc\ucf1c\uc11c Github Actions\uc758 \ud0dc\uc2a4\ud06c\uac00 \ubaa8\ub450 \uc798 \ub418\ub294\uc9c0 \ud655\uc778\ud55c\ub2e4.\\n4. \uc798 \ub410\ub2e4\uba74 EC2 \ubc30\ud3ec \uc11c\ubc84\uc5d0 Docker image\uac00 \uc131\uacf5\uc801\uc73c\ub85c \ub744\uc6cc\uc9c4\ub2e4."},{"id":"16","metadata":{"permalink":"/16","source":"@site/blog/2023-07-15-jpa-create-select-query-when-id-is-not-null-.mdx","title":"JPA\uc5d0\uc11c ID\uac00 \uc788\ub294 Entity\uc5d0 \ub300\ud574 save \uc2dc\uc5d0 select \ucffc\ub9ac\uac00 \ub098\uac00\ub294 \uc774\uc720","description":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130 \uc785\ub2c8\ub2e4.","date":"2023-07-15T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 15\uc77c","tags":[{"label":"jpa","permalink":"/tags/jpa"}],"readingTime":9.97,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"16","title":"JPA\uc5d0\uc11c ID\uac00 \uc788\ub294 Entity\uc5d0 \ub300\ud574 save \uc2dc\uc5d0 select \ucffc\ub9ac\uac00 \ub098\uac00\ub294 \uc774\uc720","authors":["boxster"],"tags":["jpa"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 CI/CD","permalink":"/17"},"nextItem":{"title":"\uc8fc\uae30\uc801\uc778 \ub370\uc774\ud130 \uc694\uccad\uc73c\ub85c \ubc1b\uc740 \ub370\uc774\ud130\ub97c \ud6a8\uc728\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ubc0f \uc0bd\uc785\ud558\uae30 (with. \ubc15\uc2a4\ud130)","permalink":"/15"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130 \uc785\ub2c8\ub2e4.\\n\\n\uba3c\uc800 \uc774\ubc88\uc5d0 \uae00\uc744 \uc4f0\uac8c\ub41c \uacc4\uae30\ub97c \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4. \uc800\ud76c \ud300\uc740 \uacf5\uacf5 \ub370\uc774\ud130 API\uc5d0\uc11c \ubc1b\uc544\uc628 \ucda9\uc804\uc18c\uc640, \ucda9\uc804\uae30\ub4e4\uc758 ID\ub97c \uadf8\ub300\ub85c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\ubb3c\ub860 \ub2e4\ub978 API, \uc81c\uac00 \uc81c\uc5b4\ud560 \uc218 \uc5c6\ub294 \uacf3\uc5d0 \uc758\uc874\ud558\ub294 \uac83\uc740 \uc88b\uc9c0 \uc54a\ub2e4\uace0 \uc0dd\uac01\ud569\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \ub370\uc774\ud130\ub97c \ubc1b\uc544\uc624\ub294 \uacfc\uc815\uc5d0\uc11c \ub9c8\uc8fc\ud55c \uc131\ub2a5\uc801\uc778 \ubb38\uc81c \ub54c\ubb38\uc5d0 \uadf8\ub300\ub85c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uc804\uad6d\uc758 \ucda9\uc804\uc18c\ub294 6\ub9cc\uac1c, \ucda9\uc804\uc18c \uc548\uc5d0 \uc874\uc7ac\ud558\ub294 \ucda9\uc804\uae30\ub294 23\ub9cc\uae30\uc785\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uacf5\uacf5 \ub370\uc774\ud130\ub294 \ucda9\uc804\uc18c\uc640, \ucda9\uc804\uae30\uc758 \uc815\ubcf4\ub97c \ub530\ub85c \uc81c\uacf5\ud558\ub294 \uac83\uc774 \uc544\ub2cc \uc911\ubcf5\ub41c \ucda9\uc804\uc18c\ub97c \ud3ec\ud568\ud55c \ub370\uc774\ud130\ub97c \ucda9\uc804\uae30 \uac1c\uc218\ub9cc\ud07c\uc778 23\ub9cc\uac1c\uc758 row\ub85c \uc81c\uacf5\ud569\ub2c8\ub2e4.\\n\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\uac00 ID\ub97c \ub530\ub85c \ubd80\uc5ec\ud558\uac8c \ub41c\ub2e4\uba74, \ucda9\uc804\uc18c\ub97c \uc800\uc7a5\ud558\ub294 \uacfc\uc815\uc5d0\uc11c \ubc1b\uc544\uc624\ub294 ID\ub85c \ucda9\uc804\uae30\ub97c \uc5f0\uacb0\ud574\uc918\uc57c\ud558\ub294\ub370 \uadf8\ub807\uac8c \ub41c\ub2e4\uba74 \uc140 \uc218 \uc5c6\uc774 \ub9ce\uc740 \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\\n\uc7a0\uae50 \uc0dd\uac01\ud574\ubcf8\ub2e4\uba74\\n1. \ucda9\uc804\uc18c\ub97c \uac01\uac01 \uc800\uc7a5\ud558\uace0 ID\ub97c \ubd80\uc5ec\ubc1b\ub294 \ucffc\ub9ac `6\ub9cc\ubc88` (ID\ub97c \uc54c\uc544\uc640\uc57c\ud558\uae30 \ub54c\ubb38\uc5d0 batch\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.)\\n2. \ucda9\uc804\uc18c\uc5d0\uc11c \ubc1b\uc544\uc628 ID\ub97c \ucda9\uc804\uae30\uc5d0 \ub9e4\ud551\ud558\uace0 \uc800\uc7a5\ud558\ub294 \ucffc\ub9ac `\ucd5c\uc18c 1\ubc88` (\ub9cc\uc57d batch\ub85c 23\ub9cc\uac74\uc744 \ud55c\ubc88\uc5d0 \uc800\uc7a5\ud55c\ub2e4\ub294 \uac00\uc815)\\n\\n\ud558\uc9c0\ub9cc ID\ub97c \uadf8\ub300\ub85c \uc0ac\uc6a9\ud558\uac8c \ub41c\ub2e4\uba74,\\n1. \ucda9\uc804\uc18c\ub97c \uc800\uc7a5\ud558\ub294 \ucffc\ub9ac `\ucd5c\uc18c 1\ubc88` (\ub9cc\uc57d batch\ub85c 6\ub9cc\uac74\uc744 \ud55c\ubc88\uc5d0 \uc800\uc7a5\ud55c\ub2e4\ub294 \uac00\uc815)\\n2. \ucda9\uc804\uae30\ub97c \uc800\uc7a5\ud558\ub294 \ucffc\ub9ac `\ucd5c\uc18c 1\ubc88` (\ub9cc\uc57d batch\ub85c 23\ub9cc\uac74\uc744 \ud55c\ubc88\uc5d0 \uc800\uc7a5\ud55c\ub2e4\ub294 \uac00\uc815)\\n\\n23\ub9cc\uac74\uc774 \ub118\ub294 \uc815\ubcf4\ub97c \ud655\uc778\ud588\uc744 \ub54c, ID\ub294 \uc911\ubcf5\ub418\uc9c0 \uc54a\uc558\uace0, \uc911\ubcf5\ud558\uc9c0 \uc54a\uc744 \uac83\uc774\ub77c \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4. \uadf8 \ubfd0\ub9cc \uc544\ub2c8\ub77c \ucc98\uc74c \ud55c\ubc88\ub9cc \uc800\uc7a5\ud558\ub294 \uac83\uc774 \uc544\ub2cc \uc8fc\uae30\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\ub41c \uc815\ubcf4\ub97c\\n\ubc18\uc601\ud574\uc8fc\uace0 `update` or `save` \ud574\uc8fc\uc5b4\uc57c\ud558\uae30 \ub54c\ubb38\uc5d0, ID\ub97c \uadf8\ub300\ub85c \uac00\uc9c0\uace0 \uc788\ub294 \uac83\uc774 \ud6e8\uc52c \ud6a8\uc728\uc801\uc774\ub77c \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc871\uc774 \uae38\uc5c8\uc2b5\ub2c8\ub2e4. \uac01\uc124\ud558\uace0 \uc774\ub7f0 \ubc29\uc2dd\uc73c\ub85c ID\ub97c \uc9c1\uc811 \ub123\uc5b4\uc8fc\ub294 \uacbd\uc6b0 \ubc1c\uc0dd\ud558\ub294 \ubb38\uc81c\uc5d0 \ub300\ud574 \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## ID\ub97c \uc9c1\uc811 \ub123\uc5b4\uc900 Entity\ub97c \uc800\uc7a5\ud560 \ub54c\\n\\n\uba3c\uc800 \uac04\ub2e8\ud55c \uc608\uc81c Entity\ub85c \uc124\uba85\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Entity\\npublic class ChargeStation {\\n\\n @Id\\n private String stationId;\\n\\n private String stationName;\\n\\n ...\\n}\\n\\n```\\n\ubcf4\ud1b5\uc758 Entity\uc640 \ub2e4\ub978 \ubd80\ubd84\uc740 Id\ub97c \uc9c1\uc811 \ud560\ub2f9\ud558\uae30 \ub54c\ubb38\uc5d0 `@GeneratedValue(strategy = GenerationType.IDENTITY)` \uc774\ub7ec\ud55c ID \uc0dd\uc131 \uc804\ub7b5\uc5d0 \ub300\ud55c \uc815\ubcf4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 `save()` \ucf54\ub4dc\ub97c \ud638\ucd9c\ud558\uba74 \uc5b4\ub5a4 \ucffc\ub9ac\uac00 \ub098\uac00\ub294\uc9c0 \ud655\uc778\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc640 \uac19\uc774 \uc544\uc8fc \uac04\ub2e8\ud55c \uc120\ub989\uc5ed \ucda9\uc804\uc18c\ub97c \uc800\uc7a5\ud558\ub294 \ud14c\uc2a4\ud2b8\ub97c \uc2e4\ud589\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n```java\\n@DataJpaTest\\nclass ChargeStationRepositoryTest {\\n\\n @Autowired\\n private ChargeStationRepository chargeStationRepository;\\n\\n @Test\\n void \ucda9\uc804\uc18c\ub97c_\uc800\uc7a5\ud55c\ub2e4() {\\n ChargeStation station = ChargeStationFixture.\uc120\ub989\uc5ed_\ucda9\uc804\uc18c_\ucda9\uc804\uae30_2\uac1c_\uc0ac\uc6a9\uac00\ub2a5_1\uac1c;\\n\\n chargeStationRepository.save(station);\\n\\n ChargeStation expect = chargeStationRepository.findByStationId(station.getStationId()).get();\\n assertThat(expect).isEqualTo(station);\\n }\\n}\\n```\\n\\n\uba3c\uc800 \ucf54\ub4dc\ub9cc \ubcf4\uba74 \uba3c\uc800 `chargeStationRepository.save()` \ud638\ucd9c\uacfc \ud568\uaed8 insert \ucffc\ub9ac 1\ubc88, \uadf8\ub9ac\uace0 `chargeStationRepository.findByStationId()`\uc5d0\uc11c select \ucffc\ub9ac 1\ubc88\\n\ucd1d 2\ubc88 \ubc1c\uc0dd\ud560 \uac83\uc774\ub77c\uace0 \uc720\ucd94\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![query-three-times](https://github.com/car-ffeine/design-system/assets/106640954/f48b7f0f-3f39-41ce-8fcd-94b995e95fae)\\n\\n\ud558\uc9c0\ub9cc \uc608\uc0c1\uacfc \ub2e4\ub974\uac8c \uc704\uc758 \uc0ac\uc9c4\uacfc \uac19\uc774 \ucffc\ub9ac\uac00 \ucd1d 3\ubc88 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uccab\ubc88\uc9f8\ub294 \ud638\ucd9c\ud558\uc9c0 \uc54a\uc740 station id\ub85c station\uc744 \uc870\ud68c\ud558\ub294 \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc720\ub97c \ucc3e\uae30 \uc704\ud574 `save()` \uba54\uc11c\ub4dc\ub97c \ub514\ubc84\uae45 \ud574\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n### save \uc2dc SELECT \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud558\ub294 \uc774\uc720\\n\\n\\n![save-method](https://github.com/car-ffeine/design-system/assets/106640954/b1db00b7-d7fb-4647-912c-6f8e2fe44974)\\n\ub85c\uc9c1\uc740 \uac04\ub2e8\ud574\ubcf4\uc785\ub2c8\ub2e4. `isNew()` \ub97c \ud1b5\ud574 \uc0c8\ub85c\uc6b4 Entity\uc778\uc9c0 \ud655\uc778\ud55c \ud6c4, \uc0c8\ub85c\uc6b4 Entity\ub77c\uba74 `persist()`, \uc544\ub2c8\ub77c\uba74 `merge()`\ub97c \ud638\ucd9c\ud569\ub2c8\ub2e4.\\n\\n\uc5ec\uae30\uc11c `EntityManager#persist()` \uba54\uc11c\ub4dc\ub97c \uac04\ub2e8\ud788 \ub9d0\uc500\ub4dc\ub9ac\uba74, \uc0c8\ub85c\uc6b4 Entity\ub97c \uc601\uc18d\ud654\ud558\ub294 \uba54\uc11c\ub4dc\ub85c \ud2b8\ub79c\uc7ad\uc158\uc774 \ucee4\ubc0b\ub420 \ub54c \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \uc800\uc7a5\ud569\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 `EntityManager#merge()` \uba54\uc11c\ub4dc\ub294 \uc900\uc601\uc18d \uc0c1\ud0dc\uc758 Entity\ub97c \uc601\uc18d \uc0c1\ud0dc\ub85c \ubcc0\uacbd\ud558\ub294\ub370 \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uc774\ub54c \uc601\uc18d\uc131 \ucee8\ud14d\uc2a4\ud2b8\uc5d0 \uc874\uc7ac\ud558\uc9c0 \uc54a\ub294 \uac1d\uccb4\ub77c\uba74 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \uc870\ud68c \ud6c4 \uc601\uc18d\ud654\ud558\ub294 \uc791\uc5c5\uc744 \uc218\ud589\ud569\ub2c8\ub2e4.\\n\\n`merge()`\ub97c \ud638\ucd9c\ud558\uae30 \ub54c\ubb38\uc5d0 SELECT \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud558\uace0, \uc601\uc18d\ud654\ud558\ub294 \uc791\uc5c5\uc744 \uc218\ud589\ud558\ub294 \uac83 \uc785\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc81c\uac00 \uc800\uc7a5\ud55c \uac1d\uccb4\ub294 \ud655\uc2e4\ud788 \uc0c8\ub85c\uc6b4 Entity\uac00 \ub9de\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc `entityInformation.isNew()` \uba54\uc11c\ub4dc\ub294 false\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc5b4\ub5a4 \uac83\uc744 \uae30\uc900\uc73c\ub85c \uc0c8\ub85c\uc6b4 Entity\uc778 \uac83\uc744 \uad6c\ubd84\ud558\ub294\uc9c0 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n### \uc0c8\ub85c\uc6b4 Entity\ub97c \uad6c\ubd84\ud558\ub294 \uae30\uc900\\n\\n\uc77c\ub2e8, \ub514\ubc84\uae45\uc744 \ud1b5\ud574 isNew \uba54\uc11c\ub4dc\ub97c \ud655\uc778\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n![is-new](https://github.com/car-ffeine/design-system/assets/106640954/e4a56694-c623-46d8-badd-3345d557e29f)\\n\\n\uac04\ub2e8\ud569\ub2c8\ub2e4. \uba3c\uc800 Entity\uc5d0 ID\ub97c \uac00\uc838\uc635\ub2c8\ub2e4. \uadf8\ub9ac\uace0 id\uac00 `primitive` \ud0c0\uc785\uc778\uc9c0 \ud655\uc778 \ud6c4, \uc544\ub2d0\uacbd\uc6b0 id\uac00 null \uc774\uba74 \uc0c8\ub85c\uc6b4 Entity, \uc544\ub2d0\uacbd\uc6b0 false\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub54c, `primitve` \ud0c0\uc785\uc774\ub77c\uba74, id\uac00 \uc22b\uc790\uc778\uc9c0 \ud655\uc778 \ud6c4 id\uac00 0\uc774\uba74 \uc0c8\ub85c\uc6b4 Entity, \uc544\ub2d0\uacbd\uc6b0 false\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4.\\n\\n## ID\ub97c \uc9c1\uc811 \ub123\uc5b4\uc8fc\ub294 \uac1d\uccb4\ub294 JPA \uc0ac\uc6a9\uc744 \ud3ec\uae30\ud574\uc57c\ud560\uae4c?\\n\\n\uacb0\ub860\ubd80\ud130 \ub9d0\uc500\ub4dc\ub9ac\uba74 \uc544\ub2d9\ub2c8\ub2e4. \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c \uc0c8\ub85c\uc6b4 Entity \uc784\uc744 \uc99d\uba85\ud560 \uc218 \uc788\ub2e4\uba74 `merge()`\uac00 \uc544\ub2cc `persist()`\ub97c \ud638\ucd9c\ud558\ub3c4\ub85d \ub9cc\ub4e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uadf8\ub7fc \uc5b4\ub5bb\uac8c?\\n\uba3c\uc800 save() \uba54\uc11c\ub4dc\uc758 \ud544\ub4dc \uc911 `JpaEntityInformation`\uc774\ub77c\ub294 \ud544\ub4dc\ub97c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![entity-info](https://github.com/car-ffeine/design-system/assets/106640954/d9956fe6-07c7-41a9-9d6b-7c01b5f31c5d)\\n\\n\uc774 \uc778\ud130\ud398\uc774\uc2a4\ub294 Entity\uc758 \ucd94\uac00 \uc815\ubcf4\ub97c \uc54c\uae30 \uc704\ud574 \ud544\ub4dc\uc5d0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud574\ub2f9 \uc778\ud130\ud398\uc774\uc2a4\uc758 \uad6c\ud604\uccb4\ub294 `JpaEntityInformationSupport`, `JpaMetamodelEntityInformation`, `JpaPersistableEntityInformation` \uc774\ub807\uac8c 3\uac1c\uc758 \ud074\ub798\uc2a4\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c\ub3c4 `isNew()`\uac00 \uad6c\ud604\ub418\uc5b4 \uc788\uc744\uac70\ub77c \ucd94\uce21\uc744 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub514\ubc84\uae45\uc744 \ud1b5\ud574 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\uae4c \uc704\uc758 \uc0ac\uc9c4\uc73c\ub85c \ubcf4\uace0 \uc2e4\uc81c\ub85c \uc2e4\ud589\ub410\ub358 `isNew()` \uba54\uc11c\ub4dc\uc758 \uc8fc\uc778\uc740 `JpaMetamodelEntityInformation` \ud074\ub798\uc2a4\uc600\uc2b5\ub2c8\ub2e4. \uadf8\ub798\uc11c \ud574\ub2f9 \ud074\ub798\uc2a4\ub294 \uc81c\uc678\ud558\uace0 \ub2e4\ub978 \ud074\ub798\uc2a4\ub97c \ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 `JpaPersistableEntityInformation` \ud074\ub798\uc2a4\uc785\ub2c8\ub2e4.\\n![is-new-persistable](https://github.com/car-ffeine/design-system/assets/106640954/dc2293c3-2854-4619-9ef6-d08e55b4581b)\\n\uc544\uc8fc \uac04\ub2e8\ud558\uac8c entity\uc758 `isNew()`\ub97c \ud638\ucd9c\ud55c\ub2e4\uace0 \uc801\ud600\uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc `Persistable` \uc778\ud130\ud398\uc774\uc2a4\ub97c \uad6c\ud604\ud55c Entity\uc758 `isNew()` \ub97c \ud638\ucd9c\ud558\ub294 \uac83 \uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \ub0a8\uc740 \ud558\ub098\uc758 \ud074\ub798\uc2a4\ub97c \ud655\uc778\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![info-support](https://github.com/car-ffeine/design-system/assets/106640954/f1d654c0-e741-4db7-8e7f-4e758c36133a)\\n\\n\uc704 \uc0ac\uc9c4\ucc98\ub7fc \uc774 \ud074\ub798\uc2a4\uac00 Entity \ub9c8\ub2e4 `Persistable` \uad6c\ud604 \uc720\ubb34\uc5d0 \ub530\ub77c \ub3d9\uc801\uc73c\ub85c \uad6c\ud604\uccb4\ub97c \ubcc0\uacbd\ud574\uc8fc\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \ub2f5\uc774 \ub098\uc628 \uac83 \uac19\uc2b5\ub2c8\ub2e4. ID\ub97c \uc9c1\uc811 \ud560\ub2f9\ud558\ub294 Entity\uc5d0 `Persistable`\uc744 \uad6c\ud604\ud574\uc8fc\uba74 \ub429\ub2c8\ub2e4.\\n\\n### Persistable \uad6c\ud604\ud558\uae30\\n```java\\n@Entity\\npublic class ChargeStation implements Pesistable{\\n\\n @Id\\n private String stationId;\\n\\n private String stationName;\\n\\n @CreatedDate\\n private LocalDateTime createdTime;\\n\\n ...\\n\\n @Override\\n public Object getId() {\\n return getStationId();\\n }\\n\\n @Override\\n public boolean isNew() {\\n return createdTime == null;\\n }\\n}\\n```\\n\\n\uac04\ub2e8\ud788 \ub9cc\ub4e4\uc5b4\ubd24\uc2b5\ub2c8\ub2e4. `@CreatedDate`\ub294 Entity\uac00 \ucc98\uc74c \uc601\uc18d\ud654\ub420 \ub54c \ub3d9\uc791\ud558\uae30 \ub54c\ubb38\uc5d0 \uc774 Entity\uc758 CreateTime \ud544\ub4dc\uac00 null \uc774\uba74 \uc0c8\ub85c\uc6b4 Entity\ub77c\uace0 \ud655\uc2e0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub7fc \uc774\ub807\uac8c \uc778\ud130\ud398\uc774\uc2a4\ub97c \uad6c\ud604\ud558\uace0 \uc544\uae4c \uc2e4\ud589\ud588\ub358 \ud14c\uc2a4\ud2b8\ub97c \ub2e4\uc2dc \uc2e4\ud589\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![solved](https://github.com/car-ffeine/design-system/assets/106640954/ea5db719-9919-42f4-b431-00e14d6fea5e)\\n\\n\uae54\ub054\ud558\uac8c \uad6c\ud604\ub41c \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4. \uc6d0\ud558\ub358\ub300\ub85c \ucffc\ub9ac\uac00 2\ubc88 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\uc774\ub7f0 `Persistable`\uc744 `@MappedSuperClass`\ub97c \ud1b5\ud574 \ub354 \uae54\ub054\ud558\uac8c \uad6c\ud604\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \ub530\ub85c \uc124\uba85\ub4dc\ub9ac\uc9c0\ub294 \uc54a\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n#### \uacb0\ub860\\nJPA\ub294 \ub9ce\uc740 \ud3b8\uc758 \uae30\ub2a5\uc744 \uc81c\uacf5\ud574\uc8fc\ub294 \uac83 \uac19\uc544\ubcf4\uc785\ub2c8\ub2e4. \ucac4\uc9c0\ub9d9\uc2dc\ub2e4."},{"id":"15","metadata":{"permalink":"/15","source":"@site/blog/2023-07-14-data-update-process-with-boxter.mdx","title":"\uc8fc\uae30\uc801\uc778 \ub370\uc774\ud130 \uc694\uccad\uc73c\ub85c \ubc1b\uc740 \ub370\uc774\ud130\ub97c \ud6a8\uc728\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ubc0f \uc0bd\uc785\ud558\uae30 (with. \ubc15\uc2a4\ud130)","description":"\uc548\ub155\ud558\uc138\uc694~","date":"2023-07-14T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 14\uc77c","tags":[{"label":"\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4","permalink":"/tags/\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4"},{"label":"\uc11c\ubc84","permalink":"/tags/\uc11c\ubc84"}],"readingTime":9.215,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"},{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"15","title":"\uc8fc\uae30\uc801\uc778 \ub370\uc774\ud130 \uc694\uccad\uc73c\ub85c \ubc1b\uc740 \ub370\uc774\ud130\ub97c \ud6a8\uc728\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ubc0f \uc0bd\uc785\ud558\uae30 (with. \ubc15\uc2a4\ud130)","authors":["jay","boxster"],"tags":["\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4","\uc11c\ubc84"]},"prevItem":{"title":"JPA\uc5d0\uc11c ID\uac00 \uc788\ub294 Entity\uc5d0 \ub300\ud574 save \uc2dc\uc5d0 select \ucffc\ub9ac\uac00 \ub098\uac00\ub294 \uc774\uc720","permalink":"/16"},"nextItem":{"title":"\uce74\ud398\uc778\ud300 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98\ub97c \uc124\uba85\ud574\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4","permalink":"/14"}},"content":"\uc548\ub155\ud558\uc138\uc694~\\n\uc6b0\ud14c\ucf54 \uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\uc624\ub298\uc740 \uce74\ud398\uc778 \ud300\uc758 \ud504\ub85c\uc81d\ud2b8\ub97c \uc9c4\ud589\ud558\uba74\uc11c \'\ubc15\uc2a4\ud130\'\uc640 \ud568\uaed8 \uc5b4\ub5a4 \ubb38\uc81c\ub97c \uacaa\uace0 \ud574\uacb0\ud588\ub294\uc9c0 \uc801\uc5b4\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n* \ubc30\uc6b0\ub294 \ub2e8\uacc4\uc774\ub2e4 \ubcf4\ub2c8 \ud2c0\ub9b0 \ubd80\ubd84\uc774 \uc788\uc744 \uc218 \uc788\ub294\ub370, \ud53c\ub4dc\ubc31 \ubd80\ud0c1\ub4dc\ub9bd\ub2c8\ub2e4 :)\\n\\n\uba3c\uc800 \uae00\uc744 \uc4f0\uae30 \uc804\uc5d0 \ubb38\uc81c \uc0c1\ud669\uc5d0 \ub300\ud574 \uac04\ub2e8\ud558\uac8c \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\\n## \ubb38\uc81c \uc0c1\ud669\\n\\n\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc804\uae30\ucc28 \ucda9\uc804\uc18c \uacf5\uacf5 API\ub97c \ud65c\uc6a9\ud558\uc5ec \ucda9\uc804\uc18c\uc758 \ud63c\uc7a1\ub3c4 \uc81c\uacf5 \ubc0f \uc5ec\ub7ec \uc11c\ube44\uc2a4\ub97c \uc81c\uacf5\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uc11c\ube44\uc2a4\ub97c \uc0ac\uc6a9\uc790\ub4e4\uc5d0\uac8c \uc81c\uacf5\ud558\uae30 \uc704\ud574\uc11c \ub2e4\uc74c\uacfc \uac19\uc740 \uc791\uc5c5\ub4e4\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n1. \uccab \uc2e4\ud589\uc2dc \uacf5\uacf5 API \ub370\uc774\ud130\ub97c \ubaa8\ub450 \ubd88\ub7ec\uc11c \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \uc0bd\uc785\ud569\ub2c8\ub2e4.\\n2. \ud63c\uc7a1\ub3c4\ub97c \uc81c\uacf5\ud558\uae30 \uc704\ud574\uc11c \uc8fc\uae30\uc801\uc778 \uc2dc\uac04 (\uc544\uc9c1 \uc815\ud558\uc9c4 \uc54a\uc558\uc9c0\ub9cc ex.12\uc2dc\uac04) \ub2e8\uc704\ub85c \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\uc758 \uc0c1\ud0dc\ub97c \uc5c5\ub370\uc774\ud2b8 \ud558\uae30 \uc704\ud574\uc11c \ub2e4\uc2dc \ub370\uc774\ud130\ub97c \uc694\uccad\uc744 \ud569\ub2c8\ub2e4.\\n3. \uc0c8\ub86d\uac8c \ucd94\uac00\ub41c \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub294 \ubaa8\ub450 Insert\ud574\uc8fc\uace0, \uae30\uc874\uc5d0 \uc788\ub358 \ucda9\uc804\uc18c \ud639\uc740 \ucda9\uc804\uae30\uac00 \uc5c5\ub370\uc774\ud2b8 \ub410\ub2e4\uba74 \ubcc0\uacbd\ub41c \ub370\uc774\ud130\ub85c \uc5c5\ub370\uc774\ud2b8 \ud574\uc90d\ub2c8\ub2e4.\\n\\n\\n\uc800\ub791 \ubc15\uc2a4\ud130\ub294 2~3\ubc88 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub294 \uc5ed\ud560\uc744 \ub9e1\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\ud14c\uc774\ube14\uc758 \uad00\uacc4\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n```\\ncharge_station <---1------N---\x3e charger\\n charger <---1------1---\x3e charger_status\\n```\\n\\n\uc800\ud76c\ub294 \uc774 \ubb38\uc81c\ub97c \uc5b4\ub5bb\uac8c \ud574\uacb0 \ud588\ub294\uc9c0 \ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\\n## \ubb38\uc81c \ud574\uacb0 \uacfc\uc815\\n\\n\uc804\uc81c\uc870\uac74\\n\\n- \uccab \uc2e4\ud589 \ubaa8\ub4e0 \ud14c\uc774\ube14\uc740 \ucd08\uae30\ud654 \uc0c1\ud0dc\uc774\ub2e4.\\n- \ub370\uc774\ud130\ub294 9999\uac74\uc744 \uae30\uc900\uc73c\ub85c \ud55c\ub2e4.\\n- \uba54\uc11c\ub4dc \uccab \uc2dc\ud589\uc5d0\uc11c\ub294 \ubaa8\ub4e0 \ub370\uc774\ud130\uac00 \uc0c8\ub86d\uac8c insert \ub418\uace0\\n- \uadf8 \ub2e4\uc74c \uba54\uc11c\ub4dc \uc2dc\ud589\uc5d0\uc11c\ub294 \uc77c\ubd80 \ub370\uc774\ud130\ub294 \ucd94\uac00\ub418\uace0, \uc77c\ubd80\ub294 \uc5c5\ub370\uc774\ud2b8 \ub41c\ub2e4.\\n\\n\\n## Ver1. findAll() \uc870\ud68c \ud6c4 \uac01\uac01 save() \ud574\uc8fc\uae30 (\uc57d14\ucd08)\\n\\n\uc800\ud76c\uac00 \ucc98\uc74c\uc5d0 \uc0dd\uac01\ud55c \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\uc54c\uc544\uc11c \ubc14\ub010 \uac83\ub4e4\uc740 \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uace0, \uc0c8\ub85c\uc6b4 \uac74 \uc800\uc7a5\ud574\uc8fc\uae30 \ub54c\ubb38\uc5d0 \uac04\ub2e8\ud55c \ubc29\ubc95\uc73c\ub85c \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc2e4\uc81c\ub85c \ud574\ubcf8 \uacb0\uacfc, \uc0bd\uc785\uc758 \uacbd\uc6b0\ub294 SELECT \ucffc\ub9ac\ubb38 \uc2e4\ud589 \ud6c4 INSERT \ucffc\ub9ac\ubb38\uc744 \ubc1c\uc0dd \uc2dc\ucf30\uace0,\\n\uc5c5\ub370\uc774\ud2b8 \uc2dc\uc5d0\ub3c4 SELECT \ud6c4 UPDATE \ud639\uc740 INSERT\ub97c \ubc1c\uc0dd \uc2dc\ucf30\uc2b5\ub2c8\ub2e4. (\ubcc0\uacbd \uc0ac\ud56d \uc5c6\uc73c\uba74 SELECT\ub9cc)\\n\\n\uc774\ub294 \uc2dd\ubcc4\uc790\uc5d0 \ub530\ub978 JPA \uc791\ub3d9 \ubc29\uc2dd \ub54c\ubb38\uc778\ub370\uc694.\\n\uc774 \ubc29\ubc95\uc758 \uacb0\uacfc\ub294 \uc57d 14\ucd08\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 \uc774\ub807\uac8c \ubd88\ud544\uc694\ud55c SELECT \uc791\uc5c5\uc744 \ub9c9\uc544\ubcf4\uace0\uc790 \ub2e4\ub978 \ubc29\ubc95\uc744 \uad6c\uc0c1\ud574\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n\uae30\ubcf8\uc801\uc73c\ub85c Jdbc\ub97c \uc774\uc6a9\ud574\uc11c Batch Insert\uc640 Batch Update\ub97c \uc0ac\uc6a9\ud558\uae30\ub85c \ud588\uace0, \uc774 \uc791\uc5c5\uc744 \uc704\ud574\uc11c \ubcc0\uacbd \ud639\uc740 \uc0bd\uc785\ub420 \ub370\uc774\ud130\ub4e4\uc744 \uc9c1\uc811 \ucc3e\ub294 \uacfc\uc815\uc774 \uc911\uc694\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\\n## Ver2. \ubcc0\uacbd \uac10\uc9c0\ub97c \uc9c1\uc811 \ud574\uc8fc\uace0, \uc790\ub8cc\uad6c\uc870\ub85c \ubc30\uce58 \ub370\uc774\ud130 \ubaa8\uc73c\uae30 : O(n^2) (\uc57d 11\ucd08)\\n\\n\ub450 \ubc88\uc9f8\ub85c \uc800\ud76c\uac00 \uc0dd\uac01\ud55c \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\uba3c\uc800 \ub370\uc774\ud130 \ucd94\uac00 \ubc0f \ubcc0\uacbd \uac10\uc9c0 \ubd80\ubd84\uc785\ub2c8\ub2e4.\\n\\n\uae30\uc874 \uc5c5\ub370\uc774\ud2b8 \uc2dc\uc5d0 SELECT\uc640 UPDATE(or INSERT) \ub450\ubc88\uc758 \ucffc\ub9ac\uac00 \ub098\uac00\ub294 \uac83\uc774 \ub9d8\uc5d0 \ub4e4\uc9c0 \uc54a\uc544\uc11c\\n\ubcc0\uacbd \uac10\uc9c0\ub97c \uc9c1\uc811 \ud574\uc8fc\ub824\uace0 \uba54\uc11c\ub4dc\ub97c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\uac00 \uc0dd\uac01\ud55c \ubcc0\uacbd \uac10\uc9c0\ub294 \uc0dd\uac01\ubcf4\ub2e4 \uac04\ub2e8\ud55c\ub370\uc694.\\n\ub3c4\uba54\uc778\uc5d0 \uba54\uc11c\ub4dc\ub97c \ub9cc\ub4e4\uc5b4\uc11c \ud544\ub4dc\ub97c if\ubb38\uc73c\ub85c \ud558\ub098\uc529 \ube44\uad50\ud574\uc92c\uc2b5\ub2c8\ub2e4.\\n\\n\ucda9\uc804\uc18c\uc758 \ub370\uc774\ud130 \ud2b9\uc9d5\uc0c1 \ub370\uc774\ud130\uac00 \uc790\uc8fc \ubc14\ub00c\ub294 \ub370\uc774\ud130\ub294 \ube44\uad50\uc801 \ucd08\ubc18\uc5d0 \ube44\uad50\ud558\ub3c4\ub85d \uad6c\ud604\ud558\uace0, \uc790\uc8fc \ubc14\ub00c\uc9c0 \uc54a\ub294 \ub370\uc774\ud130\ub294 \ud6c4\uc5d0 \ube44\uad50\ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ub370\uc774\ud130 \uc800\uc7a5 \ubc0f \uc5c5\ub370\uc774\ud2b8 \ubd80\ubd84\uc785\ub2c8\ub2e4.\\n\uba3c\uc800 findAll()\ub85c \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30 \ub4f1 \uad00\ub828\ub41c \ubaa8\ub4e0 \ub370\uc774\ud130\ub97c Map\uc5d0 \ub123\uc5c8\uc2b5\ub2c8\ub2e4.\\nMap\uc758 \uad6c\uc870\ub85c \uae30\uc874\uc5d0 \ud14c\uc774\ube14\uc5d0 \uc800\uc7a5\ub41c \ubaa8\ub4e0 \ub370\uc774\ud130\ub97c \uc790\ub8cc\uad6c\uc870\uc5d0 \ub123\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uacf5\uacf5 API\ub97c \ubd88\ub7ec\uc640\uc11c, \ub611\uac19\uc774 Map\uc758 \uad6c\uc870\ub85c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n(Station \uc548\uc5d0\ub294 `List`\uac00 \uc874\uc7ac)\\n\\n\uc800\ud76c\ub294 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uc18c\uc5d0 \ud574\ub2f9\ud558\ub294 \ubaa8\ub4e0 \ucda9\uc804\uae30\ub4e4\uc744 \ube44\uad50\ud558\uba74\uc11c \ubcc0\uacbd \uac10\uc9c0\ub97c \ud574\uc918\uc57c\ud558\uae30 \ub54c\ubb38\uc5d0\\n\uac01\uac01\uc758 Map.values()\uc778 `List : \uae30\uc874 \ucda9\uc804\uc18c`\uc640 `List : \uc5c5\ub370\uc774\ud2b8\ub41c \ucda9\uc804\uc18c`\ub97c \ube44\uad50\ud574\uc92c\uc2b5\ub2c8\ub2e4.\\n\\n\ube44\uad50\ub97c \ud558\uba74\uc11c \uc0c8\ub85c \uc0bd\uc785\ub41c \ucda9\uc804\uc18c\uc640 \uc5c5\ub370\uc774\ud2b8 \ub41c \ucda9\uc804\uc18c\ub97c \uac01\uac01 \ucc98\ub9ac\ud574\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ucda9\uc804\uc18c\uc758 \ubcc0\uacbd \uac10\uc9c0\ub97c \uc704\ud574\uc11c \ucda9\uc804\uae30\ub4e4\uc740 \ucda9\uc804\uc18c \uc548\uc5d0 List\ub85c \uc18d\ud574\uc788\uae30 \ub54c\ubb38\uc5d0 O(n^2)\uc758 \uc2dc\uac04 \ubcf5\uc7a1\ub3c4\ub97c \uac00\uc9c0\uace0 \uc804\uccb4 \ub370\uc774\ud130\ub4e4\uc740 \uc57d 23\ub9cc \uac74\uc774\ubbc0\ub85c, \uc804\uccb4 \ub370\uc774\ud130\ub97c \ub300\uc0c1\uc73c\ub85c \ud55c\ub2e4\uba74 \uc57d 530\uc5b5\ubc88\uc758 \uc5f0\uc0b0\uc774 \uc774\ub904\uc84c\uaca0\ub124\uc694.\\n\\nVer1\uc5d0 \ube44\ud574\uc11c\ub294 \ud06c\uc9c0\ub294 \uc54a\uc9c0\ub9cc \ud3c9\uade0\uc801\uc73c\ub85c \uc57d 2\ucd08 \uc815\ub3c4 \uc904\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\n\\n## Ver3. \ubcc0\uacbd \uac10\uc9c0\ub97c \uc9c1\uc811 \ud574\uc8fc\uace0, \uc790\ub8cc\uad6c\uc870\ub85c \ubc30\uce58 \ub370\uc774\ud130 \ubaa8\uc73c\uae30 : O(1) (\uc57d 10\ucd08)\\nVer2\uc640 \uac70\uc758 \uc720\uc0ac\ud55c \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\\n\ucc28\uc774\uc810\uc740 Map \uc790\ub8cc \uad6c\uc870 \uc0ac\uc6a9\ubc29\ubc95\uc744 \ubcc0\uacbd\ud588\uc2b5\ub2c8\ub2e4.\\n\uae30\uc874 2\uc911 for\ubb38\uc5d0\uc11c, 1\uc911 for\ubb38\uc744 \ub3cc\uba74\uc11c \ud0a4 \uac12\uc744 \ud1b5\ud574\uc11c \uc2e0\uaddc \ub370\uc774\ud130\uc640 \uc5c5\ub370\uc774\ud2b8 \ub420 \ub370\uc774\ud130\ub4e4\uc744 \ubd84\ub958\ud558\uace0, \uc774\ub4e4\uc744 \uac01\uac01 List\uc5d0 \ub123\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uc774\ub97c \ud1b5\ud574\uc11c Ver2\uc5d0 \ube44\ud574\uc11c 1\ucd08\uc815\ub3c4 \uc904\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\n\\n## Ver4. \uc774\uc804 \ubc29\uc2dd + Fetch Join \uc0ac\uc6a9\ud558\uae30 (\uc57d 6\ucd08)\\n\ub9c8\uc9c0\ub9c9 \ubc29\ubc95\uc740 \uc870\ud68c \uacfc\uc815\uc758 \uc2dc\uac04 \ub2e8\ucd95\uc785\ub2c8\ub2e4.\\n\\n\ucc98\uc74c\uc5d0 Stations\ub97c findAll()\ud558\ub294 \ucffc\ub9ac\ub97c \ud655\uc778\ud574\ubcf4\ub2c8 N+1 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uadf8 \uc774\uc720\ub294 Station\uc5d0\uc11c Chargers\ub97c \uc9c0\uc5f0\ub85c\ub529\uc73c\ub85c \uc124\uc815 \ud588\ub294\ub370, \uc774\ub97c \uadf8\ub300\ub85c get \uba54\uc11c\ub4dc\ub97c \ud1b5\ud574 \uc870\ud68c\ud574\uc11c \ud574\ub2f9 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\\n\\n```java\\nList findAll(); // \uae30\uc874\\n\\n@Query(\\"SELECT DISTINCT c FROM ChargeStation c JOIN FETCH c.chargers\\"); // Fetch Join \uc801\uc6a9\\nList findAll();\\n```\\n\\n\ub530\ub77c\uc11c \uc704\uc5d0 \ucf54\ub4dc\uc640 \uac19\uc774 Fetch Join\uc744 \uc774\uc6a9\ud574\uc11c \ucc98\uc74c\uc5d0 \ub370\uc774\ud130\ub97c \uac00\uc838\uc654\uc2b5\ub2c8\ub2e4.\\n\uc774\ub807\uac8c \ud6a8\uc728\uc801\uc778 \uc870\ud68c\ub85c \ubcc0\uacbd\ud558\uba74\uc11c \uc2dc\uac04\uc744 \ub9ce\uc774 \uc904\uc77c \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\n### \uc9c0\uae08\uae4c\uc9c0\uc758 \ubc29\ubc95\uc744 \uc815\ub9ac\ub97c \ud558\uc790\uba74\\nVer1 \uacfc \uac19\uc740 \ubc29\uc2dd\uc5d0\uc11c\ub294 \uc5c5\ub370\uc774\ud2b8 \uacfc\uc815\uc5d0\uc11c JPA\uc758 \uc2dd\ubcc4\uc790\uc5d0 \ub530\ub978 \ucc98\ub9ac \ubc29\uc2dd\uc73c\ub85c \uc778\ud574 [SELECT + UPDATE] or [SELECT + INSERT] \uc640 \uac19\uc774 \ucffc\ub9ac\uac00 \ub450 \ubc88\uc529 \ub098\uac14\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c Ver3\uae4c\uc9c0 \uac1c\uc120\uc744 \ud558\uae30 \uc704\ud574\uc11c \uc800\uc7a5\uacfc \uc5c5\ub370\uc774\ud2b8\ub97c \ud55c \ubc88\uc5d0 JDBC\ub97c \uc774\uc6a9\ud574\uc11c Batch\ub85c \ucc98\ub9ac\ud574\uc8fc\ub294 \ubc29\uc2dd\uc744 \uc120\ud0dd\ud588\uace0,\\n\\n\ubcc0\uacbd \uac10\uc9c0 + \ubc30\uce58 \ub370\uc774\ud130\ub97c \ubaa8\uc73c\uae30 \uc704\ud574\uc11c \uc790\ub8cc\uad6c\uc870\ub97c \uc774\uc6a9\ud574\uc11c \uc2dc\uac04\uc744 \uc870\uae08\uc529 \ub2e8\ucd95 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ub9c8\uc9c0\ub9c9\uc73c\ub85c Ver4\uc5d0\uc11c\ub294 findAll()\uc5d0\uc11c \ubc1c\uc0dd\ud558\ub294 N+1\uc758 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uba74\uc11c \uc2dc\uac04\uc744 \ub2e8\ucd95\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c \ub3d9\uc77c \uc791\uc5c5\uc744 14\ucd08\uc5d0\uc11c 6\ucd08 \uc815\ub3c4\ub85c \uc904\uc77c \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4!"},{"id":"14","metadata":{"permalink":"/14","source":"@site/blog/2023-07-14-server-architecture.mdx","title":"\uce74\ud398\uc778\ud300 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98\ub97c \uc124\uba85\ud574\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4","description":"\uc548\ub155\ud558\uc138\uc694 \uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4 \uce74\ud398\uc778\ud300 \ub204\ub204\uc785\ub2c8\ub2e4","date":"2023-07-14T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 14\uc77c","tags":[{"label":"ec2","permalink":"/tags/ec-2"},{"label":"aws","permalink":"/tags/aws"},{"label":"\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4","permalink":"/tags/\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4"},{"label":"\ubc30\ud3ec","permalink":"/tags/\ubc30\ud3ec"},{"label":"\uc11c\ubc84","permalink":"/tags/\uc11c\ubc84"},{"label":"\uc544\ud0a4\ud14d\ucc98","permalink":"/tags/\uc544\ud0a4\ud14d\ucc98"}],"readingTime":10.495,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"14","title":"\uce74\ud398\uc778\ud300 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98\ub97c \uc124\uba85\ud574\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4","authors":["nunu"],"tags":["ec2","aws","\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4","\ubc30\ud3ec","\uc11c\ubc84","\uc544\ud0a4\ud14d\ucc98"]},"prevItem":{"title":"\uc8fc\uae30\uc801\uc778 \ub370\uc774\ud130 \uc694\uccad\uc73c\ub85c \ubc1b\uc740 \ub370\uc774\ud130\ub97c \ud6a8\uc728\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ubc0f \uc0bd\uc785\ud558\uae30 (with. \ubc15\uc2a4\ud130)","permalink":"/15"},"nextItem":{"title":"\ucda9\uc804\uc18c \ub9ac\uc2a4\ud2b8 \ud074\ub9ad\uc2dc \ub9c8\ucee4\uc5d0 \uac04\ub2e8\uc815\ubcf4 \ubaa8\ub2ec\uc744 \ub744\uc6b0\ub294 \uae30\ub2a5 \ucd94\uac00\uc5d0\uc11c \uacaa\uc5c8\ub358 \ud2b8\ub7ec\ube14 \uc288\ud305","permalink":"/13"}},"content":"\uc548\ub155\ud558\uc138\uc694 \uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4 \uce74\ud398\uc778\ud300 \ub204\ub204\uc785\ub2c8\ub2e4\\n\\n\uc774\ubc88\uc5d0 \uce74\ud398\uc778 \ud300\uc5d0\uc11c \ubc30\ud3ec \uc544\ud0a4\ud14d\ucc98\ub97c \uacb0\uc815\ud558\uac8c \ub418\uc5c8\ub358 \uacfc\uc815\uc5d0 \ub300\ud574\uc11c \uc815\ub9ac\ub97c \ud574\ubcf4\uace0 \uc2f6\uc5b4\uc11c \uae00\uc744 \uc4f0\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\ud0a4\ud14d\ucc98\uc640 \uc11c\ubc84\uac00 \ubc30\ud3ec\ub418\ub294 \uacfc\uc815\uc744 \ubcf4\uc5ec\ub4dc\ub9ac\uba74\uc11c \uc2dc\uc791\ud558\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\n![\ubc30\ud3ec \uc544\ud0a4\ud14d\ucc98](https://blog.kakaocdn.net/dn/dKVRTG/btsnFE7Nb82/GRONsIJPqd8WFVzjzqsgqk/img.png)\\n\\n\uc11c\ubc84\uac00 \ubc30\ud3ec\ub418\ub294 \uacfc\uc815\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n![server image](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdto7By%2FbtsnD31hYHy%2F7rWKwxulxXzfhRigE60Sd0%2Fimg.png)\\n\\n## \uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4 \uc778\uc2a4\ud134\uc2a4\uc5d0 \ub300\ud55c \uc18c\uac1c\\n\\n\uc6b0\ud14c\ucf54\uc5d0\uc11c \uc120\ud0dd\ud560 \uc218 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub294 \ucd1d 2\uac00\uc9c0 \uc885\ub958\uc785\ub2c8\ub2e4.\\n\\n1. \ud37c\ube14\ub9ad \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\\n - \ucea0\ud37c\uc2a4\uc5d0\uc11c\ub9cc SSH \uc811\uadfc\uc774 \uac00\ub2a5\ud55c \uc778\uc2a4\ud134\uc2a4\uc785\ub2c8\ub2e4.\\n - \ubbf8\ub9ac \uc5f4\ub824\uc788\ub294 \ud3ec\ud2b8\ub4e4\ub9cc \ud5c8\uc6a9\uc774 \ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\\n - \uac19\uc740 \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub07c\ub9ac\ub294 \ubaa8\ub4e0 \ud3ec\ud2b8\uac00 \ud5c8\uc6a9\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4\\n2. \ud504\ub77c\uc774\ube57 \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\\n - \ud37c\ube14\ub9ad \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub97c \ud1b5\ud574\uc11c\ub9cc \uc811\uadfc\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n - \uac19\uc740 \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub07c\ub9ac\ub294 \ubaa8\ub4e0 \ud3ec\ud2b8\uac00 \ud5c8\uc6a9\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1\ubc88 \uc778\uc2a4\ud134\uc2a4\ub97c 2\uac1c \uc0ac\uc6a9 \uac00\ub2a5\ud558\uace0, 2\ubc88 \uc778\uc2a4\ud134\uc2a4\ub97c 1\uac1c \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n\\n\uad8c\uc7a5\ub418\ub294 \ud658\uacbd\uc5d0\uc11c 1\uac1c\ub294 db \uc11c\ubc84\ub85c \uc0ac\uc6a9\ud558\uace0, \ub098\uba38\uc9c0 2\uac1c\ub294 \uc790\uc720\ub86d\uac8c \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \uadf8\uc804\uc5d0 \uc54c\uba74 \uc88b\uc544\uc694\\n\\n\uc5ec\uae30\uc11c\ub294 Self Hosted Runner\ub97c \uc0ac\uc6a9\ud588\ub294\ub370\uc694.\\n\\nSelf Hosted Runner\uc5d0 \ub300\ud55c \ub0b4\uc6a9\uc740 [\uc5ec\uae30](https://be-student.tistory.com/75#%EC%99%9C%20Self%20Hosted%20Runner%EC%95%BC%3F-1) \uc5d0 \uc798 \ub098\uc640\uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc678\ubd80 IP\ub85c\ubd80\ud130 SSH \uc811\uadfc\uc774 \ubd88\uac00\ub2a5\ud558\uae30\uc5d0, Self Hosted Runner \ub098, Jenkins \uac19\uc740 \ubc29\ubc95\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5c8\ub294\ub370, \ub7ec\ub2dd \ucee4\ube0c\ub97c \uace0\ub824\ud574\uc11c Self Hosted Runner\ub97c \uc120\ud0dd\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \ubc30\ud3ec \uc544\ud0a4\ud14d\ucc98\uc5d0 \ub300\ud55c \uace0\ubbfc\\n\\n\uc800\ud76c \ud300\uc774 \uc774\ubc88 \uc544\ud0a4\ud14d\ucc98\ub97c \ub9cc\ub4e4\uae30 \uc704\ud574\uc11c \uace0\ubbfc\ud588\ub358 \uc810\ub4e4\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc5b4\ub5bb\uac8c \ud558\uba74 \uc7a5\uc560\uc758 \uc601\ud5a5\uc744 \ucd5c\uc18c\ud654\ud560 \uc218 \uc788\uc744\uae4c?\\n2. \uc6b4\uc601 \uc11c\ubc84\ub97c \ub098\uc911\uc5d0 \ucd94\uac00\ud558\uac8c \ub418\uc5c8\uc744 \ub54c, \uc5b4\ub5bb\uac8c \uc911\ubcf5\uc73c\ub85c \uad00\ub9ac\ub418\ub294 \ubd80\ubd84\uc744 \ucd5c\uc18c\ud654\ud560 \uc218 \uc788\uc744\uae4c?\\n3. 2\ucc28 \ub370\ubaa8\ub370\uc774\uae4c\uc9c0\uc758 \uacfc\uc81c\uc778 \uac1c\ubc1c \uc11c\ubc84\ub97c \uc5b4\ub5bb\uac8c \uad6c\uc131\ud560 \uc218 \uc788\uc744\uae4c?\\n\\n\uc5ec\uae30\uc11c 1\ubc88\uc744 \uac00\uc7a5 \uba3c\uc800 \uc0dd\uac01\ud55c \uc544\ud0a4\ud14d\ucc98\ub97c \uad6c\uc131\ud558\uac8c \ub418\uc5c8\ub294\ub370, \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc120\ud0dd\uc758 \uae30\uc900\uc774 \ub418\uc5c8\ub358 \uac83\uc740 \ucd1d 3\uac00\uc9c0\uc600\uc2b5\ub2c8\ub2e4.\\n\\n1. DB\ub294 \ud504\ub77c\uc774\ube57 \uc11c\ube0c\ub137\uc5d0 \uc704\uce58\uc2dc\ud0a4\uace0, \uc6b0\ub9ac \uc778\uc2a4\ud134\uc2a4\ub97c \uac70\uccd0\uc11c\ub9cc \uc811\uadfc\uc774 \uac00\ub2a5\ud558\uac8c \ud55c\ub2e4.\\n - \uc774 \ubd80\ubd84\uc740 \ubcf4\uc548\uc744 \uc704\ud574\uc11c \uc5b4\uca54 \uc218 \uc5c6\uc774 \uc120\ud0dd\ud558\uac8c \ub41c \ubd80\ubd84\uc785\ub2c8\ub2e4.\uc774 \ubd80\ubd84\uc744 \uace0\ub824\ud558\ub2e4 \ubcf4\ub2c8, \ucd5c\uc18c\ud55c\uc73c\ub85c \uad6c\uc131\ud560 \uc218 \uc788\ub294 \uad6c\uc870\uac00 db \uc6a9 private \uc778\uc2a4\ud134\uc2a4 1\uac1c, \uadf8\ub9ac\uace0 \uc6b0\ub9ac\uac00 \uc0ac\uc6a9\ud560 public \uc778\uc2a4\ud134\uc2a4 1\uac1c\uac00 \ub429\ub2c8\ub2e4\\n2. \uc6b4\uc601 \uc11c\ubc84\ub97c \ub098\uc911\uc5d0 \ucd94\uac00\ud558\uac8c \ub418\uc5c8\uc744 \ub54c, \uc5b4\ub5bb\uac8c \uc911\ubcf5\uc73c\ub85c \uad00\ub9ac\ub418\ub294 \ubd80\ubd84\uc744 \ucd5c\uc18c\ud654\ud560 \uc218 \uc788\uc744\uae4c?\\n - \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc5d0 CD \ud234\uc774\ub098, \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uc124\uce58\ud558\uac8c \ub418\uba74, \uc6b4\uc601 \uc11c\ubc84\uc5d0\ub3c4 \ub3d9\uc77c\ud558\uac8c \uc791\uc5c5\uc744 \ud574\uc57c \ud569\ub2c8\ub2e4.\\n - \uc774 \ubd80\ubd84\uc744 \ucd5c\uc18c\ud654\ud558\uae30 \uc704\ud574\uc11c, \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc640, CD \ud234, \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uc124\uce58\ud55c \uc778\uc2a4\ud134\uc2a4\ub97c \ubd84\ub9ac\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n3. \uc5b4\ub5bb\uac8c \ud558\uba74 \uc7a5\uc560\uc758 \uc601\ud5a5\uc744 \ucd5c\uc18c\ud654\ud560 \uc218 \uc788\uc744\uae4c?\\n - \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc640, CD \ud234, \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uc124\uce58\ud55c \uc778\uc2a4\ud134\uc2a4\ub97c \ubd84\ub9ac\ud558\uac8c \uc54a\ub294\ub2e4\uba74 \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc5d0\uc11c \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c, CD \ud234\uacfc \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc5d0\ub3c4 \uc601\ud5a5\uc744 \ubbf8\uce58\uac8c \ub429\ub2c8\ub2e4. \uc774 \ubd80\ubd84\uc744 \uc0dd\uac01\ud588\uc744 \ub54c\ub3c4, \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc640, CD \ud234, \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uc124\uce58\ud55c \uc778\uc2a4\ud134\uc2a4\ub97c \ubd84\ub9ac\ud574\uc57c \ud55c\ub2e4\uace0 \uacb0\uc815\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4\\n - \ud55c \ubd80\ubd84\uc758 \uc7a5\uc560\uac00 \ub2e4\ub978 \ud234\uae4c\uc9c0 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uac8c \ub9cc\ub4e4\uac8c \ub418\uc5b4\uc11c, \ub864\ubc31\uc774\ub098, \uc0c1\ud669 \ud30c\uc545\uc744 \ud558\uae30 \ud798\ub4e4\uac8c \ub9cc\ub4e4\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uacfc\uc815\ub4e4\uc744 \uc0dd\uac01\ud588\uc744 \ub54c, \uc778\uc2a4\ud134\uc2a4 1\uac1c\ub97c \uac1c\ubc1c \uc11c\ubc84\uc6a9\uc73c\ub85c, \uc778\uc2a4\ud134\uc2a4 1\uac1c\ub97c CD \ud234\uacfc \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uc124\uce58\ud55c \uc778\uc2a4\ud134\uc2a4\ub85c \uc0ac\uc6a9\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \uc2e4\uc81c \ub0b4\ubd80 \uad6c\uc131\uc740 \uc5b4\ub5bb\uac8c \ub420\uae4c\uc694?\\n\\n### \uac1c\ubc1c \uc11c\ubc84\\n\\n\uc774 \uc778\uc2a4\ud134\uc2a4\uc5d0\ub294 \ucd1d 2\uac00\uc9c0 \uae30\ub2a5\uc774 \ub4e4\uc5b4\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1. \ud504\ub860\ud2b8 \uc11c\ubc84\\n - react\ub85c \ub418\uc5b4\uc788\ub294 \ud504\ub860\ud2b8\uc5d4\ub4dc \ucf54\ub4dc\ub97c \uc0ac\uc6a9\uc790\uc5d0\uac8c \uc804\ub2ec\ud574 \uc8fc\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n2. \ubc31\uc5d4\ub4dc \uc11c\ubc84\\n - spring\uc73c\ub85c \ub418\uc5b4\uc788\ub294 api \uc11c\ubc84\uc785\ub2c8\ub2e4.\\n\\n\ubb3c\ub860, \uc774\ub807\uac8c \ud558\uba74 \ub450 \uacf3 \uc911 \ud55c \uacf3\uc5d0 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c, \ud504\ub860\ud2b8 \uc11c\ubc84\uc640 \ubc31\uc5d4\ub4dc \uc11c\ubc84\uac00 \ubaa8\ub450 \uc601\ud5a5\uc744 \ubc1b\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uac19\uc774 \uad00\ub9ac\ud558\uac8c \ub41c \uccab \ubc88\uc9f8 \uc774\uc720\ub85c \ube44\uc6a9\uc774 \ub4e4\uae30 \ub54c\ubb38\uc5d0 \ube44\uc6a9\uc758 \ubb38\uc81c\ub97c \uace0\ub824\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uac1c\ubc1c \uc11c\ubc84\uc5d0\uc11c \ud504\ub860\ud2b8 \uc11c\ubc84\uc640 \ubc31\uc5d4\ub4dc \uc11c\ubc84\ub97c \uad00\ub9ac\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub450 \ubc88\uc9f8 \uc774\uc720\ub85c\ub294, \uc544\uc9c1 \ud504\ub85c\uc81d\ud2b8 \ucd08\ucc3d\uae30 \uc774\uae30 \ub54c\ubb38\uc5d0, \ubc31\uc5d4\ub4dc\uc5d0\uc11c \uc7a5\uc560\uac00 \ub0ac\uc744 \ub54c, \ud504\ub860\ud2b8\uc5d0\uc11c \uc77c\uc815 \uc774\uc0c1\uc758 \uc5d0\ub7ec \ucc98\ub9ac\uac00 \ubd88\uac00\ub2a5\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud504\ub85c\uc81d\ud2b8\uac00 \ub9ce\uc774 \uc9c4\ud589\ub418\uc5c8\ub2e4\uba74, \ud504\ub860\ud2b8\uc5d4\ub4dc\ub9cc\uc73c\ub85c \ud639\uc740 \uc7a5\uc560\uac00 \ub098\uc9c0 \uc54a\uc740 \uc11c\ubc84\ub97c \ud65c\uc6a9\ud574 \uc5d0\ub7ec \ucc98\ub9ac\ub97c \ud560 \uc218 \uc788\uc9c0\ub9cc, \uc544\uc9c1\uc740 \uadf8\ub7f0 \uae30\ub2a5\uc744 \uad6c\ud604\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc640\ub294 \ubcc4\uac1c\ub85c \uc2e4\ud589 \uc2dc \ud3b8\uc758\ub97c \uc704\ud574\uc11c \ub3c4\ucee4\ub97c \uc0ac\uc6a9\ud574 \uac1c\ubc1c \uc11c\ubc84\ub97c \uad00\ub9ac\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### CD \ud234\uacfc \ubaa8\ub2c8\ud130\ub9c1 \ud234\\n\\n\uc774 \uc778\uc2a4\ud134\uc2a4\uc5d0\ub294 \ucd1d 3\uac00\uc9c0 \uae30\ub2a5\uc774 \ub4e4\uc5b4\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1. CD \ud234\\n - \uc704\uc5d0\uc11c \uc124\uba85\ub4dc\ub9b0 \uac83\ucc98\ub7fc, self hosted runner \uac00 \ub3d9\uc791\ud558\uac8c \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4\\n2. \ubcf4\uc548\uc744 \uc704\ud55c \ub9ac\ubc84\uc2a4 \ud504\ub85d\uc2dc\\n - \uc800\ud76c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c \uad6c\uae00 \uc9c0\ub3c4\ub97c \uc0ac\uc6a9\ud558\uac8c \ub418\ub294\ub370, \uc774\ub54c API \ud0a4\ub97c \uc0ac\uc6a9\ud558\uac8c \ub429\ub2c8\ub2e4. \uc774\ub807\uac8c \ud558\uba74, API \ud0a4\ub97c \ub178\ucd9c\uc2dc\ud0a4\uc9c0 \uc54a\uace0, \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n - \uc774 API \ud0a4\ub97c \ub178\ucd9c\uc2dc\ud0a4\uc9c0 \uc54a\uae30 \uc704\ud574\uc11c, \ub9ac\ubc84\uc2a4 \ud504\ub85d\uc2dc\ub97c \ud558\ub098 \ub450\uace0, \uc5ec\uae30\uc11c API \ud0a4\ub97c \ucd94\uac00\ud574 \uc694\uccad\uc744 \ubcf4\ub0b4\ub294 \ubc29\uc2dd\uc73c\ub85c \uad6c\uc131\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n3. \ubaa8\ub2c8\ud130\ub9c1 \ud234\\n - \uc800\ud76c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c \uc544\uc9c1 \ub3c4\uc785\ud558\uc9c0 \uc54a\uc558\uc9c0\ub9cc, \ud604\uc7ac \uc774\uc288\ub85c\ub294 \uc62c\ub77c\uac00 \uc788\ub294 \uc0c1\ud0dc\uc785\ub2c8\ub2e4.\\n - Actuator, \ud504\ub85c\uba54\ud14c\uc6b0\uc2a4, \uadf8\ub77c\ud30c\ub098 \uc774 3\uac00\uc9c0\ub97c \ud65c\uc6a9\ud574\uc11c \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uad6c\uc131\ud558\uac8c \ub420 \uc608\uc815\uc785\ub2c8\ub2e4\\n\\n\uc704 \uae30\ub2a5\ub4e4\uc774 \ud55c \uc778\uc2a4\ud134\uc2a4\uc5d0 \ubaa8\uc5ec\uc788\uae30\uc5d0, \uc704\uc758 \uae30\ub2a5\ub4e4\uc740 \ucd94\ud6c4\uc5d0 \uc6b4\uc601 \uc11c\ubc84\uac00 \ucd94\uac00\ub418\uc5c8\uc744 \ub54c, \uc911\ubcf5\uc73c\ub85c \uad00\ub9ac\ud558\uc9c0 \uc54a\uc544\ub3c4 \ub429\ub2c8\ub2e4.\\n\\n## \ubc30\ud3ec \uacfc\uc815 \ub354 \uc790\uc138\ud788 \uc54c\uc544\ubcf4\uae30\\n\\n\uc544\ub798\uc5d0 \uc0ac\uc9c4\uc5d0\uc11c \ubcf4\uc774\ub294 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c \ubc30\ud3ec\ub97c \uc9c4\ud589\ud558\uace0 \uc788\ub294\ub370\uc694\\n\\n![server image](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdto7By%2FbtsnD31hYHy%2F7rWKwxulxXzfhRigE60Sd0%2Fimg.png)\\n\\n1. \uc0ac\uc6a9\uc790\uac00 push\ub97c \ud558\uba74, github actions\uc5d0\uc11c \ub3c4\ucee4 \ube4c\ub4dc\ub97c \uc9c4\ud589\ud558\uace0, \ub3c4\ucee4 \ud5c8\ube0c\uc5d0 \uc774\ubbf8\uc9c0\ub97c \uc62c\ub9bd\ub2c8\ub2e4.\\n2. \ub3c4\ucee4 \ud5c8\ube0c\uc5d0 \uc774\ubbf8\uc9c0\uac00 \uc62c\ub77c\uac04 \uc774\ud6c4\uc5d0, self hosted runner \uac00 \uc791\ub3d9\uc744 \uc2dc\uc791\ud569\ub2c8\ub2e4.\\n3. \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud574\uc11c, \uc774\ubbf8\uc9c0\ub97c \ubc1b\uace0, \ucee8\ud14c\uc774\ub108\ub97c \uc2e4\ud589\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c, \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc5d0 \ubc30\ud3ec\ub97c \uc9c4\ud589\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \ub290\ub080 \uc810\\n\\n\uc88b\uc740 \uc544\ud0a4\ud14d\ucc98\ub97c \uc124\uacc4\ud558\uae30 \uc704\ud574\uc11c\ub294 \uace0\ub824\ud574\uc57c \ud560 \uc810\ub4e4\uc774 \uc815\ub9d0 \ub9ce\ub2e4\ub294 \uac83\uc744 \ub2e4\uc2dc \ud55c\ubc88 \ub290\uaf08\uc2b5\ub2c8\ub2e4.\\n\\n\uc6b4\uc601 \uc11c\ubc84\uac00 \ucd94\uac00\ub41c\ub2e4\ub358\uac00, \uc778\uc2a4\ud134\uc2a4\uac00 \ub298\uc5b4\ub098\uace0, \uc904\uc5b4\ub4dc\ub294 \uc0c1\ud669\uc5d0 \uc720\uc5f0\ud558\uac8c \ub300\ucc98\ud560 \uc218 \uc788\ub3c4\ub85d \uc124\uacc4\ub97c \ud574\uc57c \ud55c\ub2e4\ub294 \uac83\uc744 \ub2e4\uc2dc \ud55c\ubc88 \ub290\uaf08\uc2b5\ub2c8\ub2e4.\\n\\n\uc911\ubcf5\uc73c\ub85c \uad00\ub9ac\ub420 \ud3ec\uc778\ud2b8\ub97c \uc904\uc5ec\uc57c \ud55c\ub2e4\ub294 \uac83\ub3c4 \ub2e4\uc2dc \ud55c\ubc88 \ub290\ub084 \uc218 \uc788\uc5c8\uace0\uc694\\n\\n\uae34 \uae00\uc744 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4"},{"id":"13","metadata":{"permalink":"/13","source":"@site/blog/2023-07-14-trouble-shooting-with-info-window.mdx","title":"\ucda9\uc804\uc18c \ub9ac\uc2a4\ud2b8 \ud074\ub9ad\uc2dc \ub9c8\ucee4\uc5d0 \uac04\ub2e8\uc815\ubcf4 \ubaa8\ub2ec\uc744 \ub744\uc6b0\ub294 \uae30\ub2a5 \ucd94\uac00\uc5d0\uc11c \uacaa\uc5c8\ub358 \ud2b8\ub7ec\ube14 \uc288\ud305","description":"Untitled","date":"2023-07-14T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 14\uc77c","tags":[{"label":"react","permalink":"/tags/react"},{"label":"google maps api","permalink":"/tags/google-maps-api"},{"label":"useSyncExternalStore","permalink":"/tags/use-sync-external-store"}],"readingTime":17.7,"hasTruncateMarker":false,"authors":[{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"}],"frontMatter":{"slug":"13","title":"\ucda9\uc804\uc18c \ub9ac\uc2a4\ud2b8 \ud074\ub9ad\uc2dc \ub9c8\ucee4\uc5d0 \uac04\ub2e8\uc815\ubcf4 \ubaa8\ub2ec\uc744 \ub744\uc6b0\ub294 \uae30\ub2a5 \ucd94\uac00\uc5d0\uc11c \uacaa\uc5c8\ub358 \ud2b8\ub7ec\ube14 \uc288\ud305","authors":["scent"],"tags":["react","google maps api","useSyncExternalStore"]},"prevItem":{"title":"\uce74\ud398\uc778\ud300 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98\ub97c \uc124\uba85\ud574\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4","permalink":"/14"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc18c\uac1c\ud569\ub2c8\ub2e4.","permalink":"/11"}},"content":"![Untitled](https://file.notion.so/f/s/16a32751-2088-4261-8bf6-3d556c0bf2e8/Untitled.png?id=020fb0e2-81d8-4dca-bb76-cf4536ca7b29&table=block&spaceId=e725e94b-8029-47f5-aecb-8eb1ef7c939f&expirationTimestamp=1689364800000&signature=3KH3gvfzTgKmmFsrNBluQ3evQ6jwe2C-tj8LqB6gQyw&downloadName=Untitled.png)\\n\\n\uc704 \uc774\ubbf8\uc9c0\ub294 \ud604\uc7ac\uae4c\uc9c0 \uad6c\ud604\ud55c \uc9c0\ub3c4\uc758 \ubaa8\uc2b5\uc774\ub2e4. \uad6c\ud604\ub41c \uae30\ub2a5\uc740 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uc11c\ubc84\uc5d0 \uc694\uccad\ud574 \ubc1b\uc544\uc628 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \ubc14\ud0d5\uc73c\ub85c \ud654\uba74\uc5d0 \ub9c8\ucee4\ub97c \ud45c\uc2dc\ud558\ub294 \uae30\ub2a5\\n- \ud654\uba74\uc774 \uc774\ub3d9\ud558\uac70\ub098 \uc90c\uc778, \uc90c \uc544\uc6c3\uc744 \ud560 \uc2dc \ud654\uba74\uc758 \ub9c8\ucee4 \uc815\ubcf4\uac00 \ucd5c\uc2e0\ud654 \ub418\ub294 \uae30\ub2a5\\n- \ub9c8\ucee4 \uc815\ubcf4\ub97c \ucd5c\uc2e0\ud654 \ud560 \ub54c \ud654\uba74\uc5d0\uc11c \uc0ac\ub77c\uc9c4 \ub9c8\ucee4\ub97c dom\uc5d0\uc11c \uc81c\uac70\ud558\ub294 \uae30\ub2a5\\n- \ub9c8\ucee4 \uc815\ubcf4\ub97c \ucd5c\uc2e0\ud654 \ud560 \ub54c \uc774\uc804 \ud654\uba74\uc5d0\uc11c\ub3c4 \uc788\uc5c8\ub358 \ub9c8\ucee4\ub97c \uc7ac\uc0dd\uc131 \ud558\uc9c0 \uc54a\ub294 \uae30\ub2a5\\n- \ub9c8\ucee4\ub97c \ud074\ub9ad\ud588\uc744 \uc2dc \ud574\ub2f9 \ub9c8\ucee4\uc5d0 \ub300\ud55c \uac04\ub2e8 \uc815\ubcf4\ub97c \ubaa8\ub2ec\ub85c \ub744\uc6cc\uc8fc\ub294 \uae30\ub2a5\\n- \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c \ub9c8\ucee4\ub4e4\uc5d0 \ub300\ud55c \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \ub9ac\uc2a4\ud2b8\ub85c \ubcf4\uc5ec\uc8fc\ub294 \uae30\ub2a5\\n\\n\uc774\ubc88\uc5d0 \uc0c8\ub85c \ucd94\uac00\ud558\uace0\uc790 \ud55c \uae30\ub2a5\uc740 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- \ucda9\uc804\uc18c \ub9ac\uc2a4\ud2b8\uc5d0\uc11c \ucda9\uc804\uc18c\ub97c \uc120\ud0dd\ud558\uba74 \ud654\uba74\uc758 \uc911\uc2ec\uc774 \uc120\ud0dd\ud55c \ucda9\uc804\uc18c \ub9c8\ucee4\ub85c \uc774\ub3d9\ud558\uace0, \ucda9\uc804\uc18c\uc758 \uac04\ub2e8 \uc815\ubcf4\ub97c \ubaa8\ub2ec\ub85c \ub744\uc6cc\uc8fc\ub294 \uae30\ub2a5\\n\\n\uc704 \uae30\ub2a5\uc744 \uad6c\ud604\ud558\uae30 \uc704\ud574\uc120 google maps api\uc758 InfoWindow\uac1d\uccb4\ub97c \uc774\uc6a9\ud574\uc57c \ud55c\ub2e4. \uc0ac\uc6a9 \ubc29\uc2dd\uc740 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n```jsx\\nconst infowindow = new google.maps.InfoWindow({\\n content: contentString,\\n ariaLabel: \'Uluru\',\\n});\\n\\nconst marker = new google.maps.Marker({\\n position: uluru,\\n map,\\n title: \'Uluru (Ayers Rock)\',\\n});\\n\\ninfowindow.open({\\n anchor: marker,\\n map,\\n});\\n```\\n\\n\uac04\ub2e8\ud558\uac8c \uc694\uc57d\ud558\uc790\uba74 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- `InfoWindow` \uc0dd\uc131\uc790 \ud568\uc218\ub97c \ud1b5\ud574 `infoWindow` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4.\\n - \uc0dd\uc131\uc2dc dom \uc694\uc18c \ud639\uc740 string\uc744 \uc804\ub2ec\ud574 `infoWindow`\uac00 \uc0dd\uc131\ub420 dom\uc704\uce58\ub97c \uc9c0\uc815\ud574\uc900\ub2e4.\\n- `marker` \uc778\uc2a4\ud134\uc2a4\ub97c `infoWindow` \uc778\uc2a4\ud134\uc2a4\uc758 `open` \uba54\uc11c\ub4dc\uc5d0 \uc778\uc790\ub85c \uc804\ub2ec\ud55c\ub2e4.\\n- `infoWindow` \uc0dd\uc131 \uc2dc \uc804\ub2ec\ud588\ub358 dom\uc694\uc18c\uc758 \uc704\uce58\uac00 `marker`\uc758 \uc704\uce58\ub85c \uace0\uc815\ub418\uba74\uc11c \ud654\uba74\uc5d0 \uadf8\ub824\uc9c4\ub2e4.\\n\\n---\\n\\n![Untitled](https://file.notion.so/f/s/3079d7b9-8226-46b1-9482-054d1ea78016/Untitled.png?id=bce7685b-8a95-429c-bb75-98a4402cfc17&table=block&spaceId=e725e94b-8029-47f5-aecb-8eb1ef7c939f&expirationTimestamp=1689364800000&signature=jKnY-AhoxwqTiWrMi66uUtIamSOZDj8GGBTzgKeu_qY&downloadName=Untitled.png)\\n\\n\ucda9\uc804\uc18c \uc815\ubcf4\ub97c \ubcf4\uc5ec\uc8fc\ub294 \uc704 `StationList` \ucef4\ud3ec\ub10c\ud2b8\ub294 \ucda9\uc804\uc18c \uc815\ubcf4\uc5d0 \uc811\uadfc\ud560 \ub54c react-query\ub97c \ud1b5\ud574 \uc11c\ubc84 \uc0c1\ud0dc\ub97c \uc9c1\uc811 \ub0b4\ub824 \ubc1b\uc544 \ucef4\ud3ec\ub10c\ud2b8 \ub0b4\ubd80 \ub9ac\uc2a4\ud2b8\ub97c \ub80c\ub354\ub9c1 \ud55c\ub2e4.\\n\\n\ub610\ud55c, `StationMarkersContainer`\uc5d0\uc11c\ub3c4 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c react-query\uc758 \uc11c\ubc84 \uc0c1\ud0dc\uc5d0\uc11c \ucc38\uc870\ud574 \ub9c8\ucee4\ub97c \ub80c\ub354\ub9c1 \ud558\uace0 \uc788\ub2e4.\\n\\n\ub530\ub77c\uc11c `StationList` \ucef4\ud3ec\ub10c\ud2b8\uc640 `StationMarkersContainer`\ub294 \uac01\uac01 \ub530\ub85c \uc11c\ubc84 \uc0c1\ud0dc\uc5d0 \uc811\uadfc\ud574 \ub80c\ub354\ub9c1\uc744 \uc218\ud589\ud558\uace0 \uc788\uc73c\ubbc0\ub85c \ub458 \uc0ac\uc774\uc5d0\ub294 \uc5b4\ub5a0\ud55c \uc5f0\uacb0 \uace0\ub9ac\uac00 \uc5c6\ub2e4.\\n\\n\uc5ec\uae30\uc11c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uac8c \ub418\uc5c8\ub2e4.\\n\\n---\\n\\n\ud604\uc7ac\uae4c\uc9c0\uc758 \ucf54\ub4dc\uc5d0\uc11c\ub294 `infoWindow`\uc778\uc2a4\ud134\uc2a4\ub97c `StationMarkersContainer`\ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \uc0dd\uc131\ud55c\ub2e4. \uc774\ub97c \ud558\uc704 \ucef4\ud3ec\ub10c\ud2b8\uc778 `StationMarker`\uc5d0 \ub0b4\ub824\uc8fc\uace0, \uc774 \ucef4\ud3ec\ub10c\ud2b8 \ub0b4\ubd80\uc5d0\uc11c `marker`\uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4.\\n\\n\uc774\ubc88\uc5d0 \uad6c\ud604\ud558\uae30\ub85c \ud55c \uae30\ub2a5\uc740 `StationList`\uc758 \ud56d\ubaa9 \uc911 \ud558\ub098\ub97c \uc120\ud0dd\ud588\uc744 \uc2dc \uc120\ud0dd\ub41c \ucda9\uc804\uc18c\uc5d0 \ud574\ub2f9\ud558\ub294 \ub9c8\ucee4\uc5d0 \uac04\ub2e8 \uc815\ubcf4 \ubaa8\ub2ec\uc774 \ub728\uba70 \ud654\uba74\uc744 \ud574\ub2f9 \ub9c8\ucee4\uac00 \uc911\uc2ec\uc73c\ub85c \uc624\ub3c4\ub85d \uc774\ub3d9 \uc2dc\ud0a4\ub294 \uac83\uc774\uc5c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc9c0\uae08\uc758 \ucf54\ub4dc \uad6c\uc870\uc0c1 `StationList`\uc640 `StationMarkersContainer`\uc0ac\uc774\uc5d0\ub294 \uc5b4\ub5a0\ud55c \uc5f0\uacb0 \uace0\ub9ac\ub3c4 \uc5c6\uc73c\ubbc0\ub85c `infoWindow`\uc640 `marker`\uc5d0 `StationList`\ub294 \uc811\uadfc\ud560 \uc218 \uc5c6\ub294 \uc0c1\ud0dc\uac00 \ub41c\ub2e4.\\n\\n\uc774\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574\uc11c \ub2e4\uc74c\uacfc \uac19\uc740 \ubc29\ubc95\uc744 \uc0ac\uc6a9\ud558\uae30\ub85c \ud588\ub2e4.\\n\\n- `infoWindow`\uc778\uc2a4\ud134\uc2a4\ub97c root \ub2e8\uc5d0\uc11c \uc0dd\uc131\ud574 \uc804\uc5ed\uc801\uc73c\ub85c \uad00\ub9ac\ud55c\ub2e4.\\n- \uc0dd\uc131\ub420 `marker` \uc778\uc2a4\ud134\uc2a4\ub4e4\uc744 \ubc30\uc5f4 \ud615\ud0dc\uc758 \uc804\uc5ed \uc0c1\ud0dc\ub85c \uad00\ub9ac\ud55c\ub2e4.\\n\\n\uc704 \ub0b4\uc6a9\uc744 \ub9d0\ub85c\ub9cc \ubcf8\ub2e4\uba74 \ubcc4\ub85c \uc5b4\ub824\uc6b8 \uac83 \uc5c6\uc5b4 \ubcf4\uc774\uc9c0\ub9cc \uc2e4\uc81c \uad6c\ud604\uc744 \uc9c4\ud589\ud574\ubcf4\ub2c8 \ub0b4\ubd80\uc801\uc73c\ub85c \ud070 \ubb38\uc81c\uac00 \ub450 \uac00\uc9c0 \uc874\uc7ac\ud588\ub2e4.\\n\\n1. \ub530\ub85c \ubaa8\ub4c8\uc744 \ubd84\ub9ac\ud574 `infoWindow`\ub97c \uc0dd\uc131\ud560 \uc218 \uc5c6\ub2e4.\\n2. `marker`\uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uc8fc\uccb4\uac00 `StationMarkersContainer`\uac00 \ub418\uc5b4\uc11c\ub294 \uc548\ub41c\ub2e4.\\n\\n\uac01\uac01\uc758 \ubb38\uc81c\uc810\uc744 \uc0b4\ud3b4\ubcf4\uc790.\\n\\n---\\n\\n### 1. \ub530\ub85c \ubaa8\ub4c8\uc744 \ubd84\ub9ac\ud574 `infoWindow`\ub97c \uc0dd\uc131\ud560 \uc218 \uc5c6\ub2e4.\\n\\n`infoWinodw`\ub97c \uc804\uc5ed \uc0c1\ud0dc\ub85c \ub9cc\ub4e4\uc5b4 \uc0ac\uc6a9\ud558\uae30 \uc704\ud574 \ucc98\uc74c\uc73c\ub85c \ud588\ub358 \uc0dd\uac01\uc740 `infoWindowStore.ts`\ub85c \ubaa8\ub4c8\uc744 \ubd84\ub9ac\ud558\uc5ec `infoWindow`\ub97c \uc0dd\uc131\ud574 store\uc758 \ucd08\uae30\uac12\uc73c\ub85c \uc9c0\uc815\ud558\ub294 \uac83\uc774\uc5c8\ub2e4.\\n\\n\uc704 \uc0dd\uac01\uc744 \uac00\uc9c0\uace0 \uadf8\ub300\ub85c \uad6c\ud604\ud574\ubcf4\uc558\ub354\ub2c8 `google`\uc744 \ucc38\uc870\ud560 \uc218 \uc5c6\ub2e4\ub294 \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\ub2e4. `InfoWindow`\uc0dd\uc131\uc790 \ud568\uc218\ub294 `google.maps.InfoWindow`\ub97c \ud1b5\ud574 \uc811\uadfc\ud560 \uc218 \uc788\uae30 \ub54c\ubb38\uc5d0 \ud574\ub2f9 \uc5d0\ub7ec\ub294 `infoWindow`\uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud560 \uc218 \uc5c6\ub2e4\ub294 \uac83\uc744 \uc758\ubbf8\ud588\ub2e4.\\n\\n\uc65c `google`\uc744 \ucc38\uc870\ud560 \uc218 \uc5c6\ub294\uc9c0 \uc774\uc720\ub97c \ubd84\uc11d\ud574\ubcf4\ub2c8 \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\uc558\ub2e4.\\n\\n\uc6b0\ub9ac \ud300\uc774 \uad6c\uae00 \uc9c0\ub3c4 \ub85c\ub4dc\ub97c \uc704\ud574 \uc120\ud0dd\ud55c \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 `@googlemaps/react-wrapper`\uc774\ub2e4. \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc758 \ub3d9\uc791\uc744 \uc0b4\ud3b4\ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- `Wrapper`\ucef4\ud3ec\ub10c\ud2b8\uac00 `@googlemaps/js-loader`\ub77c\uc774\ube0c\ub7ec\ub9ac\uc758 `Loader`\uc0dd\uc131\uc790 \ud568\uc218\ub97c \ud638\ucd9c\ud55c\ub2e4.\\n- \uc0dd\uc131\ub41c `loader`\uc778\uc2a4\ud134\uc2a4\uc758 `load`\uba54\uc11c\ub4dc\ub97c \uc2e4\ud589\uc2dc\ucf1c \uc9c0\ub3c4\uc758 \ub85c\ub529 \uc791\uc5c5\uc744 \uc2dc\uc791\ud55c\ub2e4.\\n - `load` \uba54\uc11c\ub4dc\ub294 \ucd5c\uc885\uc801\uc73c\ub85c `Promise`\uc744 \ubc18\ud658\ud558\ub294\ub370, \uc9c0\ub3c4 \ub85c\ub4dc\uc5d0 \uc131\uacf5\ud558\uba74 `resolve(window.google)` \uc744 \uc2e4\ud589\uc2dc\ucf1c `google`\uc744 \uc804\uc5ed\uc801\uc73c\ub85c \uc0ac\uc6a9 \uac00\ub2a5\ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5b4\uc900\ub2e4.\\n- \uc9c0\ub3c4\uc758 \ub85c\ub529\uc774 \uc644\ub8cc\ub418\uba74 `Wrapper`\uc758 `render` props\ub97c \ud1b5\ud574 \ubc1b\uc740 \ucf5c\ubc31 \ud568\uc218\ub97c \uc2e4\ud589\uc2dc\ud0a8\ub2e4.\\n - `render`\ucf5c\ubc31 \ud568\uc218\ub294 \ub85c\ub529 \uc0c1\ud0dc\ub97c \ub098\ud0c0\ub0b4\ub294 Status\ub97c \ud30c\ub77c\ubbf8\ud130\ub85c \ub118\uaca8 \ubc1b\uc544 \ud638\ucd9c\ub41c\ub2e4.\\n\\n\ucd5c\uc885\uc801\uc73c\ub85c `render`\ub97c \uc2e4\ud589 \uc2dc\ucf30\uc744 \ub54c \ubc18\ud658 \ub418\ub294 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c\ub294 `google` \ub85c\ub529 \ub418\uc5b4 \uc804\uc5ed\uc801\uc73c\ub85c \uc811\uadfc\uc774 \uac00\ub2a5\ud568\uc744 \ubcf4\uc7a5\ud560 \uc218 \uc788\uc73c\ubbc0\ub85c \uc774\ub54c\ubd80\ud130 `google`\uc5d0 \uc811\uadfc\uc774 \uac00\ub2a5\ud574\uc9c4\ub2e4. \u2192 \ub530\ub77c\uc11c `Wrapper`\ub97c \ud1b5\ud574 \ubc18\ud658\ub418\ub294 \ucef4\ud3ec\ub10c\ud2b8\uc758 \ud558\uc704 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c `google.maps.Map`\uc0dd\uc131\uc790 \ud568\uc218\ub97c \uc0ac\uc6a9\ud574 \uc9c0\ub3c4\ub97c \uc0dd\uc131\ud560 \uc218 \uc788\uac8c \ub41c\ub2e4.\\n\\n`infoWindow`\ub97c \uc0dd\uc131\ud558\uae30 \uc704\ud574 \ub9cc\ub4e0 \uc0c8\ub85c\uc6b4 \ubaa8\ub4c8\uc740 \uccab `import`\uc2dc\uae30\uc5d0 \ud3c9\uac00\ub420 \uac83\uc774\uae30 \ub54c\ubb38\uc5d0 `Wrapper`\uc758 \ud558\uc704 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c `import`\ub97c \uc218\ud589\ud55c\ub2e4\uba74 \ub85c\ub4dc\uac00 \uc644\ub8cc\ub41c \uc774\ud6c4 \uc2dc\uc810\uc77c \uac83\uc774\ubbc0\ub85c `window.google`\uc774 \ub4f1\ub85d\ub418\uc5b4 `google`\uc5d0 \uc811\uadfc\uc774 \uac00\ub2a5\ud560 \uac83\uc73c\ub85c \uc608\uc0c1\ud588\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc6f9\ud329\uc744 \ud1b5\ud55c \ubc88\ub4e4\ub9c1 \uacfc\uc815\uc5d0\uc11c \ubaa8\ub4c8\uc774 \ub4a4\uc11e\uc5ec \ud30c\uc77c\uc758 \ud3c9\uac00 \uc2dc\uae30\ub97c \ubcf4\uc7a5\ud560 \uc218 \uc5c6\uc5b4\uc838 \uc0c8\ub85c \ub9cc\ub4e0 \ubaa8\ub4c8\uc5d0\uc11c\ub294 `google`\uc5d0 \ub300\ud55c \uc811\uadfc\uc774 \ubd88\uac00\ub2a5\ud574\uc9c0\uac8c \ub418\uc5c8\ub2e4. \uc6f9\ud329\uc744 \uc880 \ub354 \uacf5\ubd80\ud574\ubcf8\ub2e4\uba74 \uc774 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \uc218 \uc788\uc744 \uac83 \uac19\uc558\uc9c0\ub9cc, \ub108\ubb34 \uc9c0\uc5fd\uc801\uc778 \ubd80\ubd84\uc5d0\uc11c \ub9ce\uc740 \uc2dc\uac04\uc744 \ub4e4\uc774\uae30 \ubcf4\ub2e8 \uae30\uc874\uc5d0 \uac1c\ubc1c\ud558\ub358 \ubc29\uc2dd\uc744 \ud1b5\ud574 \ubb38\uc81c\ub97c \ud574\uacb0\ud574\ubcf4\uae30\ub85c \uacb0\uc815\ud588\ub2e4.\\n\\n\ucd5c\uc885\uc801\uc73c\ub85c \ubb38\uc81c\ub97c \ud574\uacb0\ud55c \ubc29\uc2dd\uc740 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- `InfoWindow`\uc0dd\uc131\uc790 \ud568\uc218\ub97c \ud638\ucd9c\ud560 `CarFfeineInfoWindowInitializer`\ucef4\ud3ec\ub10c\ud2b8\ub97c \ub9cc\ub4e0\ub2e4.\\n- `Wrapper`\ub85c \uac10\uc2f8\uc9c4 \ucef4\ud3ec\ub10c\ud2b8 \ud558\uc704\uc5d0 `CarFfeineInfoWindowInitializer` \ucef4\ud3ec\ub10c\ud2b8\ub97c \ucd94\uac00\ud55c\ub2e4.\\n- `google`\uc5d0 \uc811\uadfc\uc774 \uac00\ub2a5\ud55c \uc0c1\ud0dc\ub97c \ubcf4\uc7a5\ubc1b\uc740 `CarFfeineInfoWindowInitializer`\ub0b4\ubd80\uc5d0\uc11c `infoWindow`\uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4.\\n- `store`\uc5d0 `infoWindow`\uc778\uc2a4\ud134\uc2a4\ub97c `set`\ud574\uc8fc\uc5b4 \uc804\uc5ed\uc801\uc73c\ub85c `infoWindow`\ub97c \uc0ac\uc6a9 \uac00\ub2a5\ud558\ub3c4\ub85d \ud55c\ub2e4.\\n\\n---\\n\\n### 2. `marker`\uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uc8fc\uccb4\uac00 `StationMarkersContainer`\uac00 \ub418\uc5b4\uc11c\ub294 \uc548\ub41c\ub2e4.\\n\\n\uc774\ubc88 \ud300 \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c \uc9c0\ub3c4\ub97c \uad6c\ud604\ud558\uae30 \uc704\ud574 google maps api\ub97c \uc0ac\uc6a9\ud558\uac8c \ub418\uc5c8\ub2e4. \ub72c\uae08\uc5c6\uc774 \uc774 \uc774\uc57c\uae30\ub97c \ud55c \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- google maps api\ub294 \ubc14\ub2d0\ub77c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8\ub97c \uae30\ubc18\uc73c\ub85c \ub3d9\uc791\ud55c\ub2e4.\\n- \uc774\ubc88 \ud300 \ud504\ub85c\uc81d\ud2b8\ub294 \ub9ac\uc561\ud2b8\ub97c \uae30\ubc18\uc73c\ub85c \uac1c\ubc1c\uc744 \uc9c4\ud589\ud560 \uac83\uc774\ub2e4.\\n- \uc9c0\ub3c4\ub97c \uadf8\ub9ac\uae30 \uc704\ud574\uc11c \ubc14\ub2d0\ub77c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8\uc640 \ub9ac\uc561\ud2b8\uc758 \uc801\uc808\ud55c \uc870\ud654\uac00 \ud544\uc694\ud558\ub2e4.\\n- \ub2e4\uc18c \ud63c\ub780\uc2a4\ub7ec\uc6b8 \uc218 \uc788\ub294 \uc9c0\ub3c4\uc758 \uc870\uc791 \ubc29\uc2dd\uc744 \ub9ac\uc561\ud2b8\uc640 \uc870\ud654\ub86d\uac8c \uc0ac\uc6a9\ud558\uae30 \uc704\ud574\uc11c \ucef4\ud3ec\ub10c\ud2b8 \uc124\uacc4\uc2dc \ucef4\ud3ec\ub10c\ud2b8\uc758 \ucc45\uc784\uc744 \ud655\uc2e4\ud558\uac8c \uad6c\ubd84\ud574\uc57c\uaca0\ub2e4\ub294 \uc0dd\uac01\uc744 \ud558\uac8c \ub418\uc5c8\ub2e4.\\n\\n\uc774 \ucef4\ud3ec\ub10c\ud2b8\uc758 \ucc45\uc784\uc5d0 \ub300\ud55c \ubb38\uc81c\ub85c \uc778\ud574 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uc8fc\uccb4\uc5d0 \ub300\ud574 \ub9ce\uc740 \uace0\ubbfc\uc744 \ud558\uac8c \ub418\uc5c8\ub2e4.\\n\\n\uc77c\ub2e8 \uc6d0\ub798 \ucf54\ub4dc \uad6c\uc870\uc5d0\uc11c \ub9c8\ucee4\ub97c \uadf8\ub9ac\uae30 \uc704\ud574 \ucef4\ud3ec\ub10c\ud2b8\ub97c \ub2e4\uc74c\uacfc \uac19\uc774 \ucd94\uc0c1\ud654 \ud588\ub2e4.\\n\\n- `StationMarkersContainer` \ucef4\ud3ec\ub10c\ud2b8\\n - \ub9ac\uc561\ud2b8 \ucffc\ub9ac\ub97c \ud1b5\ud574 \ubc1b\uc544\uc628 \uc11c\ubc84 \uc0c1\ud0dc(\ucda9\uc804\uc18c \uc815\ubcf4 \ubc30\uc5f4)\ub85c `StationMarker`\ub97c \ud638\ucd9c\ud55c\ub2e4.\\n- `StationMarker` \ucef4\ud3ec\ub10c\ud2b8\\n - \uc0c1\uc704\uc5d0\uc11c \ub0b4\ub824\ubc1b\uc740 \ucda9\uc804\uc18c \uc815\ubcf4 props\ub97c \ud1b5\ud574 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4. (google maps api\uc5d0\uc11c\ub294 \uc778\uc2a4\ud134\uc2a4 \uc0dd\uc131\uc774 \uace7 \ub80c\ub354\ub9c1\uc744 \uc758\ubbf8\ud55c\ub2e4)\\n - \uc0dd\uc131\ud55c `marker` \uc778\uc2a4\ud134\uc2a4\uc5d0 `infoWindow` \uc778\uc2a4\ud134\uc2a4\uc758 `open` \uba54\uc11c\ub4dc\ub97c \ud2b8\ub9ac\uac70 \ud558\ub294 \ud074\ub9ad \uc774\ubca4\ud2b8 \ub9ac\uc2a4\ub108\ub97c \ucd94\uac00\ud574\uc900\ub2e4.\\n - `useEffect`\uc758 \ud074\ub9b0\uc5c5 \ud568\uc218\ub97c \uc774\uc6a9\ud574 \ucda9\uc804\uc18c \uc815\ubcf4\uac00 \ucd5c\uc2e0\ud654 \ub418\uc5c8\uc744 \ub54c \ub9c8\ucee4\uac00 \ub354\uc774\uc0c1 \ud654\uba74\uc5d0 \ubcf4\uc774\uc9c0 \uc54a\ub294\ub2e4\uba74 `marker` \uc778\uc2a4\ud134\uc2a4\uc758 `setMap(null)` \uba54\uc11c\ub4dc\ub97c \ud638\ucd9c\ud574 google maps api\uc5d0\uc11c \ub9c8\ucee4\ub97c \uc9c0\uc6b0\ub3c4\ub85d \ud55c\ub2e4. (\ub9c8\ucee4 \ub80c\ub354\ub9c1 \ucd5c\uc801\ud654)\\n\\n\uac04\ub7b5\ud788 \uc124\uba85\ud558\uc790\uba74 `StationMarkersContainer` \ucef4\ud3ec\ub10c\ud2b8\ub294 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uc11c\ubc84\uc5d0\uc11c \ubc1b\uc544 `StationMarker`\ub97c \ud638\ucd9c\ud558\ub294 \uc5ed\ud560\ub9cc\uc744 \uc218\ud589\ud558\uace0, \ub9c8\ucee4\uc5d0 \ub300\ud55c \ubaa8\ub4e0 \uc138\ubd80 \ub85c\uc9c1\uc740 `StationMarker`\uac00 \uc218\ud589\ud558\ub3c4\ub85d \ucef4\ud3ec\ub10c\ud2b8\ub97c \ucd94\uc0c1\ud654 \ud574\ubcf4\uc558\ub2e4.\\n\\n\uc774\ub984\uc5d0\uc11c\ub3c4 \ub4dc\ub7ec\ub098\ub4ef `StationMarker` \ucef4\ud3ec\ub10c\ud2b8\uac00 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uc8fc\uccb4\uac00 \ub418\uc5b4\uc57c \ubc14\ub2d0\ub77c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8\uc640 \ub9ac\uc561\ud2b8\uc758 \ud63c\uc885\uc778 \uc774 \ud504\ub85c\uc81d\ud2b8\uc758 \ucf54\ub4dc\ub97c \ucd94\ud6c4 \uc720\uc9c0\ubcf4\uc218 \ud560 \ub54c \ubb38\uc81c\uac00 \uc5c6\uc73c\ub9ac\ub77c \ud310\ub2e8\ud588\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774\ub807\uac8c \ucd94\uc0c1\ud654 \ub41c \ucef4\ud3ec\ub10c\ud2b8\ub4e4\uc740 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \ubc30\uc5f4 \ud615\uc2dd\uc758 \uc804\uc5ed \uc0c1\ud0dc\uc5d0 \ub2f4\uc544 \uad00\ub9ac\ud558\uace0\uc790 \ud560 \ub54c \ubb38\uc81c\uac00 \ub418\uc5c8\ub2e4.\\n\\n---\\n\\n\uc77c\ub2e8 \uba3c\uc800 \uc11c\ubc84\uc5d0\uc11c \ub0b4\ub824 \ubc1b\uc740 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c `station`\uc774\ub77c\uace0 \ud558\uc790, \uc6b0\ub9ac\ub294 \uc774 `station`\uc744 \ud1b5\ud574 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\uace0\uc790 \ud55c\ub2e4.\\n\\n\uc774\ub54c \uc0dd\uac01 \ud560 \uc218 \uc788\ub294 \uac00\uc7a5 \uac04\ub2e8\ud55c \ubc29\ubc95\uc740 `station`\uc5d0\uc11c `map` \uba54\uc11c\ub4dc\ub97c \ud1b5\ud574 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\uc5ec \uc774 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \ud558\uc704 \ucef4\ud3ec\ub10c\ud2b8\uc778 `StationMarker`\uc5d0 \ub118\uaca8\uc8fc\ub294 \ubc29\uc2dd\uc77c \uac83\uc774\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774 \ubc29\uc2dd\uc740 \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uac83\uc774 \uace7 \ud654\uba74\uc5d0 \ub80c\ub354\ub9c1\uc744 \ubc1c\uc0dd\uc2dc\ud0a4\ub294 \uac83\uc744 \uc758\ubbf8\ud558\ub294 google maps api\uc758 \ud2b9\uc131\uc0c1 \uc6b0\ub9ac\uac00 \ucc98\uc74c \uc124\uacc4\ud55c \ucef4\ud3ec\ub10c\ud2b8\uc758 \ucc45\uc784\uc744 \ubc18\ud558\ub294 \uad6c\uc870\ub97c \ub9cc\ub4e4\uc5b4\ub0b4\uac8c \ub41c\ub2e4.\\n\\n\uc790\uc138\ud788 \uc124\uba85\ud574\ubcf4\uc790\uba74 \ub9c8\ucee4\uc758 \ub80c\ub354\ub9c1\uc740 `StationMarkersContainer`\uac00 \uc218\ud589\ud558\uace0 \uc788\ub294\ub370 \ud654\uba74\uc5d0 \ubcf4\uc774\uc9c0 \uc54a\ub294 \ub9c8\ucee4\ub97c \uc9c0\uc6b0\ub294 \uc5ed\ud560\uc740 `StationMarker`\ucef4\ud3ec\ub10c\ud2b8\uac00 \uc218\ud589\ud558\uace0 \uc788\uace0, \uc774\ubca4\ud2b8 \ud578\ub4e4\ub7ec\uc758 \ucd94\uac00 \uc5ed\uc2dc \ub9c8\ucee4\uac00 \uc0dd\uc131\ub41c \uc774\ud6c4\uc5d0 \ud558\uc704 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \uc774\ub97c \uc218\ud589\ud558\ub294 \uad34\uc0c1\ud55c \ucf54\ub4dc\uac00 \ub9cc\ub4e4\uc5b4\uc9c0\uac8c \ub41c\ub2e4.\\n\\n\ucd94\ud6c4 \ucf54\ub4dc\uc758 \uc720\uc9c0\ubcf4\uc218\uc131\uc744 \uc704\ud574\uc120 \ud53c\ud574\uc57c \ud560 \ubc29\uc2dd\uc784\uc774 \uba85\ud655\ud588\ub2e4.\\n\\n\ud574\uacb0 \ubc29\uc2dd\uc744 \uace0\ubbfc\ud574\ubcf4\ub2e4\uac00 \ub2e4\uc74c\uacfc \uac19\uc740 \ud574\uacb0 \ubc29\uc548\uc744 \uc0dd\uac01\ud558\uac8c \ub418\uc5c8\ub2e4.\\n\\n`StationMarker` \ucef4\ud3ec\ub10c\ud2b8\uc758 \uc5ed\ud560\\n\\n- `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4.\\n- `marker` \uc778\uc2a4\ud134\uc2a4\uc758 \uc774\ubca4\ud2b8 \ud578\ub4e4\ub7ec\ub97c \ucd94\uac00\ud55c\ub2e4.\\n- \uc0dd\uc131\ub41c `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \ubc30\uc5f4 \ud615\uc2dd\uc758 \uc804\uc5ed \uc0c1\ud0dc\uc5d0 \ucd94\uac00\ud55c\ub2e4.\\n- \ucda9\uc804\uc18c \uc815\ubcf4\uac00 \ucd5c\uc2e0\ud654 \ub418\uc5c8\uc744 \ub54c \ub9c8\ucee4\uac00 \ud654\uba74\uc5d0 \ubcf4\uc774\uc9c0 \uc54a\ub294 \uc0c1\ud0dc\uac00 \ub418\uc5c8\ub2e4\uba74 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc804\uc5ed \uc0c1\ud0dc\uc5d0\uc11c \uc0ad\uc81c\ud55c\ub2e4.\\n\\n\uc704\uc640 \uac19\uc774 `StationMarker` \uc758 \uc5ed\ud560\uc744 \uc7a1\uac8c \ub418\uba74 \uae30\uc874\uc758 \ucef4\ud3ec\ub10c\ud2b8 \uc124\uacc4 \uad6c\uc870\ub97c \ud574\uce58\uc9c0 \uc54a\uc73c\uba74\uc11c \uc804\uc5ed \uc0c1\ud0dc\uc5d0 `marker`\uc778\uc2a4\ud134\uc2a4\ub97c \uc798 \ucd94\uac00\ud560 \uc218 \uc788\uac8c \ub41c\ub2e4. \ud558\uc9c0\ub9cc \uc774\ub807\uac8c \ub418\uba74 `StationMarker` \ucef4\ud3ec\ub10c\ud2b8\ub294 \ub2e4\uc74c\uc758 \ud070 \ubb38\uc81c\ub4e4\uc744 \uac00\uc9c0\uac8c \ub41c\ub2e4.\\n\\n1. `marker`\ub4e4\uc744 \uac00\uc9c0\ub294 \uc804\uc5ed \uc0c1\ud0dc\ub97c \uad6c\ub3c5\ud558\uace0 \uc788\ub294 \ucef4\ud3ec\ub10c\ud2b8\uac00 \uc0c8\ub85c \uc0dd\uc131\ub418\ub294 \ub9c8\ucee4\uc758 \uac1c\uc218\ub9cc\ud07c \ub9ac\ub80c\ub354\ub9c1 \ub41c\ub2e4.\\n2. \ud604\uc7ac \uc0ac\uc6a9\ud558\uace0 \uc788\ub294 \uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac \ub3c4\uad6c\uc758 \ud2b9\uc131\uc0c1 \uc774\uc804 \uc0c1\ud0dc\ub97c \ucc38\uc870\ud574\uc640\uc57c `marker`\ub97c \ucd94\uac00\ud560 \uc218 \uc788\uac8c \ub418\ub294\ub370, \uc774 \ub54c \uc774\uc804 \uc0c1\ud0dc\uac00 \ucd5c\uc2e0\uc758 \uc0c1\ud0dc\uc784\uc744 \ubcf4\uc7a5\ud558\uc9c0 \ubabb\ud560 \uc218 \uc788\ub2e4.\\n\\n\uc774 \ub450 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \ubc29\uc2dd\uc744 \uace0\ubbfc\ud574\ubcf4\uc558\uc744 \ub54c \ub2e4\uc74c\uacfc \uac19\uc740 \uacb0\ub860\uc5d0 \ub3c4\ub2ec\ud558\uac8c \ub418\uc5c8\ub2e4.\\n\\n- \ud604\uc7ac \uc0ac\uc6a9\ud558\uace0 \uc788\ub294 \uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac \ub3c4\uad6c\ub294 React 18\uc5d0 \uc0c8\ub85c \ucd94\uac00\ub41c `useSyncExternalState` \ud6c5\uc744 \uae30\ubc18\uc73c\ub85c `recoil`\uacfc \ube44\uc2b7\ud558\uac8c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub3c4\ub85d \uacc4\uce35\uc744 \ubd84\ub9ac\ud558\uc5ec \ub9cc\ub4e0 \ub3c4\uad6c\uc774\ub2e4.\\n- \uae30\uc874\uc5d0 \uc0ac\uc6a9\ud558\ub358 \uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac \ub3c4\uad6c\uc758 \uba54\uc11c\ub4dc `useExternalState`, `useExternalValue`, `useSetExternalState` \uc774\uc678\uc5d0 `store` \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc9c1\uc811 \uc811\uadfc\ud558\uc5ec \ucd5c\uc2e0\uc758 \uc0c1\ud0dc\ub97c \ucc38\uc870\ud558\ub294 `getStoreSnapShot` \uba54\uc11c\ub4dc\ub97c \ucd94\uac00\ud55c\ub2e4.\\n- `store`\uc5d0 \uc9c1\uc811 \uc811\uadfc\ud574 \ubc1b\uc544\uc628 \ucd5c\uc2e0\uc758 \uc0c1\ud0dc\ub294 \ubc14\ub2d0\ub77c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8 \uac1d\uccb4 \uc774\ubbc0\ub85c \ub9ac\uc561\ud2b8\uc758 \ub9ac\ub80c\ub354\ub9c1\uc744 \ubc1c\uc0dd \uc2dc\ud0a4\uc9c0 \uc54a\ub294\ub2e4.\\n- \ub9ac\ub80c\ub354\ub9c1\uc73c\ub85c \uc778\ud55c \ubb38\uc81c\uc810\ub4e4\uc744 `getStoreSnapShot` \uba54\uc11c\ub4dc\ub97c \ucd94\uac00\ud568\uc73c\ub85c\uc368 \ud574\uacb0\ud560 \uc218 \uc788\ub2e4.\\n\\n---\\n\\n\uc0c8\ub85c\uc6b4 \uae30\ub2a5 \ucd94\uac00\ub97c \uc704\ud574 \ub9c8\uc8fc\ud588\ub358 \uc55e\uc120 \ub450 \uac00\uc9c0\uc758 \ubb38\uc81c\uc640 \ud574\uacb0 \ubc29\uc2dd\uc744 \uc0b4\ud3b4 \ubcf4\uc558\ub2e4. \uadf8\ub798\uc11c \ucd5c\uc885\uc801\uc73c\ub85c \uc774\uc804\uae4c\uc9c0 \uacc4\uc18d\ud574\uc11c \uace0\ubbfc\ud574\uc654\ub358 \ubb38\uc81c\ub97c \ud574\uacb0\ud55c \uacfc\uc815\uc744 \uac04\ucd94\ub824\ubcf4\uc790\uba74 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uc11c\ubc84\uc5d0\uc11c \ubc1b\uc544\uc640 \ub80c\ub354\ub9c1 \ud558\ub294 `StationList` \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c `marker` \uc778\uc2a4\ud134\uc2a4 \ubc30\uc5f4\uc744 \uc800\uc7a5\ud558\uace0 \uc788\ub294 `store`\uc778\uc2a4\ud134\uc2a4\uc5d0 \uc9c1\uc811 \uc811\uadfc\ud574 \ucd5c\uc2e0\uc758 `marker`\uc778\uc2a4\ud134\uc2a4\ub4e4\uc744 \uac00\uc838\uc628\ub2e4.\\n- \ucda9\uc804\uc18c \ubaa9\ub85d\uc5d0\uc11c \uc0ac\uc6a9\uc790\uac00 \ucda9\uc804\uc18c\ub97c \ud074\ub9ad\ud588\uc744 \ub54c \uc804\uc5ed\uc73c\ub85c \uad00\ub9ac\ub418\ub294 `infoWindow` \uc778\uc2a4\ud134\uc2a4\uc758 `open`\uba54\uc11c\ub4dc\uc5d0 `marker` \uc778\uc2a4\ud134\uc2a4\ub4e4 \uc911 \uc120\ud0dd\ub41c `marker`\ub97c \uc804\ub2ec\ud574 \uac04\ub2e8 \uc815\ubcf4 \ubaa8\ub2ec\uc744 \ub744\uc6cc\uc900\ub2e4."},{"id":"11","metadata":{"permalink":"/11","source":"@site/blog/2023-07-10-google-maps-api-with-car-ffeine.mdx","title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc18c\uac1c\ud569\ub2c8\ub2e4.","description":"\uc9c0\ub3c4 api \ubca4\ub354 \uc120\ud0dd \uc774\uc720","date":"2023-07-10T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 10\uc77c","tags":[{"label":"react","permalink":"/tags/react"},{"label":"google maps","permalink":"/tags/google-maps"},{"label":"google maps api","permalink":"/tags/google-maps-api"},{"label":"react-wrapper","permalink":"/tags/react-wrapper"},{"label":"@googlemaps/react-wrapper","permalink":"/tags/googlemaps-react-wrapper"}],"readingTime":8.165,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"11","title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc18c\uac1c\ud569\ub2c8\ub2e4.","authors":["gabriel"],"tags":["react","google maps","google maps api","react-wrapper","@googlemaps/react-wrapper"]},"prevItem":{"title":"\ucda9\uc804\uc18c \ub9ac\uc2a4\ud2b8 \ud074\ub9ad\uc2dc \ub9c8\ucee4\uc5d0 \uac04\ub2e8\uc815\ubcf4 \ubaa8\ub2ec\uc744 \ub744\uc6b0\ub294 \uae30\ub2a5 \ucd94\uac00\uc5d0\uc11c \uacaa\uc5c8\ub358 \ud2b8\ub7ec\ube14 \uc288\ud305","permalink":"/13"},"nextItem":{"title":"jasypt\ub97c \ud65c\uc6a9\ud558\uc5ec \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\uc790","permalink":"/12"}},"content":"## \uc9c0\ub3c4 api \ubca4\ub354 \uc120\ud0dd \uc774\uc720\\n\\n\uad6d\ub0b4 \uc11c\ube44\uc2a4 \uc911\uc778 \uc9c0\ub3c4 \uc11c\ube44\uc2a4\ub85c\ub294 google, naver, kakao\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uc911\uc5d0\uc11c\ub3c4 google maps api\ub294 css\ub85c `\uc9c0\ub3c4\uc758 \ud14c\ub9c8\ub97c \uc9c1\uc811 \uc2a4\ud0c0\uc77c\ub9c1\ud560 \uc218 \uc788\ub294 \uae30\ub2a5\uc774 \uc788\uc5b4\uc11c \uc120\ud0dd`\ud558\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\ngoogle maps api\ub97c \uc0ac\uc6a9\ud558\uae30 \uc704\ud574\uc11c \ubcc4\ub3c4\uc758 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc0ac\uc6a9\uc774 \ud544\uc218\ub294 \uc544\ub2c8\uc9c0\ub9cc\\n\\n\uc800\ud76c \ud300\uc5d0\uc11c \ub300\uc911\uc801\uc778 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uacfc \uae30\ubcf8 \ud658\uacbd \uc124\uc815\ubc95\uc744 \ubaa8\ub450 \ud14c\uc2a4\ud2b8 \ud588\uc744 \ub54c, \ubc18\ub4dc\uc2dc \uc0ac\uc6a9\ud558\uace0 \uc2f6\uc740 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \uc874\uc7ac\ud558\uc5ec \ube44\uad50\ub97c \uae30\ub85d\uc73c\ub85c \ub0a8\uae30\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n# google maps api \uad00\ub828 \ub77c\uc774\ube0c\ub7ec\ub9ac\\n\\n(\uc120\ud0dd\ud55c \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc740 \u2705\uc73c\ub85c \ud45c\uc2dc\ud588\uc2b5\ub2c8\ub2e4.)\\n\\n### google maps API\\n\\nhttps://github.com/tomchentw/react-google-maps\\n\\n\uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 \uad6c\uae00\uc5d0\uc11c \uacf5\uc2dd\uc73c\ub85c \uc81c\uacf5\ud558\ub294 \uc9c0\ub3c4 api\ub85c, HTML DOM\uc5d0 \uad6c\uae00 \uc9c0\ub3c4\ub97c \ubd80\ucc29\ud558\uace0, \uc0ac\uc6a9(\uc870\uc791)\ud560 \uc218 \uc788\ub3c4\ub85d \ub3c4\uc640\uc90d\ub2c8\ub2e4. \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 `vanilla Javascript \uae30\ubc18\uc73c\ub85c \ub3d9\uc791`\ud569\ub2c8\ub2e4.\\n\\n### **@types/google.maps** \u2705\\n\\nhttps://www.npmjs.com/package/@types/google.maps\\n\\nTypeScript\uc5d0\uc11c \uad6c\uae00 \uc9c0\ub3c4\ub97c \uc0ac\uc6a9\ud560 \ub54c `\ud0c0\uc785\uc744 \uc81c\uacf5`\ud574\uc8fc\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\n### **@googlemaps/js-api-loader**\\n\\nhttps://www.npmjs.com/package/@googlemaps/js-api-loader\\n\\n\uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 \uad6c\uae00\uc5d0\uc11c \uacf5\uc2dd\uc73c\ub85c \uc81c\uacf5\ud558\ub294 \uc9c0\ub3c4 \ud638\ucd9c api\ub85c, api key\ub9cc \ub118\uaca8\uc8fc\ub354\ub77c\ub3c4 \uad6c\uae00 \uc9c0\ub3c4\ub97c \uc2a4\ud06c\ub9bd\ud2b8 \ud615\ud0dc\ub85c \ubd88\ub7ec\uc640\uc8fc\ub294 \uc5ed\ud560\uc744 \ud558\ub294 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4. \ubcc4\ub3c4\ub85c html \uc870\uc791 \uc5c6\uc774 \ubd88\ub7ec\uc628 `\ub77c\uc774\ube0c\ub7ec\ub9ac\uc5d0\uc11c \uad6c\uae00 \uc9c0\ub3c4\ub97c \uaebc\ub0b4\uc11c \ub3d9\uc801\uc73c\ub85c \uc0ac\uc6a9`\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. vanilla Javascript \uae30\ubc18\uc73c\ub85c \ub3d9\uc791\ud558\uc5ec \uc5b4\ub514\uc5d0\uc11c\ub098 \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n\\n### \ub300\uc911\uc801\uc778 \ub77c\uc774\ube0c\ub7ec\ub9ac \ube44\uad50\\n\\n| | react-google-maps | @react-google-maps/api | @googlemaps/react-wrapper |\\n| --- | --- | --- | --- |\\n| \ub9c1\ud06c | https://www.npmjs.com/package/react-google-maps | https://www.npmjs.com/package/@react-google-maps/api | https://www.npmjs.com/package/@googlemaps/react-wrapper |\\n| \uc124\uba85 | \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 \uac1c\uc778\uc774 \ub9cc\ub4e0 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c, google maps API\ub97c react DOM \uc704\uc5d0 \uc62c\ub824\uc11c \uc0ac\uc6a9\ud558\uac8c \ub3d5\uc2b5\ub2c8\ub2e4.
    \uad6c\uae00 \uc9c0\ub3c4\uc640 \ub9c8\ucee4\ub97c react component \ucc98\ub7fc \uc0ac\uc6a9\ud558\uc5ec react\uc2a4\ub7fd\uac8c \ub80c\ub354\ub9c1 \ud558\ub294 \uac83\uc744 \uc9c0\uc6d0\ud569\ub2c8\ub2e4.
    react \uc9c4\uc601\uc5d0\uc11c \uac00\uc7a5 \ub300\uc911\uc801\uc73c\ub85c \uc0ac\uc6a9\ub418\ub294 \uad6c\uae00 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc600\uc9c0\ub9cc 2018\ub144 \uc774\ud6c4\ub85c \uc5c5\ub370\uc774\ud2b8\uac00 \ub04a\uacbc\uc2b5\ub2c8\ub2e4. | \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub3c4 \uac1c\uc778\uc774 \ub9cc\ub4e0 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c \uc55e\uc11c \uc18c\uac1c\ud55c react-google-maps\ub97c \uac1c\ub7c9\ud558\uc5ec \ub9cc\ub4e0 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.
    \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc5ed\uc2dc react\uc5d0 \uc9c0\ub3c4\ub098 \ub9c8\ucee4 \ucef4\ud3ec\ub10c\ud2b8\ub97c \ud638\ucd9c\ud574\uc11c \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4.
    \ud604\uc7ac react \uc9c4\uc601\uc5d0\uc11c \uac00\uc7a5 \ub300\uc911\uc801\uc73c\ub85c \uc0ac\uc6a9\ub418\ub294 \uad6c\uae00 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc785\ub2c8\ub2e4. | \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 \uad6c\uae00\uc5d0\uc11c \uacf5\uc2dd\uc73c\ub85c \uc81c\uacf5\ud558\ub294 react\uc6a9 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.
    \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 \uc55e\uc11c \uc18c\uac1c\ud55c js-api-loader\ub97c \ud65c\uc6a9\ud558\uc5ec \ub9cc\ub4e0 Wrapper \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc81c\uacf5\ud558\ub294\ub370, \uad6c\uae00 \uc9c0\ub3c4\ub97c \ud638\ucd9c\ud558\ub294 \uacfc\uc815\uc5d0\uc11c \uc218\uc2e0\uc911, \uc2e4\ud328, \uc131\uacf5\uc5d0 \ub530\ub77c \uc9c0\ub3c4\ub97c \ubcf4\uc5ec\uc904 \uc9c0, \ub85c\ub529\uc911 \ucef4\ud3ec\ub10c\ud2b8\ub97c \ubcf4\uc5ec\uc904 \uc9c0, \uc5d0\ub7ec \ucef4\ud3ec\ub10c\ud2b8\ub97c \ubcf4\uc5ec\uc904 \uc9c0 \uacb0\uc815\ud558\ub294 \uae30\ub2a5\uc774 \uc788\uc2b5\ub2c8\ub2e4.
    \uc774\uc678\uc5d0\ub294 \uae30\uc874\uc758 js-api-loader\uc758 \uae30\ub2a5\uacfc \uc644\ubcbd\ud558\uac8c \ub3d9\uc77c\ud569\ub2c8\ub2e4. (\ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc5f4\uc5b4\uc11c \uc9c1\uc811 \ud655\uc778\ud574\ubd24\uc2b5\ub2c8\ub2e4.) |\\n| \uc120\ud0dd\uc5ec\ubd80 | | | \u2705 |\\n\\n# \ub77c\uc774\ube0c\ub7ec\ub9ac \uc120\ud0dd \uc774\uc720\\n\\n\uc800\ud76c \ud504\ub85c\uc81d\ud2b8\ub294 `\uc2e4\uc2dc\uac04 \uc804\uae30\uc790\ub3d9\ucc28 \ucda9\uc804\uc18c \uc9c0\ub3c4 \ubc0f \uc0ac\uc6a9 \ud1b5\uacc4 \uc870\ud68c \uc11c\ube44\uc2a4` \ub2e4\ubcf4\ub2c8 \uc9c0\ub3c4 \uc704\uc5d0 \ub744\uc6cc\uc918\uc57c \ud560 \ub9c8\ucee4\ub97c \ucd5c\uc801\ud654 \ud558\ub294 \uacfc\uc815\uc774 \uad49\uc7a5\ud788 \uc911\uc694\ud569\ub2c8\ub2e4.\\n\\n1. \uc804\uad6d 6\ub9cc\uc5ec \uac1c\uc758 \ub9c8\ucee4\ub97c \uc804\ubd80 \ubcf4\uc5ec\uc904 \uc218 \uc5c6\ub2e4.\\n2. \ud604\uc7ac \ub514\uc2a4\ud50c\ub808\uc774 \uc601\uc5ed\uc758 \ub9c8\ucee4\ub9cc\uc744 \ud638\ucd9c\ud574\uc57c\ud55c\ub2e4.\\n3. \uadf8 \ub9c8\ucee4\ub4e4\uc758 \ub80c\ub354\ub9c1 \uacfc\uc815\uc744 \uc800\uc218\uc900\uc5d0\uc11c \ub2e4\ub8f0 \uc218 \uc788\uc5b4\uc57c \ud55c\ub2e4.\\n\\n\uc774\ub7f0 \uc6d0\uce59\uc744 \uac00\uc9c0\uace0 \uc788\uae30\uc5d0 \ub300\uc911\uc801\uc778 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4(react-google-maps, @react-google-maps/api)\uc740 \uc800\ud76c\uc758 \uc120\ud0dd\uc9c0\uc5d0 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uad6c\uae00 \uc9c0\ub3c4\ub294 \uc624\ub85c\uc9c0 vanilla\ub85c \uc81c\uacf5\ub418\ub294 \uc0c1\ud0dc\uc5d0\uc11c \uc9c1\uc811 \uc81c\uc5b4\ud558\uae30\ub85c \uacb0\uc815\ud558\uc600\uace0, \ub9c8\ucee4\ub97c \uad00\ub9ac\ud558\ub294 \uc8fc\uccb4 \ub610\ud55c \uad6c\uae00 \uc9c0\ub3c4\uc5d0\uc11c \uc9c1\uc811 \ucee8\ud2b8\ub864\uc744 \ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uad6c\uae00 \uc9c0\ub3c4\ub97c \ud638\ucd9c\ud558\ub294 \uc791\uc5c5\uc740 @googlemaps/react-wrapper\uc5d0 \ub9e1\uae30\uace0, \ubd88\ub7ec\uc628 \uad6c\uae00 \uc9c0\ub3c4\ub294 vanilla\ub85c \ud1b5\uc81c\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc9c0\ub3c4\uc758 \uc870\uc791, \uc9c0\ub3c4\uc5d0 \ub9c8\ucee4\ub97c \ucc0d\ub294 \uacfc\uc815\uc744 \ubaa8\ub450 `\uacf5\uc2dd \ubb38\uc11c\uc5d0 \ub098\uc640\uc788\ub294 \ubc29\ubc95\ub300\ub85c \ud1b5\uc81c`\ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\uae30\uc874\uc758 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc740 \ub9c8\ucee4\ub098 \uc9c0\ub3c4\ub97c \ucef4\ud3ec\ub10c\ud2b8\ud654 \ud55c \uc0c1\ud0dc\uc774\uae30\uc5d0 \ucd5c\uc801\ud654 \uacfc\uc815\uc5d0\uc11c \uc800\ud76c\uac00 \uc81c\uc5b4\ud560 \uc218 \uc5c6\ub294 \ubd80\ubd84\ub4e4\uc774 \uc788\ub2e4\uace0 \uc0dd\uac01\ud569\ub2c8\ub2e4. \ub530\ub77c\uc11c \ud2b8\ub7ec\ube14\uc288\ud305 \uacfc\uc815\uc5d0\uc11c \ub9c8\ucee4\uc758 \ud638\ucd9c \uc2dc\uc810, \uba54\ubaa8\ub9ac\uc5d0\uc11c \ud574\uc81c\ud558\ub294 \uc2dc\uc810, \ub80c\ub354\ub9c1\ud558\ub294 \uc2dc\uc810 \ub4f1\uc758 \uc791\uc5c5\ub4e4\uc744 \ud6e8\uc52c \ub354 \uc138\ubc00\ud558\uac8c \ud558\ub824\uba74 google maps api\uc744 \uc788\ub294 \uadf8\ub300\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc9c0\ub3c4\uc5d0 \uad00\ub828\ub41c \uae30\ub2a5\uc740 react DOM \uc704\uc5d0\uc11c\uac00 \uc544\ub2cc vanilla \ud658\uacbd\uc5d0\uc11c \uc791\uc5c5\uc744 \ud560 \uac83\uc785\ub2c8\ub2e4.\\n\\n# \uad6c\uae00 \uc9c0\ub3c4 \uc81c\uc5b4 \uc804\ub7b5\\n\\n1. \uad6c\uae00 \uc9c0\ub3c4\uc640 \ub9c8\ucee4\ub294 \ud56d\uc0c1 \ubc14\ub2d0\ub77c \ud658\uacbd(react DOM \ubc14\uae65)\uc5d0\uc11c \ub3d9\uc791\ud558\uac8c \ud55c\ub2e4.\\n2. \ubc14\ub2d0\ub77c \ud658\uacbd\uc5d0\uc11c\ub9cc \ub3d9\uc791\ud558\uac8c \ud558\uc5ec \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c\uc758 \uc7ac \ub80c\ub354\ub9c1\uc744 \uc77c\uc808 \ubc29\uc9c0\ud55c\ub2e4.\\n3. \ub9c8\ucee4\ub098 \uc9c0\ub3c4\uc758 \ub3d9\uc791 \uc774\ubca4\ud2b8\uc5d0 \uc758\ud574 UI\ub97c \uc870\uc791\ud574\uc57c\ud558\ub294 \uacbd\uc6b0\uc5d0\ub294 react DOM \uc870\uc791\uc744 \ud558\ub3c4\ub85d \ud55c\ub2e4.\\n4. \ubc14\ub2d0\ub77c \ud658\uacbd\uc778 google maps api\uc640 react DOM \uc0ac\uc774\uc758 \uc81c\uc5b4 \uacfc\uc815\uc5d0\ub294 useSyncExternalStore \ud6c5\uc744 \uc774\uc6a9\ud558\uc5ec \ub9ac\uc561\ud2b8 UI\ub97c \uac15\uc81c\ub85c \ub3d9\uae30\ud654 \uc2dc\ud0ac \uc218 \uc788\ub3c4\ub85d \ud55c\ub2e4.\\n\\n\uad6c\uae00 \uc9c0\ub3c4\ub294 \ubc14\ub2d0\ub77c \ud658\uacbd\uc5d0\uc11c, \uac01\uc885 UI \ud1b5\uc81c\ub294 \ub9ac\uc561\ud2b8\uc5d0\uc11c \ud1b5\ud569\ud558\uc5ec \uc0ac\uc6a9\ud558\ub294 \ud658\uacbd\uc744 \uad6c\uc0c1\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc2dc\uc911\uc5d0 \ub098\uc640\uc788\ub294 \ub300\ubd80\ubd84\uc758 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc744 \ud65c\uc6a9\ud558\uc5ec \ube44\uad50\ud558\uace0 \ud14c\uc2a4\ud2b8\ud55c \uacb0\uacfc @googlemaps/react-wrapper\ub97c \uc120\ud0dd\ud558\ub294 \uac83\uc774 \ucd5c\uc801\ud654\uc640 \uc0dd\uc0b0\uc131, \uc571 \uc548\uc815\uc131\uc744 \ubaa8\ub450 \ud655\ubcf4\ud560 \uc218 \uc788\ub294 \uc120\ud0dd\uc774\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud604\uc7ac \uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\uc911\uc778 \uc9c0\ub3c4 \uc81c\uc5b4\uc5d0 \uad00\ud55c \ubc29\ubc95\uc740 \uc774\ud6c4\uc5d0 \uc791\uc131 \ub420 \uae00\uc5d0\uc11c \uc0c1\uc138\ud558\uac8c \uc124\uba85\ud558\uaca0\uc2b5\ub2c8\ub2e4."},{"id":"12","metadata":{"permalink":"/12","source":"@site/blog/2023-07-10-kiara-jasypt.mdx","title":"jasypt\ub97c \ud65c\uc6a9\ud558\uc5ec \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\uc790","description":"\uc11c\ub860","date":"2023-07-10T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 10\uc77c","tags":[{"label":"jasypt","permalink":"/tags/jasypt"},{"label":"Spring","permalink":"/tags/spring"}],"readingTime":5.59,"hasTruncateMarker":false,"authors":[{"name":"\ud0a4\uc544\ub77c","title":"Backend","url":"https://github.com/kiarakim","imageURL":"https://github.com/kiarakim.png","key":"kiara"}],"frontMatter":{"slug":"12","title":"jasypt\ub97c \ud65c\uc6a9\ud558\uc5ec \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\uc790","authors":["kiara"],"tags":["jasypt","Spring"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc18c\uac1c\ud569\ub2c8\ub2e4.","permalink":"/11"},"nextItem":{"title":"Pull Request \uc2dc \uc790\ub3d9\uc73c\ub85c test \uc2e4\ud589\ud558\uae30","permalink":"/9"}},"content":"## \uc11c\ub860\\n\\n\uc548\ub155\ud558\uc138\uc694 \uce74\ud398\uc778\ud300 `\ud0a4\uc544\ub77c`\uc785\ub2c8\ub2e4.\\n\\n\uc774\ubc88 \ud504\ub85c\uc81d\ud2b8\ub97c \uc2dc\uc791\ud558\uba74\uc11c \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\ub294 \ubc29\ubc95\uc73c\ub85c jasypt\ub97c \uc54c\uac8c\ub418\uc5b4\\n\\n\uc0ac\uc6a9\ud558\ub294 \ubc29\ubc95\uc744 \uc775\ud600 \uc800\ud76c \ud504\ub85c\uc81d\ud2b8\uc5d0 \uc801\uc6a9\ud574\ubcfc \uacc4\ud68d\uc785\ub2c8\ub2e4.\\n\\n## \ud504\ub85c\ud37c\ud2f0 \uc554\ud638\ud654\ub294 \uc65c \ud544\uc694\ud560\uae4c?\\n\\n```java\\nspring:\\n datasource:\\n url: \ub370\uc774\ud130\ubca0\uc774\uc2a4 url\\n username: \uacc4\uc815\\n password: \ube44\ubc00\ubc88\ud638\\n```\\n\\n\ud504\ub85c\uc81d\ud2b8\ub97c \uc9c4\ud589\ud558\uba74\uc11c yml \ud30c\uc77c\uc5d0 DB \uc5f0\uacb0 URL\uc774\ub098 \uacc4\uc815, \ube44\ubc00\ubc88\ud638 \uac19\uc774 \ub178\ucd9c\ub418\uc5b4\uc120 \uc548 \ub418\ub294 \ubbfc\uac10\ud55c \uc815\ubcf4\ub4e4\uc774 \ub9ce\uc2b5\ub2c8\ub2e4.\\n\\ngit\uc758 public repository\uc640 CI/CD\ub97c \uc5f0\ub3d9\ud574 \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \ubc30\ud3ec\ud55c\ub2e4\uba74 \uc911\uc694\ud55c \uc815\ubcf4\uac00 \ud0c8\ucde8\ub420 \uac00\ub2a5\uc131\uc774 \uc788\uc8e0.\\n\\nJasypt \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\uba74 \ud3c9\ubb38\uc73c\ub85c \ub41c \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc811\uc18d \uc815\ubcf4\ub97c \uc554\ud638\ud654 \ud558\uc5ec \ubc29\uc5b4\ub9c9\uc744 \ud55c \uacb9 \uc313\uc744 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uac04\ub7b5\ud558\uac8c \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc18c\uac1c\ud558\uace0 \uc0ac\uc6a9 \ubc29\ubc95\uc744 \uc54c\uc544\ubcfc\uae4c\uc694?\\n\\n## jasypt\ub294 \ubb50\uc9c0?\\n\\nJasypt\uc774\ub780 \uc27d\uac8c \uc554\ud638\ud654 \uae30\ub2a5\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\ub3c4\ub85d \uc81c\uacf5\ud558\ub294 Java \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.\\n\\n\ubbfc\uac10\ud55c \ud3c9\ubb38 \uc815\ubcf4\ub97c \uc554\ud638\ud654\ud558\uace0, \uc544\ub798\ucc98\ub7fc \uc124\uc815 \uac12\uc744 \uc9c0\uc815\ud558\uba74 \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc774 \uc2e4\ud589\ub420 \ub54c \uc790\ub3d9\uc73c\ub85c \uc774\ub97c \ubcf5\ud638\ud654\ud558\uc5ec \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790\uac00 \ud3b8\ud558\uac8c \uc554\ud638\ud654 \uae30\ub2a5\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\ub3c4\ub85d \uc81c\uacf5\ud558\ub294 Java \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c\\n\\n\uacf5\uc2dd \ud648\ud398\uc774\uc9c0\ub294 http://www.jasypt.org/ \uc5d0 \uac00\uba74 \ub354 \uc790\uc138\ud55c \uc815\ubcf4\ub97c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uc0ac\uc6a9 \ubc29\ubc95\\n\\n\uc815\ub9d0 \uac04\ub2e8\ud558\uac8c \ub77c\uc774\ube0c\ub7ec\ub9ac \ucd94\uac00, key\uac12 \ub118\uaca8\uc8fc\uae30, \uc554\ud638\ud654 \uc138 \uac00\uc9c0 \ub2e8\uacc4\ub85c \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\uc5ec \uad00\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### 1. \ub77c\uc774\ube0c\ub7ec\ub9ac \ucd94\uac00 (= \uc758\uc874\uc131 \ucd94\uac00)\\n\\n```java\\nimplementation \\"com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.3\\"\\n```\\n\\n### 2. Jasypt \uc124\uc815 \ubc0f Bean \ub4f1\ub85d\\n\\nkey\ub97c \uc0ac\uc6a9\ud574\uc11c Bean\uc744 \ub4f1\ub85d\ud558\ub294 \uae30\ubcf8 \uc124\uc815\uc785\ub2c8\ub2e4. \uc5ec\uae30\uc11c Bean\uc758 \uc774\ub984\uc744 jasyptEncryptor\ub77c\uace0 \uc124\uc815\ud588\ub2e4\uba74 \ud504\ub85c\ud37c\ud2f0 \ub4f1\ub85d\ud574\uc57c \ud569\ub2c8\ub2e4.\\n\\n```java\\n@Configuration\\npublic class JasyptConfig {\\n\\n private String ENCRYPT_KEY = \\"hello\\";\\n\\n @Bean(name = \\"jasyptEncryptor\\")\\n public StringEncryptor stringEncryptor() {\\n PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();\\n\\n SimpleStringPBEConfig config = new SimpleStringPBEConfig();\\n\\n config.setPassword(ENCRYPT_KEY);\\n config.setAlgorithm(\\"PBEWithMD5AndDES\\");\\n config.setKeyObtentionIterations(1000);\\n config.setPoolSize(1);\\n config.setSaltGeneratorClassName(\\"org.jasypt.salt.RandomSaltGenerator\\");\\n config.setStringOutputType(\\"base64\\");\\n encryptor.setConfig(config);\\n return encryptor;\\n }\\n}\\n```\\n\\n```java\\njasypt:\\n encryptor:\\n bean: jasyptEncryptor\\n```\\n\\n### 3. \uc554\ud638\ud654\\n\\n\ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud560 \uc900\ube44\ub294 \uac70\uc758 \ub2e4 \ub05d\ub0ac\uc2b5\ub2c8\ub2e4. \uc774\uc81c \uc554\ud638\ud654\ud558\uc5ec \ud504\ub85c\ud37c\ud2f0\uc5d0 \uc791\uc131\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub54c \uc554\ud638\ud654 \ud558\ub294 \ubc29\ubc95\uc740, \uc544\ub798 \uc0ac\uc774\ud2b8\uc5d0 \uc811\uc18d\ud574 \ud3c9\ubb38\uacfc \ud0a4\ub97c \uc785\ub825\ud55c \ud6c4 \ub098\uc628 \uc554\ud638\ubb38\uc744 \ud504\ub85c\ud37c\ud2f0 \ud30c\uc77c\uc5d0 \'ENC(\uc554\ud638\ubb38)\' \ub85c \uc791\uc131\ud569\ub2c8\ub2e4.\\n\\n[\uc554\ubcf5\ud638\ud654 \uc0ac\uc774\ud2b8](https://www.devglan.com/online-tools/jasypt-online-encryption-decryption)\\n\\n![\ud3c9\ubb38](https://github.com/kiarakim/algorithm/assets/101039161/b0293dfc-e0d8-45a0-91af-becf790a1002)\\n\\n```java\\n datasource:\\n url: \ub370\uc774\ud130\ubca0\uc774\uc2a4 url\\n username: \uacc4\uc815\\n password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)\\n```\\n\\n\ub098\uba38\uc9c0\ub3c4 \ub9c8\uc800 \uc554\ud638\ud654\ud574\uc90d\uc2dc\ub2e4.\\n\\n```java\\n datasource:\\n url: ENC(j94r94hQbd1SfFHGCUeweg+GGDosfnxP8dL0FQxfXtE=)\\n username: ENC(vp3Gw8kLpwDZhmMMqf88/Q==)\\n password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)\\n```\\n\\n## \uc2e4\ud589\\n\\n\uc62c\ubc14\ub978 \uc554\ud638\ubb38\uc744 \uc785\ub825\ud588\ub2e4\uba74 \uc815\uc0c1\uc801\uc73c\ub85c \uc2e4\ud589\uc774 \ub429\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\ub098 \uc774\ub54c \uc784\uc758\ub85c \uc554\ud638\ubb38\uc744 \uc218\uc815\ud55c\ub2e4\uba74 \ub2e4\uc74c\uacfc \uac19\uc774 \ube4c\ub4dc\ub97c \uc2e4\ud328\ud569\ub2c8\ub2e4.\\n\\n![\uc2e4\ud589 \uc2e4\ud328](https://github.com/kiarakim/algorithm/assets/101039161/d003df00-bf4f-4ed2-a1ee-293cd7da6fc1)\\n\\n\uadf8\ub7f0\ub370 \ubb54\uac00 \uc774\uc0c1\ud558\uc9c0 \uc54a\ub098\uc694?\\n\\n\ud504\ub85c\ud37c\ud2f0\ub294 \ubd84\uba85 \uc554\ud638\ud654 \ud588\ub294\ub370 \ud0a4\uac00 \ucf54\ub4dc\uc5d0 \uadf8\ub300\ub85c \ub178\ucd9c\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\nGit\uc758 public Repository\uc5d0 \ubc30\ud3ec\ud558\uba74 \ub2e4\ub978 \uc0ac\ub78c\ub4e4\ub3c4 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \uc774 \ud0a4\ub97c \uc5b4\ub514\uc5d0 \uc228\uae38 \uc218 \uc788\uc744\uae4c\uc694?\\n\\n\uc800\ub294 \ucc98\uc74c\uc5d0 \uc77c\ubc18 file\uc5d0 \ud0a4\ub97c \ub123\uc5b4\ub193\uace0 \ud30c\uc77c\uc744 \uc77d\uc5b4\uc624\ub294 \uc2dd\uc73c\ub85c \ud0a4\ub97c \uad00\ub9ac\ud558\ub824\uace0 \ud588\uc2b5\ub2c8\ub2e4. \ub2f9\uc5f0\ud788 \ud574\ub2f9 \ud30c\uc77c\uc740 .gitignore\ub85c \ucee4\ubc0b \ub300\uc0c1\uc5d0\uc11c \uc81c\uc678\ud574\uc57c\uaca0\uc8e0.\\n\\n\uadf8\ub7f0\ub370 \uc774\uac83\ubcf4\ub2e4 \ub354 \uc27d\uace0 \ube60\ub978 \ubc29\ubc95\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ubc14\ub85c \ud658\uacbd\ubcc0\uc218\ub97c \uc124\uc815\ud558\ub294 \uac83\uc774\uc8e0.\\n\\n### + \ud658\uacbd\ubcc0\uc218 \uc124\uc815\\n\\n```java\\nprivate String ENCRYPT_KEY = \\"hello\\";\\n```\\n\uae30\uc874\uc758 \ud0a4\ub97c \uad00\ub9ac\ud558\ub294 \ubc29\uc2dd\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc6b0\uc120 \uc774 \ud0a4\ub97c \ud504\ub85c\ud37c\ud2f0\uc5d0\uc11c \uad00\ub9ac\ud558\ub3c4\ub85d \uc124\uc815\ud574\ubcfc\uae4c\uc694?\\n\\n```java\\n// JasyptConfig.class\\n@Value(\\"${jasypt.encryptor.password}\\")\\n private String ENCRYPT_KEY;\\n```\\n```java\\n// application.yml\\njasypt:\\n encryptor:\\n password: hello\\n```\\n\\n\uc774\uc81c \ud658\uacbd\ubcc0\uc218\ub97c \uc124\uc815\ud574\ubd05\uc2dc\ub2e4.\\n\\nRun > Edit Configurations... \uacbd\ub85c\ub85c \ub4e4\uc5b4\uac00\uba74\\n\\nRun/Debug Configurations \ucc3d\uc774 \ub098\uc624\ub294\ub370\\n\\nEnvironment variables: \ubd80\ubd84\uc5d0 ENCRYPT_KEY=hello\\n\\n\ub77c\uace0 \uc801\uc5b4\uc8fc\uc138\uc694.\\n\\n\uadf8 \ud6c4 \ub2e4\uc2dc yml \ud30c\uc77c\ub85c \ub3cc\uc544\uc640 \uae30\uc874 hello\ub85c \ub418\uc5b4\uc788\ub294 \ubd80\ubd84\uc744 ${ENCRYPT_KEY}\ub85c \ubcc0\uacbd\ud558\uace0 \uc2e4\ud589\ud55c\ub2e4\uba74 \uc815\uc0c1\uc801\uc73c\ub85c \uc791\ub3d9\ub429\ub2c8\ub2e4.\\n\\n```java\\njasypt:\\n encryptor:\\n password: ${ENCRYPT_KEY}\\n```\\n\\n\uae34 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4."},{"id":"9","metadata":{"permalink":"/9","source":"@site/blog/2023-07-09-github_actions_pull_request_test.mdx","title":"Pull Request \uc2dc \uc790\ub3d9\uc73c\ub85c test \uc2e4\ud589\ud558\uae30","description":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4.","date":"2023-07-09T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 9\uc77c","tags":[{"label":"github","permalink":"/tags/github"},{"label":"action","permalink":"/tags/action"},{"label":"pr","permalink":"/tags/pr"},{"label":"test","permalink":"/tags/test"}],"readingTime":8.985,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"9","title":"Pull Request \uc2dc \uc790\ub3d9\uc73c\ub85c test \uc2e4\ud589\ud558\uae30","authors":["boxster"],"tags":["github","action","pr","test"]},"prevItem":{"title":"jasypt\ub97c \ud65c\uc6a9\ud558\uc5ec \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\uc790","permalink":"/12"},"nextItem":{"title":"webpack\uc73c\ub85c msw \uc124\uc815\ud558\uae30","permalink":"/10"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4.\\n## Pull Request\uc2dc \uc790\ub3d9\uc73c\ub85c test\ub97c \uc2e4\ud589\ud558\uba74 \uc88b\uc740 \uc810\\npull request \uc0dd\uc131 \uc2dc \uc790\ub3d9\uc73c\ub85c \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub824\uc900\ub2e4\uba74 \ub2e4\ub978 \ud300\uc6d0\uc758 pr\uc744 \uad73\uc774 \uc81c \ub85c\uceec\uc5d0 clone\ud558\uc5ec \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub824\ubcf4\uc9c0 \uc54a\uc544\ub3c4 \ub429\ub2c8\ub2e4.\\n\ub9ce\uc740 \uc2dc\uac04\uc744 \ub2e8\ucd95\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 test\uac00 \uc2e4\ud328\ud55c\ub2e4\uba74 \uac15\uc81c\ub85c Merge\uac00 \ub418\uc9c0 \uc54a\ub3c4\ub85d \ud55c\ub2e4\uba74 \uc2e4\uc218\ub85c \ud14c\uc2a4\ud2b8\uac00 \ub418\uc9c0 \uc54a\ub294 \ucee4\ubc0b\uc744 \uc62c\ub9ac\ub294 \uac83\uc744 \ubc29\uc9c0\ud560 \uc218 \uc788\uaca0\uc8e0.\\n\\n\uc774 \ub450\uac00\uc9c0\ub9cc\uc73c\ub85c\ub3c4 \uc0dd\uc0b0\uc131\uc774 \ub9ce\uc774 \uc62c\ub77c\uac08 \uac83\uc744 \uae30\ub300\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uc5b4\ub5bb\uac8c \ud560 \uc218 \uc788\ub098\uc694\\n\\nGithub Action\uc744 \uc774\uc6a9\ud558\uc5ec \uc124\uc815\ud55c \uc870\uac74\uc5d0 \ub9de\ub294 \uc0c1\ud669\uc5d0\uc11c \uba85\ub839\uc5b4\ub97c \uc2e4\ud589\ud558\uc5ec test\ub97c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### Github Action \ud30c\uc77c \uc0dd\uc131\\n\\n1. \uba3c\uc800 \ucd5c\uc0c1\uc704 \ud3f4\ub354\uc5d0 `.github/workflows` \ud3f4\ub354\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.\\n2. \ud574\ub2f9 \ud3f4\ub354 \ub0b4\uc5d0 `example.yml`\uc744 \uc0dd\uc131\ud569\ub2c8\ub2e4.\\n3. \uc544\ub798\uc640 \uac19\uc774 yml \ud30c\uc77c\uc744 \uc791\uc131\ud569\ub2c8\ub2e4.\\n\\n```yml\\nname: pr test\\n\\non:\\n pull_request:\\n branches:\\n - main\\n - develop\\n\\npermissions:\\n contents: read\\n\\njobs:\\n test:\\n name: merge-test\\n runs-on: ubuntu-latest\\n environment: test\\n defaults:\\n run:\\n working-directory: ./backend\\n steps:\\n - uses: actions/checkout@v3\\n - name: Set up JDK 17\\n uses: actions/setup-java@v3\\n with:\\n java-version: \'17\'\\n distribution: \'adopt\'\\n - name: Grant execute permission for gradlew\\n run: chmod +x gradlew\\n - name: Test with Gradle\\n run: ./gradlew build\\n```\\n\\n### Job \uc774\ub984 \uc124\uc815\\n\ubcf5\uc7a1\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uba3c\uc800 **name** \uc18d\uc131\uc740 github action\uc5d0\uc11c \ubcf4\uc5ec\uc9c8 Job\uc758 \uc774\ub984\uc744 \uc815\ud558\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4.\\n\\n\uc9c0\uae08\uc740 `pr test`\ub85c \ud574\ub450\uc5c8\uc2b5\ub2c8\ub2e4. \uadf8\ub7fc \uc544\ub798 \uc0ac\uc9c4\uacfc \uac19\uc774 \ubc18\uc601\ub429\ub2c8\ub2e4.\\n\\n![workflows name](https://github.com/car-ffeine/car-ffeine.github.io/assets/106640954/28494d8e-66b5-4eec-a98a-414968b03306)\\n\\n### workflow \ud2b8\ub9ac\uac70 \uc124\uc815\\n\ub2e4\uc74c\uc73c\ub860 `on` \uc18d\uc131\uc785\ub2c8\ub2e4. \uc774 \uc18d\uc131\uc740 workflow\ub97c \uc2e4\ud589\ud560 \uc774\ubca4\ud2b8\ub97c \uc9c0\uc815\ud558\ub294\ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \ud2b9\uc815 \uc774\ubca4\ud2b8 \uc720\ud615\uacfc \uc870\uac74\uc744 \uae30\ubc18\uc73c\ub85c workflow\ub97c \ud2b8\ub9ac\uac70\ud558\ub3c4\ub85d \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uc5b4 \uc544\ub798\uc640 \uac19\uc774 \uc815\uc758\ud588\uc2b5\ub2c8\ub2e4.\\n```yml\\non:\\n push:\\n branches:\\n - main\\n pull_request:\\n branches:\\n - develop\\n```\\n\uadf8\ub807\ub2e4\uba74 \uc774 workflow\uac00 \uc791\ub3d9\ub418\ub294 \uc2dc\uc810\uc740 `main` \ube0c\ub79c\uce58\uc5d0 **push**\uac00 \ub418\uac70\ub098 `develop` \ube0c\ub79c\uce58\uc5d0 **pull request**\ub97c \ubcf4\ub0bc \ub54c \uc791\ub3d9\ud569\ub2c8\ub2e4.\\n\\n### \uad8c\ud55c \ubd80\uc5ec\\n```yml\\npermissions:\\n contents: read\\n```\\n\uc774\ub7f0 \uad8c\ud55c\uc744 \uc8fc\uac8c \ub41c\ub2e4\uba74 \uc774 job\uc740 \uc77d\uae30 \uad8c\ud55c\ubc16\uc5d0 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \uc2e4\uc218\ub85c \ub2e4\ub978 \uac83\uc744 \ucd94\uac00\ud558\uc9c0 \ubabb\ud558\uac8c \ub9c9\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n### \ub3d9\uc791\ud560 \uba85\ub839\uc5b4 \uc785\ub825\\n```yml\\njobs:\\n test:\\n name: merge-test\\n runs-on: ubuntu-latest\\n environment: test\\n defaults:\\n run:\\n working-directory: ./backend\\n steps:\\n - uses: actions/checkout@v3\\n - name: Set up JDK 17\\n uses: actions/setup-java@v3\\n with:\\n java-version: \'17\'\\n distribution: \'adopt\'\\n - name: Grant execute permission for gradlew\\n run: chmod +x gradlew\\n - name: Test with Gradle\\n run: ./gradlew build\\n```\\n\\n#### name\\n\uc81c\uc77c \uac04\ub2e8\ud788 \ubcfc \uc218 \uc788\ub294 **name** \uc124\uc815\uc740 \uc544\ub798 \uc0ac\uc9c4\ucc98\ub7fc \uc5b4\ub5a4\uc2dd\uc73c\ub85c \ubcf4\uc5ec\uc904\uc9c0 \uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![job image](https://github.com/car-ffeine/car-ffeine.github.io/assets/106640954/43ebf3c1-4632-447f-89c3-0e74ed01dc3c)\\n\\n#### runs-on\\n**runs-on** \uc18d\uc131\uc785\ub2c8\ub2e4. \ud574\ub2f9 \uc6b4\uc601\uccb4\uc81c\ub97c \uc0ac\uc6a9\ud55c\ub2e4\uace0 \uc815\uc758\ud558\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4. \uc9c0\uae08\uc740 \uc800\ud76c\uac00 \uc0ac\uc6a9\ud560 ec2\uc640 \uac19\uc740 \ud658\uacbd\uc778 `ubuntu`\uc5d0\uc11c \uc791\ub3d9\ud558\ub3c4\ub85d \uc124\uc815\ud588\uc9c0\ub9cc,\\n`windows-latest`, `macos-latest`\ub85c \ubcc0\uacbd\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### environment\\n**environment** \uc18d\uc131\uc785\ub2c8\ub2e4. \ud574\ub2f9 \uc18d\uc131\uc740 \uaf2d \ud544\uc694\ud55c \ubd80\ubd84\uc774 \uc544\ub2c8\uc9c0\ub9cc branch\uc758 rule \uc124\uc815\uc5d0 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ud658\uacbd\uc744 \ud55c\uaebc\ubc88\uc5d0 \uad00\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \ubd80\ubd84\uc740 \uc544\ub798\uc5d0 branch rule\uc744 \uc815\ud558\ub294 \ubd80\ubd84\uc744 \ubcf4\uc2dc\uba74 \uc544\ub9c8 \uc774\ud574\uac00 \ub420 \uac83 \uc785\ub2c8\ub2e4.\\n\\n#### defaults\\n\ud574\ub2f9 \uc18d\uc131\uc740 \uc5b4\ub5a4 \ud3f4\ub354\uc5d0\uc11c \uba85\ub839\uc5b4\ub97c \uc2e4\ud589\ud560 \uc9c0 \uc9c0\uc815\ud569\ub2c8\ub2e4. \uc9c0\uae08\uc758 \uc800\ud76c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c\ub294 \ud55c repository\uc5d0 backend, frontend \ud3f4\ub354\ub97c \ub098\ub204\uc5c8\uae30 \ub54c\ubb38\uc5d0 backend \ud3f4\ub354\ub85c \uc774\ub3d9\ud558\uc5ec \uba85\ub839\uc5b4\ub97c \uc2e4\ud589\ud574\uc57c \ud569\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c **working-directory**\ub97c `./backend`\ub77c\uace0 \uc9c0\uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n#### steps\\n\\n\uc81c\uc77c \uc911\uc694\ud55c **steps**\uc785\ub2c8\ub2e4. \ud574\ub2f9 \uc18d\uc131\uc740 \uc5b4\ub5a4 \uba85\ub839\uc5b4\ub97c \uc5b4\ub5a4 \uc21c\uc11c\ub85c \uc2e4\ud589\uc2dc\ud0ac\uc9c0 \uc815\uc758\ud569\ub2c8\ub2e4. \uc9c0\uae08\uc758 workflow\uc5d0\uc120\\n\\n1. Java 17 \uc124\uce58\\n2. gradlew \ud30c\uc77c\uc5d0 \uc2e4\ud589 \uad8c\ud55c \ubd80\uc5ec\\n3. gradle build \uc2e4\ud589\\n\\n\uc21c\uc73c\ub85c \ub3d9\uc791\ud569\ub2c8\ub2e4.\\n\\n### \ub2e4\ub978 \uc870\uac74\uacfc \uc774\ubca4\ud2b8\ub3c4 \ucd94\uac00\ud558\uace0 \uc2f6\uc5b4\uc694\\n\\n\uc800\ud76c \ud504\ub85c\uc81d\ud2b8\ub294 \ud558\ub098\uc758 repository\uc5d0\uc11c frontend, backend \ucf54\ub4dc\ub97c \uac19\uc774 \uad00\ub9ac\ud558\ub294 \uc0c1\ud669\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc frontend \ucf54\ub4dc\ub97c \uc218\uc815\ud588\ub2e4\uace0 java \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub9ac\ub294 \uac83\uc740 \uc624\ud788\ub824 \uc0dd\uc0b0\uc131\uc774 \uc904\uc5b4\ub4e4\uaca0\uc8e0.\\n\\n\uadf8\ub9ac\uace0 frontend\ub3c4 \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub9ac\uace0 \uc2f6\uc9c0\ub9cc gradle\uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7f4 \ub54c \uac04\ub2e8\ud55c \uc18d\uc131\uc744 \ucd94\uac00\ud558\uba74 **\ud30c\uc77c\uc758 \ubcc0\uacbd\uc5d0 \ub530\ub77c \ud574\ub2f9 job\uc744 \uc2e4\ud589\ud560 \uc870\uac74**\uc744 \uc815\uc758\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```yml\\non:\\n pull_request:\\n branches:\\n - main\\n - develop\\n paths:\\n - backend/**\\n - .github/**\\n```\\n\uc704\uc640 \ub2ec\ub9ac \uc9c0\uae08 **pull request**\uc5d0\ub294 \uc18d\uc131\uc774 \ud558\ub098\uac00 \ub354 \uc788\ub294\ub370\uc694. **paths**\ub97c \uc801\uc6a9\ud558\uba74 `backend` \ud3f4\ub354 \ud558\uc704\uc758 \ubb34\uc5b8\uac00 \ubcc0\uacbd\uc774 \uc788\ub294 **pull request**\uc5d0\ub9cc \uc791\ub3d9\uc744 \ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc backend\uc758 workflow \ud30c\uc77c\uc5d0 **paths** \uc18d\uc131\uc744 \ud558\ub098 \ucd94\uac00\ud558\uace0, \ube44\uc2b7\ud55c frontend workflow\ub97c \ub9cc\ub4e4\uc5b4\uc8fc\uba74 \ub418\uaca0\uc8e0.\\n```yml\\nname: frontend test\\n\\non:\\n pull_request:\\n branches:\\n - main\\n - develop\\n paths:\\n - frontend/**\\n\\npermissions:\\n contents: read\\n\\njobs:\\n test:\\n name: jest\\n runs-on: ubuntu-latest\\n environment: test\\n defaults:\\n run:\\n working-directory: ./frontend\\n steps:\\n - uses: actions/checkout@v3\\n - name: NPM Install\\n run: npm i\\n - name: Jest run\\n run: npm run test\\n```\\n\uc774\ub7f0 \uc2dd\uc73c\ub85c yml \ud30c\uc77c\uc744 \ud558\ub098 \ucd94\uac00\ud558\uba74 **frontend\uc758 \uc218\uc815\uc774 \uc77c\uc5b4\ub0a0 \ub54c\ub294 jest**\ub97c \uc2e4\ud589\ud558\uace0, **backend \ud3f4\ub354\uc758 \uc218\uc815\uc774 \uc77c\uc5b4\ub098\uba74 gradlew**\ub97c \uc2e4\ud589\ud558\uac8c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## Test\uac00 \uc2e4\ud328\ud558\ub294 PR\uc740 Merge \ub9c9\uae30\\n\\nTest\uac00 \uc2e4\ud328\ud558\ub294 Pull Request\uac00 Merge \ub418\ub294 \uc77c\uc740 \uc808\ub300\ub85c \uc5c6\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uadf8\ub7f0 \uc2e4\uc218\ub97c \ubc29\uc9c0\ud558\ub824\uba74 \ud300\uc6d0 \uc804\ubd80\uac00 \ub9ac\ubdf0\ud560 \ub54c \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub824\ubd10\uc57c\ud558\ub294 \uadc0\ucc2e\uc74c\uc774 \uc0dd\uae38 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc0ac\ub78c\uc740 \uc2e4\uc218\ud574\ub3c4 \uae30\uacc4\ub294 \uac70\uc9d3\ub9d0\uc744 \ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc790\ub3d9\uc73c\ub85c \ub9c9\ub3c4\ub85d \ub3d9\uc791\ud558\uac8c \ub9cc\ub4e4\uc5b4\ub193\uc73c\uba74 \uadf8\ub7f4 \uc77c\uc744 \ubbf8\uc5f0\uc5d0 \ubc29\uc9c0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### Environments \ud655\uc778\ud558\uae30\\n\uba3c\uc800 \ud574\ub2f9 Repository\uc758 Settings -> Environments \ud0ed\uc73c\ub85c \ub4e4\uc5b4\uac11\ub2c8\ub2e4.\\n![environments](https://github.com/car-ffeine/car-ffeine.github.io/assets/106640954/4e3e867a-1037-46bc-865a-6d7d52527518)\\n\uc544\uae4c **environment** \uc18d\uc131\uc744 \ubcf4\uba74 `test`\ub77c\uace0 \uc124\uc815\ud574\ub193\uc740 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ud658\uacbd\uc774 \uc5ec\uae30\uc5d0 \uc801\uc6a9\ub429\ub2c8\ub2e4.\\n\\n### Branch rule \uc815\uc758\ud558\uae30\\n\uc774\ubc88\uc5d0\ub294 \ud574\ub2f9 Repository\uc758 Settings -> Branches \ud0ed\uc73c\ub85c \ub4e4\uc5b4\uac11\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc6d0\ud558\ub294 branch\uc5d0 \ub4e4\uc5b4\uac00 `edit` \ubc84\ud2bc\uc744 \ub204\ub985\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc0ac\uc9c4\uacfc \uac19\uc774 **Require deployments to succeed before merging** \uc18d\uc131\uc744 \ud074\ub9ad\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc544\ub798\uc640 \uac19\uc774 \uc5b4\ub5a4 \ud658\uacbd\uc744 \uc801\uc6a9\ud560 \uac83\uc778\uc9c0 \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uc18d\uc131\uc740 \ud574\ub2f9 \ubc30\ud3ec\uac00 \uc131\uacf5\ud574\uc57c merge \ud560 \uc218 \uc788\ub3c4\ub85d \ube0c\ub79c\uce58\ub97c \ubcf4\ud638\ud558\ub294 \uae30\ub2a5\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc800\ud76c\ub294 frontend\uc640 backend Job\uc758 \ud658\uacbd\uc744 \ub458 \ub2e4 test\ub77c\ub294 \uc774\ub984\uc73c\ub85c \uc815\uc758\ud588\uae30 \ub54c\ubb38\uc5d0 \ud558\ub098\uc758 environment\ub9cc \uc120\ud0dd\ud574\ub3c4 \ub458 \ub2e4 \uc801\uc6a9\ub418\ub294 \ud6a8\uacfc\ub97c \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n![branch rule](https://github.com/car-ffeine/car-ffeine.github.io/assets/106640954/02be4679-56a2-4e47-ae01-b7025f8778a4)\\n\\n#### \uc801\uc6a9 \ud6c4\\n\\n\uc544\ub798\uc640 \uac19\uc774 merge\uac00 \uc548\ub41c\ub2e4\ub294 \uae00\uacfc \ube68\uac04\uc0c9\uc73c\ub85c \uacbd\uace0 \ud45c\uc2dc\ub97c \ud574\uc8fc\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n![blocked](https://github.com/car-ffeine/car-ffeine.github.io/assets/106640954/7dfba566-c8c8-4a24-a0e3-42081f3af31c)\\n\\n\\n## \uacb0\ub860\\n\\n\uac04\ub2e8\ud55c github action\uc744 \ud1b5\ud574\uc11c \uc0dd\uc0b0\uc131\uc744 \ub9ce\uc774 \uc62c\ub9b4 \uc218 \uc788\ub294 \uc88b\uc740 \uae30\ub2a5\uc778 \uac83 \uac19\uc2b5\ub2c8\ub2e4. \ub2e4\ub978 \ud300\ub4e4\ub3c4 \uc774 \uae30\ub2a5\uc744 \ub3c4\uc785\ud558\uc5ec \uc0ac\uc6a9\ud558\ub294 \uac83\uc744 \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4."},{"id":"10","metadata":{"permalink":"/10","source":"@site/blog/2023-07-09-msw-setup-with-webpack.mdx","title":"webpack\uc73c\ub85c msw \uc124\uc815\ud558\uae30","description":"\uc6f9\ud329\uc5d0\uc11c msw \uc124\uc815","date":"2023-07-09T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 9\uc77c","tags":[{"label":"msw","permalink":"/tags/msw"},{"label":"webpack","permalink":"/tags/webpack"}],"readingTime":4.35,"hasTruncateMarker":false,"authors":[{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"}],"frontMatter":{"slug":"10","title":"webpack\uc73c\ub85c msw \uc124\uc815\ud558\uae30","authors":["scent"],"tags":["msw","webpack"]},"prevItem":{"title":"Pull Request \uc2dc \uc790\ub3d9\uc73c\ub85c test \uc2e4\ud589\ud558\uae30","permalink":"/9"},"nextItem":{"title":"\uc2a4\ud504\ub9c1\uc5d0\uc11c \ubc1c\uc0dd\ud55c \uc5d0\ub7ec \ub85c\uadf8\ub97c \uc2ac\ub799\uc73c\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95","permalink":"/8"}},"content":"## \uc6f9\ud329\uc5d0\uc11c msw \uc124\uc815\\n\\n\uc774\ubc88 \ud300 \ud504\ub85c\uc81d\ud2b8\ub294 CRA\uc640 \uac19\uc740 \ubcf4\uc77c\ub7ec \ud50c\ub808\uc774\ud2b8 \ucf54\ub4dc\ub97c \uc0ac\uc6a9\ud558\uc9c0 \ubabb\ud558\uac8c \uc81c\ud55c\uc774 \uc788\ub2e4. \ub610\ud55c \uc694\uc998 \ub9ce\uc774 \uc0ac\uc6a9\ub41c\ub2e4\ub294 Vite\uc758 \uc0ac\uc6a9\ub3c4 \uc81c\ud55c\uc774 \uc788\uace0, \uc6f9\ud329\uc73c\ub85c \ud504\ub85c\uc81d\ud2b8\ub97c \uc2dc\uc791\ud558\ub3c4\ub85d \uac15\uc81c\ud558\uace0 \uc788\ub2e4.\\n\\n\ud300\uc6d0 \ubaa8\ub450 \ud55c \ubc88\ub3c4 \uc6f9\ud329\uc744 \ud1b5\ud574 \ud504\ub85c\uc81d\ud2b8\ub97c \uc2dc\uc791\ud574\ubcf8 \uacbd\ud5d8\uc774 \uc5c6\uc5b4 \ud504\ub860\ud2b8\uc5d4\ub4dc \ud300\uc6d0 \uac01\uc790 \uac1c\uc778 \ub808\ud3ec\uc5d0\uc11c \uc6f9\ud329 \uacf5\ubd80\ub97c \uc9c4\ud589\ud55c \ud6c4 \uc5b4\ub290\uc815\ub3c4 \uc9c4\ucc99\uc774 \uc788\uc744 \ub54c \ud300 \ub808\ud3ec\uc5d0 \ud504\ub85c\uc81d\ud2b8\ub97c \uc2dc\uc791\ud558\uae30\ub85c \ud588\ub2e4.\\n\\n\ub2e4\ud589\ud788 \uc6f9\ud329\uc73c\ub85c \uc2dc\uc791\ud558\ub294 \ud504\ub85c\uc81d\ud2b8\uc5d0 \ub300\ud55c \ub9ce\uc740 \ucc38\uace0 \uc790\ub8cc\ub4e4\uc774 \uc788\uc5b4 \uccab \ub9ac\uc561\ud2b8 \ud504\ub85c\uc81d\ud2b8 \ud654\uba74\uc744 \ub744\uc6b0\ub294\ub370 \uae4c\uc9c0\ub294 \uadf8\ub9ac \uc624\ub79c \uc2dc\uac04\uc774 \uac78\ub9ac\uc9c0 \uc54a\uc558\ub2e4. \uadf8\ub807\uac8c \ubaa8\ub4e0 \ud300\uc6d0\uc774 \uccab \uc6f9\ud329 \ud504\ub85c\uc81d\ud2b8\ub97c \uc131\uacf5\uc2dc\ud0a8 \ud6c4 \ubaa8\uc5ec \ud300 \ud504\ub85c\uc81d\ud2b8 \ucd08\uae30 \uc124\uc815\uc744 \uc2dc\uc791\ud574\ubcf4\uc558\ub2e4.\\n\\neslint, prettier, \uc6f9\ud329 \ub4f1\ub4f1 \uc5ec\ub7ec \uc124\uc815\ub4e4\uc744 \ud558\uace0 \ud544\uc694\ud55c \ud328\ud0a4\uc9c0\ub97c \uc124\uce58\ud558\ub294\ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\ub2e4. \ud070 \ub370\uc774\ud130\ub97c \ub2e4\ub8e8\ub294 \ubc31\uc5d4\ub4dc\uc758 \uac1c\ubc1c \uc18d\ub3c4\ub97c \uace0\ub824\ud574 \ud504\ub860\ud2b8\uc5d4\ub4dc \uac1c\ubc1c\uc744 \uc9c4\ud589\ud558\uae30 \uc704\ud574\uc11c \ubbf8\uc158\uc911\uc5d0 \ubc30\uc6e0\ub358 MSW \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\uae30\ub85c \uacb0\uc815\ud588\ub294\ub370, \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \uc6b0\ub9ac \ud300\uc758 \uac1c\ubc1c \ud658\uacbd\uc5d0\uc11c \ub3d9\uc791\ud558\uc9c0 \uc54a\uc558\ub2e4.\\n\\n\uc65c \ub3d9\uc791\ud558\uc9c0 \uc54a\ub294\uc9c0 \uc6d0\uc778\uc744 \ucc3e\uc544\ubcf4\ub2c8 MSW service worker \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\ub2e4\ub294 \uc624\ub958 \uba54\uc138\uc9c0\uac00 \ub098\uc624\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc5c8\ub2e4. \uc6d0\uc778\uc744 \ub354 \uc790\uc138\ud788 \uc54c\uc544\ubcf4\ub2c8 public \ud3f4\ub354\uc5d0 \uc788\ub294 \ud30c\uc77c\ub4e4\uc740 \uc6f9\ud329\uc774 \ubc88\ub4e4\ub9c1\uc744 \uc9c4\ud589\ud560 \ub54c \ud3ec\ud568\uc774 \ub418\uc9c0 \uc54a\ub294\ub2e4\ub294 \uac83\uc744 \uc54c \uc218 \uc788\uc5c8\uace0, \uc774\ub97c \uc5b4\ub5bb\uac8c \ud574\uacb0\ud560 \uc9c0 \ud300\uc6d0\ub4e4\uacfc \ubc29\ubc95\uc744 \ucc3e\uc544\ubcf4\uc558\ub2e4.\\n\\n\uc57d \ud55c\uc2dc\uac04\ucbe4 \uc9c0\ub0ac\uc744 \ubb34\ub835 copy-webpack-plugin \ud328\ud0a4\uc9c0\ub97c \ud1b5\ud574 public \uacbd\ub85c\uc5d0 \uc788\ub294 \ud30c\uc77c\ub4e4\ub3c4 \ube4c\ub4dc \ud3f4\ub354\uc5d0 \ud3ec\ud568\uc2dc\ud0ac \uc218 \uc788\ub2e4\ub294 \uac83\uc744 \uc54c\uac8c \ub418\uc5c8\ub2e4. \ud558\uc9c0\ub9cc \uc774 copy-webpack-plugin\uc5d0 \ub300\ud55c \uc0ac\uc6a9\ubc95\uc774 \ubbf8\uc219\ud574 public \ud3f4\ub354\uc5d0 \uc788\ub294 mockServiceWorker.js \ud30c\uc77c\ub9cc \ube4c\ub4dc \ud3f4\ub354\ub85c \uc62e\uacbc\uc5b4\uc57c \ud588\ub294\ub370 index.html\uacfc \uac19\uc740 \ub2e4\ub978 \ud30c\uc77c\ub4e4 \uae4c\uc9c0 \ud55c\uaebc\ubc88\uc5d0 \ube4c\ub4dc \ud3f4\ub354\ub85c \uc62e\uaca8\uc9c0\uac8c \ub418\uc5c8\ub2e4.\\n\\n\uc774\ub7f0 \uc800\ub7f0 \ubc29\ubc95\ub4e4\uc744 \uc2dc\ub3c4\ud574\ubcf4\ub2e4 webpack.config.js \ud30c\uc77c\uc758 plugins\uc5d0 \uc544\ub798\uc640 \uac19\uc740 \uc124\uc815\uc744 \ucd94\uac00 \ud574\uc8fc\uc5b4 MSW\ub97c \ud504\ub85c\uc81d\ud2b8\uc5d0 \uc801\uc6a9\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\ub2e4.\\n\\n```jsx\\nnew CopyWebpackPlugin({\\n patterns: [\\n { from: \'public/mockServiceWorker.js\', to: \'.\' }, // msw service worker\\n ],\\n}),\\n```\\n\\n\uc124\uc815\uc744 \uac04\ub2e8\ud788 \ubcf4\uba74 public \uacbd\ub85c\uc5d0 \uc788\ub294 mockServiceWorker.js \ud30c\uc77c\uc744 \ube4c\ub4dc \ud6c4 \ud3f4\ub354\uc758 \ub8e8\ud2b8 \ub514\ub809\ud1a0\ub9ac\uc5d0 \ucd94\uac00\ud574\uc900\ub2e4\ub294 \uc124\uc815\uc774\ub2e4.\\n\\n\ubb38\uc81c \uc0c1\ud669\uacfc \ud574\uacb0 \ubc29\ubc95\uc744 \uac04\ub2e8\ud558\uac8c \ub2e4\uc2dc \uc815\ub9ac\ud574\ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n1. MSW\ub97c \uc801\uc6a9\ud574\ubcf4\ub824\uace0 \ud568.\\n2. \uc6f9\ud329\uc5d0\uc11c \uac1c\ubc1c \uc11c\ubc84\ub97c \uc5f4\uc5c8\uc744 \ub54c MSW \uc2e4\ud589\uc744 \uc704\ud574 \ud544\uc694\ud55c mockServiceWorker.js \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\ub2e4\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud568.\\n3. \ubb38\uc81c\uc758 \uc6d0\uc778\uc740 \uc6f9\ud329\uc5d0\uc11c \ubc88\ub4e4\ub9c1\uc744 \uc9c4\ud589\ud560 \ub54c public \ud3f4\ub354 \ud558\uc704 \uacbd\ub85c\uc5d0 \uc788\ub294 \ud30c\uc77c\ub4e4\uc744 \ubb34\uc2dc\ud558\uae30 \ub54c\ubb38\uc774\uc5c8\uc74c.\\n4. \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574 public \uacbd\ub85c\uc5d0 \uc788\ub294 mockServiceWorker.js \ud30c\uc77c\uc744 \ubc88\ub4e4\ub9c1 \ud6c4 \ud3f4\ub354\uc758 \ub8e8\ud2b8 \ub514\ub809\ud1a0\ub9ac\uc5d0 \uc800\uc7a5\ud558\ub3c4\ub85d \ud558\ub294 \uc124\uc815\uc744 \ucd94\uac00\ud574\uc90c."},{"id":"8","metadata":{"permalink":"/8","source":"@site/blog/2023-07-07-error-slack-notification.mdx","title":"\uc2a4\ud504\ub9c1\uc5d0\uc11c \ubc1c\uc0dd\ud55c \uc5d0\ub7ec \ub85c\uadf8\ub97c \uc2ac\ub799\uc73c\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95","description":"\uc548\ub155\ud558\uc138\uc694 \uce74\ud398\uc778\ud300 nunu\uc785\ub2c8\ub2e4.","date":"2023-07-07T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 7\uc77c","tags":[{"label":"spring","permalink":"/tags/spring"},{"label":"slack","permalink":"/tags/slack"},{"label":"error","permalink":"/tags/error"}],"readingTime":11.83,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"8","title":"\uc2a4\ud504\ub9c1\uc5d0\uc11c \ubc1c\uc0dd\ud55c \uc5d0\ub7ec \ub85c\uadf8\ub97c \uc2ac\ub799\uc73c\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95","authors":["nunu"],"tags":["spring","slack","error"]},"prevItem":{"title":"webpack\uc73c\ub85c msw \uc124\uc815\ud558\uae30","permalink":"/10"},"nextItem":{"title":"\uae43 \ucee4\ubc0b \uba54\uc2dc\uc9c0\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud560 \uc21c \uc5c6\uc744\uae4c?","permalink":"/7"}},"content":"\uc548\ub155\ud558\uc138\uc694 \uce74\ud398\uc778\ud300 nunu\uc785\ub2c8\ub2e4.\\n\\n\uc624\ub298\uc740 \uc2a4\ud504\ub9c1\uc5d0\uc11c \ubc1c\uc0dd\ud55c \uc5d0\ub7ec \ub85c\uadf8\ub97c \uc2ac\ub799\uc73c\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95\uc5d0 \ub300\ud574\uc11c \uc54c\uc544\ubcf4\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\ubaa9\ucc28\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc2a4\ud504\ub9c1\uc5d0\uc11c \ub85c\uadf8\ub97c \ub0a8\uae30\ub294 \ubc29\ubc95\\n2. Slf4 j\uc758 \ub3d9\uc791\uc6d0\ub9ac\\n3. Logback\uc758 \ub3d9\uc791\uc6d0\ub9ac\\n4. Logback\uc744 \uc0ac\uc6a9\ud574\uc11c \uc2ac\ub799\uc73c\ub85c \uc5d0\ub7ec \ub85c\uadf8\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95\\n\\n## \uc2a4\ud504\ub9c1\uc5d0\uc11c \ub85c\uadf8\ub294 \uc5b4\ub5bb\uac8c \ucc0d\uc744\uae4c?\\n\\n\uc2a4\ud504\ub9c1\uc5d0\uc11c \ub85c\uadf8\ub97c \ucc0d\ub294 \ubc29\ubc95\uc740 \uc5ec\ub7ec \uac00\uc9c0\uac00 \uc788\uc9c0\ub9cc, \uac00\uc7a5 \uac04\ub2e8\ud55c \ubc29\ubc95\uc740 `System.out.println()`\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc785\ub2c8\ub2e4.\\n\\n```java\\n@RestController\\npublic class TestController {\\n\\n @GetMapping(\\"/test\\")\\n public String test() {\\n System.out.println(\\"test\\");\\n return \\"test\\";\\n }\\n}\\n```\\n\\n\ub2f9\uc5f0\ud558\uc9c0\ub9cc, \uc131\ub2a5\uc774 \uc548 \uc88b\uc544\uc11c \uc2e4\uc81c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\uc2a4\ud504\ub9c1\uc5d0\uc11c\ub294 Slf4 j\ub97c \ud1b5\ud574\uc11c \ub85c\uadf8\ub97c \ub0a8\uae38 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Slf4j // private final Logger log = LoggerFactory.getLogger(this.getClass()); \uc640 \uac19\ub2e4.\\n@RestController\\npublic class TestController {\\n\\n @GetMapping(\\"/test\\")\\n public String test() {\\n log.info(\\"test\\");\\n return \\"test\\";\\n }\\n}\\n```\\n\\n\uc774 \ucf54\ub4dc\ub97c \ud1b5\ud574\uc11c \ub85c\uadf8\ub97c \ub0a8\uae38 \uc218 \uc788\ub294\ub370, \uc790\ub3d9\uc73c\ub85c \ucf58\uc194\uc5d0 \ucd9c\ub825\uc774 \ub429\ub2c8\ub2e4.\\n\\n## \uc2a4\ud504\ub9c1\uc5d0\uc11c \ub85c\uae45\uc740 \uc5b4\ub5bb\uac8c \uc791\ub3d9\ud558\ub294 \uac70\uc9c0?\\n\\n\uc2a4\ud504\ub9c1 4\uae4c\uc9c0\ub294 `Commons Logging`\uc744 \uc0ac\uc6a9\ud588\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n`Commons Logging`\uc740 `JCL`\uc774\ub77c\uace0\ub3c4 \ubd88\ub9ac\uba70, `JDK Logging`, `Log4 j,` `Logback` \ub4f1 \ub2e4\uc591\ud55c \ub85c\uae45 \ud504\ub808\uc784\uc6cc\ud06c\ub97c \uc9c0\uc6d0\ud569\ub2c8\ub2e4.\\n\\nJCL \uc740 \ub7f0\ud0c0\uc784\uc5d0 \uc5b4\ub5a4 \ub85c\uae45 \ud504\ub808\uc784\uc6cc\ud06c\ub97c \uc0ac\uc6a9\ud560\uc9c0 \uacb0\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub7f0\ud0c0\uc784\uc5d0 \uc5b4\ub5a4 \ub85c\uae45 \ud504\ub808\uc784\uc6cc\ud06c\ub97c \uc0ac\uc6a9\ud560\uc9c0 \uacb0\uc815\ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \ud074\ub798\uc2a4 \ub85c\ub354\uc5d0\uac8c \uc9c8\uc758\ub97c \ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \uc791\ub3d9\ud558\uac8c \ub418\ub294\ub370\\n\\n\ud074\ub798\uc2a4 \ub85c\ub354\uc5d0\uac8c \uc9c8\uc758\ub97c \ud588\uc744 \uacbd\uc6b0\uc5d0 \uba87 \uac00\uc9c0 \ubb38\uc81c\uc810\uc774 \uc0dd\uae41\ub2c8\ub2e4\\n\\n1. \ud074\ub798\uc2a4 \ub85c\ub354\uc5d0 \uba85\ud655\ud55c \ud45c\uc900\uc774 \uc5c6\uace0, \ubd80\ubaa8 \uc790\uc2dd \ubaa8\ub378\uc774 \uc788\uc5b4\uc11c, \ud074\ub798\uc2a4 \ub85c\ub354\uc5d0 \ub530\ub77c\uc11c \ub2e4\ub978 \uacb0\uacfc\uac00 \ub098\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4. [\ucc38\uace0](http://articles.qos.ch/classloader.html)\\n2. \ud074\ub798\uc2a4\ub85c\ub354\ub294 gc\uc758 \ub3d9\uc791\uc5d0 \ubc29\ud574\ub97c \uc77c\uc73c\ucf1c\uc11c \uba54\ubaa8\ub9ac \ub204\uc218\ub97c \ubc1c\uc0dd\uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4. [\ucc38\uace0](https://cwiki.apache.org/confluence/display/COMMONS/Logging+UndeployMemoryLeak)\\n\\n`@Slf4j` \uc5b4\ub178\ud14c\uc774\uc158\uc744 \ubd99\uc774\uba74, \ucef4\ud30c\uc77c \uc2dc\uc810\uc5d0 `private final Logger log = LoggerFactory.getLogger(this.getClass());` \uc640 \uac19\uc740 \ucf54\ub4dc\ub85c \ubcc0\ud658\ub429\ub2c8\ub2e4.\\n\\n\uc2a4\ud504\ub9c1 5\uc5d0\uc11c\ub294 Slf4j \uac00 \uc0ac\uc6a9\ud558\ub294 \uac83\ucc98\ub7fc, \ucef4\ud30c\uc77c \ud0c0\uc784\uc5d0 \uc5b4\ub5a4 \ub85c\uae45 \ud504\ub808\uc784\uc6cc\ud06c\ub97c \uc0ac\uc6a9\ud560\uc9c0 \uacb0\uc815\ud558\ub294 \uae30\ub2a5\uc744 \uc791\uc131\ud588\uace0, `Commons Logging`\uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n[spring 5\uc5d0\uc11c \ubcc0\uacbd\ub418\uc5c8\ub2e4\ub294 \ub9c1\ud06c](https://docs.spring.io/spring-framework/docs/5.0.0.RC3/spring-framework-reference/overview.html#overview-logging)\\n\\n## Slf4 j\uc5d0 \ub300\ud574\uc11c \uc54c\uc544\ubcf4\uc790\\n\\nSlf4 j\ub294 \ub85c\uae45\uc744 \uc704\ud55c \uc778\ud130\ud398\uc774\uc2a4\ub97c \uc81c\uacf5\ud558\ub294 \ud504\ub808\uc784\uc6cc\ud06c\uc785\ub2c8\ub2e4.(Simple Logging Facade for Java)\\n\\n\ucef4\ud30c\uc77c \ud0c0\uc784\uc5d0, \uc5b4\ub5a4 \ub85c\uadf8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud560\uc9c0 \uacb0\uc815\ud558\ub294 \uae30\ub2a5\uc744 \uc81c\uacf5\ud569\ub2c8\ub2e4.\\n\\n\ub85c\uadf8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \ubc14\uafb8\ub824\uace0 \ud588\uc744 \ub54c, \uae30\uc874 \ucf54\ub4dc\ub294 \ud558\ub098\ub3c4 \uac74\ub4dc\ub9ac\uc9c0 \uc54a\uace0, \ub85c\uadf8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub9cc \ubc14\uafd4\uc8fc\uba74 \ub418\ub3c4\ub85d \ud574\uc90d\ub2c8\ub2e4.\\n\\n### \uc870\uae08 \ub354 \uc790\uc138\ud55c \ub3d9\uc791 \uc6d0\ub9ac\ub97c \uc54c\uc544\ubcf4\uc790\\n\\n![only slf4j](https://blog.kakaocdn.net/dn/lCcTc/btsmBw3OEJz/1njLV283KdUWc9qyppEdak/img.png)\\n\\nSlf4 j \ub9cc\uc744 \uc0ac\uc6a9\ud588\uc744 \uacbd\uc6b0 \uc704 \uc0ac\uc9c4 \uac19\uc740 \ud615\ud0dc\ub85c \uc694\uccad\uc774 \ucc98\ub9ac\uac00 \ub429\ub2c8\ub2e4.\\n\\nSlf4 j \ub77c\ub294 \uc778\ud130\ud398\uc774\uc2a4\ub97c \ud1b5\ud574\uc11c \ub85c\uadf8\ub97c \ub0a8\uae30\uace0, \uc5b4\ub5a4 \ub85c\uadf8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud560\uc9c0\ub294 `Slf4j binding`\uc774\ub77c\ub294 \uac83\uc744 \ud1b5\ud574\uc11c \uacb0\uc815\ud569\ub2c8\ub2e4.\\n\\n`Slf4j binding` \uc740 `Slf4j`\uc758 \uc778\ud130\ud398\uc774\uc2a4\ub97c \uad6c\ud604\ud558\uace0 \uc788\uc9c0 \uc54a\uc740 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc758 \uad6c\ud604\uccb4\ub97c \uc5f0\uacb0\ud574 \uc8fc\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\n\uadf8 \uad6c\ud604\uccb4\ub85c `Slf4j-log4 j12-{version}. jar` \uac19\uc740 \uac83\uc774 \uc788\ub2e4.\\n\\n\uc774\uc640\ub294 \ub2e4\ub974\uac8c Logback \uc740 Slf4 j \ub97c \uad6c\ud604\ud558\uace0 \uc788\uae30\uc5d0, `Slf4j binding` \uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc544\ub3c4 \ub429\ub2c8\ub2e4.\\n\\n![logback example](https://blog.kakaocdn.net/dn/IYC3k/btsmy0einLF/F0aiMnteJeGB00fkGdBjRK/img.png)\\n\\n\uc704 \uc0ac\uc9c4\ucc98\ub7fc `Slf4j binding` \uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0, `Logback` \ubc14\ub85c \uc0ac\uc6a9\ud558\ub294 \uac83\ub3c4 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n\\n\uadf8\ub807\ub2e4\uba74 Slf4 j\ub97c \ubc14\ub85c \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc740 \ucf54\ub4dc\uc5d0\uc11c `Slf4j` \ub97c \uc0ac\uc6a9\ud558\ub824\uba74 \uc5b4\ub5bb\uac8c \ud574\uc57c \ud560\uae4c\uc694?\\n\\n![slf4j working principle](https://blog.kakaocdn.net/dn/msTPw/btsmziy04VE/sXSOKYvi9yXSoiRmg6mIGk/img.png)\\n\\n\uc704 \uc0ac\uc9c4\ucc98\ub7fc `Slf4j bridge` \ub97c \ud1b5\ud574\uc11c \uc678\ubd80 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\ucc98\ub7fc \uac08\uc544 \ub07c\uc6b8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n`Log4j2` \ub97c \uc0ac\uc6a9\ud558\ub294 \ucf54\ub4dc\ub97c \uc804\ud600 \ubc14\uafb8\uc9c0 \uc54a\uc544\ub3c4, `Bridge` \uac00 `Slf4j` \ub97c \ud1b5\ud574 `Logback`\uc73c\ub85c \uc790\uc5f0\uc2a4\ub7fd\uac8c \ub85c\uadf8\ub97c \ub0a8\uae38 \uc218 \uc788\ub3c4\ub85d \ud574\uc90d\ub2c8\ub2e4.\\n\\n## Logback\uc5d0 \ub300\ud574\uc11c \uc54c\uc544\ubcf4\uc790\\n\\nLogback \uc740 \uc2a4\ud504\ub9c1\uc5d0\uc11c \uae30\ubcf8\uc73c\ub85c \uc0ac\uc6a9\ub420 \ub9cc\ud07c \uc778\uae30 \uc788\ub294 \ub85c\uadf8 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.\\n\\n![logback \ub3d9\uc791 \uacfc\uc815](https://logback.qos.ch/manual/images/chapters/architecture/underTheHoodSequence2_small.gif)\\n\\n\uacf5\uc2dd\ubb38\uc11c\uc5d0\uc11c \uc544\uc8fc \ud575\uc2ec\uc801\uc778 \ub3d9\uc791\uc6d0\ub9ac\ub97c \uc124\uba85\ud574\uc8fc\uace0 \uc788\ub294 \uc0ac\uc9c4\uc774\ub77c\uc11c \uac00\uc838\uc654\uc2b5\ub2c8\ub2e4.\\n\\n\ub108\ubb34 \uc5b4\ub824\uc6cc \ubcf4\uc5ec\uc11c, \uc870\uae08 \uc790\uc138\ud558\uac8c \uac01\uac01\uc758 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud574\uc11c \uc54c\uc544\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\n\uc774\uc5d0 \ub300\ud574 \uc54c\uc544\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\n## \ub85c\uadf8\ubc31\uc758 \uad6c\uc131\uc694\uc18c\\n\\n### Appender\\n\\nAppender\ub294 \ub85c\uadf8\ub97c \uc5b4\ub514\uc5d0 \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\n\uc678\ubd80\ub85c\ubd80\ud130 \uc5b4\ub5a4 \ub370\uc774\ud130\ub97c \ubc1b\uc544\uc11c, \uc5b4\ub5a4 \ubc29\uc2dd\uc73c\ub85c \ucc98\ub9ac\ud560\uc9c0\uc5d0 \ub300\ud574\uc11c \uc804\uccb4\uc801\uc73c\ub85c \uc124\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uae30\ubcf8\uc801\uc73c\ub85c \uc218\ub9ce\uc740 Appender \uac00 \uc81c\uacf5\ub418\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n- ConsoleAppender\\n- FileAppender\\n- RollingFileAppender\\n- AsyncAppender\\n- DBAppender\\n- SMTPAppender\\n- SocketAppender\\n- SyslogAppender\\n\\n\uc800\ud76c\ub294 Slack\uc5d0 \uc54c\ub9bc\uc744 \uc8fc\ub294 \uac83\uc774 \ubaa9\uc801\uc774\uae30 \ub54c\ubb38\uc5d0, SlackAppender\ub97c \uc0ac\uc6a9\ud558\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc SlackAppender\ub294 \uc81c\uacf5\ub418\uace0 \uc788\uc9c0 \uc54a\uae30\uc5d0 \uc9c1\uc811 \uad6c\ud604\uc744 \ud574\uc57c \ud558\ub294\ub370\uc694\\n\\n\uc774\ub97c \uad6c\ud604\ud588\uc744 \ub54c, Slack API \uac00 \ub05d\ub0a0 \ub54c\uae4c\uc9c0, \uacc4\uc18d \uae30\ub2e4\ub9ac\uace0 \uc788\uc744 \ud544\uc694\uac00 \uc5c6\uae30\uc5d0, AsyncAppender\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9 \ubc29\ubc95\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4. xml \uae30\ubc18\uc73c\ub85c \uac00\ub2a5\ud55c\ub370\uc694\\n\\n```xml\\n\\n \\n myapp.log\\n \\n %logger{35} -%kvp -%msg%n\\n \\n \\n\\n \\n \\n \\n\\n \\n \\n \\n\\n```\\n\\n\ub9cc\uc57d \uc5ec\uae30\uc5d0 \uc788\ub294 \uae30\ub2a5\ub4e4\ub85c \ubd80\uc871\ud558\ub2e4\uba74, \uc9c1\uc811 Appender \ub97c \uad6c\ud604\ud574\uc11c \uc0ac\uc6a9\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc9c1\uc811 \uad6c\ud604\ud558\ub824\uba74 AppenderBase\ub97c \uc0c1\uc18d\ubc1b\uc544\uc11c \uad6c\ud604\ud558\uba74 \ub429\ub2c8\ub2e4.\\n\\n\uc774 \ud074\ub798\uc2a4\ub294 \ud544\uc694\ud55c \ubd80\ubd84\uc774 \ub300\ubd80\ubd84 \uad6c\ud604\ub418\uc5b4 \uc788\uace0, appender \ub9cc \uad6c\ud604\ud558\uba74 \ubc14\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub2f9\uc5f0\ud558\uc9c0\ub9cc \ud544\uc694\ud558\ub2e4\uba74 override \ub3c4 \uac00\ub2a5\ud558\uc8e0\\n\\n### Layout\\n\\nLayout \uc740 \ub85c\uadf8\ub97c \uc5b4\ub5a4 \ud615\uc2dd\uc73c\ub85c \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\nAppender\ub294 \ub85c\uadf8\ub97c \uc5b4\ub514\uc5d0 \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud558\uace0, Layout \uc740 \ub85c\uadf8\ub97c \uc5b4\ub5a4 \ud615\uc2dd\uc73c\ub85c \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud558\ub3c4\ub85d \ud558\ub294 \uac83\uc774 \uc774\uc0c1\uc801\uc774\uc9c0\ub9cc\\n\\nLogback \uc740 Appender\uc5d0\uc11c Layout \uc744 \uc9c1\uc811 \uc9c0\uc815\ud560 \uc218 \uc788\ub3c4\ub85d \ud574\uc8fc\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c, \uc9c1\uc811 Layout \uc744 \ub9cc\ub4e4\uc9c0 \uc54a\uace0, Appender \uc5d0\uc11c \uae30\uc874\uc5d0 \uc774\ubbf8 \uc788\ub294 \ud328\ud134\ub9cc \uc0ac\uc6a9\ud558\ub824\uace0 \ud569\ub2c8\ub2e4\\n\\n### Encoder\\n\\nEncoder\ub294 Layout \uacfc \ube44\uc2b7\ud55c \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\nLayout \uc740 \ub85c\uadf8\ub97c \uc5b4\ub5a4 \ud615\uc2dd\uc73c\ub85c \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud558\uace0, Encoder \ub294 \uc2e4\uc81c byte \ud615\ud0dc\ub85c \ubcc0\ud658\ud558\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\nSlack\uc758 webhook\uc744 \uc0ac\uc6a9\ud560 \uac83\uc774\uc9c0\ub9cc, AppenderBase\ub97c \uc0ac\uc6a9\ud558\uae30\uc5d0, \uc774\ubc88\uc5d0\ub294 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n### Filter\\n\\nFilter\ub294 \ub85c\uadf8\ub97c \uc5b4\ub5a4 \uc870\uac74\uc5d0 \ub530\ub77c\uc11c \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\nFilter \ub294 Appender\ub97c \ub4f1\ub85d\ud558\uba70 \uac19\uc774 \ub4f1\ub85d\ud560 \uc218 \uc788\ub294\ub370\uc694\\n\\n\uc774\ubc88 \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c\ub294 Level \uc774 ERROR \uc774\uc0c1\uc778 \uac83\ub9cc \ucd9c\ub825\ud558\ub3c4\ub85d \ud558\uace0 \uc2f6\uae30\uc5d0, LevelFilter\ub97c \uc0ac\uc6a9\ud558\uba74 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n```xml\\n\\n \\n \\n INFO\\n ACCEPT\\n DENY\\n \\n \\n \\n %-4relative [%thread] %-5level %logger{30} -%kvp -%msg%n\\n \\n \\n \\n \\n \\n \\n\\n```\\n\\n\uc640 \ube44\uc2b7\ud558\uac8c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5b4 \ubcf4\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 \uc2e4\uc81c\ub85c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c error \ubc1c\uc0dd \uc2dc slack\uc73c\ub85c \uc54c\ub9bc\uc744 \uc8fc\ub294 \uac83\uc744 \uad6c\ud604\ud574 \ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## \uc2ac\ub799\uc5d0 \ucd94\uac00\ud558\ub294 \ubc29\ubc95\\n\\n[\uc774 \ube14\ub85c\uadf8](https://velog.io/@king/slack-incoming-webhook)\ub97c \ubcf4\uace0\uc11c \uc791\uc131\ud588\uc2b5\ub2c8\ub2e4\\n\\n## \uc2e4\uc81c \uad6c\ud604\\n\\n\uad6c\ud604\ub41c \uacb0\uacfc\ubb3c\uc740 \uc544\ub798\uc640 \uac19\uc2b5\ub2c8\ub2e4\\n\\n![slack appender](https://blog.kakaocdn.net/dn/d3z7QG/btsmQCCV69f/NwiyNhQGZOBnKBP2hT8kf0/img.png)\\n\\n### SlackAppender \uad6c\ud604\ud558\uae30\\n\\n```java\\npublic class SlackAppender extends AppenderBase {\\n\\n @Override\\n protected void append(final ILoggingEvent eventObject) {\\n final var restTemplate = new RestTemplate();\\n final var url = \\"https://hooks.slack.com/services/\\";\\n final Map body = createSlackErrorBody(eventObject);\\n restTemplate.postForEntity(url, body, String.class);\\n }\\n\\n private Map createSlackErrorBody(final ILoggingEvent eventObject) {\\n final String message = createMessage(eventObject);\\n return Map.of(\\n \\"attachments\\", List.of(\\n Map.of(\\n \\"fallback\\", \\"\uc694\uccad\uc744 \uc2e4\ud328\ud588\uc5b4\uc694 :cry:\\",\\n \\"color\\", \\"#2eb886\\",\\n \\"pretext\\", \\"\uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\uc5b4\uc694 \ud655\uc778\ud574\uc8fc\uc138\uc694 :cry:\\",\\n \\"author_name\\", \\"car-ffeine\\",\\n \\"text\\", message,\\n \\"fields\\", List.of(\\n Map.of(\\n \\"title\\", \\"\uc6b0\uc120\uc21c\uc704\\",\\n \\"value\\", \\"High\\",\\n \\"short\\", false\\n ),\\n Map.of(\\n \\"title\\", \\"\uc11c\ubc84 \ud658\uacbd\\",\\n \\"value\\", \\"local\\",\\n \\"short\\", false\\n )\\n ),\\n \\"ts\\", eventObject.getTimeStamp()\\n )\\n )\\n );\\n }\\n\\n private String createMessage(final ILoggingEvent eventObject) {\\n final String baseMessage = \\"\uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\\\\n\\";\\n final String pattern = baseMessage + \\"```%s %s %s [%s] - %s```\\";\\n final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(\\"yyyy-MM-dd HH:mm:ss.SSS\\");\\n return String.format(pattern,\\n simpleDateFormat.format(eventObject.getTimeStamp()),\\n eventObject.getLevel(),\\n eventObject.getThreadName(),\\n eventObject.getLoggerName(),\\n eventObject.getFormattedMessage());\\n }\\n}\\n```\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c url\uc744 \uc9c1\uc811 \uc785\ub825\ud558\uc2dc\uba74 \ub429\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0, \uc774\ub807\uac8c \ub9cc\ub4e0 SlackAppender\ub97c logback-spring.xml \uc5d0 \ub4f1\ub85d\ud558\uba74 \ub429\ub2c8\ub2e4.\\n\\n```xml\\n\\n\\n\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n \\n\\n\\n```\\n\\n\uc774\ub807\uac8c \ud558\uba74, racingcar \ud328\ud0a4\uc9c0\uc5d0\uc11c \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud560 \ub54c\ub9cc slack\uc73c\ub85c \uc54c\ub9bc\uc744 \ubc1b\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\\n![slack appender](https://blog.kakaocdn.net/dn/d3z7QG/btsmQCCV69f/NwiyNhQGZOBnKBP2hT8kf0/img.png)\\n\\n\uc774\ubc88 \uae00\uc5d0\uc11c\ub294 log \ub808\ubca8\uc5d0 \ub530\ub77c slack \uc73c\ub85c \uc54c\ub9bc\uc744 \ubc1b\ub294 \ubc29\ubc95\uc744 \uc54c\uc544\ubcf4\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\uae34 \uae00\uc744 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4"},{"id":"7","metadata":{"permalink":"/7","source":"@site/blog/2023-07-06-auto-issue-number-commit-msg.mdx","title":"\uae43 \ucee4\ubc0b \uba54\uc2dc\uc9c0\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud560 \uc21c \uc5c6\uc744\uae4c?","description":"\ud504\ub85c\uc81d\ud2b8 \ube0c\ub79c\uce58\uba85 \ucee8\ubca4\uc158\uc774 feat/\uc774\uc288\ubc88\ud638\uc5ec\uc11c, \ube0c\ub79c\uce58\uba85\uc5d0\uc11c \uc774\uc288\ubc88\ud638\ub9cc \uac00\uc838\uc628 \ub2e4\uc74c \ucee4\ubc0b\ud560 \ub54c\ub9c8\ub2e4 \ucee4\ubc0b \uba54\uc2dc\uc9c0 \uc544\ub798\ub2e8(footer)\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud574\uc8fc\uace0 \uc2f6\uc5c8\ub2e4. \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ub41c\ub2e4\uba74 \uae5c\ube61\ud558\uace0 \uc774\uc288 \ubc88\ud638\ub97c \uc548 \uc801\ub294 \uc77c\ub3c4 \uc5c6\uace0, \uc2dc\uac04\ub3c4 \ub2e8\ucd95\ud560 \uc218 \uc788\uae30 \ub54c\ubb38\uc774\ub2e4.","date":"2023-07-06T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 6\uc77c","tags":[{"label":"git","permalink":"/tags/git"},{"label":"commit","permalink":"/tags/commit"},{"label":"message","permalink":"/tags/message"},{"label":"issue","permalink":"/tags/issue"},{"label":"auto","permalink":"/tags/auto"}],"readingTime":2.89,"hasTruncateMarker":false,"authors":[{"name":"\uc57c\ubbf8","title":"Frontend","url":"https://github.com/feb-dain","imageURL":"https://github.com/feb-dain.png","key":"yummy"}],"frontMatter":{"slug":"7","title":"\uae43 \ucee4\ubc0b \uba54\uc2dc\uc9c0\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud560 \uc21c \uc5c6\uc744\uae4c?","authors":["yummy"],"tags":["git","commit","message","issue","auto"]},"prevItem":{"title":"\uc2a4\ud504\ub9c1\uc5d0\uc11c \ubc1c\uc0dd\ud55c \uc5d0\ub7ec \ub85c\uadf8\ub97c \uc2ac\ub799\uc73c\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95","permalink":"/8"},"nextItem":{"title":"[DB] \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud574\ubcf4\uc790","permalink":"/6"}},"content":"\ud504\ub85c\uc81d\ud2b8 \ube0c\ub79c\uce58\uba85 \ucee8\ubca4\uc158\uc774 feat/\uc774\uc288\ubc88\ud638\uc5ec\uc11c, \ube0c\ub79c\uce58\uba85\uc5d0\uc11c \uc774\uc288\ubc88\ud638\ub9cc \uac00\uc838\uc628 \ub2e4\uc74c \ucee4\ubc0b\ud560 \ub54c\ub9c8\ub2e4 \ucee4\ubc0b \uba54\uc2dc\uc9c0 \uc544\ub798\ub2e8(footer)\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud574\uc8fc\uace0 \uc2f6\uc5c8\ub2e4. \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ub41c\ub2e4\uba74 \uae5c\ube61\ud558\uace0 \uc774\uc288 \ubc88\ud638\ub97c \uc548 \uc801\ub294 \uc77c\ub3c4 \uc5c6\uace0, \uc2dc\uac04\ub3c4 \ub2e8\ucd95\ud560 \uc218 \uc788\uae30 \ub54c\ubb38\uc774\ub2e4.\\n\\n\uc544\ub798 \uc21c\uc11c\ub300\ub85c \uc9c4\ud589\ud55c\ub2e4\uba74 \uc774\uc288 \ubc88\ud638 POSTFIX \uc790\ub3d9\ud654\ub97c \ud560 \uc218 \uc788\ub2e4.\\n\\n### 1) \ud504\ub85c\uc81d\ud2b8 \ud3f4\ub354\uc5d0 .githooks \ud3f4\ub354 \uc0dd\uc131\\n\\n### 2) .githooks \ud3f4\ub354\uc5d0 commit-msg \ud30c\uc77c \uc0dd\uc131\\n\\n```shell\\n#!/bin/bash\\n\\nCOMMIT_MESSAGE_FILE_PATH=$1\\nMESSAGE=$(cat \\"$COMMIT_MESSAGE_FILE_PATH\\")\\n\\n# \ucee4\ubc0b \uba54\uc2dc\uc9c0\uac00 \uc5c6\uc744 \ub54c, \ucee4\ubc0b \ubc29\uc9c0\\nif [[ $(head -1 \\"$COMMIT_MESSAGE_FILE_PATH\\") == \'\' ]]; then\\n exit 0\\nfi\\n\\n# \ube0c\ub79c\uce58\uba85\uc5d0\uc11c \uc774\uc288 \ubc88\ud638\ub9cc \ucd94\ucd9c (\'/\' \ub4a4\uc5d0 \uc788\ub294 \ubb38\uc790\ub9cc \ucd94\ucd9c)\\nPOSTFIX=$(git branch | grep \'\\\\*\' | sed \'s/* //\' | sed \'s/^.*\\\\///\' | sed \'s/^\\\\([^-]*-[^-]*\\\\).*/\\\\1/\')\\n\\nCOMMIT_SOURCE=$2\\nCURRENT_BRANCH=$(git branch --show-current)\\n\\n# [[ \\"$CURRENT_BRANCH\\" != \\"$POSTFIX\\" ]] \ud83d\udc49\ud83c\udffb \ud604\uc7ac \ube0c\ub79c\uce58\uba85\uacfc POSTFIX\uac00 \ub611\uac19\uc73c\uba74 POSTFIX \uc785\ub825 \ubc29\uc9c0\\n# [ \\"$COMMIT_SOURCE\\" != \\"merge\\" ] \ud83d\udc49\ud83c\udffb merge\ud560 \ub54c, POSTFIX \uc785\ub825 \ubc29\uc9c0\\n# [[ \\"$MESSAGE\\" != *\\"[#$POSTFIX]\\"* ]] \ud83d\udc49\ud83c\udffb \uc774\ubbf8 POSTFIX\uac00 \uc874\uc7ac\ud560 \ub54c, POSTFIX \uc911\ubcf5 \uc785\ub825 \ubc29\uc9c0\\nif [[ \\"$CURRENT_BRANCH\\" != \\"$POSTFIX\\" ]] && [ \\"$COMMIT_SOURCE\\" != \\"merge\\" ] && [[ \\"$MESSAGE\\" != *\\"[#$POSTFIX]\\"* ]]; then\\n printf \\"%s\\\\n\\\\n[#%s]\\" \\"$MESSAGE\\" \\"$POSTFIX\\" > \\"$COMMIT_MESSAGE_FILE_PATH\\"\\nfi\\n```\\n\\n\ud83e\uddd0 \uc774\uc288 \ubc88\ud638 \ucd94\ucd9c\uc5d0 \uc0ac\uc6a9\ub41c \uba85\ub839\uc5b4 \uc124\uba85\\n\\n- grep \'\\\\*\' \ud83d\udc49 `*` \ud45c\uc2dc\ub41c \ube0c\ub79c\uce58(\ud604\uc7ac \uc704\uce58\uc758 \ube0c\ub79c\uce58)\ub97c \uac00\uc838\uc628\ub2e4.\\n- sed \'s/_ //\' \ud83d\udc49 `*` \uc81c\uac70\\n- sed \'s/\\\\([^/]_\\\\)._/\\\\1/\' \ud83d\udc49 `/` \uc774\ud6c4\uc758 \ubb38\uc790\ub9cc \ucd94\ucd9c\\n- sed \'s/^\\\\([^-]_-[^-]_\\\\).\\\\_/\\\\1/\' \ud83d\udc49 \ud558\ub098\uc758 \uc774\uc288\uc5d0 \uc5ec\ub7ec \ube0c\ub79c\uce58\ub97c \ub9cc\ub4e4\uba74\uc11c feat/10-1 \uc774\ub7f0 \ud615\ud0dc\ub85c \ube0c\ub79c\uce58\ub97c \ub9cc\ub4e4 \uacbd\uc6b0, \uccab \ubc88\uc9f8 \'-\' \uc55e \ub4a4\ub9cc \ucd94\ucd9c (ex. 10-1)\\n\\n### 3) \ud504\ub85c\uc81d\ud2b8 \ud3f4\ub354\uc5d0 Makefile \ud30c\uc77c \uc0dd\uc131\\n\\n```shell\\ninit:\\n git config core.hooksPath .githooks\\n chmod +x .githooks/commit-msg\\n git update-index --chmod=+x .githooks/commit-msg\\n\\n # chmod +x .githooks/commit-msg \ud83d\udc49\ud83c\udffb macOS, \ub9ac\ub205\uc2a4\uc5d0\uc11c \uc2a4\ud06c\ub9bd\ud2b8 \uad8c\ud55c \ubd80\uc5ec\\n # git update-index --chmod=+x .githooks/commit-msg\\n # \ud83d\udc49 macOS, \ub9ac\ub205\uc2a4\uc5d0\uc11c \ube0c\ub79c\uce58\uac00 \ubc14\ub014 \ub54c\ub9c8\ub2e4 \uc2a4\ud06c\ub9bd\ud2b8 \uc2e4\ud589\uc2dc\ucf1c\uc918\uc57c \ud558\ub294 \ubb38\uc81c \ud574\uacb0\\n```\\n\\n### 4) \uc544\ub798 \ucf54\ub4dc \uc2e4\ud589\\n\\n\uc0c8\ub85c git clone\uc744 \ud560 \ub54c\ub9c8\ub2e4 \uc544\ub798 \ucf54\ub4dc\ub97c \uc2e4\ud589\uc2dc\ucf1c\uc918\uc57c \ud55c\ub2e4. \ud55c \ubc88\ub9cc \uc2e4\ud589\uc2dc\ud0a4\uba74 \uacc4\uc18d \uc801\uc6a9\ub41c\ub2e4. (window \uae30\uc900)\\n\\n```shell\\ngit config core.hooksPath .githooks\\n```\\n\\n\u2757macOS\ub294 git clone \ud560 \ub54c\ub9c8\ub2e4 \uc544\ub798 \ucf54\ub4dc\ub97c \uc2e4\ud589\uc2dc\ucf1c\uc918\uc57c \ud55c\ub2e4.\\n\\n```shell\\nmake\\n```\\n\\n---\\n\\n\ucc38\uace0 \ube14\ub85c\uadf8\\nhttps://blog.deering.co/commit-convention/"},{"id":"6","metadata":{"permalink":"/6","source":"@site/blog/2023-07-05-nunu-db-optimization.mdx","title":"[DB] \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud574\ubcf4\uc790","description":"\uc548\ub155\ud558\uc138\uc694 \uce74\ud398\uc778\ud300 \ub204\ub204\uc785\ub2c8\ub2e4","date":"2023-07-05T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 5\uc77c","tags":[{"label":"DB","permalink":"/tags/db"},{"label":"JPA","permalink":"/tags/jpa"},{"label":"Hibernate","permalink":"/tags/hibernate"},{"label":"Spring","permalink":"/tags/spring"}],"readingTime":8.16,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"},{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"6","title":"[DB] \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud574\ubcf4\uc790","authors":["nunu","boxster"],"tags":["DB","JPA","Hibernate","Spring"]},"prevItem":{"title":"\uae43 \ucee4\ubc0b \uba54\uc2dc\uc9c0\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud560 \uc21c \uc5c6\uc744\uae4c?","permalink":"/7"},"nextItem":{"title":"pr \ubcf8\ubb38\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \ub2ec\uc544\uc8fc\ub294 \uae30\ub2a5\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4","permalink":"/5"}},"content":"\uc548\ub155\ud558\uc138\uc694 \uce74\ud398\uc778\ud300 `\ub204\ub204`\uc785\ub2c8\ub2e4\\n\\n\uc774\ubc88\uc5d0\ub294 \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud558\ub294 \uacfc\uc815\uc5d0\uc11c \uc54c\uac8c \ub41c \ub0b4\uc6a9\uc744 \uacf5\uc720\ud558\ub824\uace0 \ud569\ub2c8\ub2e4\\n\\n## \uc774\ubc88 \ucd5c\uc801\ud654\uc758 \ubaa9\ud45c\\n\\n\uc804\uae30\ucc28 \ucda9\uc804\uc18c\uc5d0 \ub300\ud55c \uacf5\uacf5 \ub370\uc774\ud130\ub97c \uac00\uc838\uc624\uace0, \uadf8 \ub370\uc774\ud130\ub97c DB \uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud574\ubcf4\uc790\\n\\n## \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c \uc0bd\uc785\ud558\ub294 \uacfc\uc815\\n\\n\uc800\ud76c \ud300\uc758 \uc694\uad6c\uc0ac\ud56d\uc744 \uac04\ub2e8\ud558\uac8c \uc815\ub9ac\ud558\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4\\n\\n1. \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c \uacf5\uacf5 \ub370\uc774\ud130\uc5d0\uc11c \uc804\uae30\ucc28 \ucda9\uc804\uc18c\uc640 \uc804\uae30\ucc28 \ucda9\uc804\uae30\uc5d0 \ub300\ud55c \ub370\uc774\ud130\ub97c \uac00\uc838\uc628\ub2e4\\n - \ucda9\uc804\uc18c\ub294 6\ub9cc \uac1c, \ucda9\uc804\uae30\ub294 23\ub9cc \uac1c\uc758 \ub370\uc774\ud130\uac00 \uc874\uc7ac\ud55c\ub2e4.\\n - \ud55c \ubc88\uc5d0 \uac00\uc838\uc62c \uc218 \uc788\ub294 \uc591\uc740 9999\uac1c \uae4c\uc9c0\ub2e4.\\n2. \uc774 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294\ub2e4\\n - \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub294 1:N \uad00\uacc4\uc774\ub2e4\\n\\n## \ucd5c\uc801\ud654 \uc804\uc740 \uc5b4\ub5a4 \uc0c1\ud669\uc774\uc5c8\ub294\ub370?\\n\\n![before_optimize](https://veiled-starfish-4c7.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Ffb934c88-4589-4096-90bc-36b4bc88f6a2%2FUntitled.png?id=f7f7c2af-7b95-42e8-8d95-ddd952e53005&table=block&spaceId=9db11c89-12d2-4910-8822-5ffecbdb8ccd&width=2000&userId=&cache=v2)\\n\\n\uc704 \uc0ac\uc9c4\uc744 \uc798 \ubcf4\uc2dc\uba74 \uc544\uc2e4 \uc218 \uc788\uc73c\uc2dc\uaca0\uc9c0\ub9cc, 2000\uac1c\ub97c \uc800\uc7a5\ud558\ub294\ub370, 231.762 \ucd08\uac00 \uc0ac\uc6a9\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ubb3c\ub860 \ucd9c\ub825\uc744 \uc704\ud55c \uc2dc\uac04\ub3c4 \ud3ec\ud568\ub418\uc5c8\uae30\uc5d0, 230\ucd08 \uc815\ub3c4\ub77c\uace0 \uc0dd\uac01\ud558\uc154\ub3c4 \uc88b\uc2b5\ub2c8\ub2e4\\n\\n1\ub9cc \uac1c\ub77c\uba74? 231.762\ucd08 \\\\* 5 = 1,158.81\ucd08\\n\\n23\ub9cc \uac1c\ub77c\uba74? 1158.81 \\\\* 23 = 26,652.63\ucd08\\n\\n\uc2dc\uac04\uc73c\ub85c \ubc14\uafd4\ubcf4\uba74 7.4 \uc2dc\uac04\uc774 \uac78\ub9b0\ub2e4\ub294 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\n1. \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud560 \ub54c\ub9c8\ub2e4, \uc0c8\ub85c\uc6b4 Transaction \uc774 \uc0dd\uc131\ub41c\ub2e4.\\n\\n### \uc5b4\ub5bb\uac8c \uac1c\uc120\ud560 \uc218 \uc788\uc744\uae4c?\\n\\n\ub370\uc774\ud130\ub97c \uc800\uc7a5\ud560 \ub54c\ub9c8\ub2e4, \uc0c8\ub85c\uc6b4 Transaction \uc774 \uc0dd\uc131\ub418\ub294 \uac83\uc744 \ubc29\uc9c0\ud558\uae30 \uc704\ud574, \uc804\uccb4\ub97c \ud558\ub098\uc758 \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\ub294\ub2e4\\n\\n## \uc804\uccb4\ub97c \ud55c \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\uc740 \ubc84\uc804\\n\\n![all_in_transaction](https://veiled-starfish-4c7.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F9ff34622-4a26-4acd-980c-ae175c83143d%2FUntitled.png?id=979aa2c5-e972-4c52-a44a-1669c497c84e&table=block&spaceId=9db11c89-12d2-4910-8822-5ffecbdb8ccd&width=2000&userId=&cache=v2)\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c 2000\uac1c\ub97c \uc800\uc7a5\ud558\ub294\ub370 65\ucd08 \uac00 \uc0ac\uc6a9\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n1\ub9cc \uac1c\ub77c\uba74? 65\ucd08 \\\\* 5 = 325\ucd08\\n\\n23\ub9cc \uac1c\ub77c\uba74? 325\ucd08 \\\\* 23 = 7,475\ucd08\\n\\n\uc2dc\uac04\uc73c\ub85c \ubc14\uafd4\ubcf4\uba74 2\uc2dc\uac04\uc774 \uac78\ub9b0\ub2e4\ub294 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n\uc804\uccb4\uc801\uc73c\ub85c 3\ubc30 \uc815\ub3c4 \ube68\ub77c\uc84c\uc2b5\ub2c8\ub2e4\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\n1. 23\ub9cc \uac1c\uc758 \uc800\uc7a5\uc774 \ubaa8\ub450 \ud55c \ud2b8\ub79c\uc7ad\uc158\uc774 \ub418\uc5b4\uc11c, \ud558\ub098\uac00 \uc2e4\ud328\ud558\uba74 23\ub9cc\uac1c\ub97c \uc0c8\ub85c \uc800\uc7a5\ud574\uc57c \ud558\ub294 \uc0c1\ud669\uc5d0 \ucc98\ud55c\ub2e4\\n\\n### \uc5b4\ub5bb\uac8c \uac1c\uc120\ud560 \uc218 \uc788\uc744\uae4c?\\n\\n23\ub9cc\uac1c\uc758 \uc800\uc7a5\uc774 \ubaa8\ub450 \ud55c \ud2b8\ub79c\uc7ad\uc158\uc774 \ub418\ub294 \uac83\uc744 \ubc29\uc9c0\ud558\uae30 \uc704\ud574, 1\ub9cc \uac1c\uc529 \uc601\uc18d\ud654\uc2dc\ud0a8\ub2e4\\n\\n## 1\ub9cc \uac1c\uac00 \ud55c \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\uc778 \ubc84\uc804\\n\\n![separateTransaction](https://blog.kakaocdn.net/dn/c2mgfd/btsmrWCfnKy/9Y6Dv8vYzcftsket61tub1/img.png)\\n\\n\uc131\ub2a5\uc0c1\uc73c\ub85c \uac1c\uc120\ud55c \ubd80\ubd84\uc740 \uadf8\ub807\uac8c \ud06c\uc9c0 \uc54a\uc9c0\ub9cc, \uc2e4\ud328\ud588\uc744 \ub54c, 1\ub9cc \uac1c\ub9cc \ub2e4\uc2dc \uc800\uc7a5\ud558\uba74 \ub418\uae30\uc5d0, \ud6e8\uc52c \ube60\ub974\uac8c \ubcf5\uad6c\uac00 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n\\n\uc5ec\uae30\uc11c PageNo\ub77c\ub294 \ud074\ub798\uc2a4\ub294, i\ub97c \ubc14\ub85c \ucc38\uc870\ud588\uc744 \uacbd\uc6b0, effectively final\uc744 \ubcf4\uc7a5\ud560 \uc218 \uc5c6\uc5b4\uc11c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc131\ub2a5\uc740 \uc804\uccb4\ub97c \ud55c \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\uc740 \ubc84\uc804\uacfc \ud070 \ucc28\uc774\uac00 \ub098\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\n1. id \uc0dd\uc131 \uc804\ub7b5\uc774 `GenerationType.IDENTITY` \uc774\uae30\uc5d0, \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud560 \ub54c\ub9c8\ub2e4, DB\uc5d0\uc11c id\ub97c \uc0dd\uc131\ud574\uc57c \ud55c\ub2e4.\\n\\nJPA\uc5d0 \uc788\ub294 \uc4f0\uae30 \uc9c0\uc5f0\uc744 \uc804\ud600 \ud65c\uc6a9\ud560 \uc218 \uc5c6\uace0, DB\uc5d0\uc11c id\ub97c \uc0dd\uc131\ud558\uae30 \uc704\ud574, DB\uc640 \ub9e4\ubc88 \ud1b5\uc2e0\uc744 \ud574\uc57c \ud55c\ub2e4.\\n\\n### \uc5b4\ub5bb\uac8c \uac1c\uc120\ud560 \uc218 \uc788\uc744\uae4c?\\n\\nid\ub97c \ubbf8\ub9ac \uc0dd\uc131\ud574\uc11c, DB \uc5d0\uc11c id \ub97c \uc0dd\uc131\ud558\ub294 \uacfc\uc815\uc744 \uc0dd\ub7b5\ud55c\ub2e4\\n\\nID \uc0dd\uc131 \uc804\ub7b5\uc744 `GenerationType.Table\uc758` \ud615\ud0dc\ub85c \ubc14\uafd4\uc11c, DB\uc5d0\uc11c id\ub97c \uc0dd\uc131\ud558\ub294 \uacfc\uc815\uc744 \uc904\uc5ec\uc11c, \uc131\ub2a5\uc744 \uac1c\uc120\ud55c\ub2e4\\n\\n## 1\ub9cc \uac1c\uac00 \ud55c \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\uc774\uace0, id\ub97c \ubbf8\ub9ac \uc0dd\uc131\ud55c \ubc84\uc804\\n\\n\uc774\ub54c batch size\ub97c 1000 \ub2e8\uc704\ub85c \uc124\uc815\ud574\uc11c 1000\uac1c\uc529 id \uac00 \ub298\uc5b4\ub098\ub3c4\ub85d \uc124\uc815\ud588\ub2e4\\n\\n![charger_generator](https://blog.kakaocdn.net/dn/bFjNWb/btsmuoLmzVh/GddHebu2V43fpk2t3IUmz0/img.png)![station_generator](https://blog.kakaocdn.net/dn/pae8w/btsmrANjAGi/gjUhD6sMvBLpmsPl9c1tAk/img.png)\\n\\n```\\nspring.jdbc.template.fetch-size=10000\\n```\\n\\n![10000batch_size](https://blog.kakaocdn.net/dn/mtBFp/btsmtEt48jp/3mFOfrIBWbjJhHHuyP4zPk/img.png)\\n\\n1\uc790\ub9ac \uc22b\uc790\ub294 \uc55e\uc5d0\uc11c\ubd80\ud130 n(\ub9cc\uac1c)\ub97c \uc758\ubbf8\ud558\uace0, 2\ubc88\uc9f8 \uc22b\uc790\ub294 1\ub9cc \uac1c\ub97c \uc800\uc7a5\ud558\ub294 \ub370 \uac78\ub9b0 \uc2dc\uac04(ms)\uc744 \uc758\ubbf8\ud569\ub2c8\ub2e4.\\n\\n\ucc98\uc74c 1\ub9cc \uac1c\ub294 142\ucd08\uac00 \uac78\ub9ac\uace0, 2\ub9cc \uac1c\ub294 285\ucd08\uac00 \uac78\ub838\uc2b5\ub2c8\ub2e4.\\n\\n23\ub9cc \uac1c\ub77c\uba74? 142 \\\\* 26 = 3,266\ucd08\\n\\n\ucc98\uc74c\uacfc \ube44\uad50\ud558\uc790\uba74 7.4\uc2dc\uac04\uc774 \uac78\ub9ac\ub294 \uac83\uc5d0\uc11c 54\ubd84 \uc815\ub3c4 \uac78\ub9ac\ub294 \uac83\uc73c\ub85c \uac1c\uc120\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\n\ud558\ub098\uc758 \uc2a4\ub808\ub4dc\uc5d0\uc11c\ub9cc \ub3d9\uc791\ud558\uae30\uc5d0, \uc131\ub2a5\uc774 \uac1c\uc120\ub418\uc5c8\uc9c0\ub9cc, \uc5ec\uc804\ud788 \ub290\ub9bd\ub2c8\ub2e4.\\n\\n\ud558\ub098\uc758 \uc2a4\ub808\ub4dc\uc5d0\uc11c\ub9cc \ub3d9\uc791\ud558\uae30\uc5d0, \ud558\ub098\uc758 \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n### \uc5b4\ub5bb\uac8c \uac1c\uc120\ud560 \uc218 \uc788\uc744\uae4c?\\n\\n\uc5ec\ub7ec \uc2a4\ub808\ub4dc\uc5d0\uc11c \ub3d9\uc791\ud558\uac8c \ud558\uace0, \uc5ec\ub7ec \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\uac8c \ud569\ub2c8\ub2e4.\\n\\n## \uc5ec\ub7ec \uc2a4\ub808\ub4dc\uc5d0\uc11c \ub3d9\uc791\ud558\uace0, \uc5ec\ub7ec \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\ub294 \ubc84\uc804\\n\\n![multi_thread](https://blog.kakaocdn.net/dn/bPV2aa/btsmrSfU2D4/phDwk77XiKvwiXa5geX0PK/img.png)\\n\\n\uc774 \ubc84\uc804\uc5d0\uc11c 89991 \uac1c\ub97c \uc800\uc7a5\ud558\ub294\ub370 \ucd1d 157\ucd08\uac00 \uac78\ub838\uc2b5\ub2c8\ub2e4.\\n\\n23\ub9cc \uac1c\ub77c\uba74? 157 \\\\* 3 = 471\ucd08\\n\\n\uc2dc\uac04\uc73c\ub85c \ubc14\uafd4\ubcf4\uba74 5\ubd84\ub3c4 \ucc44 \uac78\ub9ac\uc9c0 \uc54a\ub294 \uc2dc\uac04\uc774\uc8e0\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\nhikari connection pool \uc0ac\uc774\uc988\ub97c 10\uc73c\ub85c \uc124\uc815\ud588\ub294\ub370, 10\uac1c\uc758 \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\uba74\uc11c \uc800\uc7a5\uc744 \ud558\ub2e4 \ubcf4\ub2c8, 10\uac1c\uc758 \ucee4\ub125\uc158\uc744 \ubaa8\ub450 \uc0ac\uc6a9\ud558\uace0 \ub098\uc11c, 11\ubc88\uc9f8\ubd80\ud130\ub294 \ucee4\ub125\uc158\uc744 \uac00\uc838\uc624\uae30 \uc704\ud574, \uae30\ub2e4\ub824\uc57c \ud558\ub294 \uc0c1\ud669\uc774 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\\n### \uc5b4\ub5bb\uac8c \uac1c\uc120\ud560 \uc218 \uc788\uc744\uae4c?\\n\\nhikari connection pool \uc0ac\uc774\uc988\ub97c 25\ub85c \uc124\uc815\ud574\uc11c, 25\uac1c\uc758 \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\ub3c4\ub85d \ud569\ub2c8\ub2e4.\\n\\n```\\nspring.datasource.hikari.maximum-pool-size=25\\n```\\n\\n## \uc5ec\ub7ec \uc2a4\ub808\ub4dc\uc5d0\uc11c \ub3d9\uc791\ud558\uace0, \uc5ec\ub7ec \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\ub294 \ubc84\uc804 2\\n\\n![multi_thread2](https://blog.kakaocdn.net/dn/vJEoD/btsmsfau8Mv/j0CT8fVrAp3LKGRMmyMVeK/img.png)\\n\\n\ucd1d 13\ub9cc \uac1c\uc758 \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud558\ub294\ub370, 147\ucd08\uac00 \uac78\ub9ac\uace0, db \uc778\uc2a4\ud134\uc2a4\uc758 cpu \uc0ac\uc6a9\ub960\uc774 100%\uc5d0 \uac00\uae4c\uc6cc\uc838\uc11c ec2 \uac00 \ub2e4\uc6b4\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\ndb\uc758 cpu \uc0ac\uc6a9\ub7c9\uc744 \uace0\ub824\ud558\uc9c0 \uc54a\uace0, 23\ub9cc \uac1c\uac00 \uc870\uae08 \ub118\ub294 \ub370\uc774\ud130\ub97c 25\uac1c\uc758 \ucee4\ub125\uc158\uc744 \ud65c\uc6a9\ud574 \uc800\uc7a5\ud558\ub824\uace0 \ud588\uc2b5\ub2c8\ub2e4\\n\\n# \uacb0\ub860\\n\\n1. \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud560 \ub54c\ub9c8\ub2e4, transaction\uc744 \uc0ac\uc6a9\ud558\uc9c0 \ub9d0\uc790\\n2. \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud560 \ub54c\ub9c8\ub2e4, id\ub97c \uc0dd\uc131\ud558\uc9c0 \ub9d0\uc790\\n3. \uc5ec\ub7ec \uc2a4\ub808\ub4dc\uc5d0\uc11c \ub3d9\uc791\ud558\uace0, \uc5ec\ub7ec \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\uc790\\n4. db\uc758 cpu \uc0ac\uc6a9\ub7c9\uc744 \uace0\ub824\ud558\uc790\\n\\n\uae34 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4"},{"id":"5","metadata":{"permalink":"/5","source":"@site/blog/2023-07-04-github_actions_pullrequest_issue.mdx","title":"pr \ubcf8\ubb38\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \ub2ec\uc544\uc8fc\ub294 \uae30\ub2a5\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4","description":"\uc548\ub155\ud558\uc138\uc694 \uc6b0\ud14c\ucf54 \uce74\ud398\uc778\ud300 \ub204\ub204\uc785\ub2c8\ub2e4","date":"2023-07-04T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 4\uc77c","tags":[{"label":"github","permalink":"/tags/github"},{"label":"action","permalink":"/tags/action"},{"label":"pr","permalink":"/tags/pr"},{"label":"issue","permalink":"/tags/issue"}],"readingTime":3.19,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"5","title":"pr \ubcf8\ubb38\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \ub2ec\uc544\uc8fc\ub294 \uae30\ub2a5\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4","authors":["nunu"],"tags":["github","action","pr","issue"]},"prevItem":{"title":"[DB] \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud574\ubcf4\uc790","permalink":"/6"},"nextItem":{"title":"\ud070 \ud2c0\uc5d0\uc11c \ubc14\ub77c\ubcf4\ub294 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98 \uacc4\ud68d","permalink":"/4"}},"content":"\uc548\ub155\ud558\uc138\uc694 \uc6b0\ud14c\ucf54 \uce74\ud398\uc778\ud300 \ub204\ub204\uc785\ub2c8\ub2e4\\n\\n\ube60\ub974\uac8c \uacb0\uacfc\ubd80\ud130 \ubcf4\uace0 \uac00\uc2dc\uc8e0.\\n\\n## \uc5b4\ub5a4 \uacb0\uacfc\uac00 \ub098\uc654\ub098\uc694?\\n\\npr\uc758 \ubcf8\ubb38 \ub05d\uc5d0, \uc5f0\uad00\ub41c \uc774\uc288 \ubc88\ud638\ub97c \ub2ec\uc544\uc8fc\ub294 \uae30\ub2a5\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ubc11\uc5d0 \uc0ac\uc9c4\uc744 \ubcf4\uc2dc\uba74 \uc27d\uac8c \uc774\ud574\ud558\uc2e4 \uc218 \uc788\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://user-images.githubusercontent.com/80899085/250614527-e2672cf2-786a-434c-a8b6-8b374de4d689.png)![img](https://user-images.githubusercontent.com/80899085/250614882-d99aa570-51e2-4565-ab4c-ccdbd4d36e57.png)\\n\\ngithub\uc5d0\uc11c issue \ubc88\ud638\uac00 pr\uc5d0 \ub2f4\uaca8\uc788\ub2e4\uba74 2\uac00\uc9c0 \uc7a5\uc810\uc774 \uc0dd\uae30\ub294\ub370\uc694.\\n\\n1. issue\ub97c \ud074\ub9ad\ud588\uc744 \ub54c, \uc790\ub3d9\uc73c\ub85c \uadf8 issue\ub85c \ub118\uc5b4\uac08 \uc218 \uc788\uc2b5\ub2c8\ub2e4. (\ud638\ubc84\ub9cc\uc73c\ub85c \uc774\uc288\uc5d0 \ub300\ud55c \uac04\ub2e8\ud55c \uc815\ubcf4\ub97c \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4)\\n2. pr \uc774 merge \ub418\uc5c8\uc744 \ub54c, \uc790\ub3d9\uc73c\ub85c issue \uac00 close \ub429\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc744 \uc190\uc73c\ub85c \uc9c4\ud589\ud558\ub294 \uac83\ubcf4\ub2e4, \uc790\ub3d9\uc73c\ub85c \uc9c4\ud589\ud558\uac8c \ub418\uba74 \uc2e4\uc218\ub3c4 \uc904\uc5b4\ub4e4\uace0, \uac1c\ubc1c \uacfc\uc815\uc774 \ud3b8\ud574\uc9c8 \uac83 \uac19\uc544\uc11c \uc774 \uae30\ub2a5\uc744 \uc81c\uc791\ud558\uac8c \ub418\uc5c8\ub294\ub370\uc694\\n\\n## \uc911\uc694\ud55c \uc810\\n\\n**\uc774 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub824\uba74 \ubc11\uc5d0\uc11c \uc18c\uac1c\ud574\ub4dc\ub9b4 \ube0c\ub79c\uce58 \ub124\uc774\ubc0d \uaddc\uce59\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.**\\n\\n## \ube0c\ub79c\uce58 \uc774\ub984 \uaddc\uce59\\n\\n- \ube0c\ub79c\uce58 \uc774\ub984\uc740 `\ud0c0\uc785/\uc774\uc288\ubc88\ud638` \uc73c\ub85c \uad6c\uc131\ud569\ub2c8\ub2e4. ex) `feat/1`\\n- \ud0c0\uc785\uc740 `feat`, `fix`, `docs`, `refactor`, `test` \ub4f1 \uc5ec\ub7ec \uac00\uc9c0\uac00 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub807\uac8c \ud588\uc744 \ub54c, \uc774\uc288 \ubc88\ud638\ub97c \ube0c\ub79c\uce58 \uba85\uc5d0\uc11c\ubd80\ud130 \uac00\uc838\uc62c \uc218 \uc788\uae30\uc5d0, \uc790\ub3d9\ud654\ub97c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uaddc\uce59\uc774 \uc544\ub2cc, feat/action \uac19\uc740 \ud615\ud0dc\uac00 \ub41c\ub2e4\uba74 issue \ubc88\ud638\ub97c \ucc3e\uae30 \uc5b4\ub835\uaca0\uc8e0?\\n\\n## \uc0ac\uc6a9 \ubc29\ubc95\\n\\n\uc791\uc131\ub41c \ucf54\ub4dc\ubd80\ud130 \ubcf4\uc2dc\uace0, \uc124\uba85\uc744 \ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\ub798\uc5d0 \uc791\uc131\ub41c \ucf54\ub4dc\ub97c. github/workflows/assign\\\\_issue\\\\_number\\\\_to\\\\_pr\\\\_body.yml\ub85c \uc800\uc7a5\ud558\uc2dc\uba74 \ub05d\uc785\ub2c8\ub2e4.\\n\\n```yml\\nname: assign_issue_number_to_pr_body\\n\\non:\\n pull_request:\\n types: [ opened ]\\n branches-ignore:\\n - develop\\n\\njobs:\\n append_issue_number_to_pr_body:\\n runs-on: ubuntu-latest\\n steps:\\n - name: append feature number to pr body pr branch = feat/(issueNumber)\\n uses: actions/github-script@v4\\n with:\\n github-token: ${{ secrets.GITHUB_TOKEN }}\\n script: |\\n const pr = await github.pulls.get({\\n owner: context.repo.owner,\\n repo: context.repo.repo,\\n pull_number: context.issue.number\\n });\\n const body = pr.data.body;\\n const issueNumber= pr.data.head.ref.split(\'/\')[1];\\n const newBody = body + \\"\\\\n\\\\n\\" + \\"close #\\" + issueNumber;\\n await github.pulls.update({\\n owner: context.repo.owner,\\n repo: context.repo.repo,\\n pull_number: context.issue.number,\\n body: newBody\\n });\\n```\\n\\n## \uc9c4\ud589 \uacfc\uc815\\n\\n1. pr \uc774 \uc0dd\uc131\ub418\uba74, pr\uc5d0 \ub300\ud55c \uc815\ubcf4\ub97c \uac00\uc838\uc635\ub2c8\ub2e4.\\n2. pr\uc758 \ubcf8\ubb38\uc744 \uac00\uc838\uc635\ub2c8\ub2e4.\\n3. pr\uc758 \ube0c\ub79c\uce58 \uc774\ub984\uc5d0\uc11c \uc774\uc288 \ubc88\ud638\ub97c \uac00\uc838\uc635\ub2c8\ub2e4.\\n4. pr\uc758 \ubcf8\ubb38\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.\\n5. pr\uc758 \ubcf8\ubb38\uc744 \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c, \uc9c1\uc811 pr\uc758 \ubcf8\ubb38\uc744 \uc218\uc815\ud558\uc9c0 \uc54a\uc544\ub3c4, \uc790\ub3d9\uc73c\ub85c \uc774\uc288 \ubc88\ud638\uac00 \ucd94\uac00\ub418\uae30\uc5d0, \uc2e4\uc218\ub97c \uc904\uc77c \uc218 \uc788\uc73c\ub2c8, \ud55c \ubc88 \uc2dc\ub3c4\ud574 \ubcf4\uc138\uc694"},{"id":"4","metadata":{"permalink":"/4","source":"@site/blog/2023-07-03-jay-infra.mdx","title":"\ud070 \ud2c0\uc5d0\uc11c \ubc14\ub77c\ubcf4\ub294 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98 \uacc4\ud68d","description":"\uc11c\ub860","date":"2023-07-03T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 3\uc77c","tags":[{"label":"java17","permalink":"/tags/java-17"},{"label":"infra","permalink":"/tags/infra"},{"label":"ec2","permalink":"/tags/ec-2"},{"label":"ci","permalink":"/tags/ci"},{"label":"cd","permalink":"/tags/cd"},{"label":"aws","permalink":"/tags/aws"}],"readingTime":7.19,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"4","title":"\ud070 \ud2c0\uc5d0\uc11c \ubc14\ub77c\ubcf4\ub294 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98 \uacc4\ud68d","authors":["jay"],"tags":["java17","infra","ec2","ci","cd","aws"]},"prevItem":{"title":"pr \ubcf8\ubb38\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \ub2ec\uc544\uc8fc\ub294 \uae30\ub2a5\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4","permalink":"/5"},"nextItem":{"title":"Java 17 \uc744 \ub3c4\uc785\ud55c \uc774\uc720","permalink":"/3"}},"content":"## \uc11c\ub860\\n\\n\uc548\ub155\ud558\uc138\uc694\ud83d\udc4b\ud83d\udc4b `\uce74\ud398\uc778` \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\ud68c\uc758\ub97c \ud558\uba74\uc11c \uc774\ubc88 \uc8fc \uc81c\uac00 \ub9e1\uc740 \ud30c\ud2b8\ub294 \uc11c\ubc84 \uc778\ud504\ub77c\uc785\ub2c8\ub2e4.\\n\\n\uc544\uc9c1\uc740 EC2 \uc2a4\ud399\uacfc \ub370\uc774\ud130\ub4e4\uc774 \uc815\ud655\ud788 \ub098\uc624\uc9c4 \uc54a\uc558\uc9c0\ub9cc,\\n\uc6b0\ud14c\ucf54\uc5d0\uc11c \uc801\uc740 EC2 \uc2a4\ud399\uc744 \uc81c\uacf5\ud55c\ub2e4\ub294 \uae30\uc900\uc73c\ub85c \uacc4\ud68d\ub3c4\ub97c \uc801\uc5b4\ubcfc \uc0dd\uac01\uc785\ub2c8\ub2e4.\\n\\n\\n## \uc0c1\ud669 \uc778\uc2dd\\n\\n\uc608\uc0c1\ud558\ub294 \uc0c1\ud669\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n- API\uc758 \ub370\uc774\ud130\ub97c \ub2e4\ub8e8\ub294 \uc0c1\ud669\uc5d0\uc11c \ucd5c\uc18c \uc57d 150\ub9cc \uac74\uc5d0\uc11c \ucd5c\uc545 \uc57d 3700\ub9cc \uac74\uc758 \ub370\uc774\ud130\ub97c \ub2e4\ub8f9\ub2c8\ub2e4.\\n- \uc774\uc804 \uae30\uc218\ub97c \ubd24\uc744 \ub54c EC2\uc758 \uac1c\uc218\ub294 \ub9ce\uc774 \ub098\ub220\uc8fc\ub294 \uac83\uc73c\ub85c \ud30c\uc545 \ub410\uc2b5\ub2c8\ub2e4. (\uc774 \ubd80\ubd84\uc740 \ub2ec\ub77c\uc9c8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.)\\n- \uc0c1\ud669\uc5d0 \ub530\ub77c\uc11c \uacf5\uacf5 API\ub97c \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\ub294 \uc11c\ubc84\uc640, \uc81c\uacf5 \uc11c\ubc84\ub97c \ub098\ub20c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n- Conflict\uac00 \ub098\uc9c0 \uc54a\uae30 \uc704\ud574\uc11c \uc548\uc815\uc801\uc778 \uac80\uc99d\uc744 \uac70\uce5c \ud6c4 Merge\ub97c \ud574\uc57c\ud569\ub2c8\ub2e4.\\n- \ud504\ub85c\uc81d\ud2b8\uc758 \ubc84\uc804\uc774 \uac31\uc2e0\ub41c\ub2e4\uba74 EC2 \uc11c\ubc84\uc5d0\uc11c \uc790\ub3d9\uc73c\ub85c \uc2a4\ud06c\ub9bd\ud2b8\ub97c \uc791\ub3d9\uc2dc\ucf1c Pull \ubc0f \uc11c\ubc84 \uc7ac\ubc30\ud3ec\ub97c \ud574\uc57c\ud569\ub2c8\ub2e4.\\n- \uc11c\ubc84\uc758 \ubc84\uc804\uc774 \ubc14\ub00c\ub294 \uacbd\uc6b0 \uae30\uc874 \uc11c\ubc84\ub97c \ub044\uace0 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\ub97c \ud0a4\uba74 \uc0ac\uc6a9\uc790\uac00 \uc774\uc6a9\ud560 \uc218 \uc5c6\ub294 \ud140\uc774 \uc0dd\uae30\uae30 \ub54c\ubb38\uc5d0 \ubb34\uc911\ub2e8 \ubc30\ud3ec\ub97c \ud574\uc57c\ud569\ub2c8\ub2e4.\\n\\n## \ubb38\uc81c\uc810\\n\\n\uc704\uc5d0 \uc0c1\ud669\uc5d0\uc11c \ud30c\uc545\ub418\ub294 \ubb38\uc81c\uc810\ub4e4\uc740 \uba3c\uc800 \uc801\uc740 \uc131\ub2a5\uc758 EC2 \uc11c\ubc84\ub85c \uc778\ud574 \ub370\uc774\ud130\ub97c \ubc1b\uc544\uc624\ub294 \uacfc\uc815 \ud639\uc740 \uc5c5\ub370\uc774\ud2b8 \uacfc\uc815\uc5d0\uc11c \uc11c\ubc84\uac00 \ud130\uc9c8 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\uc131\ub2a5\uc774 \uc88b\ub2e4\uba74 \ud558\ub098\ub85c \ubaa8\ub4e0 \uac83\uc744 \ud560 \uc218 \uc788\uc9c0\ub9cc, \uadf8\ub807\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \ud604\uc7ac \uc5ec\ub7ec \uac1c\uc758 EC2\ub97c \uae30\uc900\uc73c\ub85c \uc544\ud0a4\ud14d\ucc98\ub97c \uad6c\uc131\ud560 \uc608\uc815\uc785\ub2c8\ub2e4.\\n\\n## \ubb38\uc81c \ud574\uacb0\uc744 \uc704\ud55c \ud604\uc7ac \uc0dd\uac01\\n\\n### \uc11c\ubc84\uc758 \uae30\ub2a5 \ubd84\uc0b0\\n\uc704\uc5d0\uc11c \uc5b8\uae09\ud55c \uac83\ucc98\ub7fc \uc11c\ubc84\uc758 \uc131\ub2a5\uc774 \ubc1b\uccd0\uc8fc\uc9c0 \ubabb\ud560 \uac00\ub2a5\uc131\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uc131\ub2a5\uc744 \uc0dd\uac01\ud574\uc11c \uc774\ub97c \ub098\ub204\uae30 \uc704\ud574\uc11c\ub294 \uba3c\uc800 \ub2e4\uc74c\uacfc \uac19\uc774 \uc11c\ubc84\ub97c \ubd84\uc0b0\ud560 \ud544\uc694\uac00 \uc788\ub2e4\uace0 \uc0dd\uac01\ud569\ub2c8\ub2e4.\\n(\ubb3c\ub860 \uc11c\ubc84\uac00 \ubabb \ubc84\ud2f8 \uacbd\uc6b0\uc774\uace0, \uc5b4\ub5bb\uac8c \ub098\ub258\ub294 \uc9c0\ub294 \ud68c\uc758 \ud6c4 \uacb0\uc815\ud558\uaca0\uc9c0\ub9cc!)\\n- `\uacf5\uacf5 API \ub370\uc774\ud130 \uc801\uc7ac \ubc0f \uc8fc\uae30\uc801\uc778 \uc5c5\ub370\uc774\ud2b8`\\n- `\uc2e4\uc2dc\uac04 \ud63c\uc7a1\ub3c4\ub97c \uc704\ud55c \uc2e4\uc2dc\uac04 \ub370\uc774\ud130 \uc5c5\ub370\uc774\ud2b8`\\n- `\uc694\uccad \ucc98\ub9ac`\\n\\n\uc801\uc740 \uc131\ub2a5\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\uc640 \uc694\uccad \ucc98\ub9ac\ub97c \ub3d9\uc2dc\uc5d0 \ud55c\ub2e4\uba74, \uc11c\ubc84\uac00 \uadf8 \ubd80\ud558\ub97c \uacac\ub514\uc9c0 \ubabb\ud560 \uc218\ub3c4 \uc788\uaca0\uc8e0?\\n\ub530\ub77c\uc11c \uc11c\ubc84\uc758 \uc5ed\ud560\uc744 \ubd84\ub2f4\ud558\uace0, \uac01 \uc5ed\ud560\uc5d0 \ucda9\uc2e4\ud558\ub3c4\ub85d \uad6c\ud604\ud55c\ub2e4\uba74 \ubcf4\ub2e4 \ud6a8\uc728\uc801\uc778 \ucc98\ub9ac\ub97c \ud560 \uc218 \uc788\uc744 \uac83\uc774\ub77c\uace0 \uc608\uc0c1\ub429\ub2c8\ub2e4.\\n\\n\\n### \uc548\uc815\uc801\uc778 Merge\\n\\n\uc798\ubabb\ub41c PR\uc744 Merge \uc2dc\ucf1c\ubc84\ub9ac\uba74 \uc5b4\ub5a8\uae4c\uc694? Conflict\ub3c4 \ub0a0 \uc218 \uc788\uace0.. \uc0dd\uac01\ub9cc\ud574\ub3c4 \ub054\ucc0d\ud569\ub2c8\ub2e4.\\n\\n\ucf54\ub4dc\ub9ac\ubdf0\ub97c \ud1b5\ud574\uc11c \uc774\ub97c \uc5b4\ub290\uc815\ub3c4 \ud574\uc18c\ud55c\ub2e4\uace0 \ud574\ub3c4, \uc0ac\ub78c\uc774\ub2e4\ubcf4\ub2c8 \uc2e4\uc218\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\uc774\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574\uc11c `Github Actions`\ub97c \uc774\uc6a9\ud558\uc5ec \ubbf8\ub9ac \uc9c0\uc815\ud574\ub454 Task\ub97c \uc2dc\ud0a4\uace0, \uc774\uac8c \ud1b5\uacfc\ud55c\ub2e4\uba74 Merge\ud560 \uc218 \uc788\ub3c4\ub85d \ud560 \uc608\uc815\uc785\ub2c8\ub2e4.\\n\\n\uc774\ub807\uac8c \ud55c\ub2e4\uba74 \ud611\uc5c5\ud560 \ub54c\uc5d0\ub3c4 \uc548\uc804\ud55c Merge\uac00 \uac00\ub2a5\ud558\ub2e4\uace0 \uc0dd\uac01\ud569\ub2c8\ub2e4.\\n\\n### CI/CD\\n\\n\uc9c0\uae08\uae4c\uc9c0 \uc6b0\ud14c\ucf54 \ubbf8\uc158\uc5d0\uc11c\ub294 \ubc30\ud3ec\ub97c \ub2e4\uc74c\uacfc \uac19\uc740 \uacfc\uc815\uc73c\ub85c \uc9c4\ud589\ud588\uc2b5\ub2c8\ub2e4.\\n\\n1. \ubc30\ud3ec\\n2. \ub9ac\ud329\ud1a0\ub9c1 \ubc0f \ucee4\ubc0b\\n3. EC2 \uc11c\ubc84\uc5d0\uc11c \uc2a4\ud06c\ub9bd\ud2b8 \uc2e4\ud589\ud558\uc5ec \uc7ac\ubc30\ud3ec\\n\\n\uc774\ub807\uac8c \ubc30\ud3ec\ub97c \ud574\ub3c4 \uc0c1\uad00\uc5c6\uc9c0\ub9cc, \ub9e4\ubc88 \ub9ac\ud329\ud1a0\ub9c1\uacfc \uae30\ub2a5 \ucd94\uac00\ub97c \ud560 \ub54c\ub9c8\ub2e4 EC2 \uc11c\ubc84\ub85c \ub4e4\uc5b4\uac00\uc11c \ube4c\ub4dc \uc2a4\ud06c\ub9bd\ud2b8\ub97c \uc0ac\uc6a9\ud574\uc11c \uc11c\ubc84\ub97c \uc7ac\uc2dc\uc791 \ud574\uc57c\ud560\uae4c\uc694?\\n\uc774\ub807\uac8c \ub41c\ub2e4\uba74 \ubd88\ud544\uc694\ud55c \uc2dc\uac04\uc774 \uc18c\ubaa8\ub418\uace0, \ubd88\ud3b8\ud55c \uc810\uc774 \ub9ce\uc744 \uac83\uc774\ub77c\uace0 \uc0dd\uac01\ub429\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c CI/CD \uac1c\ub150\uc744 \uc801\uc6a9\ud574\uc11c \uc774 \uacfc\uc815\uc744 \uc790\ub3d9\uc73c\ub85c \uc9c4\ud589\ud558\uace0\uc790 \ud569\ub2c8\ub2e4.\\n\\n\uc774 \ubd80\ubd84\uc740 \ub354 \uc54c\uc544\ubd10\uc57c\uaca0\uc9c0\ub9cc, Github Actions\ub97c \uc774\uc6a9\ud574\uc11c \uc774\ub97c \uc801\uc6a9\ud558\uba74, \uc678\ubd80\uc5d0\uc11c SSH \uc811\uadfc\uc774 \ubd88\uac00\ub2a5\ud558\uae30 \ub54c\ubb38\uc5d0 Jenkins\ub97c \uc774\uc6a9\ud560 \uc608\uc815\uc785\ub2c8\ub2e4.\\n\uae43\ud5c8\ube0c\uc758 \ubcc0\ub3d9 \uc0ac\ud56d\uc744 Webhook\uc744 \uc774\uc6a9\ud574\uc11c Jenkins\ub85c \ub118\uae30\uace0, \uc774\ub97c \ud1b5\ud574 CI\ub97c \uc801\uc6a9\ud558\uba74 \ub420 \uac83 \uac19\ub2e4\uace0 \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\ubb3c\ub860 \uc774\ub294 \uacc4\ud68d\uc774\uace0 \uacf5\ubd80\ud558\uc9c0 \uc54a\uc740 \ub2e4\ub978 \ub0b4\uc6a9\uc774 \uc788\uc744 \uc218 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc5b8\uc81c\ub4e0 \ubc14\ub014 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \ubb34\uc911\ub2e8 \ubc30\ud3ec \uc544\ud0a4\ud14d\ucc98 \uc801\uc6a9\\n\uc774 \ub610\ud55c \uc544\uc9c1\uc740 \uba3c \uc774\uc57c\uae30\uc9c0\ub9cc, \uace0\ub824\ud574 \ubcfc \uc0c1\ud669\uc774\ub77c\uc11c \uc801\uc5b4\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790\uac00 \uc774\uc6a9\ud558\uace0 \uc788\ub294 \uc11c\ube44\uc2a4\uac00 \uac11\uc790\uae30 \uc911\ub2e8\ub41c\ub2e4\uba74 \uc5b4\ub5a8\uae4c\uc694?\\n\uc800\ub294 \ud654\uac00 \ub9ce\uc774 \ub0a0 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ud53c\uce58 \ubabb\ud560 \uc0ac\uc815\uc73c\ub85c \uc11c\ubc84\uac00 \ud130\uc838\ub3c4, \uc0ac\uc6a9\uc790\uac00 \uc11c\ube44\uc2a4\ub97c \uacc4\uc18d \uc774\uc6a9\ud560 \ubc29\ubc95\uc774 \uc5c6\uc744\uae4c\uc694?\\n\\n\uc774\ub7f0 \uace0\ubbfc\uc744 \ud574\uacb0\ud558\uae30 \uc704\ud574\uc11c \ub098\uc628 \uac1c\ub150\uc774 \ubb34\uc911\ub2e8 \ubc30\ud3ec\uc785\ub2c8\ub2e4.\\n\\n`\uce74\ub098\ub9ac\uc544 \ubc30\ud3ec`, `Blue/Green \ubc30\ud3ec`, `\ub864\ub9c1`\ub4f1 \ubb34\uc911\ub2e8 \ubc30\ud3ec\ub97c \uc704\ud55c \uc5ec\ub7ec\uac00\uc9c0 \uc804\ub7b5\uc740 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4.\\n\uc774 \ubd80\ubd84\uc740 \uc544\uc9c1\uc740 \uc11c\ubc84\uc758 \uba85\uc138\uac00 \uc815\ud655\ud558\uc9c0 \uc54a\uc544\uc11c \uc5b4\ub5a4 \ubc29\uc2dd\uc73c\ub85c \uc5b4\ub5bb\uac8c \ucc98\ub9ac\ud560 \uac83\uc778\uc9c0\uc5d0 \ub300\ud574\uc11c\ub294 \uc544\uc9c1 \uc815\ud560 \uc218\ub294 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub294 \uba85\uc138\uac00 \ud655\uc2e4\ud558\uac8c \uc815\ud574\uc9c4 \ud6c4 \ud300\uc6d0\uacfc \uc7a5\ub2e8\uc810\uc744 \uc0c1\uc758\ud558\uba70 \uacb0\uc815\ud560 \uc77c\uc774\uae30 \ub54c\ubb38\uc5d0 \ud604\uc7ac\uae4c\uc9c0\ub294 \\"\uc774 \uc815\ub3c4\ub97c \uace0\ub824\ud558\uace0 \uc788\ub2e4.\\" \uc815\ub3c4\ub9cc \uc54c\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4."},{"id":"3","metadata":{"permalink":"/3","source":"@site/blog/2023-07-02-nunu-java-version.mdx","title":"Java 17 \uc744 \ub3c4\uc785\ud55c \uc774\uc720","description":"\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4\uc5d0\uc11c \uc790\ubc14 11\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \ub108\ubb34 \uc775\uc219\ud574\uc9c4 \uc0c1\ud669\uc774\uc5b4\uc11c, java 11 \ub300\uc2e0 java 17\uc744 \uc4f0\ub824\uba74 \uc4f0\ub294 \ub300\uc2e0, \uc65c java 17\uc744 \uc4f0\uba74 \uc88b\uc740\uc9c0\uc5d0 \ub300\ud574\uc11c \uc124\ub4dd\uc744 \ud558\ub294 \uc2dc\uac04\uc774 \uc788\uc5b4\uc57c \ud558\ub294\ub370\uc694","date":"2023-07-02T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 2\uc77c","tags":[{"label":"java17","permalink":"/tags/java-17"},{"label":"java11","permalink":"/tags/java-11"},{"label":"record","permalink":"/tags/record"},{"label":"toList","permalink":"/tags/to-list"},{"label":"gc","permalink":"/tags/gc"}],"readingTime":5.88,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"3","title":"Java 17 \uc744 \ub3c4\uc785\ud55c \uc774\uc720","authors":["nunu"],"tags":["java17","java11","record","toList","gc"]},"prevItem":{"title":"\ud070 \ud2c0\uc5d0\uc11c \ubc14\ub77c\ubcf4\ub294 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98 \uacc4\ud68d","permalink":"/4"},"nextItem":{"title":"git branch \uc804\ub7b5 \uc791\uc131\ud574\ubcf4\uae30","permalink":"/2"}},"content":"\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4\uc5d0\uc11c \uc790\ubc14 11\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \ub108\ubb34 \uc775\uc219\ud574\uc9c4 \uc0c1\ud669\uc774\uc5b4\uc11c, java 11 \ub300\uc2e0 java 17\uc744 \uc4f0\ub824\uba74 \uc4f0\ub294 \ub300\uc2e0, \uc65c java 17\uc744 \uc4f0\uba74 \uc88b\uc740\uc9c0\uc5d0 \ub300\ud574\uc11c \uc124\ub4dd\uc744 \ud558\ub294 \uc2dc\uac04\uc774 \uc788\uc5b4\uc57c \ud558\ub294\ub370\uc694\\n\\n\ucc98\uc74c\uc5d0\ub294 \ub2e8\uc21c\ud788 record \ud074\ub798\uc2a4\uac00 \uc88b\uc544\uc694, collect(Collectors.toList()); \ub300\uc2e0 toList() \ub9cc\uc73c\ub85c \ud574\uacb0\ud560 \uc218 \uc788\uc5b4\uc11c \uc88b\uc544\uc694\\n\\n\uae4c\uc9c0\ubc16\uc5d0 \uc124\uba85\ud560 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uac83\ub9cc\uc73c\ub85c \ub3d9\uc758\ub97c \ud574\uc918\uc11c \uc77c\ub2e8 java 17 \uc744 \uc0ac\uc6a9\ud558\uae30\ub85c \ud588\uc9c0\ub9cc, \uc774\ubc88 \uae30\ud68c\uc5d0 \uc870\uae08 \ub354 \uc790\uc138\ud558\uac8c \uc54c\uc544\ubcf4\ub824\uace0 \ud569\ub2c8\ub2e4\\n\\n## Java 17 \uacfc Java 11\uc758 \uc911\uc694\ud55c \ucc28\uc774\ub4e4\\n\\n\uae30\ub2a5\uc801\uc778 \ubd80\ubd84\uacfc, \uc228\uaca8\uc9c4 \ubd80\ubd84\uc744 \ub098\ub204\uc5b4\ubcfc \uc218 \uc788\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n## \uae30\ub2a5\uc801\uc778 \ucc28\uc774\uc810\\n\\n\uc5b8\uc81c\ub098 \uc9c1\uc811 \ucc28\uc774\ub97c \ubcf4\uba74 \ub354 \uc9c1\uad00\uc801\uc774\uae30 \ub54c\ubb38\uc5d0, \uc9c1\uc811 \ucf54\ub4dc\ub97c \ubcf4\uba74\uc11c \uc124\uba85\uc744 \ud574\ubcf4\ub824\uace0 \ud569\ub2c8\ub2e4\\n\\n### record \ud074\ub798\uc2a4\\n\\n\uac04\ub2e8\ud55c dto \ud074\ub798\uc2a4\ub97c \ub9cc\ub4e4\uc5c8\uc744 \ub54c \ucf54\ub4dc\uac00 \uc815\ub9d0 \uac04\ub2e8\ud574\uc9c0\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n#### Java 11\\n\\n```\\npublic class Dto {\\n private final int data;\\n\\n public Dto(int data) {\\n this.data = data;\\n }\\n\\n public int getData() {\\n return data;\\n }\\n}\\n```\\n\\nlombok \uc744 \uc0ac\uc6a9\ud588\uc744 \ub54c\\n\\n```\\n\\n@Getter\\n@AllArgsConstructor\\npublic class Dto {\\n private final int data;\\n}\\n```\\n\\n#### Java17\\n\\n```\\npublic record Record(int data) {\\n}\\n```\\n\\n\uc774\ub807\uac8c \ubcf4\uba74 \ud6e8\uc52c \uac04\ub2e8\ud574\uc9c4 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n#### \uc608\uc0c1\ub418\ub294 \ubb38\uc81c\uc810\\n\\nobjectMapper\ub97c \uc0ac\uc6a9\ud558\uba74 \uc5b4\ub5bb\uac8c \ub418\ub098\uc694? noArgsConstructor \uac00 \ud544\uc694\ud558\uc9c0 \uc54a\ub098\uc694?\\n\\n```java\\nclass RecordTest {\\n\\n @Test\\n void objectMapper_\ub85c_\ubcc0\ud658() throws JsonProcessingException {\\n // given\\n ObjectMapper objectMapper = new ObjectMapper();\\n Record record = new Record(1);\\n\\n // when\\n String json = objectMapper.writeValueAsString(record);\\n\\n // then\\n assertEquals(\\"{\\\\\\"data\\\\\\":1}\\", json);\\n }\\n\\n @Test\\n void string_\uc5d0\uc11c_\uac1d\uccb4\ub85c_\ubcc0\ud658() throws JsonProcessingException {\\n // given\\n String json = \\"{\\\\\\"data\\\\\\":1}\\";\\n ObjectMapper objectMapper = new ObjectMapper();\\n\\n // when\\n Record record = objectMapper.readValue(json, Record.class);\\n\\n // then\\n assertEquals(1, record.data());\\n }\\n}\\n```\\n\\n\uc774 \ud14c\uc2a4\ud2b8\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \uac83\ucc98\ub7fc \uc131\uacf5\uc801\uc73c\ub85c deserialize, serialize \uac00 \uac00\ub2a5\ud569\ub2c8\ub2e4\\n\\n### toList() method\\n\\n#### Java 11\\n\\n\uc774 \ubd80\ubd84\ub3c4 \uc815\ub9d0 \ud3b8\uc758\uc131\uc774 \ub192\ub2e4\uace0 \uc0dd\uac01\ud558\ub294 \ubd80\ubd84 \uc911 \ud558\ub098\uc778\ub370\uc694\\n\\nCollectors.toList() \ub300\uc2e0 toList() \ub9cc\uc73c\ub85c\ub3c4 \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4\\n\\n```java\\npublic class ToListWith11 {\\n\\n public static void main(String[] args) {\\n List list = List.of(1, 2, 3, 4, 5);\\n List result = list.stream()\\n .filter(i -> i > 3)\\n .collect(Collectors.toList());\\n System.out.println(result);\\n }\\n}\\n```\\n\\n#### Java 17\\n\\n```java\\npublic class ToListWith17 {\\n\\n public static void main(String[] args) {\\n List list = List.of(1, 2, 3, 4, 5);\\n List result = list.stream()\\n .filter(i -> i > 3)\\n .toList();\\n System.out.println(result);\\n }\\n}\\n```\\n\\n### switch expression\\n\\n#### Java 11\\n\\n\uc6b0\ud14c\ucf54\uc5d0\uc11c\ub294 switch, case \ub97c \uc2eb\uc5b4\ud558\uae30\uc5d0 \ubcfc \uc218\ub294 \uc5c6\uaca0\uc9c0\ub9cc\\n\\nswitch \ubb38\uc5d0\ub3c4 \uc815\ub9d0 \ud3b8\ud558\uac8c \ubc14\ub00c\uc5c8\ub294\ub370\uc694\\n\\n```java\\npublic class SwitchWith11 {\\n\\n public static void main(String[] args) {\\n String day = \\"Sunday\\";\\n int result = 0;\\n switch (day) {\\n case \\"Monday\\":\\n result = 1;\\n break;\\n case \\"Tuesday\\":\\n result = 2;\\n break;\\n case \\"Wednesday\\":\\n result = 3;\\n break;\\n case \\"Thursday\\":\\n result = 4;\\n break;\\n case \\"Friday\\":\\n result = 5;\\n break;\\n case \\"Saturday\\":\\n result = 6;\\n break;\\n case \\"Sunday\\":\\n result = 7;\\n break;\\n }\\n System.out.println(result);\\n }\\n}\\n```\\n\\n#### Java 17\\n\\n```java\\npublic class SwitchWith17 {\\n\\n public static void main(String[] args) {\\n String day = \\"Sunday\\";\\n int result = switch (day) {\\n case \\"Monday\\" -> 1;\\n case \\"Tuesday\\" -> 2;\\n case \\"Wednesday\\" -> 3;\\n case \\"Thursday\\" -> 4;\\n case \\"Friday\\" -> 5;\\n case \\"Saturday\\" -> 6;\\n case \\"Sunday\\" -> 7;\\n default -> 0;\\n };\\n System.out.println(result);\\n }\\n}\\n```\\n\\n\ucf54\ub4dc \ub7c9\uc774 \uc5c4\uccad \uc904\uc5b4\ub4e0 \uac83\uc744 \ud655\uc778\ud558\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n### instanceof pattern matching\\n\\n\ubb3c\ub860 instanceof \ub97c \uc0ac\uc6a9\ud560 \uacbd\uc6b0\uac00 \ub9ce\uc740\uac00? \ud558\uba74 \ub9ce\uc9c0\ub294 \uc54a\uaca0\uc9c0\ub9cc\\n\\n\uc544\ub798\uc640 \uac19\uc774 \ubcc0\uacbd\ub418\uc5c8\uc2b5\ub2c8\ub2e4\\n\\n#### Java 11\\n\\n```java\\npublic class InstanceOfWith11 {\\n\\n public static void main(String[] args) {\\n Object obj = \\"Hello\\";\\n if (obj instanceof String) {\\n String str = (String) obj;\\n System.out.println(str.toUpperCase());\\n }\\n }\\n}\\n```\\n\\n#### Java 17\\n\\n```java\\npublic class InstanceOfWith17 {\\n\\n public static void main(String[] args) {\\n Object obj = \\"Hello\\";\\n if (obj instanceof String str) {\\n System.out.println(str.toUpperCase());\\n }\\n }\\n}\\n```\\n\\n### number format\\n\\n\uc774 \uae30\ub2a5\uc740 12\uc5d0 \ub098\uc654\ub294\ub370\uc694\\n\\n\uc5b8\uc5b4\ubcc4\ub85c \uc22b\uc790\ub97c \ud45c\ud604\ud558\ub294 \ubc29\uc2dd\uc774 \ub2e4\ub974\uc9c0\ub9cc, \uc27d\uac8c \ud45c\ud604\ud560 \uc218 \uc788\ub3c4\ub85d \ub3c4\uc640\uc8fc\ub294 \uae30\ub2a5\uc785\ub2c8\ub2e4\\n\\n#### Java 17\\n\\n```java\\npublic class NumberFormatterWith11 {\\n public static void main(String[] args) {\\n int number = 1_000_000;\\n\\n String result = NumberFormat.getCompactNumberInstance(Locale.KOREA, NumberFormat.Style.LONG).format(number);\\n\\n System.out.println(result.equals(\\"100\ub9cc\\"));\\n }\\n}\\n```\\n\\n\ub098\uba38\uc9c0 \ubd80\ubd84\uc740 \uc0ac\uc2e4 \uadf8\ub807\uac8c \ud070 \uc5ed\ud560\uc744 \ud560 \uac83 \uac19\uc9c0\ub294 \uc54a\uc544\uc11c \uc0dd\ub7b5\ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\n## \uc228\uaca8\uc9c4 \ubd80\ubd84\ub4e4\\n\\n![gc throughput](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXhFJg%2Fbtsl9uZOa5R%2FrzrlotCERUqAWM2pknDwq0%2Fimg.png)\\n\\n\uc704\uc758 \uc0ac\uc9c4\uc740 gc \uc758 \ubc84\uc804\ubcc4 \ucc98\ub9ac\ub7c9\uc785\ub2c8\ub2e4.\\n\\nG1 GC \ub97c \uae30\uc900\uc73c\ub85c \ubcf8\ub2e4\uba74 Java8 \uacfc\uc758 \ucc28\uc774\ub294 15% \uc815\ub3c4 \ud5a5\uc0c1\ub418\uc5c8\uace0, java 11\uacfc\ub294 10% \uc815\ub3c4 \ud5a5\uc0c1\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![gc latency](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZusmb%2Fbtsl5jYN68u%2FWCKRCFnYjQK4AjkcHRNAt0%2Fimg.png)\\n\\n\uc704\uc758 \uc0ac\uc9c4\uc740 gc\uc758 \ubc84\uc804\ubcc4 \uc9c0\uc5f0\uc2dc\uac04\uc785\ub2c8\ub2e4.\\n\\nG1 GC \ub97c \uae30\uc900\uc73c\ub85c \ubcf8\ub2e4\uba74 Java8 \uacfc\uc758 \ucc28\uc774\ub294 30% \uc815\ub3c4 \ud5a5\uc0c1\ub418\uc5c8\uace0, java 11\uacfc\ub294 25% \uc815\ub3c4 \ud5a5\uc0c1\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc640 \uac19\uc774, \ub2e8\uc21c\ud558\uac8c \uc0c8\ub85c\uc6b4 \uae30\ub2a5\ub9cc \ucd94\uac00\ub418\ub294 \uac83\uc774 \uc544\ub2c8\ub77c \uafb8\uc900\ud788 \uc131\ub2a5\ub3c4 \ud5a5\uc0c1\ub418\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \ubd80\ubd84\uc744 \uace0\ub824\ud588\uc744 \ub54c, Java 17\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ucc38\uace0\\n\\n- [https://kstefanj.github.io/2021/11/24/gc-progress-8-17.html](https://kstefanj.github.io/2021/11/24/gc-progress-8-17.html)"},{"id":"2","metadata":{"permalink":"/2","source":"@site/blog/2023-07-01-nunu-gitbranch.mdx","title":"git branch \uc804\ub7b5 \uc791\uc131\ud574\ubcf4\uae30","description":"\ud604\uc7ac \uc0c1\ud669\uc740 \uc5b4\ub5a4\ub370?","date":"2023-07-01T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 1\uc77c","tags":[{"label":"git","permalink":"/tags/git"},{"label":"branch","permalink":"/tags/branch"},{"label":"git branch","permalink":"/tags/git-branch"},{"label":"github flow","permalink":"/tags/github-flow"},{"label":"gitlab flow","permalink":"/tags/gitlab-flow"},{"label":"git flow","permalink":"/tags/git-flow"}],"readingTime":10.735,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"2","title":"git branch \uc804\ub7b5 \uc791\uc131\ud574\ubcf4\uae30","authors":["nunu"],"tags":["git","branch","git branch","github flow","gitlab flow","git flow"]},"prevItem":{"title":"Java 17 \uc744 \ub3c4\uc785\ud55c \uc774\uc720","permalink":"/3"},"nextItem":{"title":"Hello World","permalink":"/1"}},"content":"## \ud604\uc7ac \uc0c1\ud669\uc740 \uc5b4\ub5a4\ub370?\\n\\n\ud604\uc7ac \uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4\uc5d0\uc11c\ub294 \ud504\ub860\ud2b8 \ucf54\ub4dc\uc640 \ubc31\uc5d4\ub4dc \ucf54\ub4dc\uac00 \uac19\uc740 \ub808\ud3ec\uc9c0\ud1a0\ub9ac\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud504\ub860\ud2b8\uc640 \ubc31\uc5d4\ub4dc\uac00 \uac19\uc774 \uc791\uc5c5\ud558\uae30\uc5d0, \uc758\ub3c4\uce58 \uc54a\uc740 \ucda9\ub3cc\uc774 \uc790\uc8fc \uc0dd\uae38 \uc218 \uc788\ub294 \uad6c\uc870\uc774\uae30\uc5d0, \uc774\ub97c git branch \uc804\ub7b5\uc73c\ub85c \ucda9\ub3cc\uc744 \uc904\uc774\uace0\uc790 \ud569\ub2c8\ub2e4\\n\\n## Git Branch \uc804\ub7b5\uc774\ub780?\\n\\ngit\uc744 \uc0ac\uc6a9\ud574\uc11c \uc18c\ud504\ud2b8\uc6e8\uc5b4 \uac1c\ubc1c\uc744 \uad00\ub9ac\ud558\ub294 \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\\n\uc5ec\ub7ec \uac1c\ubc1c\uc790\uac00 \ub3d9\uc2dc\uc5d0 \uc791\uc5c5\ud558\uace0 \ucf54\ub4dc\ub97c \ud1b5\ud569\ud560 \ub54c \uc0dd\uae30\ub294 \ucda9\ub3cc\uc744 \ud6a8\uc728\uc801\uc73c\ub85c \uc870\uc815\ud558\uae30 \uc704\ud55c \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\\n## \uc65c git branch \uc804\ub7b5\uc774 \uc911\uc694\ud55c\ub370?\\n\\n\uc544\ub798\uc5d0 \uc788\ub294 4\uac00\uc9c0\ub97c \uc81c\uc678\ud558\uace0\ub3c4 \ud6e8\uc52c \ub9ce\uc740 \uc7a5\uc810\uc774 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### 1\\\\. \ub3d9\uc2dc \uc791\uc5c5\uc774 \ud3b8\ud558\ub2e4\\n\\n\uc5ec\ub7ec \uc0ac\ub78c\uc774 \ub3c5\ub9bd\uc801\uc73c\ub85c \uc791\uc5c5\ud558\uace0, \ucee4\ubc0b\uc744 \ud560 \ub54c, \uc790\uc2e0\uc758 \ube0c\ub79c\uce58\uc5d0\uc11c \ubcc0\uacbd \uc0ac\ud56d\uc744 \ucee4\ubc0b\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\ube0c\ub79c\uce58\uac00 \ubcd1\ud569\ub420 \ub54c\ub9cc \ucda9\ub3cc\uc744 \ud574\uacb0\ud558\uba74 \ub418\ub2c8, \uc544\ubb34 \uaddc\uce59\uc774 \uc5c6\ub294 \uac83\ubcf4\ub2e4 \ucda9\ub3cc \uc2dc\uc810\uc774 \uba85\ud655\ud574\uc9c0\uae30\uc5d0 \uc0dd\uc0b0\uc131\uc744 \ub192\uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### 2\\\\. \ubaa9\uc801\uc774 \uba85\ud655\ud55c \ube0c\ub79c\uce58\\n\\n\uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc758 \uc0c1\ud0dc\uc5d0 \uba87 \uac00\uc9c0\uac00 \uc788\ub294\ub370, \uc548\uc815\ub41c \ud504\ub85c\ub355\uc158, \ud14c\uc2a4\ud2b8 \ud658\uacbd, \uae30\ub2a5 \ucd94\uac00 \ud658\uacbd... \ub4f1\uc774 \uc788\uc2b5\ub2c8\ub2e4\\n\\n\uc5ec\ub7ec \uae30\ub2a5\ubcc4 \ube0c\ub79c\uce58(\uc548\uc815\ub41c \ubc84\uc804\uc758 \ucf54\ub4dc\ub9cc\uc774 \uad00\ub9ac\ub418\ub294 \ube0c\ub79c\uce58, \ud14c\uc2a4\ud2b8 \ud658\uacbd\uc744 \uc704\ud55c \ube0c\ub79c\uce58, \uae30\ub2a5 \ucd94\uac00\ub97c \uc704\ud55c \ube0c\ub79c\uce58)\ub97c\\n\\n\ub124\uc774\ubc0d\uc744 \ud1b5\ud574 \uad6c\ubd84\ud558\uba74 \uac01\uac01\uc758 \ube0c\ub79c\uce58\uc5d0 \ub300\ud574\uc11c \ucd94\uac00\uc801\uc778 \uc124\uba85\uc744 \ud560 \ud544\uc694 \uc5c6\uc774 \uba85\ud655\ud558\uac8c \uad00\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### 3\\\\. \ubc30\ud3ec \ud30c\uc774\ud504\ub77c\uc778 \uad00\ub9ac\uac00 \ud3b8\ud568\\n\\n\ube0c\ub79c\uce58\uac00 \ub124\uc774\ubc0d\uc73c\ub85c \uba85\ud655\ud558\uac8c \uad6c\ubd84\uc774 \ub418\uc5b4\uc788\ub2e4\uba74, \uc870\uac74\uc744 \uc124\uc815\ud558\uae30 \uc27d\uc2b5\ub2c8\ub2e4.\\n\\n\ud2b9\uc815 \ud0c0\uc785\uc758 \ube0c\ub79c\uce58\uc5d0 push \ub418\uc5c8\uc744 \ub54c, pull request\ub97c \ub9cc\ub4e4\uc5c8\uc744 \ub54c \uac19\uc740 \uc870\uac74\uc5d0 \ub530\ub978 \uc2a4\ud06c\ub9bd\ud2b8\ub97c \ub9cc\ub4e4\uc5b4\ub454\ub2e4\uba74 CI/CD\ub97c \uad6c\ucd95\ud558\uae30 \uc27d\uc2b5\ub2c8\ub2e4.\\n\\n#### 4\\\\. \ubc84\uc804 \uad00\ub9ac\uac00 \ud3b8\ub9ac\ud558\ub2e4\\n\\n\uc11c\ubc84\uc5d0 \ubb38\uc81c\uac00 \uc0dd\uacbc\uc744 \ub54c, \uc5b4\ub5a4 \ube0c\ub79c\uce58\ub85c \ub3cc\uc544\uac00\uc11c \ub864\ubc31\uc744 \ud574\uc57c \ud558\ub294\uc9c0\uc5d0 \ub300\ud55c \uac83\ub4e4\uc774 \uba85\ud655\ud569\ub2c8\ub2e4.\\n\\n\uc548\uc815\ub41c \ube0c\ub79c\uce58\uac00 \uc5b4\ub5a4 \uac83\uc778\uc9c0 \uba85\ud655\ud558\uae30\uc5d0, \ub864\ubc31 \uacfc\uc815\uc5d0 \ub300\ud55c \uc758\uc0ac\uacb0\uc815\uc744 \uc904\uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 \uc5b4\ub5a4 \uc885\ub958\uac00 \uc788\ub294\uc9c0 \ub354 \uc790\uc138\ud558\uac8c \uc54c\uc544\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## Git Branch \uc804\ub7b5\uc758 \uc885\ub958\ub294?\\n\\n\ucd1d 3\uac00\uc9c0\uc758 \uc804\ub7b5\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1\\\\. Github Flow\\n\\n2\\\\. Gitlab Flow\\n\\n3\\\\. Git Flow\\n\\ngit\uc744 \uc0ac\uc6a9\ud558\uae30\uc5d0, Git Flow\ub77c\ub294 \ub124\uc774\ubc0d\uc774 \uac00\uc7a5 \uc9c1\uad00\uc801\uc774\uace0 \uc720\uba85\ud55c\ub370\uc694.\xa0\\n\\n3\uac00\uc9c0 \uc804\ub7b5 \uc911\uc5d0\uc11c \uac00\uc7a5 \ubcf5\uc7a1\ud558\uae30\uc5d0, \uc26c\uc6b4 \uc21c\uc11c\ub300\ub85c \uc9c4\ud589\ud574 \ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## 1\\\\. Github Flow\\n\\n\uadf8\ub9bc\uc73c\ub85c flow \uac04\ub2e8\ud558\uac8c \ubcf4\uace0 \uac00\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblgfI6%2FbtslEWRFdaJ%2F3KmwR2yqlfgKk0msnufYNk%2Fimg.png)\\n\\n![img2](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtUzxm%2FbtslJ1xWHzy%2FMP0s11FoCTKpqwQnUJUm30%2Fimg.png)\\n\\n\ube0c\ub79c\uce58\ub294 \ucd1d 2\uac00\uc9c0 \uc885\ub958\uac00 \uc874\uc7ac\ud569\ub2c8\ub2e4\\n\\n#### 1\\\\. master \ube0c\ub79c\uce58\\n\\n\uc5ec\uae30\uc5d0 \uba38\uc9c0\uac00 \ub418\uba74 \ubc30\ud3ec\uac00 \ub418\ub3c4\ub85d CD\ub97c \uc5f0\uacb0\ud574 \ub193\uc740 \uacbd\uc6b0\uac00 \ub9ce\uc2b5\ub2c8\ub2e4.\\n\\n\uc548\uc815\ub41c \ubc84\uc804\uc758 \ucf54\ub4dc\uac00 \uad00\ub9ac\ub418\ub294 \ube0c\ub79c\uce58\uc785\ub2c8\ub2e4.\\n\\n#### 2\\\\. feature \ube0c\ub79c\uce58\\n\\n\uae30\ub2a5 \ucd94\uac00, \ubc84\uadf8 \uc218\uc815 \ub4f1 \ubaa8\ub4e0 \uc791\uc5c5\uc740 feature \ube0c\ub79c\uce58\uc5d0\uc11c \uc77c\uc5b4\ub0a9\ub2c8\ub2e4.\\n\\nmaster \ube0c\ub79c\uce58\uc5d0\uc11c \uc0c8\ub85c\uc6b4 \ube0c\ub79c\uce58\ub97c \ub9cc\ub4e4\uc5b4\uc11c, \ub9c8\uc2a4\ud130\ub85c \uba38\uc9c0\ub418\ub294 \ub2e8\uc21c\ud55c \uc0ac\uc774\ud074\uc744 \uac00\uc9c0\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### \uc7a5\uc810\\n\\n\uc704\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \uac83\ucc98\ub7fc 2\uc885\ub958\uc758 \ube0c\ub79c\uce58\ub9cc \uc788\uae30\uc5d0, \uc815\ub9d0 \uac04\ub2e8\ud569\ub2c8\ub2e4.\\n\\n\ud559\uc2b5 \uacfc\uc815\uae4c\uc9c0\uc758 \ub7ec\ub2dd \ucee4\ube0c\uac00 \uac70\uc758 \uc5c6\ub2e4\uc2dc\ud53c \ud558\uae30\uc5d0, \uac04\ub2e8\ud55c \ud504\ub85c\uc81d\ud2b8\uc5d0 \uc801\uc6a9\ud558\uae30 \uc815\ub9d0 \uc88b\uc2b5\ub2c8\ub2e4.\\n\\n\ub9b4\ub9ac\uc988 \ub418\uc9c0 \uc54a\uc740 \ucf54\ub4dc\uac00 \ucd5c\uc18c\ud654\ub429\ub2c8\ub2e4. \ucd5c\uc2e0 \ubc84\uc804\uc758 \ucf54\ub4dc\uc640 \ucd5c\ub300\ud55c \ube60\ub974\uac8c \ub3d9\uae30\ud654\ub97c \uacc4\uc18d\ud574\uc11c \uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n#### \ub2e8\uc810\\n\\n\ubaa8\ub4e0 \ucf54\ub4dc\ub294 \ub2e4 master \ube0c\ub79c\uce58\uc5d0 \uba38\uc9c0\uac00 \ub418\uc5b4\uc57c \ud55c\ub2e4\ub294 \uc810\uc774 \uac1c\ubc1c \uc11c\ubc84\uc640, \uc6b4\uc601\uc11c\ubc84\ub97c \ub098\ub204\uae30 \uc560\ub9e4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uac1c\ubc1c \uc11c\ubc84\uc5d0 \ubc30\ud3ec\ub97c \ud558\uace0 \uc2f6\uc740 \uc0c1\ud669\uc774\ub77c\uba74, master\uc5d0 \uba38\uc9c0\uac00 \ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n\\n\uba38\uc9c0\uac00 \ub41c \uc774\ud6c4\uc5d0 cd \ud30c\uc774\ud504\ub77c\uc778\uc744 \ud1b5\ud574\uc11c \uac1c\ubc1c \uc11c\ubc84\uc640 \uc6b4\uc601 \uc11c\ubc84 \ubaa8\ub450\uc5d0 \ubc30\ud3ec\uac00 \ub429\ub2c8\ub2e4.\\n\\n\uc5ec\ub7ec \ud658\uacbd\uc744 \ub098\ub204\uace0 \uad00\ub9ac\ub97c \ud558\uace0 \uc2f6\uc73c\uc2dc\ub2e4\uba74 \ub2e4\uc74c\uc5d0 \uc18c\uac1c\ud574\ub4dc\ub9b4 \uc804\ub7b5\uc744 \uc0ac\uc6a9\ud574 \ubcf4\uc154\ub3c4 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4\\n\\n## 2\\\\. Gitlab Flow\\n\\n\uadf8\ub9bc\uc73c\ub85c flow \uac04\ub2e8\ud558\uac8c \ubcf4\uace0 \uac00\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![img2](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdlarwn%2FbtslKkYqqTR%2FXi8NnZIEXahoVFusk0xV31%2Fimg.png)\\n\\n\ubc11\uc5d0 \ud658\uacbd\uc740 \ucd1d 2\uac1c\uc758 \uc11c\ubc84\uac00 \uc874\uc7ac\ud560 \ub54c\ub97c \uac00\uc815\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1\\\\. pre-production \uc11c\ubc84\\n\\n2\\\\. production \uc11c\ubc84\\n\\n\ud3b8\uc758\ub97c \uc704\ud574 main\uc5d0 \uba38\uc9c0\ub418\ub294 \uacfc\uc815\uc740 \uac04\ub2e8\ud558\uac8c \ud45c\ud604\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![img3](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbkNc9%2FbtslJ0MBrWb%2F0CT7DVQoCDFOpbqyAko9mk%2Fimg.png)\\n\\n#### \ube0c\ub79c\uce58 \uc885\ub958\\n\\n\ucd1d 3\uac00\uc9c0 \ube0c\ub79c\uce58\uac00 \ud544\uc694\ud558\uace0, \ucd94\uac00\uc5d0 \ub530\ub77c\uc11c \ub354 \ucd94\uac00\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1\\\\. main(or develop) \ube0c\ub79c\uce58\\n\\n\uae30\ub2a5\uc5d0 \ub300\ud55c \uac1c\ubc1c\uc774 \uc644\ub8cc\ub418\uc5c8\uc9c0\ub9cc, \uc5ec\uae30\uc5d0 \uba38\uc9c0\ub418\uc5b4\ub3c4 \ubc14\ub85c \ubc30\ud3ec\ub418\uc9c0\ub294 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n2\\\\. feature\ube0c\ub79c\uce58\\n\\n\uae30\ub2a5\uc744 \uac1c\ubc1c\ud558\ub294 \ube0c\ub79c\uce58\uc785\ub2c8\ub2e4. Github Flow \uc640\ub3c4 \uc720\uc0ac\ud569\ub2c8\ub2e4.\\n\\n3\\\\. production \ube0c\ub79c\uce58\\n\\n\uc2e4\uc81c \ubc30\ud3ec\uac00 \uc77c\uc5b4\ub098\ub294 \ube0c\ub79c\uce58\uc785\ub2c8\ub2e4.\xa0\\n\\n\uc5ec\uae30\uc5d0 \uba38\uc9c0\uac00 \ub418\ub294 \uc21c\uac04 \ubc30\ud3ec\uac00 \uc77c\uc5b4\ub0a9\ub2c8\ub2e4.\\n\\n\uc704 \uc0ac\uc9c4\uc5d0 \uc788\ub294 \uac83\ucc98\ub7fc, \ud544\uc694\uc5d0 \ub530\ub77c\uc11c pre-production\uc774\ub098, staging \uac19\uc740 \ud658\uacbd\uc5d0 \ub530\ub978 \ube0c\ub79c\uce58\ub97c \ucd94\uac00\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### \ud2b9\uc9d5\\n\\n1\\\\. \ubb34\uc870\uac74 \ub2e8\ubc29\ud5a5\uc73c\ub85c \uba38\uc9c0\uac00 \uc77c\uc5b4\ub0a9\ub2c8\ub2e4.\\n\\n\uae34\uae09\ud558\uac8c \ub77c\uc774\ube0c \uc11c\ubc84\uc5d0 \uc218\uc815\uc744 \ud574\uc57c \ud560 \ub54c, production \ubd80\ud130 \uc2dc\uc791\ud558\ub294 \uac83\uc774 \uc544\ub2cc, main \ubd80\ud130 \ucc28\uadfc\ucc28\uadfc \uc62c\ub77c\uac00\uc57c \ud569\ub2c8\ub2e4\\n\\n2\\\\. \ud658\uacbd\uc5d0 \ub530\ub77c \ube0c\ub79c\uce58 \uc885\ub958\uac00 \ub298\uc5b4\ub0a0 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc704 \uc0ac\uc9c4\uc5d0\uc11c\ub294 pre-production \uc774 \uadf8 \uc608\uc2dc\uac00 \ub418\uaca0\ub124\uc694.\\n\\n#### \uc7a5\uc810\\n\\n1\\\\. Github Flow\uc5d0\uc11c \ud658\uacbd\ubcc4 \ube0c\ub79c\uce58\ub97c \ud1b5\ud574\uc11c \uac1c\ubc1c \uc11c\ubc84\ub098 pre-production \uc11c\ubc84\uc5d0 \ubc84\uc804\uc744 \uae54\ub054\ud558\uac8c \uad00\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## 3\\\\. Git Flow\\n\\n\ube0c\ub79c\uce58 \uc804\ub7b5 \uc911 \uac00\uc7a5 \ucc98\uc74c\uc73c\ub85c \uc720\uba85\ud574\uc9c4 \ube0c\ub79c\uce58 \uc804\ub7b5\uc785\ub2c8\ub2e4.\\n\\n\ubc30\ud3ec\uac00 \ud2b9\uc815 \uc8fc\uae30\ub97c \uac00\uc9c0\uace0 \uc788\ub294 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc77c \ub54c, \uac00\uc7a5 \uc801\ud569\ud569\ub2c8\ub2e4.\\n\\n\uac00\uc7a5 \ubcf5\uc7a1\ud55c \uc804\ub7b5\uc744 \uac00\uc9c0\uace0 \uc788\uc5b4\uc11c, \ubaa8\ub450\uac00 \ube0c\ub79c\uce58 \uc804\ub7b5\uc5d0 \ub300\ud574\uc11c \uc774\ud574\ud558\uace0 \uc788\ub2e4\uba74 \uc5ed\ud560\uc5d0 \ub530\ub978 \uae54\ub054\ud55c \ubd84\ub9ac\uac00 \uac00\ub2a5\ud569\ub2c8\ub2e4\\n\\n\uadf8\ub9bc\uc73c\ub85c \ubcf4\uace0 \uac00\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\n![img4](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd9WzKn%2FbtslKdkAHNP%2F2fCAqKSVxtPVWqYnBS8juk%2Fimg.png)\\n\\n\uac00\uc7a5 \uc720\uba85\ud55c \ube0c\ub79c\uce58 \uc804\ub7b5\uc774\uc9c0\ub9cc, \uac00\uc7a5 \uc5b4\ub824\uc6b4 \uc804\ub7b5\uc774\uae30\ub3c4 \ud569\ub2c8\ub2e4.\\n\\n#### \ud2b9\uc9d5\\n\\n1\\\\. \ube0c\ub79c\uce58\uc5d0 \ub300\ud574\uc11c \uc591\ubc29\ud5a5\uc73c\ub85c \uba38\uc9c0\uac00 \uc77c\uc5b4\ub0a9\ub2c8\ub2e4\\n\\nrelease \ube0c\ub79c\uce58\uc5d0\uc11c \ubc84\uadf8 \uc218\uc815\uc774 \uc77c\uc5b4\ub098\uba74, develop \ube0c\ub79c\uce58\uc5d0\ub3c4 \uba38\uc9c0\ud574\uc918\uc57c \ud569\ub2c8\ub2e4.\\n\\nhotfix \ube0c\ub79c\uce58\ub97c main \ube0c\ub79c\uce58\ubfd0\ub9cc \uc544\ub2c8\ub77c, develop \ube0c\ub79c\uce58\uc5d0\ub3c4 \uba38\uc9c0\ud574\uc918\uc57c \ud569\ub2c8\ub2e4\\n\\n\ube0c\ub79c\uce58\uc758 \uc885\ub958\uac00 5\uac00\uc9c0\ub098 \ub429\ub2c8\ub2e4\\n\\n1\\\\. main\\n\\nproduction \uc774 \ubc30\ud3ec\ub418\uc5c8\uc744 \ub54c, \uc774 \ube0c\ub79c\uce58\uc5d0 \uba38\uc9c0\ub418\ub294 \uac83\uc774 \uae30\uc900\uc774 \ub429\ub2c8\ub2e4.\\n\\n2\\\\. develop\xa0\\n\\n\uc704\uc5d0\uc11c \uc124\uba85\ub4dc\ub838\ub358 \ube0c\ub79c\uce58\ub4e4\uacfc \ud070 \ucc28\uc774\uac00 \uc5c6\uc774 \ubc30\ud3ec \uc804 \ube0c\ub79c\uce58\uc785\ub2c8\ub2e4.\\n\\n3\\\\. feature\\n\\n\uae30\ub2a5\uc744 \uac1c\ubc1c\ud560 \ub54c \uc0ac\uc6a9\ud558\ub294 \ube0c\ub79c\uce58\uc785\ub2c8\ub2e4. \uc774\uac83\ub3c4 \uc704\uc640 \ud070 \ucc28\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4\\n\\n4\\\\. release\\n\\nGitlab Flow\uc5d0\uc11c pre-production\uc5d0 \ud574\ub2f9\ud55c\ub2e4\uace0 \ubd10\ub3c4 \ubb34\ubc29\ud569\ub2c8\ub2e4.\\n\\n\uc5ec\uae30\uc11c \ubc84\uadf8 \uc218\uc815\uc774 \uc77c\uc5b4\ub0ac\uc744 \uacbd\uc6b0\uc5d0,\xa0 develop\uc5d0 \uba38\uc9c0\ud558\ub294 \uac83\uc744 \uae4c\uba39\uc73c\uba74 \uc548 \ub429\ub2c8\ub2e4.\\n\\n5\\\\. hotfix\\n\\nmain \ube0c\ub79c\uce58\uc5d0\uc11c \uc0dd\uc131\ub41c \ube0c\ub79c\uce58\ub85c, \uae34\uae09\ud55c \ubcc0\uacbd\uc0ac\ud56d\uc744 \ucc98\ub9ac\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub54c, develop\uc5d0 \uba38\uc9c0\ud558\ub294 \uac83\uc744 \uae5c\ube61\ud558\uba74 \uc548 \ub429\ub2c8\ub2e4.\\n\\n\ub354 \uc790\uc138\ud558\uac8c \uc54c\uc544\ubcf4\uc2e4 \ubd84\uc740 \uc544\ub798 \ub9c1\ud06c\ub4e4\uc744 \ud655\uc778\ud574 \ubcf4\uc138\uc694\\n\\n## \uc6b0\ub9ac \ud504\ub85c\uc81d\ud2b8\uc5d0\ub294 \uc5b4\ub5a4 \uac83\uc774 \uc801\uc808\ud560\uae4c?\\n\\n\ub098\uc911\uc5d0 \uac1c\ubc1c \uc11c\ubc84 \ud639\uc740 \uc2a4\ud14c\uc774\uc9d5 \uc11c\ubc84\ub97c \ub450\uace0 \uc2f6\uae30\uc5d0, \uc774 \ubd80\ubd84\uc5d0 \ub300\ud55c \ucc98\ub9ac\uac00 \ubd80\uc871\ud55c Github Flow\ub294 \uc801\uc808\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\nGit Flow\ub294 \uae54\ub054\ud558\uac8c \ucc98\ub9ac\ud560 \uc218 \uc788\uc9c0\ub9cc, \ub7ec\ub2dd \ucee4\ube0c\uac00 Gitlab Flow \ubcf4\ub2e4 \uc57d\uac04 \ub354 \uc788\uc5b4\uc11c, \ube60\ub974\uac8c \uac1c\ubc1c\ud558\ub294 \ucde8\uc9c0\uc5d0 \ub9de\uc9c0 \uc54a\uc544 \ubcf4\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c Gitlab Flow\ub97c \uc0ac\uc6a9\ud558\ub824\uace0 \ud569\ub2c8\ub2e4\xa0\\n\\n\ucc38\uace0\\n\\n[https://techblog.woowahan.com/2553/](https://techblog.woowahan.com/2553/)\\n\\n[https://docs.gitlab.com/ee/topics/gitlab\\\\_flow.html](https://docs.gitlab.com/ee/topics/gitlab_flow.html)"},{"id":"1","metadata":{"permalink":"/1","source":"@site/blog/2023-06-29-hello-car-ffeine.mdx","title":"Hello World","description":"\uc548\ub155\ud558\uc138\uc694","date":"2023-06-29T00:00:00.000Z","formattedDate":"2023\ub144 6\uc6d4 29\uc77c","tags":[{"label":"hello","permalink":"/tags/hello"},{"label":"world","permalink":"/tags/world"}],"readingTime":0.025,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"},{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"},{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"},{"name":"\ud0a4\uc544\ub77c","title":"Backend","url":"https://github.com/kiarakim","imageURL":"https://github.com/kiarakim.png","key":"kiara"},{"name":"\uc57c\ubbf8","title":"Frontend","url":"https://github.com/feb-dain","imageURL":"https://github.com/feb-dain.png","key":"yummy"},{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"},{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"1","title":"Hello World","authors":["boxster","nunu","jay","kiara","yummy","scent","gabriel"],"tags":["hello","world"]},"prevItem":{"title":"git branch \uc804\ub7b5 \uc791\uc131\ud574\ubcf4\uae30","permalink":"/2"}},"content":"\uc548\ub155\ud558\uc138\uc694"}]}')}}]); \ No newline at end of file diff --git a/assets/js/2e801cce.c1156dde.js b/assets/js/2e801cce.c1156dde.js deleted file mode 100644 index 002d07a..0000000 --- a/assets/js/2e801cce.c1156dde.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcar_ffeine=self.webpackChunkcar_ffeine||[]).push([[9450],{16029:n=>{n.exports=JSON.parse('{"blogPosts":[{"id":"44","metadata":{"permalink":"/44","source":"@site/blog/2023-10-20-level-4-last/index.mdx","title":"\uce74\ud398\uc778 \ub808\ubca84 \ud6c4\ubc18\uae30 \ub9ac\ud3ec\ud2b8","description":"\uc9c0\ub09c 4\uc8fc \uac04 \ubcc0\uacbd\uc0ac\ud56d \ubc0f \uc2e0\uaddc \uae30\ub2a5\uc744 \uc18c\uac1c\ud569\ub2c8\ub2e4!!","date":"2023-10-20T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 20\uc77c","tags":[{"label":"\uce74\ud398\uc778","permalink":"/tags/\uce74\ud398\uc778"},{"label":"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571","permalink":"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"},{"label":"\uc6b0\ud14c\ucf54","permalink":"/tags/\uc6b0\ud14c\ucf54"}],"readingTime":3.205,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"44","title":"\uce74\ud398\uc778 \ub808\ubca84 \ud6c4\ubc18\uae30 \ub9ac\ud3ec\ud2b8","authors":["gabriel"],"tags":["\uce74\ud398\uc778","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571","\uc6b0\ud14c\ucf54"]},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5","permalink":"/42"}},"content":"\uc9c0\ub09c 4\uc8fc \uac04 \ubcc0\uacbd\uc0ac\ud56d \ubc0f \uc2e0\uaddc \uae30\ub2a5\uc744 \uc18c\uac1c\ud569\ub2c8\ub2e4!!\\n\\n\uc774\ubc88 \uc5c5\ub370\uc774\ud2b8\uc5d0\ub294 \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc744 \uc9c1\uc811 \uc218\uc9d1\ud558\uc5ec \ud53c\ub4dc\ubc31\uc744 \ubc18\uc601\ud558\uc600\ub2f5\ub2c8\ub2e4!\\n\\n- [\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 1](https://car-ffeine.github.io/39)\\n- [\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 2](https://car-ffeine.github.io/40)\\n\\n## \ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\\n\\n### \ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc774\ub780?\\n\\n\ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc740 \uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ub418\ub294 \ub9c8\ucee4\ub4e4\uc744 \ud074\ub7ec\uc2a4\ud130\ub85c \ubb36\uc5b4\uc11c \ud45c\uc2dc\ud558\ub294 \uac83\uc744 \ub9d0\ud569\ub2c8\ub2e4. \ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc744 \uc0ac\uc6a9\ud558\uba74 \uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ub418\ub294 \ub9c8\ucee4\uc758 \uc218\ub97c \uc904\uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc740 \uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ub418\ub294 \ub9c8\ucee4\uc758 \uc218\uac00 \ub9ce\uc744 \ub54c \uc720\uc6a9\ud558\uac8c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uc5b4\ub514\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\ub098\uc694?\\n\\n\uc9c0\ub3c4\ub97c \ucd95\uc18c\ud558\ub294 \uacbd\uc6b0, \ub9c8\ucee4\uac00 \ud074\ub7ec\uc2a4\ud130\ub85c \ubb36\uc5ec \ud45c\uc2dc\ub429\ub2c8\ub2e4. \ud074\ub7ec\uc2a4\ud130\ub97c \ud074\ub9ad\ud558\uba74 \ud574\ub2f9 \uc9c0\uc5ed\uc73c\ub85c \ud655\ub300\ub429\ub2c8\ub2e4.\\n\\n![\uc9c0\uc5ed \ud074\ub7ec\uc2a4\ud130\ub9c1](./region.png)\\n![\uc11c\ubc84 \ud074\ub7ec\uc2a4\ud130\ub9c1](./clustered.png)\\n\\n## \ub3c4\uc2dc \uac80\uc0c9 \uae30\ub2a5\\n\\n### \ub3c4\uc2dc \uac80\uc0c9 \uae30\ub2a5\uc774\ub780?\\n\\n\uae30\uc874 \uac80\uc0c9\ucc3d\uc740 \ucda9\uc804\uc18c\uc758 \uc774\ub984\uacfc \uc8fc\uc18c\ub97c \uae30\ubc18\uc73c\ub85c \ud55c \uac80\uc0c9\uc774 \uac00\ub2a5\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc81c\ub294 \ub300\ud55c\ubbfc\uad6d\uc758 \uc8fc\uc694 \ub3c4\uc2dc\ub4e4\uc744 \uac80\uc0c9\ud560 \uc218 \uc788\ub294 \uae30\ub2a5\uc774 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc6d0\ud558\ub294 \uc9c0\uc5ed\uc744 \uac80\uc0c9\ud558\uace0, \ud574\ub2f9 \uc9c0\uc5ed\uc73c\ub85c \ube60\ub974\uac8c \uc774\ub3d9\ud560 \uc218 \uc788\uc73c\uba70 \uc9c0\ub3c4 \uc870\uc791\uc5d0 \ub9ce\uc740 \ub3c4\uc6c0\uc774 \ub429\ub2c8\ub2e4.\\n\\n### \uc5b4\ub514\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\ub098\uc694?\\n\\n\uac80\uc0c9\ucc3d\uc5d0 \uc6d0\ud558\ub294 \uc9c0\uc5ed\uc744 \uc785\ub825\ud558\uba74 \ubc14\ub85c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![\ub3c4\uc2dc \uac80\uc0c9\uacb0\uacfc](./city.png)\\n\\n\\n## \ub514\uc790\uc778 \uac1c\uc120\\n\\n### \uc778\ud3ec \uc708\ub3c4\uc6b0\uac00 \uac1c\uc120\ub418\uc5c8\uc5b4\uc694!\\n\\n\uae30\uc874 \uc778\ud3ec \uc708\ub3c4\uc6b0\ub294 \ucda9\uc804\uc18c\uc758 \uc774\ub984\uacfc \uc8fc\uc18c\ub9cc\uc744 \ud45c\uc2dc\ud558\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc81c\ub294 \uc0ac\uc6a9\ub7c9\uc744 \uc81c\uacf5\ud558\uba70, \uae38\ucc3e\uae30 \uae30\ub2a5\ub3c4 \uc81c\uacf5\ud569\ub2c8\ub2e4.\\n\\n![\uc778\ud3ec \uc708\ub3c4\uc6b0](./info-window.png)\\n\\n### \ucda9\uc804\uc18c \uc0ac\uc6a9 \ud1b5\uacc4 \uc815\ubcf4 \ub514\uc790\uc778\uc774 \ubcc0\uacbd\ub418\uc5c8\uc5b4\uc694\\n\\n![\ud1b5\uacc4 \uc815\ubcf4](./statistics.png)\\n\\n\uc0c8\ub85c\uc6cc\uc9c4 \ud0ed \ub514\uc790\uc778\uacfc \uc0c9\uc0c1\uc744 \uc801\uc6a9\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n### \ucda9\uc804\uc18c \ub9c8\ucee4\uac00 \uc774\uc6d0\ud654 \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![\ucda9\uc804\uc18c \ub9c8\ucee4](./marker-small.png)\\n![\ucda9\uc804\uc18c \ub9c8\ucee4](./marker-big.png)\\n\\n\uc9c0\ub3c4\ub97c \ucd95\uc18c\ud560 \uc218\ub85d \ub9c8\ucee4\uac00 \ub3c4\ub85c\ub97c \uac00\ub9ac\ub294 \ud604\uc0c1\uc774 \uc788\uc5b4 \uc0ac\uc774\uc988\uac00 \ub300\ud3ed \ucd95\uc18c\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e8, \ud655\ub300\ud558\ub294 \uacbd\uc6b0\uc5d0\ub294 \uae30\uc874\uacfc \ub3d9\uc77c\ud55c \ud615\ud0dc\uc758 \ub9c8\ucee4\ub97c \uc81c\uacf5\ud569\ub2c8\ub2e4."},{"id":"42","metadata":{"permalink":"/42","source":"@site/blog/2023-10-19-co-work.mdx","title":"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5","description":"\uc0ac\uc6a9\uc790 \ud53c\ub4dc\ubc31","date":"2023-10-19T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 19\uc77c","tags":[{"label":"collaboration","permalink":"/tags/collaboration"}],"readingTime":2.35,"hasTruncateMarker":false,"authors":[],"frontMatter":{"slug":"42","title":"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5","tags":["collaboration"]},"prevItem":{"title":"\uce74\ud398\uc778 \ub808\ubca84 \ud6c4\ubc18\uae30 \ub9ac\ud3ec\ud2b8","permalink":"/44"},"nextItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2","permalink":"/43"}},"content":"## \uc0ac\uc6a9\uc790 \ud53c\ub4dc\ubc31\\n\\n![image](https://github.com/woowacourse/service-apply/assets/106640954/e38ba7d6-7a56-43e3-926d-fdd5e1a8f80f)\\n\\n\uc800\ud76c \uc11c\ube44\uc2a4\ub97c \ubc30\ud3ec\ud558\uace0 \uc0ac\uc6a9\uc790\uc5d0\uac8c \ud53c\ub4dc\ubc31\uc744 \ubc1b\uc558\ub294\ub370, \ucd95\uc18c\ud588\uc744 \ub54c\uac00 \ub9ce\uc774 \ubd88\ud3b8\ud558\ub2e4\ub294 \ud53c\ub4dc\ubc31\uc774 \ub300\ubd80\ubd84\uc774\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc720\ub294 \uc544\ub798 \ud654\uba74\uacfc \uac19\uc2b5\ub2c8\ub2e4\\n\\n![asis](https://github.com/woowacourse-teams/2023-car-ffeine/assets/106640954/8792447a-5e3b-4afe-b556-2ef20e1c9cfd)\\n\\n\uc774\ub7f0 \uc11c\ube44\uc2a4\ub97c \ubcf8 \uc801\ub3c4 \uc5c6\uace0, \uc774\ub7f0 \uc11c\ube44\uc2a4\ub97c \uc0ac\uc6a9\ud558\uace0 \uc2f6\uc9c0\ub3c4 \uc54a\uc744 \uac83 \uc785\ub2c8\ub2e4. \ud574\ub2f9 \ubd80\ubd84\uc758 \ubb38\uc81c\ub97c \uc54c\uace0 \uc788\uc5c8\uc9c0\ub9cc \uc5b4\ub5bb\uac8c \ud45c\ud604\ud574\uc8fc\ub294 \uac83\uc774 \uc88b\uace0, \uad6c\ud604\ud560 \uc218 \uc788\ub294 \ubc29\ubc95\uc774 \ub5a0\uc624\ub974\uc9c0 \uc54a\uc544 6\ucc28 \ub370\ubaa8\ub370\uc774\uae4c\uc9c0 \ubbf8\ub8e8\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc5f4\uc2ec\ud788 \ud300 \ud68c\uc758\ub97c \ud55c \uacb0\uacfc \ud654\uba74\uc5d0 \ubcf4\uc774\ub294 \uc0ac\uc774\uc988\ub9cc\ud07c \uc77c\uc815 \ubc94\uc704\ub85c \ub098\ub220 \ucda9\uc804\uc18c \uac1c\uc218\ub97c \ubcf4\uc5ec\uc8fc\ub294 \ud074\ub7ec\uc2a4\ud130\ub9c1 \uae30\ub2a5\uc744 \ucd94\uac00\ud558\uae30\ub85c \uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \ud074\ub7ec\uc2a4\ud130 \uae30\ub2a5 \ucd94\uac00\\n\\n\ud574\ub2f9 \uae30\ub2a5\uc744 \uac04\ub2e8\ud558\uac8c \uc124\uba85\ub4dc\ub9ac\uba74 \ud654\uba74\uc758 \uc77c\uc815 \ubc94\uc704\ub85c \ub098\ub220 \ucda9\uc804\uc18c\uc758 \uac1c\uc218\ub97c \ubcf4\uc5ec\uc8fc\ub3c4\ub85d \uc11c\ubc84\uc5d0\uc11c \uacc4\uc0b0\ud558\uc5ec \ud074\ub77c\uc774\uc5b8\ud2b8\ub85c \uc804\ub2ec\ud558\ub3c4\ub85d \ud588\uc2b5\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uc804\ub2ec\ud55c \ud074\ub7ec\uc2a4\ud130\ub9c1 \ub9c8\ucee4\ub4e4\uc758 \uc704\uce58\uac00 \uc544\ub798\uc640 \uac19\uc774 \uc608\uc058\uac8c \ubcf4\uc774\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\\n![image (5)](https://github.com/woowacourse-teams/2023-car-ffeine/assets/106640954/133cb411-68bb-48f7-85a3-eca8a91d23bf)\\n\\n\ud654\uba74\uc758 \ud06c\uae30\uc5d0 \ube44\ud574 \ub9c8\ucee4\uac00 \uba87\uac1c \uc5c6\ub294 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub807\uac8c \ub41c\ub2e4\uba74 \uc0ac\uc6a9\uc790\ub294\\n\uadf8\ub807\uae30\uc5d0 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0 \ud574\ub2f9 \uae30\ub2a5\uc744 \ub2f4\ub2f9\ud55c \uac00\ube0c\ub9ac\uc5d8, \uc13c\ud2b8\uac00 \uc880 \ub354 \uc720\uc5f0\ud558\uac8c \ub9c8\ucee4\ub97c \ubcf4\uc5ec\uc8fc\ub294 \uac83\uc774 UX \uad00\uc810\uc5d0\uc11c \uc88b\ub2e4\uace0 \uc598\uae30\ud558\uc5ec\\n\\n\uc11c\ubc84 API\uc640 \ub85c\uc9c1\uc744 \ubcc0\uacbd\ud558\uc5ec \ub3d9\uc801\uc73c\ub85c \ud654\uba74\uc758 \ucda9\uc804\uc18c\ub97c \ud074\ub7ec\uc2a4\ud130\ud558\ub3c4\ub85d \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4. \uadf8\ub807\uac8c \ud558\uc5ec \uc544\ub798\uc640 \uac19\uc740 \ud654\uba74\uc744 \uc81c\uacf5\ud558\ub3c4\ub85d \ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n![final](https://github.com/woowacourse-teams/2023-car-ffeine/assets/106640954/c65d9a51-5d3d-407b-9d72-13f06580a502)\\n\\n\\n\uc774\uc0c1 \ud611\uc5c5 \uc77c\ud654 \uc600\uc2b5\ub2c8\ub2e4."},{"id":"43","metadata":{"permalink":"/43","source":"@site/blog/2023-10-19-visitors/index.mdx","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2","description":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.","date":"2023-10-19T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 19\uc77c","tags":[{"label":"\uce74\ud398\uc778","permalink":"/tags/\uce74\ud398\uc778"},{"label":"\uc11c\ube44\uc2a4 \uacbd\ud5d8","permalink":"/tags/\uc11c\ube44\uc2a4-\uacbd\ud5d8"},{"label":"\ud53c\ub4dc\ubc31","permalink":"/tags/\ud53c\ub4dc\ubc31"},{"label":"\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","permalink":"/tags/\uc804\uae30\ucc28-\uc0ac\uc6a9\uae30"},{"label":"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571","permalink":"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"}],"readingTime":0.29,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"43","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2","authors":["gabriel"],"tags":["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5","permalink":"/42"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec","permalink":"/41"}},"content":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./report-1.jpg)\\n![no offset](./report-2.jpg)\\n![no offset](./report-3.jpg)\\n![no offset](./report-4.jpg)\\n![no offset](./report-5.jpg)"},{"id":"41","metadata":{"permalink":"/41","source":"@site/blog/2023-10-18-zero-time-deploy.mdx","title":"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec","description":"\uc548\ub155\ud558\uc138\uc694! \uce74\ud398\uc778\ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.","date":"2023-10-18T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 18\uc77c","tags":[{"label":"infra","permalink":"/tags/infra"},{"label":"ec2","permalink":"/tags/ec-2"},{"label":"cd","permalink":"/tags/cd"},{"label":"aws","permalink":"/tags/aws"},{"label":"zero-time","permalink":"/tags/zero-time"},{"label":"blue-green","permalink":"/tags/blue-green"}],"readingTime":8.93,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"41","title":"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec","authors":["jay"],"tags":["infra","ec2","cd","aws","zero-time","blue-green"]},"prevItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2","permalink":"/43"},"nextItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 2","permalink":"/40"}},"content":"\uc548\ub155\ud558\uc138\uc694! \uce74\ud398\uc778\ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\\n\uc800\ud76c \uce74\ud398\uc778 \ud300\uc5d0\uc11c \ubb34\uc911\ub2e8 \ubc30\ud3ec\ub97c \uc9c4\ud589\ud588\uc2b5\ub2c8\ub2e4.\\n\uc5b4\ub5a4 \uacfc\uc815\uc73c\ub85c \uc9c4\ud589\uc744 \ud588\ub294\uc9c0 \uc791\uc131\ud574\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4!\\n\\n---\\n\\n## \uae30\uc874 \ubc30\ud3ec \ubc29\uc2dd\uacfc \ubb38\uc81c\uc810\\n\\n\uba3c\uc800 \uce74\ud398\uc778 \ud300\uc758 \uae30\uc874 \ubc30\ud3ec \ubc29\uc2dd\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n

    \\n\\n\\n1. Target branch\uc5d0 push\uac00 \ub418\uba74 Github Actions\uac00 \uc791\ub3d9\ud569\ub2c8\ub2e4.\\n2. Target branch\uc758 \uc18c\uc2a4 \ucf54\ub4dc\uac00 \ube4c\ub4dc\ub418\uc5b4\uc11c Docker Hub\uc5d0 \uc62c\ub77c\uac00\uac8c \ub429\ub2c8\ub2e4.\\n3. Github Actions\uc758 self-hosted runner\ub97c \ud1b5\ud574 infra \uc11c\ubc84\uc5d0\uc11c prod \uc11c\ubc84\ub85c \uc811\uadfc\ud558\uc5ec\uc11c \uae30\uc874\uc5d0 \ub744\uc6cc\uc9c4 \uc11c\ubc84\ub97c \ub2e4\uc6b4 \uc2dc\ud0b5\ub2c8\ub2e4.\\n4. Docker Hub\uc5d0 \uc5c5\ub85c\ub4dc\ud55c Docker image\ub97c pull\ud574\uc11c \uc11c\ubc84\ub97c \uac00\ub3d9\uc2dc\ud0b5\ub2c8\ub2e4.\\n\\n\\n
    \\n\uc774\ub7f0 \uacfc\uc815\uc73c\ub85c \ubc30\ud3ec \uc2a4\ud06c\ub9bd\ud2b8\uac00 \uc791\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uc774 \ubc29\ubc95\uc740 \uae30\uc874 \uc11c\ubc84\ub97c \ub2e4\uc6b4 \uc2dc\ud0a4\uace0 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\ub97c \ub744\uc6b8 \ub54c \ub2e4\uc6b4 \ud0c0\uc784\uc774 \uc874\uc7ac\ud55c\ub2e4\ub294 \ubb38\uc81c\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n
    \\n\\n
    \\n\uc0ac\uc6a9\uc790 \uc785\uc7a5\uc5d0\uc11c\ub294 \uc798 \uc0ac\uc6a9\ud558\uace0 \uc788\ub294\ub370 \uac11\uc790\uae30 \uc11c\ube44\uc2a4\uac00 \uc791\ub3d9\ub418\uc9c0 \uc54a\ub294\ub2e4\uba74 \uc11c\ube44\uc2a4\uc5d0 \ub300\ud55c \uc2e0\ub8b0\uc131\uc774 \ub0ae\uc544\uc9c8 \uc218\ub3c4 \uc788\uace0 \uc774\ub7f0 \uc774\uc720\ub85c \uc774\ud0c8\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n## \uae30\uc874 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uae30\\n\\n\uc800\ud76c\ub294 \uba3c\uc800 \uc81c\ud55c\ub41c EC2 \uc778\uc2a4\ud134\uc2a4\ub85c \uc778\ud574 \ub864\ub9c1 \ubc30\ud3ec\uc758 \uc7a5\uc810\uc744 \uac00\uc838\uac08 \uc218 \uc5c6\uc5c8\uace0, \uce74\ub098\ub9ac \ubc29\uc2dd \ub610\ud55c \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \ud544\uc694\ub85c\ud55c \uc804\ub7b5\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \ube44\uad50\uc801 \ub864\ubc31\ub3c4 \ube60\ub978 Blue/Green \uc804\ub7b5\uc744 \uc120\ud0dd\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uc800\ud76c\uc758 Blue/Green \ubb34\uc911\ub2e8 \ubc30\ud3ec \uc2dc\ub098\ub9ac\uc624\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\ud3b8\uc758\ub97c \uc704\ud574\uc11c [\uae30\uc874 \uc11c\ubc84(\uae30\uc874 \ud3ec\ud2b8) / \uc0c8\ub85c\uc6b4 \uc11c\ubc84(\uc0c8\ub85c\uc6b4 \ud3ec\ud2b8)] \ub77c\ub294 \uba85\uce6d\uc744 \uc0ac\uc6a9\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n
    \\n\\n1. Target branch\uc5d0 push\uac00 \ub418\uba74 Github Actions\uac00 \uc791\ub3d9\ud569\ub2c8\ub2e4.\\n2. Target branch\uc758 \uc18c\uc2a4 \ucf54\ub4dc\uac00 \ube4c\ub4dc\ub418\uc5b4\uc11c Docker Hub \uc5d0 \uc62c\ub77c\uac00\uac8c \ub429\ub2c8\ub2e4.\\n3. Github Actions\uc758 self-hosted runner\ub97c \ud1b5\ud574 infra \uc11c\ubc84\uc5d0\uc11c prod \uc11c\ubc84\ub85c \uc811\uadfc\ud574\uc11c Docker Hub\uc5d0 \uc5c5\ub85c\ub4dc\ud55c \uc0c8\ub85c\uc6b4 \ubc84\uc804\uc758 Image\ub97c\\n pull \ud574\uc635\ub2c8\ub2e4.\\n4. \ub9cc\uc57d 8080 \ud3ec\ud2b8\uc5d0 \uae30\uc874 \uc11c\ubc84\uac00 \ub744\uc6cc\uc838 \uc788\uc73c\uba74 8081 \ud3ec\ud2b8\ub97c \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \ub744\uc6cc\uc9c8 \ud3ec\ud2b8\ub85c \uc9c0\uc815\ud574\uc8fc\uace0, \ubc18\ub300\ub85c 8081 \ud3ec\ud2b8\uc5d0 \uae30\uc874 \uc11c\ubc84\uac00 \ub744\uc6cc\uc838 \uc788\uc73c\uba74 8080 \ud3ec\ud2b8\uc5d0 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \ub744\uc6cc\uc9c8 \ud3ec\ud2b8\ub85c \uc9c0\uc815\ud574\uc90d\ub2c8\ub2e4.\\n5. \ubbf8\ub9ac Docker Hub\uc5d0 \uc5c5\ub85c\ub4dc\ud55c Docker image\ub97c [image+port]\ub77c\ub294 \ub124\uc774\ubc0d\uc73c\ub85c pull\uc744 \ud55c \ud6c4 \uc0c8\ub85c\uc6b4 \ud3ec\ud2b8\ub85c \uc11c\ubc84\ub97c \uac00\ub3d9\uc2dc\ud0b5\ub2c8\ub2e4.\\n6. \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \uc81c\ub300\ub85c \uac00\ub3d9 \ub410\ub294\uc9c0 \ud655\uc778\ud558\uae30 \uc704\ud574\uc11c \ud5ec\uc2a4 \uccb4\ud06c\ub97c \uc9c4\ud589\ud569\ub2c8\ub2e4. 20\ubc88 \ub3d9\uc548 \uc11c\ubc84\uac00 \uc815\uc0c1 \ub3d9\uc791\ud558\ub294\uc9c0 Spring Actuactor\ub97c \ud1b5\ud574\uc11c \ud655\uc778\uc744 \ud569\ub2c8\ub2e4.\\n7. \uc815\uc0c1 \uc791\ub3d9\uc774 \ub410\uc74c\uc744 \ud655\uc778\ud558\uba74 \ud604\uc7ac \uc778\uc2a4\ud134\uc2a4\uc5d0\ub294 2\ub300\uc758 \uc11c\ubc84\uac00 \ub744\uc6cc\uc838\uc788\uace0 \uc694\uccad\uc740 \uc5ec\uc804\ud788 \uae30\uc874 \uc11c\ubc84\ub85c \ub4e4\uc5b4\uac00\uac8c \ub429\ub2c8\ub2e4. \ub530\ub77c\uc11c Nginx\ub97c \ud1b5\ud574 \ud3ec\ud2b8\ud3ec\uc6cc\ub529\uc744 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uc758 \ud3ec\ud2b8\ub85c\\n \uc9c0\uc815\ud574\uc8fc\uace0 \uae30\uc874 \uc11c\ubc84\ub294 \ub0b4\ub824\uc90d\ub2c8\ub2e4.\\n\\n
    \\n\uc5ec\uae30\uae4c\uc9c0\uac00 \uce74\ud398\uc778 \ud300\uc758 \uc2dc\ub098\ub9ac\uc624\uc785\ub2c8\ub2e4.\\n\uadf8\ub807\ub2e4\uba74 \ud558\ub098\uc529 \uc2a4\ud06c\ub9bd\ud2b8\ub97c \ud655\uc778\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc740 \uc8fc\uc11d\uc73c\ub85c \ub2ec\uc544\ub450\uaca0\uc2b5\ub2c8\ub2e4 :)\\n\\n
    \\n
    \\n\\n### backend-deploy.yml\\n(Github Actions\uc5d0\uc11c \uc0ac\uc6a9)\\n\\n```yml\\nname: deploy\\n\\n# 1. prod/backend branch\uc5d0 push \uc791\uc5c5\uc774 \uc77c\uc5b4\ub098\uba74 \ud574\ub2f9 \uc791\uc5c5\uc744 \uc218\ud589\ud55c\ub2e4\\non:\\n push:\\n branches:\\n - prod/backend\\n\\njobs:\\n docker-build:\\n runs-on: ubuntu-latest\\n defaults:\\n run:\\n working-directory: ./backend\\n\\n steps:\\n # 2. \ub3c4\ucee4 \ud5c8\ube0c\uc5d0 \ub85c\uadf8\uc778\\n - name: Log in to Docker Hub\\n uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a\\n with:\\n username: ${{ secrets.DOCKERHUB_USERNAME }}\\n password: ${{ secrets.DOCKERHUB_PASSWORD }}\\n - uses: actions/checkout@v3\\n\\n # 3. JDK 17 \uc124\uce58 \ubc0f \ube4c\ub4dc (\ud504\ub85c\uc81d\ud2b8 Java version)\\n - name: Set up JDK 17\\n uses: actions/setup-java@v3\\n with:\\n java-version: \'17\'\\n distribution: \'adopt\'\\n\\n - name: Gradle Caching\\n uses: actions/cache@v3\\n with:\\n path: |\\n ~/.gradle/caches\\n ~/.gradle/wrapper\\n key: ${{ runner.os }}-gradle-${{ hashFiles(\'**/*.gradle*\', \'**/gradle-wrapper.properties\') }}\\n restore-keys: |\\n ${{ runner.os }}-gradle-\\n\\n - name: Grant execute permission for gradlew\\n run: chmod +x gradlew\\n - name: Build for asciiDoc\\n run: ./gradlew bootjar\\n\\n - name: Build with Gradle\\n run: ./gradlew bootjar\\n\\n # 4. \uc0b0\ucd9c\ubb3c\uc744 Image\ub85c \ube4c\ub4dc \ud6c4 Docker Hub\uc5d0 Image Push\ud558\uae30\\n - name: Extract metadata (tags, labels) for Docker\\n id: meta\\n uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7\\n with:\\n images: woowacarffeine/backend\\n\\n - name: Build and push Docker image\\n uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671\\n with:\\n context: .\\n file: ./backend/Dockerfile\\n push: true\\n platforms: linux/arm64\\n tags: woowacarffeine/backend:latest\\n labels: ${{ steps.meta.outputs.labels }}\\n\\n\\n deploy:\\n # 5. Self-hosted \uc791\ub3d9 -> infra \uc778\uc2a4\ud134\uc2a4\uc5d0\uc11c \uc791\ub3d9\ub428\\n runs-on: self-hosted\\n if: ${{ needs.docker-build.result == \'success\' }}\\n needs: [ docker-build ]\\n steps:\\n\\n # 6. infra \uc778\uc2a4\ud134\uc2a4\uc5d0\uc11c prod \uc778\uc2a4\ud134\uc2a4\ub85c \uc811\uadfc (\uc544\ub798\ubd80\ud130\ub294 prod \uc11c\ubc84 \ub0b4\uc5d0\uc11c \uc791\uc5c5)\\n - name: Join EC2 prod server\\n uses: appleboy/ssh-action@master\\n env:\\n JASYPT_KEY: ${{ secrets.JASYPT_KEY }}\\n DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}\\n DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}\\n with:\\n host: ${{ secrets.SERVER_HOST }}\\n username: ${{ secrets.SERVER_USERNAME }}\\n key: ${{ secrets.SERVER_KEY }}\\n port: ${{ secrets.SERVER_PORT }}\\n envs: JASYPT_KEY, DATABASE_USERNAME, DATABASE_PASSWORD\\n\\n script: |\\n\\n # 7. Docker Hub\uc5d0\uc11c Image\ub97c pull\ud574\uc628\ub2e4\\n sudo docker pull woowacarffeine/backend:latest\\n\\n # 8. \ub9cc\uc57d 8080 \ud3ec\ud2b8\uac00 \ucf1c\uc838 \uc788\uc73c\uba74 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uc758 \ud3ec\ud2b8\ub294 8081\ub85c \uc124\uc815\\n if sudo docker ps | grep \\":8080\\"; then\\n export BEFORE_PORT=8080\\n export NEW_PORT=8081\\n export NEW_ACTUATOR_PORT=8089\\n\\n # 9. \ub9cc\uc57d 8081 \ud3ec\ud2b8\uac00 \ucf1c\uc838 \uc788\uc73c\uba74 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uc758 \ud3ec\ud2b8\ub294 8080\ub85c \uc124\uc815\\n else\\n export BEFORE_PORT=8081\\n export NEW_PORT=8080\\n export NEW_ACTUATOR_PORT=8088\\n fi\\n\\n # 10. Docker\ub85c \uc0c8\ub85c\uc6b4 \uc11c\ubc84\ub97c \ub744\uc6b4\ub2e4.\\n sudo docker run -d -p $NEW_PORT:8080 -p $NEW_ACTUATOR_PORT:8088 \\\\\\n -e \\"SPRING_PROFILE=prod\\" \\\\\\n -e \\"ENCRYPT_KEY=${{secrets.JASYPT_KEY}}\\" \\\\\\n -e \\"DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}}\\" \\\\\\n -e \\"DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}}\\" \\\\\\n -e \\"REPLICA_DATABASE_USERNAME=${{secrets.REPLICA_DB_USER_NAME}}\\" \\\\\\n -e \\"REPLICA_DATABASE_PASSWORD=${{secrets.REPLICA_DB_USER_PASSWORD}}\\" \\\\\\n -e \\"SLACK_WEBHOOK_URL=${{secrets.SLACK_WEBHOOK_URL}}\\" \\\\\\n --name backend$NEW_PORT \\\\\\n woowacarffeine/backend:latest\\n\\n # 11. prod \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc788\ub294 bluegreen.sh \ub97c \uc791\ub3d9\ud55c\ub2e4. (\uc774 \ub54c port \uac12\uc744 \uac19\uc774 \ub123\uc5b4\uc900\ub2e4.)\\n sudo sh /home/ubuntu/bluegreen.sh $BEFORE_PORT $NEW_PORT $NEW_ACTUATOR_PORT\\n\\n```\\n\\n
    \\n
    \\n\\n### bluegreen.sh\\n(prod \uc778\uc2a4\ud134\uc2a4 \ub0b4\ubd80\uc5d0 \uc874\uc7ac)\\n\\n```shell\\n#!/bin/bash\\n\\n# 1. Github Actions\ub97c \ud1b5\ud574 \ub118\uaca8 \ubc1b\uc740 \ud658\uacbd\ubcc0\uc218 \uac12\\nBEFORE_PORT=$1\\nNEW_PORT=$2\\nNEW_ACTUATOR_PORT=$3\\n\\necho \\"\uae30\uc874 \ud3ec\ud2b8 : $BEFORE_PORT\\"\\necho \\"\uc0c8\ub85c\uc6b4 \ud3ec\ud2b8: $NEW_PORT\\"\\necho \\"\uc0c8\ub85c\uc6b4 ACTUATOR_PORT: $NEW_ACTUATOR_PORT\\"\\n\\n\\n# 2. 20\ubc88 \ub3d9\uc548 \ud5ec\uc2a4 \uccb4\ud06c\ub97c \uc9c4\ud589\\ncount=0\\nfor count in {0..20}\\ndo\\n echo \\"\uc11c\ubc84 \uc0c1\ud0dc \ud655\uc778(${count}/20)\\";\\n\\n # 3. \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \uc791\ub3d9\ub418\ub294\uc9c0 Actuator\ub97c \ud1b5\ud574 \uac12\uc744 \ubc1b\uc544\uc634\\n STATUS=$(curl -s http://127.0.0.1:${NEW_ACTUATOR_PORT}/actuator/health-check)\\n\\n # 4. Actuator\ub97c \ud1b5\ud574 \uc131\uacf5\uc801\uc73c\ub85c \uc11c\ubc84\uac00 \ub744\uc6cc\uc9c0\uc9c0 \uc54a\uc740 \uacbd\uc6b0\\n if [ \\"${STATUS}\\" != \'{\\"status\\":\\"up\\"}\' ]\\n \\tthen\\n # 5. 10\ucd08\ub97c \uae30\ub2e4\ub9b0 \ud6c4 \ub2e4\uc2dc \ud5ec\uc2a4 \uccb4\ud06c\ub97c \uc9c4\ud589\ud55c\ub2e4.\\n \\t\\tsleep 10\\n \\t\\tcontinue\\n \\telse\\n # 6. \ud5ec\uc2a4 \uccb4\ud06c\ub97c \ud1b5\ud574 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \uc131\uacf5\uc801\uc73c\ub85c \uc791\ub3d9\ub41c\ub2e4\uba74 \uba48\ucd98\ub2e4.\\n \\t\\tbreak\\n fi\\ndone\\n\\n\\n# 7. 20\ubc88\uc758 \ud5ec\uc2a4 \uccb4\ud06c\ub97c \ud558\ub294 \ub3d9\uc548 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \uc81c\ub300\ub85c \uc791\ub3d9\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc885\ub8cc\\nif [ $count -eq 20 ]\\nthen\\n\\techo \\"\uc0c8\ub85c\uc6b4 \uc11c\ubc84 \ubc30\ud3ec\ub97c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4.\\"\\n\\texit 1\\nfi\\n\\n\\n# 8. \uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \uc131\uacf5\uc801\uc73c\ub85c \uc791\ub3d9\ud55c \uacbd\uc6b0\\n# Nginx\ub97c \ud1b5\ud574 \ud3ec\ud2b8\ud3ec\uc6cc\ub529\uc744 \uae30\uc874 \ud3ec\ud2b8\uc5d0\uc11c \uc0c8\ub85c\uc6b4 \ud3ec\ud2b8\ub85c \ubcc0\uacbd\ud574\uc900\ub2e4.\\n# \uc774 \ubd80\ubd84\uc740 .inc \ud30c\uc77c\uc744 \ud1b5\ud574 Nginx\uc5d0\uc11c \uc8fc\uc785 \ubc1b\uc544\uc11c \ud3ec\ud2b8\ub9cc \ubcc0\uacbd\ud574\ub3c4 \ub429\ub2c8\ub2e4!\\nexport BACKEND_PORT=$NEW_PORT\\nenvsubst \'${BACKEND_PORT}\' < backend.template > backend.conf\\nsudo mv backend.conf /etc/nginx/conf.d/\\nsudo nginx -s reload\\n\\n\\n# 9. \uae30\uc874 \uc11c\ubc84\ub97c \ub0b4\ub824\uc8fc\uace0, \ub3c4\ucee4 \ub9ac\uc18c\uc2a4\ub97c \uc815\ub9ac\ud574\uc900\ub2e4\\ndocker stop backend$BEFORE_PORT\\nsudo docker container prune -f\\n```\\n\\n\\n\uc774\ub807\uac8c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ubb34\uc911\ub2e8 \ubc30\ud3ec\ub97c \ub3c4\uc785\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uae34 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4 :)"},{"id":"40","metadata":{"permalink":"/40","source":"@site/blog/2023-10-15-carffeine-tester-2/index.mdx","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 2","description":"\uc548\ub155\ud558\uc138\uc694? \uc13c\ud2b8\uc640 \uac00\ube0c\ub9ac\uc5d8 \uc785\ub2c8\ub2e4.","date":"2023-10-15T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 15\uc77c","tags":[{"label":"\uce74\ud398\uc778","permalink":"/tags/\uce74\ud398\uc778"},{"label":"\uc11c\ube44\uc2a4 \uacbd\ud5d8","permalink":"/tags/\uc11c\ube44\uc2a4-\uacbd\ud5d8"},{"label":"\ud53c\ub4dc\ubc31","permalink":"/tags/\ud53c\ub4dc\ubc31"},{"label":"\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","permalink":"/tags/\uc804\uae30\ucc28-\uc0ac\uc6a9\uae30"},{"label":"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571","permalink":"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"}],"readingTime":14.665,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"},{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"}],"frontMatter":{"slug":"40","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 2","authors":["gabriel","scent"],"tags":["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec","permalink":"/41"},"nextItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 1","permalink":"/39"}},"content":"\uc548\ub155\ud558\uc138\uc694? \uc13c\ud2b8\uc640 \uac00\ube0c\ub9ac\uc5d8 \uc785\ub2c8\ub2e4.\\n\\n\uc800\ud76c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc9c0\ub09c\ubc88 [\uce74\ud398\uc778 \uc11c\ube44\uc2a4 1\ucc28 \uccb4\ud5d8](https://car-ffeine.github.io/39) \uc9c4\ud589 \uc774\ud6c4 \uc77c\ubd80 \uae30\ub2a5 \uac1c\uc120\uc774 \uc788\uc5c8\uc2b5\ub2c8\ub2e4. \uae30\ub2a5 \uac1c\uc120\uc758 \uc720\uc6a9\uc131\uc744 \ud310\ubcc4\ud558\uace0\uc790 \uce74\ud398\uc778 \uc11c\ube44\uc2a4 2\ucc28 \uccb4\ud5d8\uc744 \ub2e4\ub140\uc654\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c \ud300\uc5d0\uc11c 1\ucc28 \uccb4\ud5d8 \uc774\ud6c4 \uac1c\uc120\ud55c \uc0ac\ud56d\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n### 1. \uc9c0\uc5ed\uac80\uc0c9\\n\\n![no offset](./city-search.png)\\n\\n- \uc774\uc81c\ub294 \uac80\uc0c9\uc5b4\ub97c \uc785\ub825\ud558\ub294 \uacbd\uc6b0, \uc804\uad6d \ub3c4\uc2dc\uc758 \uc8fc\uc18c\uac00 \uac19\uc774 \uc81c\uacf5\ub429\ub2c8\ub2e4.\\n\\n### 2. \ucda9\uc804\uc18c \ub9c8\ucee4\ub97c \ud655\uc778\ud560 \uc218 \uc788\ub294 \uc9c0\ub3c4 \uc601\uc5ed \ud655\uc7a5\\n\\n![no offset](./mobile-markers.png)\\n\\n(\uae30\uc874\uc5d0\ub294 \uc704 \uc0ac\uc9c4\ubcf4\ub2e4 \uc881\uc740 \uc601\uc5ed\ub9cc\uc744 \ud638\ucd9c\ud558\ub294 \uac83\uc774 \ud5c8\uc6a9\ub418\uc5c8\ub2e4.)\\n- \ubaa8\ubc14\uc77c\uc5d0\uc11c \uc880 \ub354 \ub113\uc740 \uc601\uc5ed\uc744 \ud638\ucd9c\ud558\ub294 \uac83\uc744 \ud5c8\uc6a9\ud588\uc2b5\ub2c8\ub2e4. \uc6d0\ub798\ub294 \ub514\ubc14\uc774\uc2a4 \ub108\ube44\ub97c \uace0\ub824\ud558\uc9c0 \uc54a\uace0 \uc90c \ub808\ubca8 \uae30\uc900\uc73c\ub85c \uc694\uccad\uc744 \uc81c\ud55c\ud588\uc73c\ub098, \uc774\uc81c\ub294 \uc0ac\uc6a9\uc790 \ub514\ubc14\uc774\uc2a4\uc5d0 \ubcf4\uc774\ub294 \uc9c0\ub3c4\uc758 \uc601\uc5ed \ud06c\uae30\ub97c \uae30\ubc18\uc73c\ub85c \uc694\uccad\uc744 \uc81c\ud55c\ud558\ub294 \ubc29\uc2dd\uc744 \ub3c4\uc785\ud588\uc2b5\ub2c8\ub2e4.\\n- \uae30\uc874\uc5d0 \uc0ac\uc6a9\ud558\ub358 \ub9c8\ucee4\uc758 \ub2e8\uc810\uc740, \uadf8 \ud06c\uae30\uac00 \ub108\ubb34 \ud06c\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4. \uc774\ub85c\uc778\ud574 \ub354 \ub113\uc740 \uc601\uc5ed\uc744 \ubcf4\uc5ec\uc8fc\ub294 \uacbd\uc6b0\uc5d0 \ub9c8\ucee4\ub4e4\uc774 \uacb9\uce58\ub294 \ud604\uc0c1\uc774 \uc788\uc5c8\ub294\ub370\uc694, \uc774\ub97c \uc218\uc815\ud558\uae30 \uc704\ud574 \ud2b9\uc815 \uc601\uc5ed \ud06c\uae30 \uc774\uc0c1\uc5d0\uc11c\ub294 \ub9c8\ucee4\ub97c \uc880 \ub354 \uac04\uc18c\ud654 \ub41c \ub514\uc790\uc778\uc73c\ub85c \ubcf4\uc774\ub3c4\ub85d \uac1c\uc120\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n- \ub9c8\ucee4 \uc0ac\uc774\uc988\uac00 \uc791\uc544\uc9c0\uba74\uc11c \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ucda9\uc804\uae30 \uac1c\uc218\uac00 \ub354\uc774\uc0c1 \ub4e4\uc5b4\uac08 \uacf5\uac04\uc774 \uc5c6\uc5b4\uc84c\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \ub9c8\ucee4 \uc0c9\uc0c1\uc740 \uadf8\ub300\ub85c \uc720\uc9c0\ub97c \ud558\ub418, \uc778\ud3ec \uc708\ub3c4\uc6b0\uc5d0 \ud604\uc7ac \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ucda9\uc804\uae30 \uac1c\uc218\ub97c \ubcf4\uc5ec\uc8fc\ub294 \ubc29\uc2dd\uc73c\ub85c \ub514\uc790\uc778\uc744 \uac1c\uc120\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n## \uccb4\ud5d8 \uaddc\uce59 \uc124\uc815\\n\\n\uac1c\uc120\ud55c \uae30\ub2a5\uc774 \uc2e4\uc81c\ub85c \uc720\uc6a9\ud55c\uc9c0 \ud655\uc778\ud574\ubcf4\uae30 \uc704\ud574 \uc800\ud76c\ub294 \uce74\ud398\uc778 \uc11c\ube44\uc2a4 2\ucc28 \uccb4\ud5d8\uc758 \uaddc\uce59\uc744 \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 \uc880 \ub354 \uc758\ubbf8\uc788\ub294 \uacbd\ud5d8\uc744 \ud558\uae30\uc704\ud574 1\ucc28 \uccb4\ud5d8 \ub54c \uc815\ud588\ub358 \uaddc\uce59\uc5d0 \ub354\ud574\uc11c \ub2e4\uc74c\uacfc \uac19\uc740 \ucd94\uac00 \uaddc\uce59\uc744 \uc124\uc815\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n### \uc911\uac04\uc5d0 \ubaa9\ud45c \uc9c0\uc810\uc774 \ub9ce\uc774 \ubcc0\uacbd\ub41c\ub2e4\\n\\n\uc9c0\ub09c \uce74\ud398\uc778 \uc11c\ube44\uc2a4 1\ucc28 \uccb4\ud5d8\uc5d0\uc11c\ub294 \uc9c0\uc5ed \uac80\uc0c9\uc774 \uc5c6\uc5b4 \ubaa9\ud45c \uc9c0\uc810\uc744 \ucc3e\ub294 \uac83\uc774 \ubd88\ud3b8\ud588\uc2b5\ub2c8\ub2e4. 1\ucc28 \uccb4\ud5d8 \uc774\ud6c4 \uc9c0\uc5ed \uac80\uc0c9\uc774 \ucd94\uac00 \ub418\uc5c8\uc73c\ubbc0\ub85c \uc774 \uae30\ub2a5\uc774 \uc5bc\ub9c8\ub098 \uc720\uc6a9\ud55c\uc9c0 \uacbd\ud5d8\ud574\ubcf4\uace0\uc790 \uc774 \uaddc\uce59\uc744 \uc124\uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ucd94\uac00\ub85c \ubaa9\ud45c \uc9c0\uc810 \uc8fc\ubcc0\uc758 \ucda9\uc804\uc18c\ub97c \ud655\uc778\ud560 \ub54c \uc0c8\ub85c \ucd94\uac00\ub41c \uc9c0\ub3c4 \uc601\uc5ed \ud655\uc7a5\uc774 \uc5bc\ub9c8\ub098 \uc720\uc6a9\ud55c\uc9c0\ub3c4 \uacbd\ud5d8\ud574\ubcf4\uace0\uc790 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \uccb4\ud5d8 \uac1c\uc694\\n\\n![no offset](./routes.png)\\n1. \uc7a0\uc2e4\uc5ed \ucd9c\ubc1c\\n2. \ud558\ub0a8 \ub9cc\ub450\uc9d1\\n3. \ub2e4\uc74c \ubaa9\uc801\uc9c0 \uc124\uc815\\n4. \ud310\uad50\\n\\n## \uccb4\ud5d8 \ud6c4\uae30\\n\\n### \uc7a0\uc2e4\uc5ed \ucd9c\ubc1c\\n\\n![no offset](./from-jamsil.png)\\n\\n\uc3d8\uce74\uc5d0\uc11c EV6\ub97c \ub300\uc5ec\ud574\uc11c `\uac00\ube0c\ub9ac\uc5d8`, `\uc13c\ud2b8`, `\ud0a4\uc544\ub77c`\uac00 \uc7a0\uc2e4\uc5ed\uc5d0\uc11c \ucd9c\ubc1c\ud558\uc600\uc2b5\ub2c8\ub2e4. \uc800\ub141 \ud1f4\uadfc \uc774\ud6c4\uc5d0 \ub0a8\uc774\uc12c\uc744 \uac00\ub824\uace0 \ubaa9\uc801\uc9c0\ub97c \uc124\uc815\ud558\uc600\uc73c\ub098 \ubc30\uac00 \ub108\ubb34 \uace0\ud30c\uc11c \uac00\ub294 \uae38\uc5d0 \uc2dd\uc0ac\ub97c \ud558\uc790\uace0 \uc598\uae30\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\\n### \ud558\ub0a8 \ub9cc\ub450\uc9d1\\n\\n\ub530\ub77c\uc11c \uc9c4\uc815\ud55c \ucc98\uc74c \ubaa9\uc801\uc9c0\ub294 \uc2a4\ud0c0\ud544\ub4dc\uc600\uc73c\ub098, \uac00\ube0c\ub9ac\uc5d8\uc740 \ub3d9\ub124 \uc8fc\ubbfc\uc774\ub77c \uc2a4\ud0c0\ud544\ub4dc\ub97c \ub108\ubb34 \uc798 \uc54c\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc2a4\ud0c0\ud544\ub4dc\uc5d0 \uc804\uae30\ucc28 \ucda9\uc804\uc18c\uac00 \uc5b4\ub514\uc5d0 \uc788\ub294\uc9c0\ub3c4 \uc54c\uace0\uc788\uc73c\ubbc0\ub85c \ubaa9\uc801\uc9c0\ub97c \uae09\ud558\uac8c \ubcc0\uacbd\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4. \uc774 \ub54c \ubaa9\uc801\uc9c0 \ubcc0\uacbd\uc744 \uc704\ud574 \uc8fc\ubcc0 \uc2dd\ub2f9\uc744 \ub458\ub7ec\ubcf4\ub358 \uc911\uc5d0 \uad1c\ucc2e\uc740 \uc2dd\ub2f9\uc744 \ubc1c\uacac\ud574\uc11c \ud574\ub2f9 \uc2dd\ub2f9\uc744 \uae30\uc900\uc73c\ub85c \uc8fc\ubcc0 \ucda9\uc804\uc18c\ub97c \ud655\uc778\ud574\ubcf4\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./go-to-starfield.png)\\n\\n\uc2dd\ub2f9 \uc8fc\ubcc0\uc744 \uac00\uae30 \uc704\ud574 \uc9c0\uc5ed \uac80\uc0c9\uc744 \ucc98\uc74c\uc73c\ub85c \uc0ac\uc6a9\ud558\uc5ec \uc2dd\ub2f9\uacfc \uac00\uae4c\uc6b4 \uc9c0\uc5ed\uc744 \ud0d0\uc0c9\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c \uc2dd\ub2f9\uc5d0\ub294 \ucda9\uc804\uc18c\uac00 \uc5c6\ub2e4\ub294 \uc0ac\uc2e4\uc744 \uc54c\uac8c\ub418\uc5b4, \uadfc\ucc98 \ucda9\uc804\uc18c\ub97c \ucc3e\uc544\ubcf4\uae30 \uc704\ud574\uc11c \uc9c0\ub3c4\ub97c \ucd95\uc18c\ud588\ub354\ub2c8 1\ucc28 \uccb4\ud5d8\ub54c\uc640\ub294 \ub2ec\ub9ac \ub354 \ub113\uc740 \uc601\uc5ed\uc744 \ubcf4\uc5ec\uc92c\uc2b5\ub2c8\ub2e4. \uc774\uc804\uc5d0\ub294 \ub9c8\ucee4 \uc790\uccb4\uac00 \ubcf4\uc774\uc9c0 \uc54a\uc544 \ub2f5\ub2f5\ud558\uc600\uc73c\ub098, \uc774\uc81c\ub294 \ub354 \ub113\uc740 \uc601\uc5ed\uc744 \uc870\ud68c\ud560 \uc218 \uc788\uac8c \ub418\uc5b4 \ud3b8\ub9ac\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc9c0\ub09c \uccb4\ud5d8 \uc774\ud6c4\ub85c \ud53c\ub4dc\ubc31\uc744 \uc790\uccb4 \uc218\uc9d1\ud558\uc5ec \uac1c\ubc1c\ud55c \uae30\ub2a5\ub4e4\uc774 \ud3b8\ud558\ub2e4\ub294 \uac83\uc744 \uc2dd\ub2f9\uc5d0 \uac00\ub294 \uae38\uc5d0 \ub290\ub084 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n### \ub2e4\uc74c \ubaa9\uc801\uc9c0 \uc124\uc815\\n\\n![no offset](./mandu-mandu.png)\\n\\n\ud558\ub0a8 \ub9cc\ub450\uc9d1\uc5d0\uc11c \uc2dd\uc0ac\ub97c \ud558\ub2e4\uac00 \uc54c\uac8c\ub41c \uc0ac\uc2e4\uc740, \ub0a8\uc774\uc12c\uc740 \uc0dd\uac01\ubcf4\ub2e4 \ub108\ubb34 \uba40\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4. \uc2dd\uc0ac\ub97c \ub9c8\uce58\uace0 \ub0a8\uc774\uc12c\uc5d0 \uac00\uba74, \ucda9\uc804\ub3c4 \uc81c\ub300\ub85c \ubabb\ud558\uace0 \ub3cc\uc544\uc62c \ud310\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc2dd\uc0ac\ub97c \ud558\uba74\uc11c \ub2e4\ub978 \ubaa9\uc801\uc9c0\ub97c \uc54c\uc544\ubd24\ub294\ub370, \uac00\ube0c\ub9ac\uc5d8\uc774 \uc608\uc804\uc5d0 \uac00\ubd24\ub358 \uacf3 \uc911\uc5d0\uc11c \ub0a8\uc591\uc8fc\uc758 \ubb3c\uc758 \uc815\uc6d0\uc774 \uc2dc\uac04\uc744 \ub5bc\uc6b0\uae30 \uc88b\ub2e4\ub294 \uc18c\ub9ac\ub97c \ud558\uc600\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \ubb3c\uc758 \uc815\uc6d0\uc744 \uac80\uc0c9\ud574\ubcf4\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\ub180\ub78d\uac8c\ub3c4 \ubb3c\uc758\uc815\uc6d0\uc740 \uac80\uc0c9\uacb0\uacfc\uc5d0 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4!\\n\\n\uc5b4\uca54 \uc218 \uc5c6\uc774 \uce74\uce74\uc624 \uc9c0\ub3c4\ub85c \ubb3c\uc758 \uc815\uc6d0 \uc704\uce58\ub97c \ud655\uc778\ud558\uc5ec \uc8fc\uc18c\ub97c \uc54c\uc544\ub0b4\uc5c8\uace0, \uc774 \uc8fc\uc18c\ub97c \uce74\ud398\uc778 \uac80\uc0c9\ucc3d\uc5d0 \ub123\uc5c8\uc2b5\ub2c8\ub2e4. \uc800\ud76c\ub294 \uc774 \uacfc\uc815\uc5d0\uc11c \uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub294 \uc5c5\uccb4\uba85 \uc870\ud68c\uac00 \uc548\ub41c\ub2e4\ub294 \uac83\uc774 \uce58\uba85\uc801\uc778 \ub2e8\uc810\uc774\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\ub9cc, \uc774 \uae30\ub2a5\uc740 \uac80\uc0c9 \ud560 \ub54c\ub9c8\ub2e4 \ub9ce\uc740 \ube44\uc6a9\uc774 \uccad\uad6c\ub418\uc5b4 \ud604\uc2e4\uc801\uc73c\ub85c \uc9c0\uae08 \ub2f9\uc7a5 \uae30\ub2a5\uc744 \ub123\ub294 \uac83\uc740 \uc5b4\ub835\ub2e4\uace0 \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uacb0\uad6d \uc8fc\uc18c \uac80\uc0c9\uc744 \ud1b5\ud574 \ubb3c\uc758 \uc815\uc6d0\uacfc \uac00\uc7a5 \uac00\uae4c\uc6b4 \ucda9\uc804\uc18c\ub97c \uc54c\uc544\ub0b4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7f0\ub370! \uc9c0\ub3c4\ub97c \ucd95\uc18c\ud574\uc11c \ud655\uc778\ud574 \ubcf4\ub2c8 \ud574\ub2f9 \ucda9\uc804\uc18c\ub294 \ubb3c\uc758 \uc815\uc6d0\uacfc \uc0dd\uac01\ubcf4\ub2e4 \uba40\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./near-water.png)\\n\\n![no offset](./30-minutes.png)\\n\\n\ubb34\ub824 \uac78\uc5b4\uc11c 30\ubd84\uc774\ub098 \uac78\ub9ac\ub294 \ucda9\uc804\uc18c\uc600\uc2b5\ub2c8\ub2e4!\\n\\n\uc804\uae30\ucc28 \ucda9\uc804\uc744 \uc704\ud574 \uc655\ubcf5 1\uc2dc\uac04\uc774\ub098 \uac78\ub9ac\ub294 \uac70\ub9ac\ub97c \uac78\uc744 \uc218 \uc5c6\ub2e4\uace0 \uc0dd\uac01\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ubb3c\ub860 \uc9c0\ub09c \uccb4\ud5d8\uc5d0\uc11c \uc804\uae30\ucc28\uac00 \uc0dd\uac01\ubcf4\ub2e4 \ubc30\ud130\ub9ac\uac00 \uc624\ub798\uac04\ub2e4\ub294 \uc0ac\uc2e4\uc744 \uc54c\uace0 \uc788\uc5c8\uc9c0\ub9cc, \ub9cc\uc57d \uc800\ud76c\ucc98\ub7fc \ucda9\uc804\uc774 \uae09\ud55c \uc0ac\uc6a9\uc790\ub77c\uba74 \ubaa9\uc801\uc9c0\ub97c \ud3ec\uae30\ud560 \uc218 \ubc16\uc5d0 \uc5c6\uaca0\uad6c\ub098 \ub77c\ub294 \uc0dd\uac01\uc774 \ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub9c8\uc9c0\ub9c9\uc73c\ub85c \uc815\ud55c \ubaa9\uc801\uc9c0\ub294, \uc758\uc678\uc758 \uacb0\uc815\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uad49\uc7a5\ud788 \ubc1c\uc804\ub41c \ucca8\ub2e8 \ub3c4\uc2dc\ub85c \uc54c\ub824\uc9c4 \ud310\uad50\uc600\uc2b5\ub2c8\ub2e4!\\n\\n\uc0ac\uc2e4\uc740 \uc55e\uc73c\ub85c \uac08\uc9c0\ub3c4 \ubaa8\ub974\ub294 \ud310\uad50\ub97c \ubbf8\ub9ac \uad6c\uacbd\uc774\ub098 \ud574\ubcf4\uc790\ub294\uac8c \uc774\uc720\uc600\uc9c0\ub9cc \ube44\ubc00\uc785\ub2c8\ub2e4(?)\\n\\n\uc77c\ub2e8 \ud310\uad50\uc5ed\uc740 IT\uc11c\ube44\uc2a4 \ud68c\uc0ac\ub4e4\uc774 \ub9ce\uc774 \ubab0\ub824\uc788\ub294 \uacf3\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\ub294 \ud310\uad50\uc5ed\uc744 \uce74\ud398\uc778 \uac80\uc0c9\ucc3d\uc5d0 \uac80\uc0c9\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./pangyo.png)\\n\\n\uc9c0\ub3c4\ub97c \ud310\uad50\uc5ed\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \uc678\ubd80\uc778 \uac1c\ubc29\uc778 \ucda9\uc804\uc18c\ub97c \ucc3e\uc558\ub294\ub370, \ud310\uad50\uacf5\uc601\uc8fc\ucc28\uc7a5\uc774 \ubcf4\uc5ec\uc11c \ud574\ub2f9 \ucda9\uc804\uc18c\ub97c \ubaa9\uc801\uc9c0\ub85c \uc7a1\uace0 \ucd9c\ubc1c\ud588\uc2b5\ub2c8\ub2e4.\\n\\n### \ud310\uad50\\n\\n\ud558\ub0a8\uc5d0\uc11c \ud310\uad50\ub97c \uac00\uae30 \uc704\ud574\uc11c\ub294 \uc11c\ud558\ub0a8IC\ub97c \uc9c0\ub098\uc57c\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uac00\ub294 \uae38\uc5d0 \uc6b0\ub9ac \uc11c\ube44\uc2a4\uc5d0 \ub098\uc624\ub294 \uc815\ubcf4\uc640 \uc2e4\uc81c \uc815\ubcf4\uac00 \uc77c\uce58\ud558\ub294\uc9c0 \uc810\uac80\ucc28 \uc11c\ud558\ub0a8 \uac04\uc774 \ud734\uac8c\uc18c\ub97c \ub4e4\ub824\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \ud734\uac8c\uc18c\uc5d0\ub3c4 \ucda9\uc804\uc18c\uac00 \uc788\ub2e4\uace0 \uac80\uc0c9\uc774 \ub418\uc5c8\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4!\\n\\n![no offset](./hanam_station.png)\\n\\n\uac80\uc0c9 \ub2f9\uc2dc\uc5d0\ub294 2\ub300\uc758 \ucda9\uc804\uae30\uac00 \uc788\ub2e4\uace0 \ub098\uc654\uace0, \ub458\ub2e4 \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud558\ub2e4\uace0 \ub418\uc5b4\uc788\uc5c8\ub294\ub370 \uc2e4\uc81c\ub85c \ud655\uc778\ud574\ubcf4\ub2c8 \uc77c\uce58\ud558\ub294 \uac83\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c \uae38\uc744 \ub2ec\ub824 \ud310\uad50\uc5d0 \ub3c4\ucc29\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc8fc\ucc28\uc7a5\uc5d0 \ub4e4\uc5b4\uc624\uae30 \uc804, \uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \ud655\uc778\ud574\ubcf4\ub2c8 \ud310\uad50\uacf5\uc601\uc8fc\ucc28\uc7a5\uc758 \ucda9\uc804\uae30 \ucd1d 12\uae30 \uc911 10\uae30\uac00 \uc0ac\uc6a9\uac00\ub2a5\ud55c \uc0c1\ud0dc\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc815\uc791 \ub4e4\uc5b4\uc640\uc11c \ubcf4\ub2c8 \uc785\uad6c\ubd80\ud130 \ub108\ubb34 \ub9ce\uc740 \uc804\uae30\ucc28\ub4e4\uc774 \ucda9\uc804\uae30\ub97c \uc0ac\uc6a9\uc911\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ubb54\uac00 \uc774\uc0c1\ud558\ub2e4 \uc2f6\uc5c8\uc9c0\ub9cc, \uc544\uc9c1 \uc11c\ubc84\uc5d0 \ubc18\uc601\uc774 \uc548\ub41c\uac74\uac00? \ud558\uba74\uc11c \ube44\uc5b4\uc788\ub294 \ucda9\uc804\uae30\ub97c \ucc3e\uc558\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./empty_station.png)\\n![no offset](./charging.png)\\n\\n\ucda9\uc804\uae30\ub97c \uaf42\uace0 \ub098\uc11c \uc54c\uac8c\ub41c \uac83\uc740 \uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc5d0 \ub098\uc628 \ucda9\uc804\uc18c \ud68c\uc0ac\uba85\uacfc \ubc29\uae08 \uaf42\uc740 \ucda9\uc804\uae30 \ud68c\uc0ac\uba85\uc774 \ub2e4\ub974\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc54c\uace0\ubcf4\ub2c8 \uc74c\uc131 \uc778\uc2dd\uc73c\ub85c \ub124\ube44\uc5d0 \uac80\uc0c9\ud55c \ucda9\uc804\uc18c\ub294 \ud310\uad50\uacf5\uc601\uc8fc\ucc28\uc7a5\uc774 \uc544\ub2cc \ud310\uad50\uc5ed \ud658\uc2b9 \uc8fc\ucc28\uc7a5\uc774\ub77c \uc5c9\ub6b1\ud55c \uacf3\uc73c\ub85c \uc628 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4!!!\\n\\n\ub2e4\ud589\uc778 \uc810\uc740 \uc6b0\ub9ac \uc11c\ube44\uc2a4\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 \ucda9\uc804\uae30 \uc0ac\uc6a9 \uc5ec\ubd80 \uc815\ubcf4\uac00 \uc798\ubabb\ub41c \uac83\uc774 \uc544\ub2c8\uc5c8\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc560\ucd08\uc5d0 \uac00\uace0\uc790 \ud588\ub358 \ud310\uad50\uacf5\uc601\uc8fc\uc790\ucc3d\uc5d0 \ub300\ud55c \uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \uc815\ubcf4\uac00 \uc2e4\uc81c\uc640 \ub3d9\uc77c\ud55c\uc9c0 \ud655\uc778\ud574\ubcf4\ub7ec \uac78\uc5b4\uc11c \uc774\ub3d9\ud588\uc2b5\ub2c8\ub2e4. (\ubc14\ub85c \uc55e\uc5d0 \uc788\uc5c8\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.)\\n\\n![no offset](./no-chargers.png)\\n![no offset](./real-pangyo.png)\\n\\n\ub3c4\ucc29\ud574\ubcf4\ub2c8 1\uce35\uc758 \ucda9\uc804\uae30\ub4e4\uc774 \ubaa8\ub450 \uacf5\uc0ac\uc911\uc774\uc5c8\uace0, \uc11c\ube44\uc2a4\uc758 \uc815\ubcf4\uac00 \uc2e4\uc81c\ub85c\ub3c4 \ubd88\uc77c\uce58 \ud558\ub294 \uc904 \uc54c\uc558\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc0c1\uc138 \uc815\ubcf4\ub97c \ubcf4\ub2c8 3~6\uce35\uc5d0 \ucda9\uc804\uae30\ub4e4\uc5d0 \ub300\ud55c \uc815\ubcf4\ub77c\ub294 \uac83\uc774 \uba85\uc2dc\ub418\uc5b4 \uc788\uc5c8\uace0, \uc2e4\uc81c\ub85c\ub3c4 \uc774\uc640 \ub3d9\uc77c\ud55c \uac83\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./full-charged.png)\\n\\n\uc800\ud76c\ub294 \uc2dc\uac04\uc774 \ub108\ubb34 \ud758\ub7ec \ub2e4\uc2dc \uc7a0\uc2e4\ub85c \ub3cc\uc544\uc640 \ucc28\ub97c \ubc18\ub0a9\ud558\uace0 \uccb4\ud5d8\uc744 \ub9c8\ubb34\ub9ac \ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\\n### \ubd88\ud3b8\ud588\ub358 \uc810\\n\\n- \ub514\ubc14\uc774\uc2a4\uc5d0 \ubcf4\uc5ec\uc9c0\ub294 \uc9c0\ub3c4 \uc601\uc5ed \ud655\uc7a5\uc2dc\uc5d0 \uc6d0\ud558\ub294 \uc815\ubcf4\ub97c \ubcfc \uc218 \uc5c6\ub294 \uac83\uc774 \ubd88\ud3b8\ud588\ub2e4.\\n - \uc9c0\ub3c4\ub97c \ud655\ub300\ud574\uc8fc\uc138\uc694 \ubaa8\ub2ec\uc774 \ub728\uace0, \uc6d0\ub798 \uc788\ub358 \ucda9\uc804\uc18c \ub9c8\ucee4\uac00 \uc804\ubd80 \uc0ac\ub77c\uc9c4\ub2e4.\\n- \ud604\uc7ac \ub098\uc758 \uc704\uce58\ub97c \uc54c\uc544\ubcfc \uc218 \uc788\ub294 \uc218\ub2e8\uc774 \uc5c6\uc5b4 \ubd88\ud3b8\ud588\ub2e4.\\n - \ud604\uc704\uce58\ub97c \ub098\ud0c0\ub0b4\ub294 \ud540 (1\ucc28 \uccb4\ud5d8\uae30\uc5d0\uc11c\ub3c4 \uc5b8\uae09\ud588\ub358 \ubd80\ubd84)\\n - \ub0b4 \uc704\uce58\ub97c \uc0c1\ub300\uc801\uc73c\ub85c \uc54c \uc218 \uc788\ub294 \ub79c\ub4dc\ub9c8\ud06c\uc758 \ubd80\uc871\\n- \ud2b9\uc815 \uc7a5\uc18c(\ub9e4\uc7a5\uba85) \uac80\uc0c9\uc774 \uc548\ub3fc\uc11c \uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub9cc\uc73c\ub85c \ubaa9\uc801\uc9c0\ub97c \ucc3e\uc544\uac00\uae30 \ubd88\ud3b8\ud588\ub2e4.\\n - \uce74\uce74\uc624\ub9f5 \ub4f1\uc744 \ud65c\uc6a9\ud574 \ud2b9\uc815 \uc7a5\uc18c \uac80\uc0c9\uc744 \uc9c4\ud589\ud574\uc57c \ud588\ub2e4.\\n\\n### \ub2e4\uc74c \ubaa9\ud45c\\n\\n\uc55e\uc120 \ubd88\ud3b8\ud588\ub358\uc810\uc744 \uac1c\uc120\ud558\uae30 \uc704\ud574 \ub2e4\uc74c\uacfc \uac19\uc740 \uae30\ub2a5 \uac1c\uc120\uc744 \ucd94\uac00\ub85c \uc9c4\ud589\ud560 \uc608\uc815\uc785\ub2c8\ub2e4.\\n\\n- \ub514\ubc14\uc774\uc2a4\uc5d0 \ubcf4\uc5ec\uc9c0\ub294 \uc9c0\ub3c4 \uc601\uc5ed \ud655\uc7a5\uc5d0 \uc81c\ud55c\uc774 \uc0dd\uae30\uc9c0 \uc54a\uac8c \ucda9\uc804\uc18c \ub9c8\ucee4 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc744 \uc6b0\uc120\uc801\uc73c\ub85c \ub3c4\uc785\ud55c\ub2e4.\\n- \ud604\uc7ac \ub098\uc758 \uc704\uce58\ub97c \uc54c\uc544\ubcfc \uc218 \uc788\ub3c4\ub85d \uc9c0\ud558\ucca0 \uc5ed\uacfc \uac19\uc740 \ub79c\ub4dc\ub9c8\ucee4\ub97c \uc9c0\uc6e0\ub358 \uac83\uc744 \ub864\ubc31\ud55c\ub2e4.\\n\\n\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub9cc\uc73c\ub85c \ubaa9\uc801\uc9c0\ub97c \ucc3e\uc544\uac08 \uc218 \uc788\ub3c4\ub85d \ud558\uae30 \uc704\ud574\uc11c \ud2b9\uc815 \uc7a5\uc18c \uac80\uc0c9\uc744 \ucd94\uac00\ud558\uace0 \uc2f6\uc9c0\ub9cc, \ud574\ub2f9 \uae30\ub2a5\uc744 \uad6c\ud604\ud558\uae30 \uc704\ud574\uc120 \uac80\uc0c9\ub2f9 \ube44\uc6a9\uc774 \ub9ce\uc774 \uccad\uad6c\ub418\ub294 \uc7a5\uc18c \uac80\uc0c9 API\ub97c \ucd94\uac00\ud574\uc57c \ud588\uae30\uc5d0 \ud604\uc2e4\uc801\uc73c\ub85c \uc9c0\uae08 \ub2f9\uc7a5 \uad6c\ud604\ud558\uae30 \uc5b4\ub835\ub2e4\uace0 \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc0c1 \uce74\ud398\uc778 \uc0ac\uc6a9\uae30\uc600\uc2b5\ub2c8\ub2e4."},{"id":"39","metadata":{"permalink":"/39","source":"@site/blog/2023-10-07-carffeine-tester-1/index.mdx","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 1","description":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \uac1c\ubc1c\ud558\uba74\uc11c \uac00\uc7a5 \ub9ce\uc774 \ubc1b\uc740 \ud53c\ub4dc\ubc31 \uc911 \ud558\ub098\ub294 \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc774 \ubc18\ub4dc\uc2dc \ud544\uc694\ud558\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.","date":"2023-10-07T00:00:00.000Z","formattedDate":"2023\ub144 10\uc6d4 7\uc77c","tags":[{"label":"\uce74\ud398\uc778","permalink":"/tags/\uce74\ud398\uc778"},{"label":"\uc11c\ube44\uc2a4 \uacbd\ud5d8","permalink":"/tags/\uc11c\ube44\uc2a4-\uacbd\ud5d8"},{"label":"\ud53c\ub4dc\ubc31","permalink":"/tags/\ud53c\ub4dc\ubc31"},{"label":"\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","permalink":"/tags/\uc804\uae30\ucc28-\uc0ac\uc6a9\uae30"},{"label":"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571","permalink":"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"}],"readingTime":18.085,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"39","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 1","authors":["gabriel"],"tags":["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},"prevItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 2","permalink":"/40"},"nextItem":{"title":"\ucda9\uc804\uc18c \uc870\ud68c api \ubd84\ub9ac","permalink":"/37"}},"content":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \uac1c\ubc1c\ud558\uba74\uc11c \uac00\uc7a5 \ub9ce\uc774 \ubc1b\uc740 \ud53c\ub4dc\ubc31 \uc911 \ud558\ub098\ub294 `\uc0ac\uc6a9\uc790 \uacbd\ud5d8`\uc774 \ubc18\ub4dc\uc2dc \ud544\uc694\ud558\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\ubb34\ub798\ub3c4 \uc804\uae30\uc790\ub3d9\ucc28\ub97c \ubcf4\uc720\ud55c \ud300\uc6d0\ub4e4\uc774 \uc544\ubb34\ub3c4 \uc5c6\ub2e4\ubcf4\ub2c8 \uc2e4\uc81c \uc0ac\uc6a9\uc790\ub4e4\uc774 \uacaa\ub294 \uc5b4\ub824\uc6c0\uc744 \uc608\uc0c1\ud560 \uc218 \ubc16\uc5d0 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc804\uae30 \uc790\ub3d9\ucc28 \uc6b4\uc804\uc790\ub4e4\uc744 \ucc3e\uc544\ub0b4\uc5b4 \uc218\ucc28\ub840 \uc778\ud130\ubdf0\ub97c \uc9c4\ud589\ud558\uc600\ub294\ub370 \uc2e4\uc81c \ucc28\uc8fc\ub4e4\uc774 \uc6d0\ud558\ub294 \uae30\ub2a5\uc774 \ubb34\uc5c7\uc778\uc9c0, \uc5b4\ub5a4 \uc5b4\ub824\uc6c0\uc744 \uacaa\ub294\uc9c0\ub97c \ud655\uc778\ud558\uc5ec \uc774\ub97c \ubc14\ud0d5\uc73c\ub85c \uc11c\ube44\uc2a4\ub97c \uac1c\ubc1c\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc11c\ube44\uc2a4\ub97c \ucc98\uc74c \uac1c\ubc1c\ud558\uc600\uc744 \ub54c \uac00\uc7a5 \ub9ce\uc774 \ubc1b\uc558\ub358 \ud53c\ub4dc\ubc31\uc740 \uc571 \ub85c\ub4dc \uc18d\ub3c4\uac00 \ub108\ubb34 \ub290\ub9ac\ub2e4\ub294 \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc11c\ube44\uc2a4 \ucd08\uae30\uc5d0\ub294 \ub85c\ub529 \uc18d\ub3c4\uac00 \ub108\ubb34 \ub290\ub824\uc11c \uc0ac\uc6a9\uc790\ub4e4\uc5d0\uac8c \uc11c\ube44\uc2a4 \uc0ac\uc6a9\uc744 \uad8c\uc7a5\ud558\uae30 \ubbf8\uc548\ud55c \uc0c1\ud0dc\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc2e4\uc0ac\uc6a9\uc790\ub97c \ubaa8\uc9d1\ud558\ub294 \uac83 \ubcf4\ub2e4 `\uc11c\ube44\uc2a4 \uc548\uc815\ud654\uc5d0 \uc9d1\uc911\ud558\ub294 \uac83`\uc774 \ucd5c\uc6b0\uc120\uc774\ub77c\ub294 \ubaa9\ud45c \uc544\ub798\uc5d0 \uc11c\ube44\uc2a4\ub97c \uac1c\uc120\ud558\ub294 \uc2dc\uac04\uc744 \uac00\uc84c\uace0, \uc9c0\uae08\uc740 \ub85c\ub529 \uc18d\ub3c4\uac00 \ube60\ub974\ub2e4\ub294 \ud53c\ub4dc\ubc31\uc744 \ubc1b\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc9c0\ub09c \ud55c \ub2ec\uac04 \uc11c\ube44\uc2a4 \uc548\uc815\ud654\uc5d0 \uc9d1\uc911\uc744 \ud588\ub2e4\uba74, \uc774\uc81c\ub294 \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc744 \uac1c\uc120\ud558\ub294\ub370 \uc9d1\uc911\uc744 \ud574\uc57c\ud560 \ub54c\uac00 \uc654\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc0ac\uc6a9\uc790 \uc720\uce58\ub97c \uc704\ud574 \uc804\uae30\ucc28 \ub3d9\ud638\ud68c \uce74\ud398, \uce74\uce74\uc624\ud1a1 \uc624\ud508\ucc44\ud305, \uc790\ub3d9\ucc28 \ucee4\ubba4\ub2c8\ud2f0 \ub4f1\uc744 \ub3cc\uba74\uc11c \ud64d\ubcf4\ub97c \uc9c4\ud589\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e4\ud589\ud788\ub3c4 \ubd88\ud2b9\uc815 \ub2e4\uc218\uc758 \uc775\uba85 \uc0ac\uc6a9\uc790\ub4e4\uc744 \uc190\uc27d\uac8c \uad6c\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790\ub4e4\ub85c\ubd80\ud130 \ub9ce\uc740 \ud53c\ub4dc\ubc31\uc744 \ubc1b\uc558\uace0, \ud574\ub2f9 \ud53c\ub4dc\ubc31\uc744 \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0 \ucd5c\ub300\ud55c \ubc18\uc601\ud558\uace0\uc790 \ub178\ub825\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc, \ub300\ubd80\ubd84\uc758 \uc0ac\uc6a9\uc790\ub4e4\uc774 \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0 \ub2e8\uc21c\ud788 \ubc29\ubb38\ud558\uc5ec \ud53c\ub4dc\ubc31\uc744 \uc900 \uac83\uc77c \ubfd0 `\uc2e4\uc81c\ub85c \uc0ac\uc6a9\ud558\uba74\uc11c \ud53c\ub4dc\ubc31\uc744 \uc900 \uac83 \uac19\uc9c0\ub294 \uc54a\ub2e4\ub294 \ub290\ub08c`\uc744 \ubc1b\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790\ub4e4\uc774 \uc571\uc5d0 \uba38\ubb34\ub978 \uc2dc\uac04\uc744 GA4\ub97c \ud1b5\ud574 \ud655\uc778 \ud558\uc600\uc744 \ub54c \ud3c9\uade0 3\ubd84 \uc774\uc0c1\uc774\ub77c\ub294 \uae34 \uc2dc\uac04\uc744 \uba38\ubb3c\ub7ec\uc11c \uc571\uc744 \uaf3c\uaf3c\ud558\uac8c \uc0ac\uc6a9\ud588\uc744 \uac83\uc774\ub77c\uace0 \uae30\ub300\ub294 \ud558\uc600\uc73c\ub098, \uc0ac\uc6a9 \uc911\uc5d0 \ud53c\ub4dc\ubc31\uc744 \uc900\ub2e4\uac70\ub098 \uc0ac\uc6a9 \ud6c4\uc5d0 \ud53c\ub4dc\ubc31\uc744 \uc900 \uac83\uc774 \ub9de\ub294\uc9c0 \ud655\uc2e0\ud558\uae30 \uc5b4\ub824\uc6e0\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub807\ub2e4\uace0 \uc8fc\ubcc0\uc5d0\uc11c \uc804\uae30\ucc28\uc8fc\ub4e4\uc744 \ucc3e\uc790\ub2c8 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uc600\uc2b5\ub2c8\ub2e4. \uc77c\ub2e8 \uc804\uae30\uc790\ub3d9\ucc28 \ubcf4\uae09\ub960\uc774 \uad49\uc7a5\ud788 \ub0ae\uc558\uc73c\uba70, 40~50\ub300\uc5d0 \ud3b8\uc911\ub418\uc5b4 \uc788\uc5b4 \uc800\ud76c\uc5d0\uac8c \ud611\uc870\ud574 \uc904 \ucc28\uc8fc\ubd84\ub4e4\uc744 \uc8fc\ubcc0\uc5d0\uc11c \ucc3e\uae30 \uc5b4\ub824\uc6e0\uc2b5\ub2c8\ub2e4. (\ub300\ubd80\ubd84 \uc0dd\uc5c5\uc73c\ub85c \uc778\ud574 \ubc14\uc058\uc2ed\ub2c8\ub2e4 \u3160\u3160)\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\ub294 \uadf8\ub0e5 \uc9c1\uc811 \uc11c\ube44\uc2a4\ub97c \uc0ac\uc6a9\ud558\uba74\uc11c \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc744 \ud558\uae30\ub85c \ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n## \uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294\uc694\\n\\n\uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \uc9c0\uc6d0\ud558\ub294 \ud575\uc2ec \uae30\ub2a5\uc740 \ub2e4\uc74c\uacfc \uac19\uc558\uc2b5\ub2c8\ub2e4.\\n\\n- \uc804\uad6d \ucda9\uc804\uc18c \uc870\ud68c\\n - \uc9c0\ub3c4 \ud0d0\uc0c9\uc744 \ud1b5\ud55c \uac80\uc0c9\\n - \uac80\uc0c9\ucc3d\uc744 \ud1b5\ud55c \uac80\uc0c9\\n- \ucda9\uc804\uc18c\uc758 \uc6b4\uc601 \uc815\ubcf4 \ud655\uc778\\n- \ucda9\uc804\uc18c \ubcc4 \ucda9\uc804\uae30 \uc0c1\ud0dc \uc870\ud68c (\uc2e4\uc2dc\uac04)\\n- \ucda9\uc804\uc18c \ubc0f \ucda9\uc804\uae30 \uace0\uc7a5 \uc2e0\uace0\\n- \ucda9\uc804\uc18c \ubcc4 \ucda9\uc804\uae30 \uc0ac\uc6a9\ub7c9 \ud1b5\uacc4 \uc870\ud68c\\n- \ucda9\uc804\uc18c \ubcc4 \ub9ac\ubdf0 \uc870\ud68c\\n\\n\uc774\uc678\uc5d0\ub3c4 \ub9ce\uc740 \uae30\ub2a5\ub4e4\uc774 \uc788\uc5c8\uc9c0\ub9cc, \uc704\uc758 \uae30\ub2a5\ub4e4\uc774 \uc0ac\uc6a9\uc790\ub4e4\uc774 \uac00\uc7a5 \uc8fc\ub825\uc73c\ub85c \uc0ac\uc6a9\ud560 \uac83 \uac19\uc740 \uae30\ub2a5\ub4e4\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \uacc4\ud68d\uc744 \uc138\uc6cc\ubcf4\uc790\\n\\n\uc804\uae30\uc790\ub3d9\ucc28 \ub80c\ud2b8\uc5d0 \uc55e\uc11c \uc5b4\ub514\uc5d0 \ubc29\ubb38\ud560 \uc9c0 \ubd80\ud130 \uc815\ud574\uc57c \ud588\uc2b5\ub2c8\ub2e4.\\n\uc800\ud76c\ub294 \uba87 \uac00\uc9c0 \uc6d0\uce59\uc744 \uac00\uc9c0\uace0 \ubc29\ubb38\uc9c0\ub97c \uc815\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc798 \ubaa8\ub974\ub294 \uc9c0\uc5ed\uc77c \uac83\\n2. \ub3c4\ucc29\uc9c0\uc5d0 \ucda9\uc804\uc18c\uac00 \ubc18\ub4dc\uc2dc \uc788\uc744 \uac83\\n3. \ud0c0\uc0ac \uc571\uc744 \uc804\ud600 \uc0ac\uc6a9\ud558\uc9c0 \ub9d0 \uac83\\n\\n\uc77c\ub2e8, \uc81c\uac00 \ucc98\uc74c \uc815\ud588\ub358 \ubaa9\ud45c\ub294 \uacbd\uc0c1\ub0a8\ub3c4 \uc9c4\uc8fc\uc2dc\uc600\uc2b5\ub2c8\ub2e4.\\n\uc9c4\uc8fc\uc2dc\uc5d0\uc11c \ubcf5\uadc0\ud574\uc57c\ud558\ub294 \ud300\uc6d0\uc774 \uc788\ub358 \uc810, \ubc29\ubb38\ud574 \ubcf8 \uc801\uc774 \uc5c6\ub294 \ub3c4\uc2dc\uc778 \uc810, \uc7a5\uac70\ub9ac\ub77c\uc11c \ucda9\uc804\uae30 \uc0ac\uc6a9\uc774 \ud544\uc5f0\uc801\uc778 \uc810 \ub4f1 \uc5ec\ub7ec \uac00\uc9c0 \uc774\uc720\ub85c \uc9c4\uc8fc\uc2dc\ub97c \ubc29\ubb38\ud558\uae30\ub85c \uacb0\uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \ud0a8 \uc21c\uac04 \ub208\uc55e\uc774 \uce84\uce84\ud574\uc84c\uc2b5\ub2c8\ub2e4.\\n\\n\\"\uc9c4\uc8fc\uc2dc\uac00 \uc5b4\ub514\uc5d0 \uc788\uc9c0?\\"\\n\\n![no offset](./search-jinju.png)\\n\\n\ub2e4\ud589\ud788 \uc9c4\uc8fc\uc2dc\ub97c \uac80\uc0c9\ud558\ub2c8 \uc8fc\uc18c \uae30\ubc18\uc73c\ub85c \uac80\uc0c9\uc774 \ub418\uc5c8\uc2b5\ub2c8\ub2e4!\\n\uc9c4\uc8fc\uc2dc\ub97c \uac80\uc0c9\ud55c \uac83\uc740 \uc544\ub2c8\uc9c0\ub9cc \uac04\uc811\uc801\uc774\ub77c\ub3c4 \uac80\uc0c9\uc774 \ub418\ub294 \uac83\uc744 \ubcf4\uace0 \uc548\uc2ec\ud588\uc2b5\ub2c8\ub2e4.\\n\uc544\ubb34 \ucda9\uc804\uc18c\ub97c \ub20c\ub7ec\uc11c \uc9c4\uc8fc\uc2dc\ub85c \uc774\ub3d9\ud558\ub294 \uac83\uc740 \uac00\ub2a5\ud588\uc2b5\ub2c8\ub2e4.\\n\\n```\\n\uc5ec\uae30\uc5d0\uc11c \uc800\ub294 \uc774 \uacfc\uc815\uc5d0\uc11c \ub3c4\uc2dc\ub098 \uc9c0\uc5ed \uac80\uc0c9 \uae30\ub2a5\uc774 \ubc18\ub4dc\uc2dc \ud544\uc694\ud558\ub2e4\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n```\\n\\n\ud558\uc9c0\ub9cc \ub108\ubb34 \uba40\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uc655\ubcf5 700km\ub97c \uc0dd\uac01\ud574\uc57c\ud558\uc5ec 1\ubc15 2\uc77c\uc774 \ud544\uc218\uc600\uace0, \ud300\uc6d0\ub4e4 \uac04\uc5d0 \uc77c\uc815\uc744 \uc870\uc815\ud558\uae30\uac00 \ub108\ubb34 \uc5b4\ub824\uc6e0\uc2b5\ub2c8\ub2e4.\\n\ub530\ub77c\uc11c \ub2e4\ub978 \ub3c4\uc2dc\ub97c \ucc3e\uc544\ubcf4\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./gangnam-to-majang-road.png)\\n\\n\uadf8\ub7ec\ub358 \uc911, \uc81c\uac00 \uc804\uc5d0 \ubc29\ubb38\ud588\ub358 \ud30c\uc8fc\uc2dc\uc758 `\ub9c8\uc7a5\ud638\uc218`\uac00 \uc0dd\uac01\ub0ac\uc2b5\ub2c8\ub2e4.\\n\uc11c\uc6b8\uc5d0\uc11c \uaf64\ub098 \uba3c \uac70\ub9ac(\uc57d 50km)\uc5d0 \uc788\uc5c8\uace0, \uc801\ub2f9\ud788 \uc2dc\uac04\uc744 \ubcf4\ub0bc\ub9cc\ud55c \uc7a5\uc18c\uc600\uc2b5\ub2c8\ub2e4.\\n\ub2e4\ud589\ud788\ub3c4 \ucda9\uc804\uc18c\uc758 \uc774\ub984\uc774 `\ub9c8\uc7a5\ud638\uc218\uad00\ub9ac\uc0ac\ubb34\uc18c`\uc5ec\uc11c \uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \ud1b5\ud574 \ubc14\ub85c \ucc3e\uc744 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uc2ec\uc9c0\uc5b4 \ub9c8\uc7a5\ud638\uc218 \uc8fc\ubcc0\uc5d0\ub294 \ucda9\uc804\uc18c\uac00 \ub9ce\uc9c0 \uc54a\uc740 \ud3b8\uc774\uc5c8\uace0, \ucd08\uae09\uc18d \ucda9\uc804\uae30\uac00 \uc788\uc5b4 \uc800\ud76c \uc571\uc744 \uc2e4\ud5d8\ud558\uae30\uc5d0 \ub531 \uc88b\uc558\uc2b5\ub2c8\ub2e4.\\n\\n## \ub9c8\uc7a5\ud638\uc218\ub85c \ucd9c\ubc1c\\n\\n\uc800 `\uac00\ube0c\ub9ac\uc5d8`\uacfc `\uc81c\uc774`, `\ubc15\uc2a4\ud130`\ub294 \uc11c\uc6b8 \uc120\uc815\ub989\uc5ed\uc5d0\uc11c \uc544\uc774\uc624\ub2c95\ub97c \ub80c\ud2b8\ud558\uace0 \ub9c8\uc7a5\ud638\uc218\ub85c \ucd9c\ubc1c\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./go-to-majang-1.png)\\n\\n\ucc98\uc74c \uacc4\ud68d\ud588\ub358 \uac83 \ucc98\ub7fc \ud0c0\uc0ac\uc758 \uc571\uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0 \ub9c8\uc7a5\ud638\uc218\ub97c \uac80\uc0c9\ud558\uc5ec \uc774\ub3d9\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./go-to-majang-2.png)\\n\\n\uc804\ub0a0 \uc774\ubbf8 \uac80\uc0c9\uc744 \ud588\uc9c0\ub9cc, \ud639\uc2dc \uc0ac\uc6a9 \uc911\uc77c\uc218\ub3c4 \uc788\uae30\uc5d0 \ud55c\ubc88 \ub354 \uac80\uc0c9\ud574\ubd24\uc73c\uba70 \ud574\ub2f9 \uc2dc\uac04\ub300\uc5d0 \ucda9\uc804\uc18c\uac00 \ud3c9\uc18c\uc5d0 \ub35c \ubd90\ube4c \uac83\uc774\ub77c\ub294 \ud1b5\uacc4 \uc790\ub8cc\ub97c \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./go-to-majang-3.png)\\n\\n![no offset](./go-to-majang-4.png)\\n\\n![no offset](./go-to-majang-5.png)\\n\\n\ub9c8\uc7a5 \ud638\uc218\uae4c\uc9c0 20\ubd84 \uac70\ub9ac\ub97c \ub0a8\uae30\uace0, \uac11\uc790\uae30 \ubc30\uac00 \uace0\ud30c\uc9c4 \uc800\ud76c\ub294 \ubaa9\uc801\uc9c0\ub97c \ud2c0\uc5b4 `\ud30c\uc8fc\ub2ed\uad6d\uc218 \ubcf8\uc810`\uc744 \uac00\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \ud30c\uc8fc\ub2ed\uad6d\uc218\uac00 \uc5b4\ub514\uc5d0 \uc788\uc9c0?\\n\\n\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub97c \ud65c\uc6a9\ud558\uc5ec \ud30c\uc8fc\ub2ed\uad6d\uc218 \ubcf8\uc810 \uadfc\ucc98\uc758 \ucda9\uc804\uc18c\ub97c \uac80\uc0c9\ud574\ubcf4\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\uc790\ub3d9\ucc28 \ub0b4\ube44\uac8c\uc774\uc158\uc5d0\ub294 \ud30c\uc8fc\ub2ed\uad6d\uc218\uac00 \uc5b4\ub514\uc778\uc9c0 \ub098\uc640\uc788\uc9c0\ub9cc, \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\ub294 \uc2dd\ub2f9 \uc815\ubcf4\ub294 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\ud574\ub2f9 \uc2dd\ub2f9\uc774 \ub3c4\ub300\uccb4 \uc5b4\ub514\uc5d0 \uc788\ub294\uc9c0 \ud655\uc778\ud560 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4. (\ud30c\uc8fc\ub2ed\uad6d\uc218\uc5d0\uc11c\ub294 \uc804\uae30\ucc28 \ucda9\uc804\uc18c\uac00 \uc5c6\uc5c8\uae30 \ub584\ubb38\uc785\ub2c8\ub2e4.)\\n\\n![no offset](./songchoo-to-noodle-1.png)\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\ub294 \uc790\ub3d9\ucc28 \ub0b4\ube44\uac8c\uc774\uc158\uc5d0 \uc788\ub294 \ub3c4\ub85c\uba85 \uc8fc\uc18c\ub97c \uac80\uc0c9\ud558\uc5ec \uc704\uce58\ub97c \ud30c\uc545\ud558\ub824\uace0 \ud558\uc600\uace0, \ub2e4\uc18c \ubd80\uc815\ud655 \ud558\uc9c0\ub9cc \ub3d9\ub124\uc5d0 \uc788\ub294 \uc778\uadfc \ucda9\uc804\uc18c\ub97c \ucc3e\uc744 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \ud734\uac8c\uc18c\uc5d0 \ub4e4\ub9ac\ub2e4\\n\\n\uce74\ud398\uc778 \uc11c\ube44\uc2a4\ub85c \uac80\uc0c9\ud574\ubcf4\ub2c8 \uc2dd\ub2f9\uc73c\ub85c \uac00\ub294 \uae38 \ud734\uac8c\uc18c\uc5d0\ub3c4 \ucda9\uc804\uc18c\uac00 \uc788\ub2e4\uace0 \ud569\ub2c8\ub2e4.\\n\ud734\uac8c\uc18c \uc774\ub984\uc744 \uc785\ub825\ud558\ub2c8 \ubc14\ub85c \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./yangju-station-3.png)\\n\\n\uc2ec\uc9c0\uc5b4 \uc9c0\uae08 \uc0ac\uc6a9\uc911\uc774\ub77c\uace0 \ud569\ub2c8\ub2e4! \ub530\ub77c\uc11c \uc800\ud76c\ub294 \ud655\uc778\ud574\ubcf4\uae30 \uc704\ud574 \ud734\uac8c\uc18c\uc5d0 \ub4e4\ub9ac\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./yangju-station-1.png)\\n![no offset](./yangju-station-2.png)\\n\\n\uc2e4\uc81c\ub85c \uc0ac\uc6a9 \uc911\uc784\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4. \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \uc0ac\uc6a9\uc911\uc774\ub77c\uace0 \ub098\uc654\ub294\ub370 \uc2e4\uc81c\ub85c \uc0ac\uc6a9\uc911\uc778 \uac83\uc744 \ubcf4\ub2c8 \uacf5\uacf5 api\uac00 \ub098\ub984 \uc2e4\uc2dc\uac04\uc73c\ub85c \ub370\uc774\ud130\ub97c \uc798 \ubcf4\ub0b4\uc8fc\uace0 \uc788\ub2e4\uace0 \uc0dd\uac01\ud558\uac8c \ub418\uc5c8\uace0, \uc800\ud76c \ud300 \uc11c\ubc84\uc5d0\uc11c\ub3c4 \uc774\ub97c \uc81c\ub300\ub85c \uc218\uc9d1\ud558\uace0 \uc788\ub2e4\uace0 \uc0dd\uac01\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./yangju-station-5.png)\\n\\n\ub9d0\ub85c\ub9cc \ub4e3\ub358 \uace0\uc18d\ub3c4\ub85c \ud734\uac8c\uc18c\uc758 \uc804\uae30\ucc28 \ucda9\uc804\uc18c \ub300\uae30\uc904\uc744 \uc9c1\uc811 \ud655\uc778\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\ucc28\uc8fc \ubd84\uacfc \uc778\ud130\ubdf0 \ud558\uace0 \uc2f6\uc5c8\uc9c0\ub9cc, \ucc28 \ub0b4\ubd80\uc5d0\uc11c \ub108\ubb34 \ubc14\ube60\ubcf4\uc774\uc154\uc11c \uadf8\ub7f4 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc804\uae30\ucc28 \ucda9\uc804\uc744 \uae30\ub2e4\ub9ac\uba74\uc11c \ubb34\uc5c7\uc744 \ud560 \uc218 \uc788\uc744\uae4c\uc694?\\n\uc774 \ubd84\uc740 \ub2e4\ud589\ud788\ub3c4 \uc5c5\ubb34\ub97c \ubcf4\uace0 \uacc4\uc168\uc9c0\ub9cc, \ub2e4\ub978 \ucc28\uc8fc\ub4e4\uc740 \ubb34\uc5c7\uc744 \ud558\uace0 \ubcf4\ub0bc\uc9c0 \uad81\uae08\ud574\uc84c\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./yangju-station-4.png)\\n\\n\ud734\uac8c\uc18c\uc5d0\ub294 \ucda9\uc804\uc18c\uac00 \ud558\ub098 \ub354 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ud55c \uacf3\uc740 \uc0ac\uc6a9\uc911\uc774\uc9c0\ub9cc, \ub2e4\ub978 \ud55c \uacf3\uc740 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 \uc774 \ucda9\uc804\uc18c\ub97c \uc0ac\uc6a9\ud574\ubcf4\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./yangju-station-6.png)\\n\\n\uc0ac\uc6a9\ud560 \uc218 \uc788\uc73c\ub2c8\uae50 \ub4e4\uc5b4\uac00\ubd10\uc57c\uc9c0! \ud558\uace0 \ub3c4\ucc29\ud55c \uc21c\uac04 \uc544\ucc28 \uc2f6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\"\uc544, \ucda9\uc804\uc18c\uac00 \uc678\ubd80\uc778 \uc0ac\uc6a9 \uae08\uc9c0\uc77c \uc218 \uc788\uc5c8\uc9c0?\\"\\n\\n\uc800\ud76c\ub294 \ubd84\uba85\ud788 \uc11c\ube44\uc2a4\ub97c \uc9c1\uc811 \uac1c\ubc1c\ud588\uc73c\ub2c8\uae50 \ub2e4 \uc54c\uace0 \uc788\ub358 \uc0ac\ud56d\uc774\uc5c8\uc9c0\ub9cc, \uc804\ud600 \uc0dd\uac01\uce58 \ubabb\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc11c\ube44\uc2a4\ub97c \uac1c\ubc1c\ud558\ub294 \ub0b4\ub0b4 \uc678\ubd80\uc778 \uac1c\ubc29 \ucda9\uc804\uc18c\uc5d0 \ub300\ud55c \uc911\uc694\uc131\uc744 \uac04\ud30c\ud558\uc600\uace0, \uc774 \uae30\ub2a5\uc744 \ub123\uc5c8\uc73c\uba74\uc11c\ub3c4 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0 \ucda9\uc804\uc18c\ub97c \ubc29\ubb38\ud55c \uac83\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ubc14\ub85c \uc55e\uc5d0 \uc788\uc5b4\uc11c \ub2e4\ud589\uc774\uc5c8\uc9c0\ub9cc, \uc5b4\ucc0c\ub410\ub4e0 \uc774 \ucda9\uc804\uc18c\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\ub294 \ud734\uac8c\uc18c\ub97c \ub5a0\ub098\ub294 \ub0b4\ub0b4 \uc774 \ubb38\uc81c\uc5d0 \ub300\ud574\uc11c \ud1a0\ub860\uc744 \ud560 \uc218 \ubc16\uc5d0 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n```\\n\ubd84\uba85 \uc6b0\ub9ac\uac00 \ub9cc\ub4e0 \uc11c\ube44\uc2a4\uc778\ub370 \uc65c \ub193\ucce4\uc744\uae4c?\\n```\\n\\n## \ub9db\uc788\ub294 \uc810\uc2ec\\n\\n![no offset](./noodle-1.png)\\n\\n\ud30c\uc8fc\ub2ed\uad6d\uc218 \ubcf8\uc810\uc5d0\uc11c \ub9db\uc788\ub294 \uc2dd\uc0ac\ub97c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ube44\ub85d \uc2dd\ub2f9\uc5d0\ub294 \uc804\uae30\ucc28 \ucda9\uc804\uc18c\uac00 \uc5c6\uc5c8\uc9c0\ub9cc, \uc778\uadfc\uc5d0 \ucda9\uc804\uc18c\uac00 \uc788\uc5b4 \uc2e4\ud5d8\uc744 \ud558\ub098 \ud574\ubcfc \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc778\uadfc \ucda9\uc804\uc18c\uc640 \uc2dd\ub2f9\uc758 \uac70\ub9ac\uac00 \uac00\uae4c\uc6cc \ubcf4\uc774\ub294\ub370, \uacfc\uc5f0 \uac78\uc5b4\uac08 \uc218 \uc788\uc744\uae4c?\\n\\n\uc2e4\uc81c\ub85c \uac77\uc9c0\ub294 \uc54a\uc558\uc2b5\ub2c8\ub2e4\ub9cc \ucc28 \ud0c0\uba74\uc11c \uc9c0\ub098\uac00\uba74\uc11c \ud655\uc778\ud574\ubcf8 \uacb0\uacfc \uc9c1\uc811 \uac78\uc744 \uc218 \uc5c6\ub294 \uac70\ub9ac\uc600\uc2b5\ub2c8\ub2e4. (\uad49\uc7a5\ud788 \uac77\uae30 \uc2eb\uc740 \uc218\uc900\uc758 \uba3c \uac70\ub9ac\uc600\uc2b5\ub2c8\ub2e4.)\\n\\n\uc9d1\uc5d0 \uc788\ub294 PHEV\ub97c \ud0c8 \uae30\ud68c\uac00 \ub9ce\uc544 \uc804\uae30\ucc28 \ucda9\uc804\uc18c\ub97c \uc790\uc8fc \ubc29\ubb38\ud588\ub358 \uc800\ub294 \uc774\ub7f0 \uc810\uc744 \uc798 \uc54c\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e4\ud589\ud788 \uc774 \ubd80\ubd84\uc744 \uc798 \uc54c\uace0 \uc788\uc5c8\uae30\uc5d0 \uc800\ud76c\ub294 \uc774 \ubd80\ubd84\uc744 \uc11c\ube44\uc2a4\uc5d0 \ubc18\uc601\ud558\uc600\uace0, \ubaa8\ub4e0 \ub370\uc774\ud130\ub97c \ud3ec\uae30\ud558\uc9c0 \uc54a\uc558\ub358 \uac83\uc774 \uc633\uc740 \uc120\ud0dd\uc774\uc5c8\ub2e4\ub294 \uac83\uc744 \ud655\uc778\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./noodle-2.png)\\n\\n\uc2dd\uc0ac\uac00 \ub05d\ub098\uace0 \ub4dc\ub514\uc5b4 \ub9c8\uc7a5\ud638\uc218\ub85c \ucd9c\ubc1c\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \ub9c8\uc7a5\ud638\uc218 \ub3c4\ucc29\\n\\n\ub9c8\uc7a5\ud638\uc218\uc5d0 \ub3c4\ucc29\ud558\uc790\ub9c8\uc790 \ucda9\uc804\uc18c\uc5d0 \ubc29\ubb38\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./majang-1.png)\\n\\n\ud1b5\uacc4\uc5d0\uc11c\ub294 \uc0ac\uc6a9\ub960\uc774 \uc801\uc744 \uac83\uc774\ub77c\uace0 \ud558\uc600\ub294\ub370 \uc800\ud76c\ub9cc \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./majang-2.png)\\n![no offset](./majang-4.png)\\n\\n2\uae30 \uc911 1\uacf3\uc744 \uc800\ud76c\uac00 \uc0ac\uc6a9\ud558\uc600\uace0, \ub9c8\uc7a5\ud638\uc218\ub97c \ub3cc\uc558\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./majang-3.png)\\n\\n\uc57d 50\ubd84 \uac04 \uc0b0\ucc45\uc744 \ud558\uace0, \ub3cc\uc544\uc640\ubcf4\ub2c8 \ucda9\uc804\uae30 \ub2e4 \ub418\uc5b4\uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc2e4 \ub9c8\uc7a5\ud638\uc218 \uae4c\uc9c0 \uc624\ub294 \ub0b4\ub0b4 \ub4e0 \uc0dd\uac01\uc774\uc5c8\uc9c0\ub9cc, \uc804\uae30\ucc28\uc758 \ubc30\ud130\ub9ac\uac00 \uc0dd\uac01\ubcf4\ub2e4 \uc624\ub798 \uac04\ub2e4\ub294 \uc0dd\uac01\uc774 \ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc77c\ubd80\ub7ec \ud68c\uc0dd\uc81c\ub3d9 \uae30\ub2a5\ub3c4 \ub044\uace0, \uc5d0\uc5b4\ucee8\uc744 \uac15\ud558\uac8c \ud2c0\uc5b4\uc11c \ubc30\ud130\ub9ac\ub97c \uc18c\uc9c4\ud558\ub824\uace0 \ud558\uc600\uc73c\ub098, 85km\ub97c \uc8fc\ud589\ud558\ub294 \ub3d9\uc548 \uaca8\uc6b0 20%\ub97c \uc18c\ubaa8\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ucda9\uc804\uae30\ub97c \uaf42\uc744 \ub54c 50%\uc600\uc73c\ub098, \ud638\uc218\ub97c \ud55c\ubc14\ud034 \ub3cc\uace0 \uc624\ub2c8 \uc774\ubbf8 100%\uac00 \ub418\uc5b4\uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc5ec\ub2f4\uc774\uc9c0\ub9cc, \uc800\ud76c\uac00 \ub3cc\uc544\uc654\uc744 \ub54c \uc606 \uc790\ub9ac\uc5d0\ub294 \uc804\uae30 \ud654\ubb3c\ucc28\uac00 \uc788\uc5b4 \ucda9\uc804\uc18c\uac00 \uac00\ub4dd \ucc3c\uc2b5\ub2c8\ub2e4.\\n\\n\ub610, \uc571\uc5d0\uc11c\ub3c4 \ucda9\uc804\uae30 \uc0ac\uc6a9 \uc5ec\ubd80\uac00 \uc5c5\ub370\uc774\ud2b8 \ub418\ub294 \uac83\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./majang-5.png)\\n\\n\ubc30\ud130\ub9ac \uc131\ub2a5\uc5d0\ub294 \uc88b\uc9c0 \uc54a\uace0 \uac00\uaca9\ub3c4 \ube44\uc2f8\uc11c \uc774\ub97c \uc790\uc8fc \uc0ac\uc6a9\ud558\ub294 \uac83\uc740 \uc88b\uc9c0 \uc54a\uaca0\uc9c0\ub9cc, \uae09\ud55c \uc0ac\ub78c\ub4e4\uc740 \uae09\uc18d \ucda9\uc804\uae30\ub97c \uc0ac\uc6a9\ud558\uba74 \ub418\uaca0\uad6c\ub098 \uc2f6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n```\\n\ub530\ub77c\uc11c \uae09\uc18d\uacfc \uc644\uc18d\uc740 \ub354\ub354\uc6b1 \ub2e4\ub978 \uac1c\ub150\uc73c\ub85c \ubd10\uc57c\uaca0\ub2e4\ub294 \uc0dd\uac01\uc774 \ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n```\\n\\n\uc81c\uac00 \uadf8\ub3d9\uc548 \uacbd\ud5d8\ud588\ub358 \uc804\uae30\ucc28 \ucda9\uc804\uc18c\ub294 \uc644\uc18d \uae30\uc900\uc774\uc5c8\uae30\uc5d0 \uc2e0\uc120\ud55c \uacbd\ud5d8\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \uc120\ub989\uc73c\ub85c \ub3cc\uc544\uc624\ub2e4\\n\\n![no offset](./end.png)\\n\\n\uc120\ub989\uc73c\ub85c \ub3cc\uc544\uc640\uc11c \ucc28\ub7c9\uc744 \ubc18\ub0a9\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 \uc774\ubc88 \uc5ec\uc815\uc744 \ud1b5\ud574 \uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc5d0\uc11c \uc5b4\ub5a4 \uc810\uc744 \uac1c\uc120\ud574\uc57c\ud560\uc9c0 \uc880 \ub354 \uba85\ud655\ud558\uac8c \uc54c\uac8c\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n1. \ud604\uc7ac \uc11c\ube44\uc2a4\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 \uae30\ub2a5\ub4e4\ub85c \ucda9\uc804\uc18c\ub97c \uac80\uc0c9\ud558\ub294 \uac83\uc740 \uac00\ub2a5\ud558\uba70, \ucda9\uc804\uc18c\uc758 \uc704\uce58\ub97c \uc815\ud655\ud558\uac8c \ud30c\uc545\ud558\ub294 \uac83\ub3c4 \uac00\ub2a5\ud558\ub2e4.\\n2. \ud558\uc9c0\ub9cc \ucda9\uc804\uc18c\uac00 \uc5c6\ub294 \ubaa9\uc801\uc9c0\ub294 \uac80\uc0c9\ud560 \uc218 \uc5c6\uace0, \ud604 \uc704\uce58\uac00 \uc5b4\ub514\uc778\uc9c0 \uac00\ub2a0\ud558\uae30\uac00 \uc5b4\ub824\uc6cc\uc9c4\ub2e4.\\n3. \ucda9\uc804\uc18c\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub2e4\uace0 \ud45c\uae30\ub418\uc5b4 \uc788\ub354\ub77c\ub3c4 \uc678\ubd80\uc778 \uac1c\ubc29\uc774 \uc544\ub2d0 \uc218 \uc788\ub2e4. \uc815\ubcf4\uac00 \uc815\ud655\ud788 \uc81c\uacf5\ub428\uc5d0\ub3c4 \ubd88\uad6c\ud558\uace0 \uc774\ub97c \ub2e8\ubc88\uc5d0 \ub208\uce58\ucc44\uae30 \uc5b4\ub835\ub2e4.\\n4. \uc774\ub7ec\ud55c \ubb38\uc81c\ub97c \uc608\uc0c1\ud558\uc5ec `\uc678\ubd80\uc778 \uac1c\ubc29 \uc5ec\ubd80`\ub97c \ud544\ud130\ub9c1 \ud560 \uc218 \uc788\ub294 \uae30\ub2a5\uc744 \uc81c\uacf5\ud558\uace0 \uc788\uc74c\uc5d0\ub3c4 \ubd88\uad6c\ud558\uace0 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc558\ub2e4.\\n5. \ucda9\uc804\uc18c\uc758 \ud1b5\uacc4 \uc790\ub8cc\uc758 \uc801\uc911\ub960\uc740 \ub192\uc558\uc73c\ub098, \uc880 \ub354 \ub9ce\uc740 \ucda9\uc804\uc18c\ub97c \ub4e4\ub824 \ud655\uc778\ud574\ubd10\uc57c \ud560 \uac83 \uac19\uc558\ub2e4.\\n6. \uc804\uae30\uc790\ub3d9\ucc28\ub294 \uc0dd\uac01\ubcf4\ub2e4 \uc624\ub798\uac00\uace0 \uc0c1\ud488\uc131\uc774 \uc788\uc5c8\ub2e4. \uc8fc\ud589 \ub2a5\ub825\ub3c4 \ucda9\ubd84\ud558\uace0, \uc778\ud504\ub77c\uac00 \uc798 \ub418\uc5b4\uc788\ub2e4. \uc774\uac78 \uc65c \uc695\ud558\uc9c0? \ub77c\ub294 \uc0dd\uac01\uc774 \ub4e4\uc5c8\ub2e4.\\n7. \uc9c0\ub3c4 \ud655\ub300 \ud5c8\uc6a9 \ubc94\uc704\uac00 \ub108\ubb34 \uc881\uc544\uc11c \uc0ac\uc6a9\ud558\ub294\ub370 \ubd88\ud3b8\ud55c\uac74 \uc2e4\uc81c \uc0c1\ud669\uc5d0\uc11c \ub354 \ubd88\ud3b8\ud588\ub2e4.\\n\\n\uc774\uc0c1 \uce74\ud398\uc778 \uc0ac\uc6a9\uae30\uc600\uc2b5\ub2c8\ub2e4."},{"id":"37","metadata":{"permalink":"/37","source":"@site/blog/2023-09-22-station-api-separate.mdx","title":"\ucda9\uc804\uc18c \uc870\ud68c api \ubd84\ub9ac","description":"\uc131\ub2a5 \uac1c\uc120\uc744 \uc704\ud574 \ucda9\uc804\uc18c \uc870\ud68c API\uc758 \uc124\uacc4\ub97c \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4.","date":"2023-09-22T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 22\uc77c","tags":[{"label":"\ud611\uc5c5","permalink":"/tags/\ud611\uc5c5"},{"label":"\uc11c\ubc84 \ubd80\ud558 \uc904\uc774\uae30","permalink":"/tags/\uc11c\ubc84-\ubd80\ud558-\uc904\uc774\uae30"}],"readingTime":2.78,"hasTruncateMarker":false,"authors":[{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"}],"frontMatter":{"slug":"37","title":"\ucda9\uc804\uc18c \uc870\ud68c api \ubd84\ub9ac","authors":["scent"],"tags":["\ud611\uc5c5","\uc11c\ubc84 \ubd80\ud558 \uc904\uc774\uae30"]},"prevItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc640 \ud568\uaed8\ud558\ub294 \uc804\uae30\ucc28 \uc5ec\ud589 1","permalink":"/39"},"nextItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 1","permalink":"/38"}},"content":"\uc131\ub2a5 \uac1c\uc120\uc744 \uc704\ud574 \ucda9\uc804\uc18c \uc870\ud68c API\uc758 \uc124\uacc4\ub97c \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\uae30\uc874\uc5d0\ub294 \ucda9\uc804\uc18c \uac04\ub2e8 \uc815\ubcf4\uc640 \ub9c8\ucee4 \uc815\ubcf4\ub97c \ud55c \ubc88\uc5d0 \ubc1b\uc544\uc624\ub3c4\ub85d \uc124\uacc4\ub418\uc5b4 \uc788\uc5c8\uc9c0\ub9cc,\\n\ubc31\uc5d4\ub4dc\uc640 \ud504\ub860\ud2b8\uc5d4\ub4dc\uac00 \ud611\uc5c5\ud558\uc5ec \uac04\ub2e8 \uc815\ubcf4\uc640 \ub9c8\ucee4 \uc815\ubcf4\ub97c \uac01\uac01 \ud544\uc694\ud55c \ub9cc\ud07c\ub9cc \uc870\ud68c\ud558\ub3c4\ub85d \uba85\uc138\ub97c \uc218\uc815\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c \uba3c\uc800, \ubc31\uc5d4\ub4dc\uc640 \ud504\ub860\ud2b8\uc5d4\ub4dc\ub294 \ud568\uaed8 \ubaa8\uc5ec \uae30\ub2a5 \uc694\uad6c\uc0ac\ud56d\uacfc \uc131\ub2a5 \uac1c\uc120 \ubaa9\ud45c\ub97c \ub17c\uc758\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \ucda9\uc804\uc18c \uac04\ub2e8 \uc815\ubcf4\uc640 \ub9c8\ucee4 \uc815\ubcf4\ub97c \uac01\uac01 \uc870\ud68c\ud558\ub294 API \uc5d4\ub4dc\ud3ec\uc778\ud2b8\ub97c \uc0c8\ub85c \uc124\uacc4\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e4\uc74c\uc73c\ub85c, \ubc31\uc5d4\ub4dc\uc5d0\uc11c \uac04\ub2e8 \uc815\ubcf4 \uc870\ud68c\ub97c \uc704\ud55c API\ub97c \uad6c\ud604\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\ud544\uc694\ud55c \ud544\ub4dc\ub9cc\uc744 \uc870\ud68c\ud558\uc5ec \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \ubd80\ud558\ub97c \uc904\uc774\uace0 \uc751\ub2f5 \uc2dc\uac04\uc744 \uac1c\uc120\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\uc774\ud6c4\uc5d0\ub294 \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0\uc11c \ud574\ub2f9 API\ub97c \ud638\ucd9c\ud558\uc5ec \ud544\uc694\ud55c \uc815\ubcf4\ub97c \ubc1b\uc544\uc624\ub3c4\ub85d \uc218\uc815\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\ub9c8\uc9c0\ub9c9\uc73c\ub85c, \ub9c8\ucee4 \uc815\ubcf4 \uc870\ud68c\ub97c \uc704\ud55c API\ub97c \uad6c\ud604\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\ub9c8\ucee4 \uc815\ubcf4\ub294 \uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ub418\ub294 \uc815\ubcf4\ub85c\uc11c, \uc694\uccad\ud55c \uc601\uc5ed \uc678\ubd80\ub85c \uc9c0\ub3c4\uac00 \uc774\ub3d9\ud560 \uacbd\uc6b0 \ud638\ucd9c\ub418\ub3c4\ub85d \uc124\uacc4\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uae30\uc874\uc5d0\ub294 \uac04\ub2e8 \uc815\ubcf4 \ub9ac\uc2a4\ud2b8\ub97c \ubcf4\uc5ec\uc8fc\uae30 \uc704\ud574 \uc870\ud68c\ud558\ub358 \uc815\ubcf4\ub4e4\uc774 \ub2e4\uc218 \ud3ec\ud568\ub418\uc5b4 \uc788\uc5c8\uc9c0\ub9cc,\\n\uc774 \uc815\ubcf4\ub97c \uc81c\uc678\ud558\uace0 \ub9c8\ucee4\ub97c \ub744\uc6b0\uae30 \uc704\ud574 \ud544\uc694\ud55c \ucd5c\uc18c\ud55c\uc758 \uc815\ubcf4\ub97c \uc870\ud68c\ud558\ub3c4\ub85d \uc218\uc815\ud574 \uc11c\ubc84\uc758 \ubd80\ud558\ub97c \ub0ae\ucdc4\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7ec\ud55c \ubcc0\uacbd\uc73c\ub85c \uc778\ud574 \ucda9\uc804\uc18c \uc870\ud68c API\uc758 \uc131\ub2a5\uc774 \uac1c\uc120\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\ud544\uc694\ud55c \uc815\ubcf4\ub9cc\uc744 \uc870\ud68c\ud558\ubbc0\ub85c\uc368 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \ubd80\ud558\ub97c \uc904\uc774\uace0 \uc751\ub2f5 \uc2dc\uac04\uc744 \ub2e8\ucd95\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\ub610\ud55c, \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0\uc11c\ub294 \ud544\uc694\ud55c \uc815\ubcf4\ub9cc\uc744 \ud638\ucd9c\ud558\uc5ec \ubd88\ud544\uc694\ud55c \ub370\uc774\ud130\ub97c \ubc1b\uc544\uc624\uc9c0 \uc54a\uc544\ub3c4 \ub418\ubbc0\ub85c \ud074\ub77c\uc774\uc5b8\ud2b8 \uce21\uc758 \uc131\ub2a5\ub3c4 \ud5a5\uc0c1\ub418\uc5c8\uc2b5\ub2c8\ub2e4."},{"id":"38","metadata":{"permalink":"/38","source":"@site/blog/2023-09-22-visitors/index.mdx","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 1","description":"\uc800\ud76c \ud300\uc740 \ub2e8\uc21c \ubc29\ubb38\uc790 100\uba85\uc744 \ubaa8\uc544\uc57c\ud558\ub294 \ubbf8\uc158\uc744 \ubc1b\uc558\uc2b5\ub2c8\ub2e4.","date":"2023-09-22T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 22\uc77c","tags":[{"label":"ga4","permalink":"/tags/ga-4"},{"label":"google analytics 4","permalink":"/tags/google-analytics-4"},{"label":"\uce74\ud398\uc778","permalink":"/tags/\uce74\ud398\uc778"},{"label":"\ubc29\ubb38\uc790 \ubd84\uc11d","permalink":"/tags/\ubc29\ubb38\uc790-\ubd84\uc11d"}],"readingTime":3.82,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"38","title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 1","authors":["gabriel"],"tags":["ga4","google analytics 4","\uce74\ud398\uc778","\ubc29\ubb38\uc790 \ubd84\uc11d"]},"prevItem":{"title":"\ucda9\uc804\uc18c \uc870\ud68c api \ubd84\ub9ac","permalink":"/37"},"nextItem":{"title":"\ub9c8\ucee4 \ub80c\ub354\ub9c1 \ucd5c\uc801\ud654","permalink":"/36"}},"content":"\uc800\ud76c \ud300\uc740 \ub2e8\uc21c \ubc29\ubb38\uc790 100\uba85\uc744 \ubaa8\uc544\uc57c\ud558\ub294 \ubbf8\uc158\uc744 \ubc1b\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\ubaa9\ud45c \ub2ec\uc131\uc744 \uc704\ud574 \uc57d 2\uc8fc \uc804\uc5d0 \uc2e4\ud589 \uacc4\ud68d\uc744 \uc81c\ucd9c\ud574\uc57c \ud588\ub294\ub370\uc694\\n\\n100\uba85\uc744 \ubaa8\uc9d1\ud558\uae30 \uc704\ud574 \ub2e4\uc74c\uacfc \uac19\uc740 \uacc4\ud68d\uc744 \uc138\uc6e0\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n![no offset](./plan.png)\\n\\n---\\n\\n\uc774 \ub2f9\uc2dc \uc800\ud76c \ud300\uc758 \uac00\uc7a5 \ud070 \uace0\ubbfc\uc740, \uc804\uae30\ucc28\uac00 \uc5ec\uc804\ud788 \uc18c\uc218\uc758 \uc6b4\uc804\uc790\uc5d0\uac8c\ub9cc \ubcf4\uae09\ub418\uc5c8\ub2e4\ub294 \uc810\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ud2b9\ud788, \uc804\uae30\ucc28 \ubcf4\uae09 \uad00\ub828 \ud1b5\uacc4 \uc790\ub8cc\ub97c \ucc3e\uc544\ubcf4\uba74 \ub300\ubd80\ubd84\uc758 \ucc28\uc8fc\ub4e4\uc740 40~60\ub300\uc5d0 \uc555\ub3c4\uc801\uc73c\ub85c \ubab0\ub824\uc788\uc5b4 \uc80a\uc740 \uc5f0\ub839 \uce35\uc5d0\uc11c\ub294 \uac70\uc758 \uad6c\ub9e4\ub97c \ud558\uc9c0 \uc54a\uace0 \uc788\ub2e4\ub294 \uc0ac\uc2e4\uc744 \uc54c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\\n![no offset](./statistics.png)\\n\\n\uc704 \uc790\ub8cc\ub294 2021\ub144 7\uc6d4 \uae30\uc900\uc774\uc9c0\ub9cc, \ucd5c\uc2e0 \uc790\ub8cc\uc5d0\uc11c\ub3c4 \ub9c8\ucc2c\uac00\uc9c0\ub85c \uc80a\uc740 \uc5f0\ub839\uce35\uc5d0\uc11c\ub294 \uc804\uae30\ucc28\ub97c \ubcf4\uc720\ud55c \uc0ac\ub78c\uc744 \ucc3e\uae30 \uc5b4\ub835\ub2e4\uace0 \ub098\uc635\ub2c8\ub2e4. \uc2e4\uc81c\ub85c \uc8fc\ubcc0 \ub610\ub798\uc758 \uc6b4\uc804\uc790\ub97c \ucc3e\uc544\ubcf4\uba74 \ub300\ubd80\ubd84 \uac00\uc194\ub9b0 \ubaa8\ub378\uc744 \ud0c0\uace0 \ub2e4\ub2c8\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\ub294 \ud64d\ubcf4 \ub300\uc0c1\uc744 \uc8fc\ubcc0\uc5d0\uc11c \ucc3e\uc9c0 \uc54a\uace0 \ubd88\ud2b9\uc815 \ub2e4\uc218\uc758 \uc0ac\ub78c\ub4e4\uc744 \ubaa8\uc9d1\ud558\uae30 \uc704\ud574 \ub2e4\uc74c\uacfc \uac19\uc740 \ubc29\ubc95\uc744 \uc0ac\uc6a9\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n# \ud64d\ubcf4 \ubc29\ubc95\\n\\n## \uce74\ud398\\n\\n![no offset](./insta1.png)\\n![no offset](./naver1.png)\\n\\n\ub124\uc774\ubc84\uc5d0 \uc788\ub294 \uc804\uae30\uc790\ub3d9\ucc28 \ub3d9\ud638\ud68c \uce74\ud398 \uc911 \uac00\uc7a5 \ud070 \uacf3\uc5d0 \uae00\uc744 \uc62c\ub824 \ubc29\ubb38\uc790\ub97c \ubaa8\uc9d1\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uce74\ud398\uc5d0 \uae00\uc744 \uc62c\ub9ac\ub294 \uac83\uc740 \ubb34\ub8cc\uc774\uba70, \uce74\ud398\uc5d0 \uac00\uc785\ud55c \uc0ac\ub78c\ub4e4\uc740 \uc804\uae30\ucc28\uc5d0 \uad00\uc2ec\uc774 \uc788\ub294 \uc0ac\ub78c\ub4e4\uc774\uae30 \ub54c\ubb38\uc5d0 \uc800\ud76c\uac00 \uc6d0\ud558\ub294 \ubc29\ubb38\uc790\ub97c \ubaa8\uc9d1\ud558\uae30\uc5d0 \uc801\ud569\ud558\ub2e4\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \uce74\uce74\uc624\ud1a1 \uc624\ud508\ucc44\ud305\\n\\n![no offset](./kakao1.png)\\n![no offset](./kakao2.png)\\n\\n\uce74\uce74\uc624\ud1a1 \uc624\ud508\ucc44\ud305\uc5d0\ub294 \uc218\ub9ce\uc740 \ub300\ud654\ubc29\uc774 \uc874\uc7ac\ud569\ub2c8\ub2e4.\\n\\n\ud2b9\uc815 \uc8fc\uc81c\ub85c \ub9cc\ub4e4\uc5b4\uc9c4 \ub300\ud654\ubc29\uc774 \ub300\ubd80\ubd84\uc774\uae30\uc5d0 \uc804\uae30\ucc28\ub97c \uc8fc\uc81c\ub85c \ud55c \uc624\ud508\ucc44\ud305 \ub300\ud654\ubc29\uc744 \ucc3e\ub294 \uac83\uc740 \uc804\ud600 \uc5b4\ub835\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\uc548\ud0c0\uae5d\uac8c\ub3c4 \uc77c\ubd80 \ub2e8\ud1a1\ubc29\uc5d0\uc11c \uac15\ud1f4\ub97c \ub2f9\ud588\uc9c0\ub9cc, \ucc28\uc8fc\ub4e4\uacfc \ucc44\ud305\ud558\uba74\uc11c \ud53c\ub4dc\ubc31\uc744 \ubc1b\uc544\ubcfc \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \uae30\ud0c0 \ud64d\ubcf4 \uc218\ub2e8\\n\\n\uae30\ud0c0 \ud64d\ubcf4 \uc218\ub2e8\uc740 \uc544\uc9c1 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\ub124\uc774\ubc84 \ubc34\ub4dc, \ubcf4\ubc30\ub4dc\ub9bc\uc740 \uc0ac\uc6a9\ud558\ub294 \ud06c\ub8e8\uac00 \uc5c6\uc5b4\uc11c \ud64d\ubcf4\ub97c \ud558\uae30 \uc5b4\ub824\uc6e0\uace0, \uad6c\uae00 \uc560\ub4dc\uc13c\uc2a4\uc640 \uac19\uc740 \ub3c4\uad6c\ub294 \ube44\uc6a9\uc774 \ubc1c\uc0dd\ud558\uae30\uc5d0 \uc544\uc9c1\uc740 \uc774\ub974\ub2e4\uace0 \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n# Google Analytics 4 \ud1b5\uacc4 \uc9d1\uacc4 \uacb0\uacfc\\n\\n## \ub2e8\uc21c \ubc29\ubb38\uc790\\n\\n![no offset](./ga1.png)\\n![no offset](./ga2.png)\\n![no offset](./ga3.png)\\n![no offset](./ga4.png)\\n\uc774\ucc98\ub7fc \uc678\ubd80 \uc9c0\uc5ed\uc5d0\uc11c\ub3c4 \ub9ce\uc774 \uc811\uc18d\ud574\uc8fc\uc2e0 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n![no offset](./ga5.png)\\n![no offset](./ga6.png)\\n![no offset](./ga7.png)\\n\\n\uc9d1\uacc4 \ub41c \uc790\ub8cc\ucc98\ub7fc \ubc29\ubb38\uc790\ub4e4\uc774 \ub2e8\uc21c \ubc29\ubb38\ub9cc \ud55c \uac83\uc774 \uc544\ub2c8\ub77c, \uc218 \ub9ce\uc740 \uc774\ubca4\ud2b8\ub97c \ubc1c\uc0dd\uc2dc\ud0a4\uace0 \ud3c9\uade0 \ucc38\uc5ec \uc2dc\uac04\ub3c4 \uc0c1\ub2f9 \ubd80\ubd84 \ud655\ubcf4\ud588\uc74c\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."},{"id":"36","metadata":{"permalink":"/36","source":"@site/blog/2023-09-21-marker-rendering-optimization.mdx","title":"\ub9c8\ucee4 \ub80c\ub354\ub9c1 \ucd5c\uc801\ud654","description":"1. \uac1c\uc694","date":"2023-09-21T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 21\uc77c","tags":[{"label":"react","permalink":"/tags/react"},{"label":"useSyncExternalState","permalink":"/tags/use-sync-external-state"},{"label":"googleMap","permalink":"/tags/google-map"}],"readingTime":12.04,"hasTruncateMarker":false,"authors":[{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"}],"frontMatter":{"slug":"36","title":"\ub9c8\ucee4 \ub80c\ub354\ub9c1 \ucd5c\uc801\ud654","authors":["scent"],"tags":["react","useSyncExternalState","googleMap"]},"prevItem":{"title":"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 1","permalink":"/38"},"nextItem":{"title":"Scale-out \uc2dc Scheduling \uc911\ubcf5 \uc2e4\ud589 \ub9c9\uae30","permalink":"/35"}},"content":"### 1. \uac1c\uc694\\n\\n\uae30\uc874\uc758 \uad6c\uc870\uc5d0\uc11c\ub294 \ub9c8\ucee4 \ud558\ub098\ub97c \ub80c\ub354\ub9c1\ud558\uae30 \uc704\ud574 \ub2e4\uc74c\uacfc \uac19\uc740 \uacfc\uc815\uc744 \uac70\ucce4\ub2e4.\\n\\n1. StationMarkersContainer \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \ucda9\uc804\uc18c \uc815\ubcf4 \uc694\uccad\\n2. \ucda9\uc804\uc18c \uc815\ubcf4\ub97c props\ub85c \ub118\uaca8 Marker \ucef4\ud3ec\ub10c\ud2b8 \ud638\ucd9c\\n3. \uc9c0\ub3c4\uc5d0 \ubd80\ucc29\ub420 DOM\uc694\uc18c \uc0dd\uc131\\n4. createRoot\ub97c \ud1b5\ud574 \ub9ac\uc561\ud2b8 root \uc0dd\uc131\\n5. 2\ubc88\uc5d0\uc11c \uc0dd\uc131\ud55c DOM \uc694\uc18c\ub97c \uc804\ub2ec\ud574 \uad6c\uae00 \uc9c0\ub3c4 api\uc758 Marker \uc0dd\uc131\uc790 \ud568\uc218 \ud638\ucd9c\\n6. 3\ubc88\uc5d0\uc11c \uc0dd\uc131\ud588\ub358 root\uc758 render \uba54\uc11c\ub4dc \ud638\ucd9c\\n7. \ub9c8\ucee4 \uc778\uc2a4\ud134\uc2a4 \uc804\uc5ed \uc0c1\ud0dc\uc5d0 \uc0c8\ub85c \uc0dd\uc131\ud55c \ub9c8\ucee4 \ucd94\uac00\\n\\n\uc704 \uacfc\uc815\uc744 \uac70\ucce4\uc744 \ub54c\uc758 \ub9c8\ucee4 \ub80c\ub354\ub9c1 \ubaa8\uc2b5\uc744 \ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n![before](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/28520ee3-2fa6-4110-b4e4-8a0bb706324e)\\n\\n\ub9c8\ucee4\ub4e4\uc774 \ud55c\ubc88\uc5d0 \ub80c\ub354\ub9c1 \ub418\ub294 \uac83\uc774 \uc544\ub2c8\ub77c \uc0b0\ubc1c\uc801\uc73c\ub85c \ub80c\ub354\ub9c1 \ub418\ub294 \ubaa8\uc2b5\uc744 \ud655\uc778\ud560 \uc218 \uc788\ub2e4.\\n\\n### 2. \ubb38\uc81c \uc6d0\uc778 \ubd84\uc11d\\n\\n\ub9c8\ucee4\ub97c \ub80c\ub354\ub9c1 \ud558\uae30 \uc704\ud574 \uac70\uce58\ub294 \uacfc\uc815\uc744 \ubd84\uc11d\ud574 \ubcf4\uc558\ub2e4.\\n\\n1 ~ 3 \uacfc\uc815\uc5d0\uc11c\ub294 \uc131\ub2a5\uc5d0 \ud06c\uac8c \uc601\ud5a5\uc744 \ub07c\uce60 \uc694\uc18c\uac00 \uc5c6\uc9c0\ub9cc 4\ubc88 \uacfc\uc815\uc740 \uc77c\ubc18\uc801\uc778 \ub9ac\uc561\ud2b8 \ud504\ub85c\uc81d\ud2b8\ub97c \uac1c\ubc1c\ud560 \ub54c \uacaa\ub294 \uacfc\uc815\uc774 \uc544\ub2c8\ub2e4. \ub530\ub77c\uc11c createRoot\ub97c \ud1b5\ud574 \ub9ce\uc740 \uac1c\uc218\uc758 \ub8e8\ud2b8\ub97c \uc0dd\uc131\ud588\uc744 \ub54c\uc758 \uc601\ud5a5\uc5d0 \ub300\ud574 \uc54c\uc544\ubcf4\uc558\ub2e4.\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/494a5bc5-be5d-4a58-b5b2-77ce7d3e5de7)\\n\\n\ub9ac\uc561\ud2b8 \uacf5\uc2dd \ubb38\uc11c\ub97c \ubcf4\ub2c8 \ud398\uc774\uc9c0\uc758 \uc77c\ubd80\uc5d0 \ub9ac\uc561\ud2b8\ub97c \ubfcc\ub824\uc11c \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0\uc5d0\ub294 \ub8e8\ud2b8\ub97c \ud544\uc694\ud55c \ub9cc\ud07c \uc0dd\uc131\ud574\ub3c4 \ub41c\ub2e4\ub294 \uc774\uc57c\uae30\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\uc5c8\ub2e4. \ub530\ub77c\uc11c 4\ubc88 \uacfc\uc815 \ub610\ud55c \ubb38\uc81c\uc758 \uc6d0\uc778\uc774\ub77c\uace0 \ubcfc \uc218 \uc5c6\uc5c8\ub2e4.\\n\\n5\ubc88 \uacfc\uc815\uc740 \uad6c\uae00 \uc9c0\ub3c4\uc5d0 \ub9c8\ucee4\ub97c \ud2b9\uc815 \uc704\ub3c4 \uacbd\ub3c4\uc5d0 \uc704\uce58\uc2dc\ud0a4\uae30 \uc704\ud574\uc11c \uc5b4\uca54 \uc218 \uc5c6\uc774 \uac70\uccd0\uc57c \ud558\ub294 \uacfc\uc815\uc774\ubbc0\ub85c \uc774 \uacfc\uc815\uc740 \ubb38\uc81c\uac00 \uc788\ub354\ub77c\ub3c4 \uac1c\uc120\uc774 \ubd88\uac00\ub2a5\ud574 \uc77c\ub2e8 \uace0\ub824\ud558\uc9c0 \uc54a\uc558\ub2e4.\\n\\n6\ubc88 \uacfc\uc815\uc740 4\ubc88 \uacfc\uc815\uc5d0\uc11c \uc0dd\uc131\ud588\ub358 \ub9ac\uc561\ud2b8 \ub8e8\ud2b8\uc758 render \uba54\uc11c\ub4dc\ub97c \ud638\ucd9c\ud574 \uc2e4\uc81c\ub85c \ud654\uba74\uc5d0 \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\ub97c \uadf8\ub9ac\ub3c4\ub85d \ud558\ub294 \uacfc\uc815\uc774\ub2e4. \uc774 \uacfc\uc815 \ub610\ud55c \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\ub97c \ud654\uba74\uc5d0 \ub80c\ub354\ub9c1\ud558\uae30 \uc704\ud574\uc120 \uc5b4\uca54 \uc218 \uc5c6\uc774 \uac70\uccd0\uc57c \ud558\ub294 \uacfc\uc815\uc774\ubbc0\ub85c \uace0\ub824\ud558\uc9c0 \uc54a\uc558\ub2e4.\\n\\n> \ud558\uc9c0\ub9cc 6\ubc88 \uacfc\uc815\uc5d0\uc11c \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc9c1\uc811 \uadf8\ub9ac\ub294 \uac83\uc774 \uc544\ub2c8\ub77c \uad6c\uae00 \uc9c0\ub3c4 api\uc758 \uae30\ubcf8 \ub9c8\ucee4\ub97c \uc0ac\uc6a9\ud558\uba74 \uc131\ub2a5\uc744 \ud5a5\uc0c1\uc2dc\ud0ac \uc218 \uc788\uc9c0 \uc54a\ub0d0\uace0 \ubc18\ubb38\ud560 \uc218\ub3c4 \uc788\uc744 \uac83\uc774\ub2e4. \uc774\uc804\uc5d0\ub294 \uc774\ub7ec\ud55c \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud574 \ub9c8\ucee4\ub97c \ub80c\ub354\ub9c1 \ud588\uc5c8\ub2e4. \uc6b0\ub9ac\uc758 \uc11c\ube44\uc2a4\ub294 \ud604\uc7ac \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ucda9\uc804\uc18c \uac1c\uc218\ub97c \ub9c8\ucee4\ub97c \ud1b5\ud574\uc11c\ub3c4 \uc804\ub2ec\ud558\uae30 \ub54c\ubb38\uc5d0 \uc774\ub97c \uace0\ub824\ud574 \uae30\ubcf8 \ub9c8\ucee4\ub97c \uc0ac\uc6a9\ud560 \ub54c \ub2e4\uc74c\uc758 \ub450 \uac00\uc9c0 \ubb38\uc81c\uac00 \uc0dd\uae34\ub2e4.\\n>\\n> 1. \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ucda9\uc804\uc18c \uac1c\uc218\ub97c \uae30\ubcf8 \ub9c8\ucee4\uc5d0 \ub80c\ub354\ub9c1 \ud560 \ub54c \uc131\ub2a5\uc774 \ub9e4\uc6b0 \uc88b\uc9c0 \uc54a\ub2e4.\\n> 2. \ub9c8\ucee4\uc758 \ub514\uc790\uc778\uc744 \ubc14\uafb8\uace0\uc790 \ud560 \ub54c \ubcc0\uacbd\uc5d0 \ub300\uc751\ud558\uae30 \uc5b4\ub835\ub2e4.\\n>\\n> \ub530\ub77c\uc11c \ub9c8\ucee4\ub294 \ub9ac\uc561\ud2b8 \ub8e8\ud2b8\uc758 render \uba54\uc11c\ub4dc\ub97c \ud638\ucd9c\ud574 \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\ub97c \ub80c\ub354\ub9c1\ud558\ub294 \uac83\uc73c\ub85c \uacb0\uc815\ud588\ub2e4.\\n\\n\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ub0a8\uc740 7\ubc88 \uacfc\uc815\uc5d0\uc11c\ub294 useSyncExternalState \ud6c5\uc744 \uc0ac\uc6a9\ud574 \uc804\uc5ed\uc801\uc73c\ub85c \uad00\ub9ac\ud558\uace0 \uc788\ub358 \uc0c1\ud0dc\uc5d0 \uc218\uc815\uc744 \uac00\ud558\ub294 \uc5f0\uc0b0\uc744 \uc218\ud589\ud55c\ub2e4. \uc774 \uacfc\uc815\uc740 \uc774\uc804\uc5d0\ub3c4 \uc131\ub2a5 \uc800\ud558\ub97c \uc720\ubc1c\ud560 \uac83\uc73c\ub85c \uc608\uc0c1\ub418\ub358 \ubd80\ubd84\uc774\uc5c8\ub2e4. (\ud558\ub2e8 \ub9c1\ud06c \ucc38\uace0)\\n\\n[useSyncExternalStore \ud6c5\uc744 \ud1b5\ud574 \uad6c\ub3c5\ud55c state\uac00 \ud55c\ubc88\uc5d0 \uc5c5\ub370\uc774\ud2b8 \ub418\ub294 \uc774\uc720](https://www.notion.so/useSyncExternalStore-state-67e686eead8b4750b3015a1f75ea3e76?pvs=21)\\n\\n\uc694\uccad\uc758 \uacb0\uacfc\ub85c \ubc1b\uc544\uc628 \ub9c8\ucee4 \uc815\ubcf4\uc758 \uac1c\uc218\uac00 100\uac1c\ub77c\uace0 \uac00\uc815\ud574\ubcf4\uc790. \uc6b0\ub9ac\ub294 \uc774\uc81c \ub9c8\ucee4\ub97c \ub80c\ub354\ub9c1 \ud560 \uac83\uc774\ub2e4. \uccab \ubc88\uc9f8 \ub9c8\ucee4\uc758 \ub80c\ub354\ub9c1\uc744 \uc704\ud574 1\ubc88 ~ 6\ubc88\uc758 \uacfc\uc815\uc744 \uac70\uce5c \ud6c4 7\ubc88 \uacfc\uc815\uc744 \uc218\ud589\ud55c\ub2e4. \uadf8\ub7ec\uba74 \ub9ac\uc561\ud2b8 \uc785\uc7a5\uc5d0\uc11c\ub294 \ub9ac\uc561\ud2b8 \ub8e8\ud2b8\uc758 render \uba54\uc11c\ub4dc \ud638\ucd9c\uc5d0 \ub300\ud55c \ub3d9\uc791\uc744 \uc218\ud589\ud574\uc57c \ud558\uace0, \uc0c8\ub85c\uc6b4 \ub9c8\ucee4 \uc778\uc2a4\ud134\uc2a4\uc5d0 \ub300\ud55c \uc804\uc5ed \uc0c1\ud0dc\ub97c \ubcc0\uacbd\uc2dc\ud0a4\ub294 \ub3d9\uc791\uc744 \uc218\ud589\ud574\uc57c \ud55c\ub2e4. \ub9ac\uc561\ud2b8\uac00 \uc774 \uacfc\uc815\uc744 100\ubc88 \ubc18\ubcf5\ud558\uace0 \ub098\uba74 \uc6b0\ub9ac\ub294 \ube44\ub85c\uc18c \ubaa8\ub4e0 \ub9c8\ucee4\uac00 \ud654\uba74\uc5d0 \ub80c\ub354\ub9c1 \ub41c \ubaa8\uc2b5\uc744 \ubcfc \uc218 \uc788\uc744 \uac83\uc774\ub2e4.\\n\\n\ub098\ub294 \uc774 \ubd80\ubd84\uc5d0\uc11c \uc131\ub2a5 \uc800\ud558\uc758 \uc694\uc18c\uac00 \uc788\ub2e4\uace0 \uc0dd\uac01\ud588\ub2e4. \ub9ac\uc561\ud2b8\uc5d0\uc11c\uc758 \uc0c1\ud0dc \ubcc0\ud654\ub294 \uace7 \ub9ac\uc561\ud2b8 \ub0b4\ubd80\uc758 \ub80c\ub354\ub9c1\uc744 \uc704\ud55c \ub85c\uc9c1\uc774 \uc218\ud589\ub418\uac8c \ud568\uc744 \uc758\ubbf8\ud558\uace0, \uc774 \uacfc\uc815\uc744 \uac1c\uc120 \uc774\uc804\uc5d0\ub294 \ub9c8\ucee4\uc758 \uac1c\uc218\ub9cc\ud07c \ubc18\ubcf5\ud558\uace0 \uc788\uc5c8\ub358 \uac83\uc774\ub2e4. \uc5ec\uae30\uae4c\uc9c0 \uc0dd\uac01\ud574\ubcf4\ub2c8 \uc804\uc5ed \uc0c1\ud0dc \ubcc0\ud654\uc5d0 \ub300\ud574 \ub9ac\uc561\ud2b8\uac00 \ub80c\ub354\ub9c1\uc744 \uc704\ud55c \uc5f0\uc0b0\uc744 \uc9c4\ud589\ud560 \ub3d9\uc548\uc5d0\ub294 \ub9c8\ucee4\uc758 \ub80c\ub354\ub9c1(render \uba54\uc11c\ub4dc \ud638\ucd9c)\uc774 \uba48\ucd94\ub294 \uac83\uc774 \uc544\ub2d0\uae4c \ud558\ub294 \uc0dd\uac01\uc774 \ub4e4\uc5c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \ud06c\ub86c \uac1c\ubc1c\uc790 \ub3c4\uad6c\uc758 \ud37c\ud3ec\uba3c\uc2a4 \ud0ed\uc744 \ub4e4\uc5b4\uac00 \ubcf4\ub2c8 \uc0b0\ubc1c\uc801\uc73c\ub85c \ubc1c\uc0dd\ud558\ub358 \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc758 \ubb38\uc81c \uc6d0\uc778\uc774 \uc9d0\uc791\ud588\ub358 \uadf8 \uc6d0\uc778\uc784\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc5c8\ub2e4.\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/20926d19-79a5-4d49-b733-de1c2b87059c)\\n\\n\ud504\ub808\uc784 \uc774\ubbf8\uc9c0 \ud558\ub2e8\uc744 \ubcf4\uba74 \uc0b0\ubc1c\uc801\uc778 \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc774 \uc218\ud589\ub420 \ub54c\ub9c8\ub2e4 \uc218\ubc18\ub418\ub294 \uc5b4\ub5a4 \ud568\uc218 \ud638\ucd9c\uc774 \uc788\uc74c\uc744 \ud655\uc778\ud560 \uc218 \uc788\ub2e4.\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/20b8f1e4-eceb-4e18-82f0-8ef6cc5ee8a1)\\n\\n\uc774 \ubd80\ubd84\uc774 \ubb38\uc81c\uc758 \ud568\uc218 \ud638\ucd9c \ubd80\ubd84\uc774\ub2e4. \uc790\uc138\ud788 \uc0b4\ud3b4\ubcf4\uba74 \uc0c1\ub2e8\uc5d0 `performWorkUntilDeadline`\uc774\ub780 \ud568\uc218\uac00 \ud638\ucd9c\ub428\uc744 \ubcfc \uc218 \uc788\ub2e4.\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/d7a91ce6-4907-4c79-948b-d80a205a0697)\\n\\n\uc774 `performWorkUntilDeadline` \ub77c\ub294 \ud568\uc218\ub97c \uc870\uae08 \uc54c\uc544\ubcf4\ub2c8 \ud574\ub2f9 \ud568\uc218\ub294 \uac04\ub2e8\ud788 \ub9d0\ud574 \ub9ac\uc561\ud2b8\uc5d0\uc11c state\uc758 \ubcc0\uacbd\uc774 \ud55c\ubc88\uc5d0 \ub9ce\uc774 \ubc1c\uc0dd\ud560 \ub54c 5ms\uc758 \ub370\ub4dc\ub77c\uc778 \uc2dc\uac04\uc744 \uc904 \ub54c \uc0ac\uc6a9\ud558\ub294 \ud568\uc218\ub77c\ub294 \uac83\uc744 \uc54c\uac8c \ub418\uc5c8\ub2e4. \ubb38\uc81c\uc758 \uc6d0\uc778\uc774\ub77c\uace0 \uc0dd\uac01\ud588\ub358 \ub9c8\ucee4 \uac1c\uc218 \ub9cc\ud07c\uc758 \uc804\uc5ed \uc0c1\ud0dc \ubcc0\ud654\uac00 \uc2e4\uc81c\ub85c \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc744 \uc7a0\uc2dc \uc911\ub2e8\ud558\uac8c \ub9cc\ub4e4\uace0 \uc788\uc74c\uc744 \uc54c\uac8c \ub418\uc5c8\ub2e4.\\n\\n### 3. \ubb38\uc81c \ud574\uacb0\\n\\n\uc55e\uc11c \ubd84\uc11d\ud55c \ubb38\uc81c\ub97c \uac1c\uc120\ud574\ubcf4\uace0\uc790 \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc5d0 \ud544\uc694\ud55c \ucda9\uc804\uc18c \uc815\ubcf4 \ubc30\uc5f4\uc744 \ubd80\ubaa8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \ubc1b\uc544\uc640 \uac01 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uc790\uc2dd \ucef4\ud3ec\ub10c\ud2b8\uc5d0 \ub118\uaca8\uc8fc\uace0, \uc790\uc2dd \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \ub9c8\ucee4 \uc0dd\uc131\uacfc \ub80c\ub354\ub9c1 \ub85c\uc9c1\uc744 \uc218\ud589\ud558\ub358 \uae30\uc874\uc758 \ubc29\uc2dd\uc744 \ubd80\uc218\uace0 \ubd80\ubaa8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \ubaa8\ub4e0 \uac83\uc744 \uc77c\uad04 \ucc98\ub9ac\ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \uace0\uccd0\ubcf4\uc558\ub2e4.\\n\\n\uace0\uce58\ub294 \uacfc\uc815\uc5d0\uc11c \uae30\uc874 \ubc29\uc2dd\uc5d0\uc11c\ub294 \ub9ac\uc561\ud2b8 \uc0dd\uba85 \uc8fc\uae30\uc5d0 \uc758\uc874\ud558\uc5ec \ud654\uba74\uc5d0 \ubcf4\uc5ec\uc9c0\uc9c0 \uc54a\ub294 \ub9c8\ucee4\ub97c \uc9c0\uc6cc\uc8fc\ub358 \ub85c\uc9c1\uc744 \uc774\uc81c\ub294 \ubaa8\ub450 \uc9c1\uc811 \uad6c\ud604\ud574\uc57c \ud588\ub2e4.\\n\\n\uc774\uc804\uc758 \uc601\uc5ed\uacfc \uacb9\uce58\ub294 \ubd80\ubd84\uc5d0 \uc788\ub294 \ucda9\uc804\uc18c\ub294 \ub2e4\uc2dc \uadf8\ub9ac\uc9c0 \uc54a\uace0, \uc601\uc5ed \ubc16\uc758 \ucda9\uc804\uc18c\ub97c \ub098\ud0c0\ub0b4\ub294 \ub9c8\ucee4\ub294 \uc9c0\uc6cc\uc8fc\uace0, \uc774\uc804\uc758 \uc601\uc5ed\uacfc \uacb9\uce58\uc9c0 \uc54a\ub294 \uc0c8\ub85c \ubc1b\uc544\uc628 \ucda9\uc804\uc18c\ub294 \uadf8\ub9ac\ub3c4\ub85d \ub2e4\uc74c\uacfc \uac19\uc774 \uba54\uc11c\ub4dc\ub97c \ubd84\ub9ac\ud574\ubcf4\uc558\ub2e4.\\n\\n- \uae30\uc874\uacfc \uacb9\uce58\uc9c0 \uc54a\ub294 \uc0c8\ub85c\uc6b4 \uc601\uc5ed\uc5d0 \ub300\ud55c \ub9c8\ucee4\ub97c \uc0dd\uc131\ud558\ub294 \uba54\uc11c\ub4dc\\n- \uae30\uc874\uacfc \uacb9\uccd0\uc9c0\ub294 \uc601\uc5ed\uc5d0 \ub300\ud55c \ub9c8\ucee4\ub4e4\uc744 \ubc18\ud658\ud558\ub294 \uba54\uc11c\ub4dc\\n- \uc0c8\ub85c\uc6b4 \uc601\uc5ed \ubc16\uc5d0 \uc788\ub294 \ub9c8\ucee4\ub4e4\uc744 \uc9c0\uc6cc\uc8fc\ub294 \uba54\uc11c\ub4dc\\n- \uc0c8\ub86d\uac8c \uc0dd\uc131\ub41c \ub9c8\ucee4\ub97c \ud654\uba74\uc5d0 \ub80c\ub354\ub9c1\ud558\ub294 \uba54\uc11c\ub4dc\\n\\n\uc774 \uba54\uc11c\ub4dc\ub4e4\uc744 \ucee4\uc2a4\ud140 \ud6c5\uc73c\ub85c \ubd84\ub9ac\ud574 \ubd80\ubaa8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \uc774\ub97c \ud65c\uc6a9\ud558\ub3c4\ub85d \ud558\uc5ec \ub2e4\uc18c \ubcf5\uc7a1\ud560 \uc218 \uc788\ub294 \ub9c8\ucee4 \ub80c\ub354\ub9c1 \ub85c\uc9c1\uc744 \uc120\uc5b8\uc801\uc73c\ub85c \uad6c\ud604\ud560 \uc218 \uc788\ub3c4\ub85d \ud588\ub2e4.\\n\\n\uacb0\uacfc\uc801\uc73c\ub85c \uae30\uc874\uc5d0 \uc0ac\uc6a9\ub418\ub358 \uae30\ub2a5\ub4e4\uc744 \uadf8\ub300\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc73c\uba74\uc11c \ud654\uba74\uc5d0 \ub9c8\ucee4\uac00 \uc0b0\ubc1c\uc801\uc73c\ub85c \ub80c\ub354\ub9c1 \ub418\ub358 \ubb38\uc81c\uac00 \ud574\uacb0 \ub418\uc5c8\uace0, \ubd80\uac00\uc801\uc778 \ud6a8\uacfc\ub85c \uc804\uccb4 \ub9c8\ucee4\uc758 \ub80c\ub354\ub9c1 \uc2dc\uc810\ub3c4 \uc55e\ub2f9\uae38 \uc218 \uc788\uac8c \ub418\uc5c8\ub2e4. + \uae30\uc874\uc5d0\ub294 \uad6c\uc870\uc801\uc778 \ubb38\uc81c\ub85c \uc5f0\uc0b0\ub7c9\uc774 \ub108\ubb34 \ub9ce\uc544 \ud074\ub7ec\uc2a4\ud130\ub9c1\uc774 \ub2a6\uc5b4\uc838 \uc774\ub97c \ub3c4\uc785\ud560 \uc218 \uc5c6\uc5c8\ub358 \ubb38\uc81c\ub97c \uad6c\uc870 \uc218\uc815\uc73c\ub85c \uc778\ud574 \uc801\uc6a9\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\ub2e4.\\n\\n### \uc791\uc5c5\ud55c PR\\n\\nhttps://github.com/woowacourse-teams/2023-car-ffeine/pull/737\\n\\n## \uacb0\uacfc \ubd84\uc11d (performance \ud0ed \ud65c\uc6a9)\\n\\n### before\\n\\n\ub9c8\ucee4 \uc870\ud68c \uc694\uccad\uc774 \uc885\ub8cc\ub41c \uc2dc\uc810: \uc57d `2499ms`\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/033e8519-a1aa-43a4-959d-afeba93c1917)\\n\\n\uccab \ub9c8\ucee4 \ub80c\ub354\ub9c1 \uc2dc\uc810: `3093ms`\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/b4fc47ca-4ef3-43f4-a9a5-7117edabc225)\\n\\n\ubaa8\ub4e0 \ub9c8\ucee4 \ub80c\ub354\ub9c1 \uc885\ub8cc \uc2dc\uc810: \uc57d `3611ms`\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/2b8a4c4c-218b-419a-8a47-e3b768d35bc2)\\n\\n\ucc98\uc74c\uc73c\ub85c \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub420 \ub54c\uae4c\uc9c0 \uc18c\uc694\ub41c \uc2dc\uac04: `594ms`\\n\\n\ubaa8\ub4e0 \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc5d0 \uc18c\uc694\ub41c \uc2dc\uac04: `1112ms`\\n\\n### after\\n\\n\ub9c8\ucee4 \uc870\ud68c \uc694\uccad\uc758 \uc2dc\uc791\uc810: \uc57d `1875ms`\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/b7b8ff0c-2314-4e3f-a9f4-72c445636283)\\n\\n\ubaa8\ub4e0 \ub9c8\ucee4 \ub80c\ub354\ub9c1 \uc885\ub8cc \uc2dc\uc810: `2395ms`\\n\\n![image](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/d75c323e-5c04-42a2-ad3e-1d13ea52216e)\\n\\n\ucc98\uc74c\uc73c\ub85c \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub420 \ub54c\uae4c\uc9c0 \uc18c\uc694\ub41c \uc2dc\uac04: `519ms`\\n\\n\ubaa8\ub4e0 \ub9c8\ucee4 \ub80c\ub354\ub9c1\uc5d0 \uc18c\uc694\ub41c \uc2dc\uac04: `519ms`\\n\\n### \uac1c\uc120 \uacb0\uacfc\\n\\n\ucc98\uc74c\uc73c\ub85c \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub418\ub294 \uc2dc\uc810\uc740 \ub450 \ubc29\uc2dd \ubaa8\ub450 \ube44\uc2b7\ud55c \uacb0\uacfc\ub97c \ubcf4\uc778\ub2e4. \ud558\uc9c0\ub9cc \uac1c\uc120 \ud6c4 \ubc29\uc2dd\uc740 \ud55c\ubc88\uc5d0 \ubaa8\ub4e0 \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub418\ub294 \ubc29\uc2dd\uc774\uace0, \uac1c\uc120 \uc774\uc804\uc758 \ubc29\uc2dd\uc740 \uc0b0\ubc1c\uc801\uc73c\ub85c \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub418\ub294 \ubc29\uc2dd\uc774\ubbc0\ub85c \uac1c\uc120 \ud6c4\uc758 \ubc29\uc2dd\uc5d0\uc11c \uc804\uccb4 \ub9c8\ucee4\ub97c \ub80c\ub354\ub9c1 \ud558\ub294 \uc2dc\uc810\uc774 \ud6e8\uc52c \ube68\ub77c\uc9c0\uac8c \ub418\uc5c8\ub2e4.\\n\\n\uacb0\uacfc\uc801\uc73c\ub85c \uc804\uccb4 \ub9c8\ucee4\uac00 \ub80c\ub354\ub9c1 \ub418\ub294 \uc18d\ub3c4 \uc57d `55.6%` \ub2e8\ucd95\ud558\uac8c \ub418\uc5c8\ub2e4. \uc774 \uacb0\uacfc\ub294 \ub9c8\ucee4\uac00 \ub298\uc5b4\ub0a0 \uc218\ub85d \ub354\uc6b1 \ucc28\uc774\uac00 \uadf9\uc801\uc73c\ub85c \ubc8c\uc5b4\uc9c8 \uac83\uc73c\ub85c \uc608\uc0c1\ub41c\ub2e4.\\n\\nbefore\\n\\n![before](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/28520ee3-2fa6-4110-b4e4-8a0bb706324e)\\n\\nafter\\n\\n![after](https://github.com/woowacourse-teams/2023-car-ffeine/assets/77326660/1b1521c6-d220-4140-bbe9-fff40051c6a2)"},{"id":"35","metadata":{"permalink":"/35","source":"@site/blog/2023-09-18-scheduling.mdx","title":"Scale-out \uc2dc Scheduling \uc911\ubcf5 \uc2e4\ud589 \ub9c9\uae30","description":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4","date":"2023-09-18T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 18\uc77c","tags":[{"label":"java","permalink":"/tags/java"}],"readingTime":8.89,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"35","title":"Scale-out \uc2dc Scheduling \uc911\ubcf5 \uc2e4\ud589 \ub9c9\uae30","authors":["boxster"],"tags":["java"]},"prevItem":{"title":"\ub9c8\ucee4 \ub80c\ub354\ub9c1 \ucd5c\uc801\ud654","permalink":"/36"},"nextItem":{"title":"\uce90\uc2dc\uc640 \uc774\ubd84 \ud0d0\uc0c9\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/34"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4\\n\\n## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\\n\uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \uc8fc\uae30\uc801\uc73c\ub85c \ucda9\uc804\uae30\uc758 \uc0c1\ud0dc\uc640 \uc815\ubcf4\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\uac70\ub098, \ud1b5\uacc4\ub97c \uc800\uc7a5\ud558\ub294 \uc2a4\ucf00\uc904\ub9c1 \uc791\uc5c5\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\uc9c0\uae08\uc758 \uc800\ud76c \uc11c\ubc84\ub294 \ub2e8\uc77c \uc11c\ubc84\ub85c \uad6c\uc131\ub418\uc5b4\uc788\uc5b4 \ubb38\uc81c\uac00 \uc5c6\uc9c0\ub9cc, \ub9cc\uc57d **\uc11c\ubc84\ub97c scale-out** \ud558\uac8c \ub41c\ub2e4\uba74 \uc5b4\ub5bb\uac8c \ub420\uae4c\uc694?\\n\\n**\ub611\uac19\uc740 schedule\uc774 \uc911\ubcf5**\ub418\uc5b4 \uc2e4\ud589\ub420 \uac83\uc785\ub2c8\ub2e4. \uadf8\ub807\ub2e4\uace0 \uc5b4\ub5a4 \uc11c\ubc84\ub294 schedule\uc744 \ub3d9\uc791\ud558\uc9c0 \uc54a\ub3c4\ub85d \ud558\uace0, \uc5b4\ub5a4 \uc11c\ubc84\ub294 schedule\uc744 \ub3d9\uc791\ud558\ub3c4\ub85d \ud55c\ub2e4\uba74 \uc2a4\ucf00\uc904\uc774 \ub3d9\uc791\ud558\ub294 \uc11c\ubc84\uac00 \ub2e4\uc6b4\ub41c\ub2e4\uba74 \ub3d9\uc791\ud558\ub294\\n\uc11c\ubc84\uc758 \ub2e4\uc6b4\ud0c0\uc784\ub9cc\ud07c \uc800\ud76c \uc11c\ubc84\uc758 \ub370\uc774\ud130\ub97c \ucd5c\uc2e0\ud654\ud560 \uc218 \uc5c6\uace0, \ucd5c\uc2e0\ud654\uac00 \uc911\uc694\ud55c \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \uc0ac\uc6a9\uc790\uc758 \ubd88\ub9cc\uc744 \ucd08\ub798\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uad6c\ud604\ud574\ubcf4\uae30\\n\\nSchedule \uc815\ubcf4\ub97c \uc5b4\ub5bb\uac8c \ub2e4\ub978 \ud658\uacbd\uc5d0\uc11c \uac19\uc774 \uacf5\uc720\ud558\uc5ec \uad00\ub9ac\ud560 \uc218 \uc788\uc744\uae4c\uc694?\\n\uac04\ub2e8\ud788 \uc0dd\uac01\ud558\uba74 Local \ud658\uacbd\uc774 \uc544\ub2cc, Global \ud658\uacbd\uc5d0\uc11c \uc815\ubcf4\ub97c \uad00\ub9ac\ud558\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c Schedule\uc758 \uc815\ubcf4\ub97c \uc800\uc7a5\ud560 \uc218 \uc788\ub294 \ud14c\uc774\ube14\uc744 \uc544\ub798\uc758 Entity \uc758 \ud544\ub4dc\uc640 \uac19\uc774 \uc0dd\uc131\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Entity\\npublic class ScheduleTask extends BaseEntity {\\n\\n @Id\\n private String id;\\n\\n private String jobName;\\n\\n @Enumerated(EnumType.STRING)\\n private JobStatus status;\\n}\\n```\\n\\n\uba3c\uc800 id\ub294 \ud574\ub2f9 \uc2a4\ucf00\uc904\uc744 \uad6c\ubd84\ud560 \uc218 \uc788\ub294 id\uc5ec\uc57c \ud560 \uac83\uc785\ub2c8\ub2e4. \uac00\uc7a5 \uc27d\uac8c \uc815\ud560 \uc218 \uc788\ub294 id\ub294 \uc2a4\ucf00\uc904\uc758 **job \uc774\ub984**\uacfc,\\nSchedule\uc73c\ub85c \ub4f1\ub85d\ud55c **\uc2dc\uac04**\uc744 \uc870\ud569\ud558\uc5ec \uc0dd\uc131\ud55c\ub2e4\uba74 unique\ud558\uace0 \ubd84\uc0b0 \ud658\uacbd\uc5d0\uc11c\ub3c4 \uc27d\uac8c \uad6c\ubd84\ud560 \uc218 \uc788\ub294 id\uac00 \ub420 \uac83 \uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc544\ub798\uc640 \uac19\uc740 Business Logic \uc788\ub2e4\uace0 \uac00\uc815\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n```java\\n@Service\\npublic class BusinessLogic {\\n\\n private final ApplicationEventPublisher applicationEventPublisher;\\n\\n @Scheduled(cron = \\"0/2 * * * * *\\")\\n public void complexJob() {\\n log.info(\\"\ubcf5\uc7a1\ud55c Job \uc2dc\uc791\\");\\n }\\n\\n @Scheduled(cron = \\"0/4 * * * * *\\")\\n public void moreComplexJob() {\\n log.info(\\"\uc880 \ub354 \ubcf5\uc7a1\ud55c Job \uc2dc\uc791\\");\\n try {\\n Thread.sleep(3000);\\n } catch (InterruptedException e) {\\n throw new RuntimeException(e);\\n }\\n }\\n}\\n```\\n\ud558\ub098\ub294 \ub9e4 2\ucd08\ub9c8\ub2e4 \uc2e4\ud589 \ud6c4 \ubc14\ub85c \uc885\ub8cc\ub418\uace0, \ud558\ub098\ub294 \ub9e4 4\ucd08\ub9c8\ub2e4 \uc2e4\ud589 \ud6c4 3\ucd08\uc758 \ub300\uae30\uc640 \uc885\ub8cc\ub418\ub294 \uba54\uc11c\ub4dc\uc785\ub2c8\ub2e4.\\n\uc774\ub7f0 \uc2a4\ucf00\uc904\uc740 \uc5b4\ub5bb\uac8c \ub3d9\uc791\ud560\uae4c\uc694? \uc800\ub294 \ub2f9\uc5f0\ud788 2\ucd08\uc640 4\ucd08\ub9c8\ub2e4 \ud574\ub2f9 \uba54\uc11c\ub4dc\uac00 \uc2e4\ud589\ub420 \uc904 \uc54c\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\ub85c\uadf8\ub97c \uc0b4\ud3b4\ubcf4\uba74 \uc544\ub798\uc640 \uac19\uc740 \uacb0\uacfc\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\\n![log](https://github.com/drunkenhw/comments/assets/106640954/5e275085-fce6-43ae-88ca-d3f9c484b6f3)\\n\ubcf5\uc7a1\ud55c job\uc774 2\ubc88 \uc2e4\ud589\ub420 \ub54c, \uc880 \ub354 \ubcf5\uc7a1\ud55c job\uc774 1\ubc88 \uc2e4\ud589\ub418\ub294 \uac78 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc608\uc0c1\ud588\ub358 \uacb0\uacfc\uc785\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc2e4\ud589\ub41c \uc2dc\uac04\uc744 \uc0b4\ud3b4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n![log-with-time](https://github.com/drunkenhw/comments/assets/106640954/abbe2c65-c26b-46ba-a4e3-fc0f4e5a6612)\\n\\n\ubd84\uba85 \ub9e4 2\ucd08\uc640 4\ucd08\ub9c8\ub2e4 \uc2e4\ud589\ud558\uae30 \ub54c\ubb38\uc5d0 \uc791\uc5c5 \uc2dc\uac04\uc774 2\uc758 \ubc30\uc218\uac00 \ub418\uc5b4\uc57c\ud560\ud150\ub370\\n\\n34, 36, 36, **39**, 40, 40, **43**, 44, 44, **47**\ucd08 \ub85c \uc810\uc810 \uc791\uc5c5\uc774 \ubc00\ub9ac\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc65c \uadf8\ub7f4\uae4c\uc694? \uc2a4\ud504\ub9c1 \uacf5\uc2dd \ubb38\uc11c\uc5d0\uc11c\ub294 \uc544\ub798\uc640 \uac19\uc774 \uc124\uba85\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n> A ThreadPoolTaskScheduler can also be auto-configured if need to be associated to scheduled task execution (using @EnableScheduling for instance). The thread pool uses one thread by default and its settings can be fine-tuned using the spring.task.scheduling namespace, as shown in the following example:\\n\\n\\n[\ucc38\uace0 - \uc2a4\ud504\ub9c1 \uacf5\uc2dd \ubb38\uc11c](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.task-execution-and-scheduling)\\n\\n\uc2a4\ud504\ub9c1\uc758 Schedule\uc740 Default\ub85c \ud558\ub098\uc758 \uc2f1\uae00 \uc2a4\ub808\ub4dc\uc5d0\uc11c \ub3d9\uc791\ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\uadf8\ub807\uae30 \ub54c\ubb38\uc5d0 \ub9e4\ubc88 \uc791\uc5c5\uc774 \ubc00\ub824 \uc6d0\ud558\ub294 \uc2dc\uac04\uc5d0 \ub3d9\uc791\ud558\uc9c0 \uc54a\ub294 \ud604\uc0c1\uc774 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc Schedule\uc744 \ubd84\uc0b0 \ud658\uacbd\uc5d0\uc11c \uad6c\ubd84\ud558\uae30 \uc704\ud574\uc11c\ub294 job\uc774 \uc2e4\ud589\ub41c \uc2dc\uac04\uc774 \uc911\uc694\ud558\uae30 \ub54c\ubb38\uc5d0 \uc774\ub807\uac8c \uc791\uc5c5\uc774 \ubc00\ub824\ubc84\ub9b0\ub2e4\uba74 \uad6c\ubd84\uc744 \ud560 \uc218 \uc5c6\uac8c \ub429\ub2c8\ub2e4.\\n\ub530\ub77c\uc11c Schedule Thread Pool Size\ub97c \ub298\ub9ac\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Configuration\\npublic class ScheduleConfig implements SchedulingConfigurer {\\n @Override\\n public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {\\n ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();\\n taskScheduler.setPoolSize(10);\\n taskScheduler.setThreadNamePrefix(\\"schedule-task-\\");\\n taskScheduler.initialize();\\n taskRegistrar.setTaskScheduler(taskScheduler);\\n }\\n}\\n```\\nSchedulingConfigurer \ub97c \uad6c\ud604\ud558\uc5ec Thread Pool size\ub97c \uc77c\ub2e8 10\uac1c\ub85c \uc815\uc758\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![success](https://github.com/drunkenhw/comments/assets/106640954/14b225bc-297e-4e7d-b196-23d779f635c0)\\n\uc2a4\ub808\ub4dc \ud480\uc744 \ub298\ub838\ub354\ub2c8 \uc704\uc640 \uac19\uc774 2\uc758 \ubc30\uc218\uc758 \uc2dc\uac04\uc5d0 \uc815\ud655\ud788 \uc791\ub3d9\uc774 \ub418\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774\ub807\uac8c \uc5ec\ub7ec \uc791\uc5c5\uc744 \ub3d9\uc2dc\uc5d0 \uc2e4\ud589\ub41c\ub2e4\uba74 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \ubcd1\ubaa9\ud604\uc0c1\uc774 \ubc1c\uc0dd\ub418\uc5b4 \uc624\ud788\ub824 \uc791\uc5c5\uc774 \ub354 \ub290\ub9ac\uac8c \ub05d\ub0a0 \uc218\ub3c4 \uc788\ub2e4\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \ud574\ub2f9 \ubd80\ubd84\uc758 \uc2e4\ud589\uc744 \uad00\ub9ac\ud558\ub294 \ud074\ub798\uc2a4\ub97c \uc0dd\uc131\ud558\uc5ec \ud574\ub2f9 \ud074\ub798\uc2a4\uc5d0\uc11c Schedule\uc758 \uc791\uc5c5\uc744 \uad00\ub9ac\ud558\ub3c4\ub85d \uad6c\ud604\ud588\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Service\\npublic class BusinessLogic {\\n\\n private final ApplicationEventPublisher applicationEventPublisher;\\n\\n @Scheduled(cron = \\"0/2 * * * * *\\")\\n public void complexJobSchedule() {\\n applicationEventPublisher.publishEvent(new SchedulingEvent(this::complexJob, \\"complexJob\\", LocalDateTime.now()));\\n }\\n\\n @Scheduled(cron = \\"0/4 * * * * *\\")\\n public void moreComplexJobSchedule() {\\n applicationEventPublisher.publishEvent(new SchedulingEvent(this::moreComplexJob, \\"moreComplexJob\\", LocalDateTime.now()));\\n }\\n}\\n```\\n\ub85c\uc9c1\uc774 \uc788\ub294 BusinessLogic \uc11c\ube44\uc2a4\uc5d0\uc11c \uc2a4\ucf00\uc904\uc758 \uc2dc\uac04\ub9c8\ub2e4 \uc2e4\ud589\ud574\uc57c\ud560 \uba54\uc11c\ub4dc\ub97c Event\ub85c \ubc1c\ud589\ud569\ub2c8\ub2e4.\\n\\n```java\\n@Component\\npublic class ScheduleService {\\n\\n private final ExecutorService executorService = Executors.newFixedThreadPool(1);\\n private final Queue scheduleTasks = new ConcurrentLinkedQueue<>();\\n private final AtomicBoolean isRunning = new AtomicBoolean(false);\\n\\n @EventListener\\n public void addTask(SchedulingEvent schedulingEvent) {\\n scheduleTasks.add(schedulingEvent);\\n }\\n\\n @Scheduled(cron = \\"0/1 * * * * *\\")\\n public void polling() {\\n if (!scheduleTasks.isEmpty() || isRunning.compareAndSet(false, true)) {\\n SchedulingEvent schedulingEvent = scheduleTasks.poll();\\n executorService.execute(() -> execute(schedulingEvent));\\n }\\n }\\n}\\n```\\n\uadf8\ub9ac\uace0 \uc704\uc640 \uac19\uc740 \uc2a4\ucf00\uc904\uc744 \uad00\ub9ac\ud558\ub294 \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 Schedule Event\ub97c \ubc1b\uc544 \uc2e4\ud589\ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ud074\ub798\uc2a4\uc5d0\uc11c\ub294 ThreadPool\uc744 \uc0c8\ub85c \uc0dd\uc131\ud558\uc5ec, schedule\uc758 \uc2a4\ub808\ub4dc\uc5d0 \uc601\ud5a5\uc744 \ubc1b\uc9c0 \uc54a\ub3c4\ub85d \uad6c\ud604\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 1\ucd08\ub9c8\ub2e4 \uc2e4\ud589\ub418\ub294 \uc2a4\ucf00\uc904\uc744 \ub9cc\ub4e4\uc5b4 queue\uc5d0 \uc791\uc5c5\uc774 \uc788\ub294\uc9c0, \ud604\uc7ac \uc791\uc5c5 \uc911\uc778\uc9c0 \ud655\uc778\ud558\uc5ec \uadf8\ub807\uc9c0 \uc54a\ub2e4\uba74 queue\uc5d0\uc11c \uc791\uc5c5\uc744 \uaebc\ub0b4 \uc2e4\ud589\ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uac70\uc758 \uad6c\ud604\uc774 \ub05d\ub098\uac11\ub2c8\ub2e4. \uc774\uc81c\ub294 \ud574\ub2f9 Schedule\uc758 \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud558\uace0, \uc791\uc5c5\uc774 \uc2e4\ud328\ud588\uc744 \uc2dc\uc5d0 \ub2e4\uc2dc \uc791\uc5c5\uc744 \ud558\uae30 \uc704\ud55c \uae30\ub2a5\ub9cc \uad6c\ud604\ud558\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Component\\npublic class ScheduleService {\\n\\n ...\\n\\n private void execute(SchedulingEvent schedulingEvent) {\\n String jobId = schedulingEvent.jobId();\\n LocalDateTime executionTime = schedulingEvent.executionTime();\\n\\n if (isJobInProgressOrDone(jobId)) {\\n log.info(\\"\uc791\uc5c5\uc774 \uc2e4\ud589\uc911\uc785\ub2c8\ub2e4. {} {}\\", executionTime, jobId);\\n return;\\n }\\n ScheduleTask entity = new ScheduleTask(jobId, executionTime, JobStatus.RUNNING);\\n scheduleTaskJdbcRepository.save(entity);\\n\\n try {\\n schedulingEvent.runnable().run();\\n scheduleTaskJdbcRepository.updateById(entity.getId(), JobStatus.DONE);\\n } catch (Exception e) {\\n log.error(\\"{} \uc791\uc5c5 \uc2e4\ud589 \uc911 \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\\", jobId);\\n scheduleTaskJdbcRepository.updateById(entity.getId(), JobStatus.ERROR);\\n tasks.add(schedulingEvent);\\n }\\n }\\n\\n private boolean isJobInProgressOrDone(String jobId) {\\n Optional taskOptional = scheduleTaskRepository.findById(jobId);\\n if (taskOptional.isPresent()) {\\n ScheduleTask scheduleTask = taskOptional.get();\\n return scheduleTask.getStatus() == JobStatus.RUNNING || scheduleTask.getStatus() == JobStatus.DONE;\\n }\\n return false;\\n }\\n}\\n```\\n\uc774 \ubd80\ubd84\uc740 \uac04\ub2e8\ud558\uac8c \uad6c\ud604\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc704\uc640 \uac19\uc774 \uc791\uc5c5\uc758 \uc2e4\ud589 \uc2dc\uac04\uacfc, job\uc758 \uc774\ub984\uc73c\ub85c \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \uc870\ud68c\ud558\uace0, \uc5c6\ub2e4\uba74 \uc791\uc5c5\uc744 \uc2e4\ud589\ud558\uace0\\n\uc788\ub2e4\uba74 \uc791\uc5c5\uc774 ERROR \uc778\uc9c0 \ud655\uc778\ud558\uc5ec \uc791\uc5c5\uc744 \uc2e4\ud589\ud574\uc8fc\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n![complete](https://github.com/drunkenhw/comments/assets/106640954/3ff855db-ff8e-4aa4-8b47-ed5b2ff6dd64)\\n\\n\uc704\uc640 \uac19\uc774 \ub450 \uac1c\uc758 \uc11c\ubc84\ub97c \ub3d9\uc2dc\uc5d0 \ub744\uc6e0\uc744 \ub54c\uc5d0\ub3c4 \uc2a4\ucf00\uc904\uc774 \uc798 \uc791\ub3d9\ud558\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\uc2a4\ucf00\uc904\uc744 \uc774\ub807\uac8c \uad6c\ud604\ud560 \uc218\ub3c4 \uc788\uc9c0\ub9cc \ud658\uacbd\uc774 \ub41c\ub2e4\uba74 Message Queue\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc5b4\ub5a8\uae4c\uc694?\\n\\n\\n\ud639\uc2dc \ud2c0\ub9b0 \ubd80\ubd84\uc774 \uc788\ub2e4\uba74 \uc9c0\uc801 \ubd80\ud0c1\ub4dc\ub9bd\ub2c8\ub2e4."},{"id":"34","metadata":{"permalink":"/34","source":"@site/blog/2023-09-17-caching.mdx","title":"\uce90\uc2dc\uc640 \uc774\ubd84 \ud0d0\uc0c9\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","description":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4","date":"2023-09-17T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 17\uc77c","tags":[{"label":"java","permalink":"/tags/java"}],"readingTime":12.495,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"34","title":"\uce90\uc2dc\uc640 \uc774\ubd84 \ud0d0\uc0c9\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","authors":["boxster"],"tags":["java"]},"prevItem":{"title":"Scale-out \uc2dc Scheduling \uc911\ubcf5 \uc2e4\ud589 \ub9c9\uae30","permalink":"/35"},"nextItem":{"title":"\ud63c\uc7a1\ub3c4 \uc870\ud68c \uc18d\ub3c4\ub97c \ud30c\ud2f0\uc154\ub2dd\uacfc \uc778\ub371\uc2a4\ub85c \uac1c\uc120\ud574\ubcf4\uae30","permalink":"/33"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4\\n\\n## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\uc774\uc804 \uae00\uc5d0\uc11c\ub3c4 \uacc4\uc18d \uc124\uba85\ud588\ub4ef\uc774 \uc870\ud68c \uc131\ub2a5\uc744 \ucd5c\ub300\ud55c \ube60\ub974\uac8c \ud558\ub294 \uac83\uc774 \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \ud575\uc2ec\uc774\ub77c\uace0 \uc0dd\uac01\ud558\uae30 \ub54c\ubb38\uc5d0 \uc9c0\uae08\ub3c4 \uc608\uc804\uc5d0 \ube44\ud574 \ube68\ub77c\uc84c\uc9c0\ub9cc \ub2e4\ub978 \uac1c\uc120\uc810\uc774 \ubcf4\uc5ec \uac1c\uc120\uc744 \ud558\uace0\uc790\ud569\ub2c8\ub2e4.\\n\\n[\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30 1 (\uc778\ub371\uc2a4)](https://car-ffeine.github.io/31)\\n\\n[\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30 2 (\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ubcf5\uc81c)](https://car-ffeine.github.io/32)\\n## \uacb0\ub860\\n\uacb0\ub860\ubd80\ud130 \ub9d0\uc500\ub4dc\ub9ac\uba74 \ub85c\uceec\uc5d0\uc11c \uce90\uc2f1\uc744 \uc801\uc6a9\ud55c \ud6c4 100\uba85\uc758 \uc0ac\uc6a9\uc790\uac00 \uc9c0\ub3c4\uc758 \ub370\uc774\ud130\ub97c \uc870\ud68c\ud560 \ub54c\ub97c \uae30\uc900\uc73c\ub85c\\n\\n**TPS** 78 -> 128\\n\\n**Response Time** 1236 ms -> 751 ms\\n\\n\uc57d **64%** \uc131\ub2a5\uc774 \uac1c\uc120 \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n*(\uc800\ubc88 \uc131\ub2a5 \ud14c\uc2a4\ud2b8\uc758 \uacb0\uacfc\uac00 \ub2e4\ub978 \uc774\uc720\ub294 \ube44\uc988\ub2c8\uc2a4 \ub85c\uc9c1\uc774 \ubcc0\uacbd\ub418\uc5b4 \uc870\ud68c \ubc29\uc2dd\uc774 \ubc14\ub00c\uc5c8\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4. \uadf8\ub798\uc11c \uce90\uc2f1\uc744 \uc801\uc6a9\ud558\uae30\uc804, \ud55c \ud6c4 \ub97c \ube44\uad50\ud588\uc2b5\ub2c8\ub2e4.)*\\n\\n## Caching\\n\\n>In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.\\n\\n\uce90\uc2f1\uc740 \uc704\ud0a4 \ubc31\uacfc\uc5d0\uc11c \uc704\uc640 \uac19\uc774 \uc124\uba85\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uc989 \uba54\ubaa8\ub9ac\uc5d0 \ub370\uc774\ud130\ub97c \ubcf5\uc0ac\ubcf8\uc744 \uc62c\ub824 \uc880 \ub354 \ube60\ub974\uac8c \ub370\uc774\ud130\uc5d0 \uc811\uadfc\ud558\ub294 \ubc29\uc2dd\uc785\ub2c8\ub2e4.\\n\\n\uce90\uc2f1\uc758 \ub2e8\uc810\uc740 \uc218\uc815, \uc0bd\uc785, \uc0ad\uc81c\uac00 \ub418\uc5c8\uc744 \ub54c, \uad00\ub9ac \ud3ec\uc778\ud2b8\uac00 \ub450 \uad70\ub370\uac00 \ub41c\ub2e4\ub294 \uc810\uc785\ub2c8\ub2e4. \ub9cc\uc57d \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\ub9cc \uc0c8\ub85c\uc6b4 \uc815\ubcf4\ub97c \uc800\uc7a5\ud558\uace0, \uce90\uc2dc\uc5d0\ub294 \uc800\uc7a5\ud574\uc8fc\uc9c0 \uc54a\ub294\ub2e4\uba74 \uc0ac\uc6a9\uc790\ub294 \uadf8 \uc815\ubcf4\ub97c \ubcfc \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \uc801\uc6a9\ud55c \uc774\uc720\ub294 \ucda9\uc804\uae30\uc758 \ucda9\uc804 \uc0c1\ud0dc (\ucda9\uc804 \uc911, \ub300\uae30\uc911, \uace0\uc7a5)\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ucd5c\uc2e0\ud654\uac00 \ub418\uc5b4\uc57c\ud558\uc9c0\ub9cc, \ucda9\uc804\uc18c\uc758 \uc774\ub984\uc774\ub77c\ub358\uc9c0, \uc704\uce58, \ub2e4\ub978 \uc815\ubcf4\ub4e4\uc740 \uc27d\uac8c \ubcc0\ud558\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \ud574\ub2f9 \uc815\ubcf4\ub97c \uce90\uc2f1\ud55c\ub2e4\uba74 \uc88b\uc744 \uac83 \uac19\uc558\uc2b5\ub2c8\ub2e4.\\n\\n## \uce90\uc2f1 \uc801\uc6a9\ud558\uae30\\n\\n\uba3c\uc800 \uce90\uc2f1\uc744 \uc5b4\ub514\uc5d0\uc11c \ud558\ub294\uc9c0\ub3c4 \uc911\uc694\ud569\ub2c8\ub2e4. \ud06c\uac8c **\ub85c\uceec \uce90\uc2dc**\uc640 **\uae00\ub85c\ubc8c \uce90\uc2dc**\ub85c \ub098\ub20c \uc218 \uc788\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\uae00\ub85c\ubc8c \uce90\uc2dc\uc758 \uc7a5\uc810\uc740 \uc2a4\ucf00\uc77c \uc544\uc6c3\uc744 \ud588\uc744 \ub54c, \ubaa8\ub4e0 \uc11c\ubc84\uac00 \ub2e4 \uac19\uc740 \ub370\uc774\ud130\ub97c \ubc14\ub77c\ubcf4\uae30 \ub54c\ubb38\uc5d0 \ub370\uc774\ud130 \uc815\ud569\uc131\uc774 \uc88b\uc544\uc9d1\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc800\ud76c \uc11c\ube44\uc2a4\ub294 \ub2e8\uc77c \uc11c\ubc84\ub85c \uad6c\uc131\ub418\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0, \ub85c\uceec \uce90\uc2dc\ub97c \ud574\ub3c4 \ubb38\uc81c\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uae00\ub85c\ubc8c \uce90\uc2dc\ub97c \uc801\uc6a9\ud558\uae30 \uc704\ud574\uc11c\ub294 Redis\ub098 Memcached \uac19\uc740 \ub3c4\uad6c\ub97c \ubaa8\ub4e0 \ud300\uc6d0\uc774 \uc54c\uc544\uc57c\ud558\uc9c0\ub9cc \ub85c\uceec \uce90\uc2dc\ub294 \uadf8\ub807\uac8c \ud558\uc9c0 \uc54a\ub354\ub77c\ub3c4 \ud3b8\ud558\uac8c \uc801\uc6a9\ud560 \uc218 \uc788\ub2e4\ub294 \uc810\uc5d0\uc11c \ub85c\uceec\uc5d0 \uce90\uc2f1\ud558\ub294 \ubc29\ubc95\uc744 \uc801\uc6a9\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n### \uce90\uc2f1\ud560 \uc815\ubcf4 \uac00\uc838\uc624\uae30\\n\\n\uce90\uc2f1\uc744 \ud558\uae30 \uc704\ud574\uc11c\ub294 \uba3c\uc800 \uce90\uc2f1\ud560 \ub370\uc774\ud130\ub97c \uac00\uc838\uc640\uc57c\ud569\ub2c8\ub2e4. \uc800\ud76c \uc11c\ube44\uc2a4\ub294 \ucd9c\uc7a5 \ud639\uc740 \uc5ec\ud589\uc744 \uac00\ub294 \uc804\uae30\ucc28 \uc624\ub108\uac00 \ud575\uc2ec \ud398\ub974\uc18c\ub098\uc774\uae30 \ub54c\ubb38\uc5d0 \uc0ac\uc6a9\uc790\ub4e4\uc774 \ucc3e\ub294 \uc815\ubcf4\uc758 \uc704\uce58\ub294 \ubd88\ud2b9\uc815\ud569\ub2c8\ub2e4. \uc11c\uc6b8\uc5d0\uc11c \ub2e4\ub978 \uc9c0\ubc29\uc73c\ub85c \ucd9c\uc7a5\uc744 \uac00\ub294 \uacbd\uc6b0\ub3c4 \uc788\uc744 \uac83\uc774\uace0, \uc9c0\ubc29\uc5d0\uc11c \uc11c\uc6b8\uc5d0 \uac00\ub294 \uacbd\uc6b0\ub3c4 \uc788\uae30 \ub54c\ubb38\uc5d0, \ubaa8\ub4e0 \ub370\uc774\ud130\ub97c \uce90\uc2f1\ud574\uc57c\ud560 \uac83\uc774\ub77c \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158 \uc2e4\ud589 \uc2dc\uc5d0 \ubaa8\ub4e0 \ucda9\uc804\uc18c\ub97c \uce90\uc2f1\ud558\uae30\ub85c \uc120\ud0dd\ud588\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Configuration\\npublic class InitialStationCache implements ApplicationRunner {\\n\\n private final StationCacheRepository stationCacheRepository;\\n private final StationQueryRepository stationQueryRepository;\\n\\n @Override\\n public void run(ApplicationArguments args) {\\n log.info(\\"Initialize station cache\\");\\n List stations = stationQueryRepository.findAll();\\n stationCacheRepository.initialize(stations);\\n log.info(\\"Station cache initialized\\");\\n log.info(\\"Station cache size: {}\\", stations.size());\\n }\\n}\\n\\n```\\n\\n\uc704\uc640 \uac19\uc774 ApplicationRunner\ub97c \uad6c\ud604\ud558\uc5ec \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158 \uc2e4\ud589 \uc2dc \ubaa8\ub4e0 \ucda9\uc804\uc18c\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc624\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uc5ec\uae30\uc11c Entity\uc778 Station\uc744 \uac00\uc838\uc624\uc9c0 \uc54a\uc740 \uc774\uc720\ub294 \ud06c\uac8c \ub450\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n1. \uc9c0\ub3c4\ub85c \uc870\ud68c\ud558\ub294 \ubd80\ubd84\uc758 \uc131\ub2a5\uc744 \uac1c\uc120\ud558\uace0\uc790 \ud588\uc9c0\ub9cc, Entity\uc5d0\ub294 \uc9c0\ub3c4\ub97c \uc870\ud68c\ud560 \ub54c \ubd88\ud544\uc694\ud55c \uc815\ubcf4\ub3c4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uba54\ubaa8\ub9ac\uc0c1\uc758 \ub0ad\ube44\uac00 \uc0dd\uae38 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n2. Entity\ub97c \uce90\uc2f1\ud558\uac8c \ub41c\ub2e4\uba74 hibernate 1\ucc28 \uce90\uc2dc\uc5d0\ub3c4 \uc801\uc7ac\ub418\uace0, \ud799 \uba54\ubaa8\ub9ac\uc5d0\ub3c4 \uc801\uc7ac\ub418\ub294 \uc77c\uc774 \ubc1c\uc0dd\ud558\uc5ec \uba54\ubaa8\ub9ac\uc0c1 \ub0ad\ube44\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n### \ubc94\uc704 \uac80\uc0c9\ud558\uae30\\n\\n\ucda9\uc804\uc18c\uc758 \ub370\uc774\ud130\ub97c \uc870\ud68c\ud558\ub294 \uc870\uac74\uc740 \uc704\ub3c4, \uacbd\ub3c4\uc758 \ucd5c\uc18c, \ucd5c\ub300\uac12\uc744 \uae30\uc900\uc73c\ub85c \ub9cc\uc871\ud558\ub294 \ub370\uc774\ud130\ub97c \ubcf4\uc5ec\uc90d\ub2c8\ub2e4.\\n\uc544\ub798\uc640 \uac19\uc774 \uac04\ub2e8\ud788 \uc870\uac74\uc744 stream()\uc758 filter()\ub97c \uc0ac\uc6a9\ud574\uc11c \uad6c\ud604\ud588\uc2b5\ub2c8\ub2e4.\\n```java\\npublic class StationCacheRepository {\\n\\n private final List cachedStations;\\n\\n public List findByCoordinate(\\n BigDecimal minLatitude,\\n BigDecimal maxLatitude,\\n BigDecimal minLongitude,\\n BigDecimal maxLongitude\\n ) {\\n return cachedStations.stream()\\n .filter(it -> it.latitude().compareTo(minLatitude) >= 0 && it.latitude().compareTo(maxLatitude) <= 0)\\n .filter(it -> it.longitude().compareTo(minLongitude) >= 0 && it.longitude().compareTo(maxLongitude) <= 0)\\n .toList();\\n }\\n}\\n```\\n\ud558\uc9c0\ub9cc \ud574\ub2f9 \ubc29\ubc95\uc73c\ub85c \ub85c\uceec\uc5d0\uc11c \uc870\ud68c\ub97c \ud14c\uc2a4\ud2b8 \ud588\uc744 \ub54c \uce90\uc2dc\ub97c \uc801\uc6a9\ud55c \uac83\ubcf4\ub2e4 \ub354 \ub290\ub824\uc9c4 \uacb0\uacfc\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\uce90\uc2f1\uc744 \ud574\uc11c \ub370\uc774\ud130\ubca0\uc774\uc2a4\uae4c\uc9c0 \uc694\uccad\uc744 \ubcf4\ub0b4\uc9c0 \uc54a\ub294\ub370 \uc65c \ub354 \ub290\ub824\uc9c4 \uac83\uc77c\uae4c\uc694?\\n\\n\ub2f5\uc740 **\uc778\ub371\uc2a4** \uc600\uc2b5\ub2c8\ub2e4. Mysql \uc5d0\uc11c \uc778\ub371\uc2a4\ub294 B Tree\ub85c \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c\ub294 \uc704\ub3c4, \uacbd\ub3c4\ub85c \ubcf5\ud569 \uc778\ub371\uc2a4\uac00 \uc124\uc815\ub418\uc5b4 \uc788\uc5c8\uc9c0\ub9cc, \ud604\uc7ac \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158 \ub85c\uc9c1\uc5d0\ub294 \ud574\ub2f9 \ubd80\ubd84\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c filter\ub85c \uc21c\ud68c\ud558\ub294 \uc2dc\uac04\ubcf5\uc7a1\ub3c4\uac00 O(n)\uc774\uace0, \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c\ub294 O(log n)\uc774\uae30 \ub54c\ubb38\uc5d0 \ub354 \ub290\ub824\uc9c4 \uac83\uc785\ub2c8\ub2e4. \uadf8\ub807\ub2e4\uace0 \uc81c\uac00 \uc9c1\uc811 B tree \uc790\ub8cc\uad6c\uc870\ub97c \uc9c1\uc811 \uad6c\ud604\ud574\uc57c\ud560\uae4c\uc694?\\n\\n\ud604\uc7ac \ud574\ub2f9 \uc870\ud68c API\ub294 \uc704\ub3c4 \uacbd\ub3c4\ub85c \ubc94\uc704 \ud0d0\uc0c9\uc744 \ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uacb0\uad6d\uc5d4 station\uc758 \uc815\ubcf4\ub4e4\uc774 \uc704\ub3c4, \uacbd\ub3c4\ub85c \uc815\ub82c\ub9cc \ub418\uc5b4 \uc788\ub2e4\uba74 B tree\ub97c \uc9c1\uc811 \uad6c\ud604\ud558\uc9c0 \uc54a\ub354\ub77c\ub3c4 \uac19\uc740 \uc2dc\uac04\ubcf5\uc7a1\ub3c4 O(log n)\uc73c\ub85c \ud0d0\uc0c9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\ubb3c\ub860 B tree\uc640 \ub2e4\ub978 \ubd80\ubd84\uc740 \ud574\ub2f9 \ucda9\uc804\uc18c\uc758 \uc815\ud655\ud55c \uc704\ub3c4, \uacbd\ub3c4\ub85c \ub2e8\uc77c \uce7c\ub7fc\uc744 \uc870\ud68c\ud560 \ub54c\ub294 O(n)\uc774\uae30 \ub54c\ubb38\uc5d0 \uc774\ub7f0 \ubc29\ubc95\uc774 \ubb38\uc81c\uac00 \ub420 \uc218 \uc788\uc9c0\ub9cc, \ud574\ub2f9 \uce90\uc2dc \ub370\uc774\ud130\ub85c\ub294 \ubb34\uc870\uac74 \ubc94\uc704 \ud0d0\uc0c9\uc744 \ud558\uae30 \ub54c\ubb38\uc5d0, B tree\ub97c \uad6c\ud604\ud558\uc9c0 \uc54a\uace0 \uc774\ubd84 \ud0d0\uc0c9\uc73c\ub85c \uc870\ud68c\ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \ubcc0\uacbd\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n public void initialize(List stations) {\\n cachedStations.addAll(stations);\\n cachedStations.sort((o1, o2) -> {\\n int latitudeCompare = o1.latitude().compareTo(o2.latitude());\\n if (latitudeCompare == 0) {\\n return o1.longitude().compareTo(o2.longitude());\\n }\\n return latitudeCompare;\\n });\\n }\\n\\n private List findStations(BigDecimal minLatitude, BigDecimal maxLatitude, BigDecimal minLongitude, BigDecimal maxLongitude) {\\n int lowerBound = binarySearch(minLatitude, START_INDEX);\\n int upperBound = binarySearch(maxLatitude, lowerBound);\\n if (lowerBound == -1 || upperBound == -1) {\\n return Collections.emptyList();\\n }\\n return cachedStations.stream()\\n .skip(lowerBound)\\n .limit(upperBound - lowerBound)\\n .filter(station -> station.longitude().compareTo(minLongitude) >= 0 && station.longitude().compareTo(maxLongitude) <= 0)\\n .toList();\\n }\\n\\n private int binarySearch(BigDecimal latitude, int startIndex) {\\n int left = startIndex;\\n int right = cachedStations.size() - 1;\\n int result = -1;\\n while (left <= right) {\\n int middle = left + (right - left) / 2;\\n StationInfo middleStation = cachedStations.get(middle);\\n if (middleStation.latitude().compareTo(latitude) >= 0) {\\n result = middle;\\n right = middle - 1;\\n } else {\\n left = middle + 1;\\n }\\n }\\n return result;\\n }\\n\\n```\\n\\n\uba3c\uc800 \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc774 \uc2e4\ud589\ub420 \ub54c cache \ub370\uc774\ud130\ub97c \ucc3e\uc544 \uc800\uc7a5\ud558\ub294 \uac83 \ubfd0\ub9cc \uc544\ub2c8\ub77c, \uc704\ub3c4(Latitude)\ub97c \uae30\uc900\uc73c\ub85c \uc815\ub82c\ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc704\ub3c4\uc758 \ucd5c\uc18c, \ucd5c\ub300\uac12\uc758 \uc778\ub371\uc2a4\ub97c \uac00\uc7a5 \ud6a8\uc728\uc801\uc73c\ub85c \ucc3e\uc544\uc62c \uc218 \uc788\ub3c4\ub85d binary search\ub97c \ud558\ub294 \uba54\uc11c\ub4dc\ub97c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4. \uc774\ub807\uac8c \ud55c\ub2e4\uba74 O(log n) \uc73c\ub85c \uc704\ub3c4\uc758 \ucd5c\ub300 \ucd5c\uc18c \uc870\uac74\uc5d0 \ud3ec\ud568\ub418\ub294 \ubaa8\ub4e0 station\uc758 \uac12\uc744 \uc870\ud68c\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc870\ud68c\ud55c \ub370\uc774\ud130\ub4e4\uc758 \uac1c\uc218\ub9cc\ud07c filter\ub97c \ud1b5\ud574 \uacbd\ub3c4(longitude) \uac00 \ud3ec\ud568\ub418\ub294\uc9c0 \ud655\uc778\ud569\ub2c8\ub2e4. \ud574\ub2f9 \ubc29\uc2dd\uc758 \uad6c\ud604\uc740 B tree\uac00 \uc791\ub3d9\ud558\ub294 \ubc29\uc2dd\uacfc \uc720\uc0ac\ud560 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc774\ubd84 \ud0d0\uc0c9\uc744 \uc801\uc6a9\ud55c \uacb0\uacfc \ub85c\uceec\uc5d0\uc11c \uc751\ub2f5 \uc18d\ub3c4\uac00 120 ms -> 50 ~ 70 ms\ub85c \uc57d 2\ubc30 \ube68\ub77c\uc9c4 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uc2e4\uc2dc\uac04\uc774 \uc911\uc694\ud55c \ub370\uc774\ud130\ub294?\\n\\n\uc55e\uc11c \ub9d0\uc500\ub4dc\ub838\ub2e4\uc2dc\ud53c \uc9c0\ub3c4\ub85c \ucda9\uc804\uc18c\ub97c \uc870\ud68c\ud560 \ub54c, \ucda9\uc804\uc18c\uc758 \uc815\ubcf4\ub4e4\uc5d0\ub294 \ubc14\ub00c\uc9c0 \uc54a\ub294 \uc815\ubcf4\ubfd0\ub9cc \uc544\ub2c8\ub77c, \ucd5c\uc2e0\ud654\ud574\uc57c\ud558\ub294 \ucda9\uc804\uae30\uc758 \ud604\uc7ac \uc0c1\ud0dc \uc815\ubcf4\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub7ec\ud55c \uc815\ubcf4\ub4e4\uc740 \uce90\uc2f1\ud574\ub458 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ud558\ub354\ub77c\ub3c4, \uad00\ub9ac \ud3ec\uc778\ud2b8\uac00 \ub298\uc5b4\ub098\uae30 \ub54c\ubb38\uc5d0 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \uce90\uc2f1\ud574\ub454 \ucda9\uc804\uae30 id\ub85c \ucda9\uc804\uae30\uc758 \uc0c1\ud0dc\ub97c \ucc3e\uc544\uc640\uc11c \uc815\ubcf4\ub97c \ud569\uccd0 \ubc18\ud658\ud558\ub294 \uc2dd\uc73c\ub85c \ub9cc\ub4e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n```sql\\n select cs.station_id,\\n sum(case\\n when cs.charger_condition = \'STANDBY\' then 1\\n else 0\\n end)\\n from charger_status cs\\n where cs.station_id in (?, ?, ?, ?, ?, ?, ?)\\n group by cs.station_id\\n```\\n\uc704\uc640 \uac19\uc740 \ucffc\ub9ac\ub85c \ud574\ub2f9 \ucda9\uc804\uc18c\uc758 \ucd5c\uc2e0\ud654\ub41c \ucda9\uc804\uae30 \uc0c1\ud0dc\ub97c \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uce90\uc2f1\uc744 \ud558\uae30\uc804\uc5d0 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c \uc774\uc6a9\ud574 \ub370\uc774\ud130\ub97c \uac00\uc838\uc62c \ub54c\uc758 \ucffc\ub9ac\ub294 \uc544\ub798\uc640 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n```sql\\n select\\n distinct s.station_id\\n from\\n charge_station s\\n inner join\\n charger c\\n on (\\n c.station_id=s.station_id\\n )\\n where\\n s.latitude>=?\\n and s.latitude<=?\\n and s.longitude>=?\\n and s.longitude<=?\\n -------------------------------------------------\\n select\\n s.station_id,\\n s.station_name,\\n s.latitude,\\n s.longitude,\\n s.is_parking_free,\\n s.is_private,\\n sum(case\\n when cs.charger_condition=\'STANDBY\' then 1\\n else 0\\n end),\\n sum(case\\n when c.capacity>=50 then 1\\n else 0\\n end)\\n from\\n charge_station s\\n inner join\\n charger c\\n on (\\n c.station_id=s.station_id\\n )\\n inner join\\n charger_status cs\\n on (\\n c.station_id=cs.station_id\\n and c.charger_id=cs.charger_id\\n )\\n where\\n s.station_id in (\\n ?,?,?,?\\n )\\n group by\\n s.station_id\\n```\\n\uc6d0\ub798\ub294 \uc704\uc640 \uac19\uc774 \uc5ec\ub7ec\ubc88\uc758 Join\uc744 \ud558\uace0, 2\ubc88\uc758 \ucffc\ub9ac\uac00 \ub098\uac14\ub358 \ubc18\uba74 \uc9c0\uae08\uc740 join\uc744 \ud558\uc9c0\uc54a\ub294 \ud55c\ubc88\uc758 \uae54\ub054\ud55c \ucffc\ub9ac\ub85c \uac1c\uc120\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 **station \ud14c\uc774\ube14\uc758 \uc704\ub3c4, \uacbd\ub3c4\ub85c \ubc94\uc704 \ud0d0\uc0c9\uc744 \uc704\ud574 \uc0dd\uc131\ud588\ub358 index\ub3c4 \uc81c\uac70**\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4!\\n\\n## \uacb0\ub860\\n1. \uce90\uc2f1\ud560 \uc218 \uc788\ub294 \ubd80\ubd84\uc740 \ud558\ub294 \uac83\ub3c4 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4\\n2. \uc2dc\uac04 \ubcf5\uc7a1\ub3c4\ub97c \uacc4\uc0b0\ud574\ubd05\uc2dc\ub2e4.\\n3. \uc131\ub2a5 \uac1c\uc120 \uc7ac\ubc0c\uc2b5\ub2c8\ub2e4."},{"id":"33","metadata":{"permalink":"/33","source":"@site/blog/2023-09-11-congestion_speed_up.mdx","title":"\ud63c\uc7a1\ub3c4 \uc870\ud68c \uc18d\ub3c4\ub97c \ud30c\ud2f0\uc154\ub2dd\uacfc \uc778\ub371\uc2a4\ub85c \uac1c\uc120\ud574\ubcf4\uae30","description":"\uc548\ub155\ud558\uc138\uc694.","date":"2023-09-11T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 11\uc77c","tags":[{"label":"mysql","permalink":"/tags/mysql"}],"readingTime":6.19,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"33","title":"\ud63c\uc7a1\ub3c4 \uc870\ud68c \uc18d\ub3c4\ub97c \ud30c\ud2f0\uc154\ub2dd\uacfc \uc778\ub371\uc2a4\ub85c \uac1c\uc120\ud574\ubcf4\uae30","authors":["jay"],"tags":["mysql"]},"prevItem":{"title":"\uce90\uc2dc\uc640 \uc774\ubd84 \ud0d0\uc0c9\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/34"},"nextItem":{"title":"\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/32"}},"content":"\uc548\ub155\ud558\uc138\uc694.\\n\uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \ucda9\uc804\uc18c\uc758 \uc694\uc77c\uacfc \uc2dc\uac04\ub300 \ubcc4\ub85c \ucda9\uc804\uc18c \ud63c\uc7a1\ub3c4 \uc815\ubcf4\ub97c \uc81c\uacf5\uc744 \ucc28\ubcc4\uc801\uc778 \uae30\ub2a5\uc73c\ub85c \uc81c\uacf5\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \uad6c\ud604\ud558\uae30 \uc704\ud574\uc11c \uacf5\uacf5 \ub370\uc774\ud130\uc5d0\uc11c \uc815\ubcf4\ub97c \uc218\uc9d1\ud558\uace0\uc788\uc2b5\ub2c8\ub2e4.\\n\ud63c\uc7a1\ub3c4\ub97c \uc870\ud68c\ud558\uae30 \uc704\ud574\uc11c\ub294 \uc57d 23\ub9cc \uac74\uc758 \ucda9\uc804\uc18c * 7\uc77c * 24\uc2dc\uac04 = \uc57d 4000\ub9cc \uac74\uc758 \ub370\uc774\ud130 \uc911\uc5d0\uc11c \uc870\ud68c\ub97c \ud558\ub294 \ud615\uc2dd\uc73c\ub85c \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub108\ubb34 \ub9ce\uc740 \ub370\uc774\ud130\uac00 \uc788\ub2e4\ubcf4\ub2c8 \uc870\ud68c \uc18d\ub3c4\uac00 \ub9ce\uc774 \ub290\ub9b0\ub370\uc694.\\n\uc624\ub298\uc740 \uc774\ub97c \uc5b4\ub5bb\uac8c \uac1c\uc120\ud588\ub294\uc9c0 \uc791\uc131\ud574\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\ucc38\uace0\ub85c \ud574\ub2f9 \uae00\uc758 \uc131\ub2a5 \uce21\uc815\uc5d0 \uc774\uc6a9\ud55c \ub370\uc774\ud130\uc758 \uc218\ub294 \uc57d 20\ub9cc \uac74\uc785\ub2c8\ub2e4.\\n\\n---\\n\\n## \ubb38\uc81c \ud655\uc778\\n\uae30\uc874\uc758 \uc800\ud76c\ub294 \ub9ce\uc740 \uc591\uc758 \ub370\uc774\ud130\ub97c \uac10\ub2f9\ud558\uae30 \ud798\ub4e4\uc5b4\uc11c [\uc624\uc804, \uc624\ud6c4] \uc774\ub807\uac8c \ub450 \ubd80\ubd84\uc73c\ub85c \ub098\ub220\uc11c \ud63c\uc7a1\ub3c4\ub97c \uc870\ud68c\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc2e4\uc81c \ubc30\ud3ec\ub97c \ud558\uae30 \uc704\ud574\uc11c \ub354\uc774\uc0c1\uc740 \uc624\uc804 \uc624\ud6c4\ub85c \ub098\ub20c \uc218\uac00 \uc5c6\uc5c8\ub294\ub370\uc694.\\n\\n\uc815\uc0c1\uc801\uc778 \ub370\uc774\ud130\ub97c \uc81c\uacf5\ud558\uae30 \uc704\ud574\uc11c \uba3c\uc800 24\uc2dc\uac04 \uae30\uc900\uc73c\ub85c \ud63c\uc7a1\ub3c4\ub97c \uac31\uc2e0\ud558\ub3c4\ub85d \ub85c\uc9c1\ubd80\ud130 \ubc14\uafb8\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc704\uc640 \uac19\uc774 \ucf54\ub4dc\ub97c \ubc14\uafb8\ub2c8 \ubc14\ub85c \uc131\ub2a5\uc5d0 \ubb38\uc81c\uac00 \uc0dd\uacbc\uc2b5\ub2c8\ub2e4.\\n![img](https://postfiles.pstatic.net/MjAyMzA5MTFfMTA4/MDAxNjk0NDIwNjEzOTU3.Q1_sK5nRvBVbJ9w4bdYkofc0zX00TQmJUQPIqRQiofwg.FRujZOroDjWC4znh0pueWi84EAh9-LVKk17z2ojLi1Ig.PNG.sosow0212/image.png?type=w773)\\n\\n\uc704\uc758 \uc0ac\uc9c4\uacfc \uac19\uc774 slow-query\ub97c \ubd84\uc11d\ud574\ubcf4\uc558\uc2b5\ub2c8\ub2e4.\\n\ud63c\uc7a1\ub3c4 \uc5c5\ub370\uc774\ud2b8\uc5d0\ub3c4 \uc2dc\uac04\uc774 \uac78\ub9ac\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc9c0\ub9cc, \uc870\ud68c \uc2dc\uac04\uc740 \ucd5c\uc545\uc758 \uacbd\uc6b0 \uc57d 12\ubd84 \uc815\ub3c4\ub85c \uc0ac\uc6a9\uc790\ub4e4\uc774 \ubcfc \uc218\ub3c4 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud55c \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\uba3c\uc800 \uac00\uc7a5 \ud070 \ubb38\uc81c\ub294 \ub370\uc774\ud130\uac00 \ub9ce\uae30 \ub54c\ubb38\uc774\uace0, \ub450 \ubc88\uc9f8\ub85c\ub294 \ube44\ud6a8\uc728\uc801\uc778 API\ub85c \uc778\ud55c \ubb38\uc81c\uc785\ub2c8\ub2e4.\\n\\n\ud604\uc7ac \ud63c\uc7a1\ub3c4 \uc870\ud68c\uc2dc 0~23\uc2dc\uae4c\uc9c0 \ubaa8\ub4e0 \uc694\uc77c\uc758 \uae09\uc18d\uacfc \uc644\uc18d \ucda9\uc804\uae30\uc5d0 \ub300\ud55c \ud63c\uc7a1\ub3c4\ub97c \uac00\uc838\uc635\ub2c8\ub2e4.\\n\uad73\uc774 \uc774\ub7f4 \ud544\uc694 \uc5c6\uc774 \uc120\ud0dd\ud55c \uc694\uc77c\uc5d0\ub9cc \ud63c\uc7a1\ub3c4\ub97c \uac00\uc838\uc628\ub2e4\uba74 \ubd88\ud544\uc694\ud55c \uc870\ud68c\ub294 \uc5c6\uc744 \uac70\ub77c\uace0 \uc0dd\uac01\ud574\uc11c \uc77c\ubd80\ubd84 \ub9ac\ud329\ud1a0\ub9c1\uc744 \ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ucd94\uac00\uc801\uc73c\ub85c \ubc15\uc2a4\ud130\uac00 DB Replication\uc744 \uc801\uc6a9\ud574\uc11c, Update\ub85c \uc778\ud55c \uc18d\ub3c4 \uc800\ud558 \ud604\uc0c1\ub3c4 \ub9ce\uc774 \uc904\uc5b4\ub4e4 \uac83\uc744 \uae30\ub300\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n## \ubb38\uc81c \ud574\uacb0 \uacfc\uc815\\n\\n- \uba3c\uc800 \uae30\uc874 \ucf54\ub4dc\ub85c \uc870\ud68c\uc2dc\uc5d0 \uc18d\ub3c4\uac00 \uc5bc\ub9c8\ub098 \ub098\uc624\ub294\uc9c0 \ud655\uc778\uc744 \ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n![img](https://blogfiles.pstatic.net/MjAyMzA5MTFfMTgz/MDAxNjk0NDIwODU1MDg0.d2ig3CCgdHDwkz_7d4qyhVKM0PQ4MV8BcUwm9LjqAcAg.LdVGDSqRuArzM32ZD1tHbxsD2xG5pt8xrOrDwhR25wcg.PNG.sosow0212/image.png)\\n\uae30\uc874\uc758 \ubaa8\ub4e0 \ud63c\uc7a1\ub3c4\ub97c \ub4e4\uace0\uc624\ub294 \uacbd\uc6b0 \uc704\uc640 \uac19\uc774 536ms\uc758 \uc2dc\uac04\uc774 \uc18c\ubaa8\ub418\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://blogfiles.pstatic.net/MjAyMzA5MTFfMjA5/MDAxNjk0NDIwODk3NDE4.N3tGXL52LYr5Koc1Lwk0Tfhe3Apkao9BEI8waHIkwNgg.AUEcIoBUg8AtXMiZCc2P13Vb_DCeWnsoXH2-6acaClIg.PNG.sosow0212/image.png)\\n\uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 `day_of_week` \uc989 \ud63c\uc7a1\ub3c4\ub97c \ud655\uc778\ud558\uace0 \uc2f6\uc740 \uc694\uc77c\uc744 \ucd94\uac00\uc801\uc73c\ub85c \uc870\uac74\uc5d0 \uba85\uc2dc\ud574\uc8fc\ub2c8\\n148ms\ub85c \uc904\uc740 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n148ms\ub294 \uc544\uc9c1 \ud55c\ucc38 \ub290\ub9bd\ub2c8\ub2e4.\\n\\n\uba3c\uc800 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574\uc11c `DB Partitioning`\uc744 \uc801\uc6a9\ud588\uc2b5\ub2c8\ub2e4.\\n\\nDB Partitioning\uc5d0 \ub300\ud574 \uac04\ub2e8\ud558\uac8c \uc124\uba85\ud558\uc790\uba74 \ud070 \ud14c\uc774\ube14\uc744 \uc791\uc740 \ub2e8\uc704\ub85c \uad00\ub9ac\ud558\ub294 \uae30\ubc95\uc785\ub2c8\ub2e4.\\n\ud558\ub098\uc758 \ud14c\uc774\ube14\ub85c \ubcf4\uc774\uc9c0\ub9cc \uc774\ub97c \uc801\uc6a9\ud558\uba74 \uc2e4\uc81c\ub85c \uc5ec\ub7ec \uac1c\uc758 \ud14c\uc774\ube14\ub85c \ubd84\ub9ac\ud574\uc11c \uad00\ub9ac\ud558\ub294 \uae30\ubc95\uc774\uace0, \uc774\ub97c \ud1b5\ud574\uc11c \uc870\ud68c \ubc0f \uc5c5\ub370\uc774\ud2b8 \ucffc\ub9ac \uc131\ub2a5\uc774 \uac1c\uc120\ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c \ud300\uc740 List partitioning\uc744 \uc801\uc6a9\ud574\uc11c `day_of_week(\uc694\uc77c)`\uc744 \uae30\uc900\uc73c\ub85c \ud30c\ud2f0\uc154\ub2dd\uc744 \ud588\uc2b5\ub2c8\ub2e4.\\n![img](https://blogfiles.pstatic.net/MjAyMzA5MTFfMTE0/MDAxNjk0NDIxMzg1NTMx.Q4VBItbFdityCKdRFYqpC1qVtoi81RRqcmysYMh-9xog.d8MIYW-tatGXoaxCJ-o6vS5wydEk1yQVQTtmmZvooFIg.PNG.sosow0212/image.png)\\n\uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 day_of_week\ub97c \uae30\uc900\uc73c\ub85c \ud30c\ud2f0\uc154\ub2dd\uc744 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://blogfiles.pstatic.net/MjAyMzA5MTFfMjA5/MDAxNjk0NDIxNDM3MTI2.QXclZKmnwVTcYrkR95yPJV3vxCCzcaisaWj29WGxFucg.CO0SafuQLRmWPzAs9-9ForUnT1fjcqxXBRmX1UmB-b8g.PNG.sosow0212/image.png)\\nList Partitioning\uc744 \uc801\uc6a9\ud558\uace0 \uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 \uc870\ud68c \ucffc\ub9ac\ub97c \ub2e4\uc2dc \ub0a0\ub824\ubcf4\uba74, `partitions = p_friday` \ub85c \uc798 \ub098\ub258\uc5b4\uc9c4 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud30c\ud2f0\uc154\ub2dd \uc791\uc5c5\uc774 \uc798 \ub418\uc5c8\uc73c\ub2c8 \uc774\uc81c API\uc5d0\uc11c \uc694\uc77c \ubcc4 \ud63c\uc7a1\ub3c4 \uc870\ud68c\ub85c \ubc14\uafd4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\uba3c\uc800 \ucffc\ub9ac\ub97c \ubcc0\uacbd\ud558\uace0 \ucffc\ub9ac\ub97c \ud655\uc778\ud574\ubcf4\ub2c8 \ub2e4\uc74c\uacfc \uac19\uc774 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://postfiles.pstatic.net/MjAyMzA5MTFfMjQ5/MDAxNjk0NDIxNTcwOTg3.mgx-mdBa6J6k8erhiksOOkzrMzOMmCLX7iuRPZf7RNEg.ALwxez4qUVHB1wlIr9zsZovCBlxoIsmgCa4wNv-7t_4g.PNG.sosow0212/image.png?type=w773)\\n\uc704\uc640 \uac19\uc740 \uc870\ud68c \ucffc\ub9ac\uac00 \ub098\uc654\uc73c\ubbc0\ub85c \uc778\ub371\uc2a4\ub97c \uc544\ub798\uc640 \uac19\uc774 `station_id, day_of_week`\uc5d0 \uac78\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://postfiles.pstatic.net/MjAyMzA5MTFfMjgy/MDAxNjk0NDIxNjI2NDAz.XqGsab-JR_fQaIZhCYMiKy5r3cn85wFLwUNlmCo1Gqwg.a26_N5lnwzXX6z0JRqn35u8pGcj1TAa2nDamgRyOYjUg.PNG.sosow0212/image.png?type=w773)\\n\uc704 \uc2e4\ud589 \uc18d\ub3c4\uc5d0\uc11c execution time\uc744 \ud655\uc778\ud574\ubcf4\uba74 \uc778\ub371\uc2a4\ub97c \uac78\uace0 `134ms -> 5ms`\ub85c \uc131\ub2a5\uc774 \ub9ce\uc774 \uac1c\uc120 \ub418\uc5c8\uc74c\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://postfiles.pstatic.net/MjAyMzA5MTFfMjI3/MDAxNjk0NDIxNjc5NDIw.kbfBLWKeY70QFeByKN5xVX1WhFSpFZbJnFkx9l0zyrQg.Wv0QU9W6Fqjfr8eyyLT2MyttjDKzN2cdrItGH7CDLPYg.PNG.sosow0212/image.png?type=w773)\\n\uc2e4\ud589 \uacc4\ud68d\ub3c4 \uc758\ub3c4\ud55c\ub300\ub85c \uc798 \ub098\uc624\ub294 \uac83\uc744 \ubcf4\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n## \uc815\ub9ac\\n\\n1. DB Partitioning - (day_of_week : \uc694\uc77c)\uc744 \uae30\uc900\uc73c\ub85c \ud30c\ud2f0\uc154\ub2dd\\n2. \uc870\ud68c \ucffc\ub9ac\uc5d0 \ub9de\uac8c \uc778\ub371\uc2a4 \uc124\uc815\\n3. API \uc218\uc815 (\ubaa8\ub4e0 \uc694\uc77c\uc758 \ud63c\uc7a1\ub3c4 \uc870\ud68c -> \ud574\ub2f9 \uc694\uc77c\uc758 \ud63c\uc7a1\ub3c4 \uc870\ud68c)\\n\\n\uacb0\uacfc\uc801\uc73c\ub85c \uae30\uc874 \ud63c\uc7a1\ub3c4 \uc870\ud68c\uc2dc 511ms\uac00 \ub098\uc654\uc73c\ub098, \uc694\uc77c \ubcc4 \uc870\ud68c \ubc0f \ud30c\ud2f0\uc154\ub2dd & \uc778\ub371\uc2a4\ub97c \uc801\uc6a9\ud558\uace0 execution time = 5ms\ub85c \uac1c\uc120"},{"id":"32","metadata":{"permalink":"/32","source":"@site/blog/2023-09-11-database-replication.mdx","title":"\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","description":"\uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720","date":"2023-09-11T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 11\uc77c","tags":[{"label":"mysql","permalink":"/tags/mysql"}],"readingTime":23.75,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"32","title":"\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","authors":["boxster"],"tags":["mysql"]},"prevItem":{"title":"\ud63c\uc7a1\ub3c4 \uc870\ud68c \uc18d\ub3c4\ub97c \ud30c\ud2f0\uc154\ub2dd\uacfc \uc778\ub371\uc2a4\ub85c \uac1c\uc120\ud574\ubcf4\uae30","permalink":"/33"},"nextItem":{"title":"\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/31"}},"content":"## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\\n\uba3c\uc800 \uc774 \uae00\uc744 \uc4f0\uac8c \ub41c \uacc4\uae30\ub97c \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4. \uc9c0\ub09c \uae00\uc5d0\uc11c \uc124\uba85\ud588\ub4ef\uc774 \uc800\ud76c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c\ub294 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uac00 \uc2e4\ud589\ub418\uace0 \uc788\ub294 \uc11c\ubc84\uc758 cpu \uc0ac\uc6a9\ub960\uc774 100%\uac00 \ub418\ub294 \ubb38\uc81c\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uc774 \ubd80\ubd84\uc5d0 \ub300\ud574\uc11c\ub294 \uc870\ud68c \uc131\ub2a5\uc744 \ub192\ud600 \uc5b4\ub290\uc815\ub3c4 \ud574\uacb0\ud558\uace0\uc790 \ud588\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc870\ud68c\uac00 \uc544\ub2cc \ub9ce\uc740 \ub370\uc774\ud130\ub97c \uc77c\uc815\ud55c \uc8fc\uae30\ub85c \uc5c5\ub370\uc774\ud2b8 \ud574\uc918\uc57c\ud558\ub294 \ub85c\uc9c1\ub3c4 \ud3ec\ud568\ub418\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc5c5\ub370\uc774\ud2b8\ub97c \ud560 \ub54c \uc870\ud68c\ub97c \ud558\uac8c \ub41c\ub2e4\uba74 cpu \uc0ac\uc6a9\ub960\uc740 \ube44\uc2b7\ud560 \uac83\uc785\ub2c8\ub2e4. \uc774 \ubd80\ubd84\uc744 \ud574\uacb0\ud558\uace0\uc790 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\uacb0\ub860\ubd80\ud130 \ub9d0\uc500\ub4dc\ub9ac\uba74 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc801\uc6a9\ud55c \ud6c4 \uc131\ub2a5\uc774 \ub208\uc5d0 \ub744\uac8c \uc88b\uc544\uc84c\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ubd80\ubd84\uc740 \ub2e4\uc74c \ud3ec\uc2a4\ud305\uc5d0 \uc791\uc131\ud558\uaca0\uc2b5\ub2c8\ub2e4\\n100\uba85\uc758 \uc0ac\uc6a9\uc790\uac00 \uc9c0\ub3c4\uc758 \ub370\uc774\ud130\ub97c \uc870\ud68c\ud560 \ub54c\ub97c \uae30\uc900\uc73c\ub85c\\n\\n**TPS** 179 -> 366\\n\\n**Response** Time 550 ms -> 271 ms\\n\\n\uc57d 2\ubc30 \uac00\ub7c9 \uc131\ub2a5\uc774 \ud5a5\uc0c1\ub41c \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n# \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc774\ub780?\\n\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc774\ub780 \ud558\ub098\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \ub2e4\ub978 \ud558\ub098 \uc774\uc0c1\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub85c \ub370\uc774\ud130\uc758 \ubcf5\uc81c \ub610\ub294 \ubcf5\uc0ac\ub97c \uc218\ud589\ud558\ub294 \ud504\ub85c\uc138\uc2a4 \ub610\ub294 \uae30\uc220\uc785\ub2c8\ub2e4. \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc740 \uc8fc\ub85c \ub2e4\uc74c\uacfc \uac19\uc740 \ubaa9\uc801\uc73c\ub85c \uc0ac\uc6a9\ub429\ub2c8\ub2e4\\n\\n1. **\uace0\uac00\uc6a9\uc131**:\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc11c\ubc84\uc758 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c, \ub808\ud50c\ub9ac\uce74 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2dc\uc2a4\ud15c\uc744 \uacc4\uc18d \uc6b4\uc601\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub807\uac8c \ud558\uba74 \uc11c\ube44\uc2a4 \uc911\ub2e8 \uc2dc\uac04\uc744 \ucd5c\uc18c\ud654\ud558\uace0 \ube44\uc988\ub2c8\uc2a4 \uc5f0\uc18d\uc131\uc744 \uc720\uc9c0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n2. **\uc131\ub2a5 \ud5a5\uc0c1** :\\n\ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc0ac\uc6a9\ud558\uba74 \uc77d\uae30 \uc791\uc5c5\uc744 \ubd84\uc0b0\uc2dc\ud0ac \uc218 \uc788\uc73c\ubbc0\ub85c \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc11c\ubc84\uc758 \uc77d\uae30 \ubd80\ud558\ub97c \uc904\uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub97c \ud1b5\ud574 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc131\ub2a5\uc744 \ud5a5\uc0c1\uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n3. **\uc9c0\uc5ed\uc801 \ubd84\uc0b0** :\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \ud1b5\ud574 \ub370\uc774\ud130\ub97c \uc9c0\ub9ac\uc801\uc73c\ub85c \ubd84\uc0b0\uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub807\uac8c \ud558\uba74 \uc9c0\uc5ed\uc801\uc778 \uc0ac\uc6a9\uc790 \ub610\ub294 \uc751\uc6a9 \ud504\ub85c\uadf8\ub7a8\uc5d0 \ube60\ub974\uac8c \ub370\uc774\ud130\ub97c \uc81c\uacf5\ud560 \uc218 \uc788\uc73c\uba70, \uc9c0\uc5ed\uc801\uc778 \uaddc\uc815 \uc900\uc218 \uc694\uad6c\uc0ac\ud56d\uc744 \ucda9\uc871\uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n4. **\ubc31\uc5c5\uacfc \ubcf5\uad6c** :\\n\ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc8fc \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \ubc31\uc5c5\uc744 \uc0dd\uc131\ud558\uace0, \uc774\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc7a5\uc560 \ubcf5\uad6c\ub97c \uc218\ud589\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc8fc \ub370\uc774\ud130\ubca0\uc774\uc2a4\uac00 \uc190\uc0c1\ub418\uc5c8\uc744 \ub54c \ubc31\uc5c5 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2dc\uc2a4\ud15c\uc744 \ube60\ub974\uac8c \ubcf5\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n5. **\ub370\uc774\ud130 \ubd84\uc11d \ubc0f \ubcf4\uace0** :\\n\ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub370\uc774\ud130\ub97c \ub2e4\ub978 \ubd84\uc11d \ub610\ub294 \ubcf4\uace0 \ub3c4\uad6c\ub85c \ubcf5\uc0ac\ud558\uc5ec \ub370\uc774\ud130 \uc6e8\uc5b4\ud558\uc6b0\uc2a4 \ub610\ub294 \ubd84\uc11d \uc2dc\uc2a4\ud15c\uc5d0\uc11c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c \ud300\uc5d0\uc11c \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc801\uc6a9\ud55c \uac00\uc7a5 \ud070 \uc774\uc720\ub294 \uc131\ub2a5 \ud5a5\uc0c1\uc785\ub2c8\ub2e4. \uc544\ubb34\ub798\ub3c4 \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \uc77d\uae30 \uc791\uc5c5\uacfc \uc4f0\uae30 \uc791\uc5c5\uc774 \ub458 \ub2e4 \ube48\ubc88\ud558\uac8c \uc77c\uc5b4\ub098\uace0, \ud2b9\ud788 \uc4f0\uae30 \uc791\uc5c5\uc5d0 \ub9ce\uc740 \uc5f0\uc0b0\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc0ac\uc6a9\uc790\uc5d0\uac8c \ucd5c\uc2e0\uc758 \ub370\uc774\ud130\ub97c \uc81c\uacf5\ud558\uace0\uc790 \uc4f0\uae30 \uc791\uc5c5\uc744 \uc790\uc8fc\ud558\uc5ec \ub370\uc774\ud130\ub97c \ucd5c\uc2e0\ud654\ud558\ub354\ub77c\ub3c4, \uc77d\uae30 \uc791\uc5c5\uc774 \ub290\ub824\uc9c0\uba74 \uc544\ubb34\ub3c4 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc744 \uac83\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc774\ub807\uac8c \uc11c\ubc84\ub97c \uc5ec\ub7ec \ub300 \ub450\uc5b4 \ud558\ub098\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc11c\ubc84\uac00 \ubc1b\ub294 \ubd80\ud558\ub97c \ubd84\uc0b0\uc2dc\ud0a8\ub2e4\uba74 \uc131\ub2a5\uc774 \ud5a5\uc0c1 \ub420 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ub450\ubc88\uc9f8\ub85c\ub294 \uace0\uac00\uc6a9\uc131\uc785\ub2c8\ub2e4. \ud604\uc7ac \uc800\ud76c\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub294 \ud558\ub098\uc758 \uc11c\ubc84\ub85c SPOF \ubb38\uc81c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc801\uc6a9\ud558\uc5ec \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c \ubd84\uc0b0\ud55c\ub2e4\uba74 \ud558\ub098\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uac00 \uc7a5\uc560\uac00 \uc0dd\uaca8 \uc911\uc9c0\uac00 \ub418\ub354\ub77c\ub3c4, \ub2e4\ub978 \uc11c\ubc84\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub85c \uc11c\ube44\uc2a4\ub97c \uc774\uc5b4\ub098\uac08 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n# \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ubcf5\uc81c \ubc29\uc2dd\\n\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ubcf5\uc81c \ubc29\uc2dd\uc740 \ud06c\uac8c \ub450\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4. **Binary Log\ub85c \ubcf5\uc81c\ud558\ub294 \ubc29\uc2dd**\uacfc **GTID(Global Transaction Id)\ub97c \ud1b5\ud574 \ubcf5\uc81c\ub97c \ud558\ub294 \ubc29\uc2dd**\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## Binary log \ubcf5\uc81c \ubc29\uc2dd\\n\uba3c\uc800 Binary Log \ub294 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \uc218\ud589\ud55c \ucffc\ub9ac (\uc0ac\uc6a9\uc790 \ucd94\uac00, \uc778\ub371\uc2a4 \ucd94\uac00, Update, Insert, Delete \ub4f1 ) \ubaa8\ub4e0 \uc815\ubcf4\ub97c Binary Log\uc5d0 \uae30\ub85d\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ud574\ub2f9 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\uc5d0\ub294 \uc774\ubca4\ud2b8\ub9c8\ub2e4 Mysql \uc11c\ubc84\uc758 \uace0\uc720\ud55c Server id\ub97c \uac00\uc9c0\uace0 \uc788\ub294\ub370, \ud574\ub2f9 Id\uac00 \uac19\uc740 \uc11c\ubc84\uc5d0\uc11c\ub294 \ud574\ub2f9 \uc774\ubca4\ud2b8\ub97c \uc790\uc2e0\uc774 \ubc1c\uc0dd\uc2dc\ud0a8 \uc774\ubca4\ud2b8\ub85c \uac04\uc8fc\ud558\uace0 \uc801\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uadf8\ub7ec\ubbc0\ub85c \uac01\uac01\uc758 \uace0\uc720\ud55c server id\ub97c \uc124\uc815\ud574\uc918\uc57c \ud569\ub2c8\ub2e4.\\n\uc774 **\ubc14\uc774\ub108\ub9ac \ub85c\uadf8 \ud30c\uc77c\uc758 \uc704\uce58\uc640 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8 \ud30c\uc77c\uba85**\uc744 \ud1b5\ud574 Replica \uc11c\ubc84\ub294 Source \uc11c\ubc84\uc758 \uc774\ubca4\ud2b8\ub97c \uc801\uc6a9\ud569\ub2c8\ub2e4\\n## GTID \ubcf5\uc81c \ubc29\uc2dd\\nMysql 5.5 \ubc84\uc804 \uc774\uc0c1\ubd80\ud130\ub294 GTID \uae30\ubc18 \ubcf5\uc81c\ub3c4 \uac00\ub2a5\ud558\uac8c \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4 GTID\ub294 source id\uc640 transaction id\uac00 \uc870\ud569\ub41c \ubc29\uc2dd\uc73c\ub85c \uc0dd\uc131\ub429\ub2c8\ub2e4. source id\ub294 \ud2b8\ub79c\uc7ad\uc158\uc774 \ubc1c\uc0dd\ud55c \uc18c\uc2a4 \uc11c\ubc84\ub97c \uc2dd\ubcc4\ud558\uae30 \uc704\ud55c \uac12\uc73c\ub85c server\uc758 uuid \uc785\ub2c8\ub2e4.\\n```sql\\n+--------------------------------------+\\n| source_uuid |\\n+--------------------------------------+\\n| c3a2296b-31a2-11ee-b887-02a8cf0173ac |\\n+--------------------------------------+\\n```\\n\uc774\ub7ec\ud55c GTID\ub97c \uae30\ubc18\uc73c\ub85c Source \uc11c\ubc84\ub97c \uad6c\ubd84\ud558\uace0 Binary Log \ud30c\uc77c\uc5d0 \uae30\ub85d\ub41c GTID\ub97c \ud655\uc778\ud558\uc5ec \ub9c8\uc9c0\ub9c9\uc5d0 \uc801\uc6a9\ud55c \uc774\ubca4\ud2b8\ub97c \ud655\uc778\ud558\uace0, \uc801\uc6a9\ud558\uc9c0 \uc54a\uc740 \uc774\ubca4\ud2b8\ub97c \uc21c\ucc28\ub300\ub85c \uc2e4\ud589\uc2dc\ucf1c \ubcf5\uc81c\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \ub450\uac00\uc9c0 \ubc29\ubc95 \uc911 \uc800\ud76c\ub294 GTID \ubc29\uc2dd\uc758 \ubcf5\uc81c\ub97c \uc120\ud0dd\ud588\uc2b5\ub2c8\ub2e4. \uc774\uc720\ub294 \uac04\ub2e8\ud569\ub2c8\ub2e4.\\n```mermaid\\ngraph TD\\n Source[Source Server: BinaryLog10] --\x3e Replica1[Replica1: BinaryLog10]\\n Source[Source Server: BinaryLog10] --\x3e Replica2[Replica2: BinaryLog9]\\n```\\n\uc774\ub7f0 \ubc29\uc2dd\uc73c\ub85c \ud1a0\ud3f4\ub85c\uc9c0\ub97c \uad6c\uc131\ud588\ub2e4\uace0 \uac00\uc815\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4. Source \uc11c\ubc84\uc5d0\uc11c\ub294 Binary Log 10\ubc88 \ud30c\uc77c\uae4c\uc9c0 \uc774\ubca4\ud2b8\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 Replica1 \uc5d0\uc11c\ub294 Source \uc11c\ubc84\uc758 \uc774\ubca4\ud2b8\uac00 \ucd5c\uc2e0\ud654 \ub418\uc5b4 \uc788\uc9c0\ub9cc, Replica2 \uc11c\ubc84\ub294 \uc544\uc9c1 \ucd5c\uc2e0\ud654\uac00 \ub418\uc9c0 \uc54a\uc740 \uc0c1\ud669\uc785\ub2c8\ub2e4. \uc774 \uc0c1\ud669\uc5d0\uc11c Source Server\uc5d0 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud558\uc5ec \uc11c\ubc84\uac00 \uc911\ub2e8 \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 Replica1 \uc11c\ubc84\ub97c Source \uc11c\ubc84\ub85c \uc2b9\uaca9\ud569\ub2c8\ub2e4. \uc774\ub807\uac8c \ub41c\ub2e4\uba74 Replica1 \uc11c\ubc84\uc5d0\uc11c \ubaa8\ub4e0 \ucffc\ub9ac\uc758 \uc694\uccad\uc774 \ub4e4\uc5b4\uc624\uac8c \ub429\ub2c8\ub2e4. BinaryLog10\uc774\ub77c\ub294 \ud30c\uc77c\uc758 \uc704\uce58\uc640 \ud30c\uc77c\uc744 \ucc3e\uc744 \ubc29\ubc95\uc774 \uc5c6\uae30 \ub54c\ubb38\uc5d0 Source\uc11c\ubc84\uac00 \ubcf5\uad6c\ub418\uc9c0 \uc54a\ub294 \uc774\uc0c1 \ud639\uc740 Replica 1 \uc11c\ubc84\uc758 Relay Log\uac00 \ub0a8\uc544\uc788\uc9c0 \uc54a\ub294 \uc774\uc0c1 Replica2 \uc11c\ubc84\ub294 \uc808\ub300 \ucd5c\uc2e0\ud654\ub420 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774\ub7f0 \uc2dd\uc758 \ubc29\uc2dd\uc774\ub77c\uba74 Source \uc11c\ubc84\uac00 \uc911\ub2e8\ub418\uc5c8\uc744 \ub54c \ub2e4\ub978 \uc11c\ubc84\uac00 \ub3d9\uc791\ud558\uae30 \ub54c\ubb38\uc5d0 \uace0\uac00\uc6a9\uc131 \ubb38\uc81c\ub294 \ud574\uacb0\ub41c \uac83 \uac19\uc9c0\ub9cc, Replica2 \uc11c\ubc84\ub294 \uc544\ubb34 \uc77c\ub3c4 \ud558\uc9c0\uc54a\uace0 \ub0a8\uc544\uc788\ub294 \uc11c\ubc84, \uc989 Source \uc11c\ubc84 \ud558\ub098\uac00 \uc911\ub2e8\ub418\uc5c8\uc73c\ub098 **2\ub300\uc758 \uc11c\ubc84\uac00 \uc911\ub2e8\ub41c \uac83**\uacfc \ub9c8\ucc2c\uac00\uc9c0\uc785\ub2c8\ub2e4.\\n\\n\uc774\ub7ec\ud55c \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574 GTID\uac00 \ub4f1\uc7a5\ud588\uc2b5\ub2c8\ub2e4. GTID \ubc29\uc2dd\uc740 Binary Log\uc758 \uc704\uce58\uc640 \ud30c\uc77c\uba85\uc774 \ud544\uc694\ud55c \uac83\uc774 \uc544\ub2cc \ub2e4\uc74c \uc774\ubca4\ud2b8\uc758 GTID\ub9cc \uc788\ub2e4\uba74 \ud574\ub2f9 \uc774\ubca4\ud2b8\ub97c \ubc14\ub85c \uc801\uc6a9\ud560 \uc218 \uc788\ub2e4\ub294 \uc810\uc785\ub2c8\ub2e4. Source \uc11c\ubc84\ub85c \uc2b9\uaca9\ub41c Replica1 \uc11c\ubc84\uc5d0\uc11c **GTID\ub97c \ubc1b\uc544 \uc801\uc6a9\ud558\uc5ec \ucd5c\uc2e0\ud654**\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uc800\ud76c \ud300\uc758 \ubcf5\uc81c \ubc29\uc2dd\\n\uc774\ub7ec\ud55c \uc7a5\uc810\uc73c\ub85c GTID\uae30\ubc18 \ubcf5\uc81c \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n# \ubcf5\uc81c \ub3d9\uae30\ud654 \ubc29\uc2dd\\n\\n\ubcf5\uc81c \ubc29\uc2dd\uc5d0\ub294 \ud06c\uac8c \ub450\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4. **\ube44\ub3d9\uae30 \ubcf5\uc81c**\uc640 **\ubc18\ub3d9\uae30 \ubcf5\uc81c**\uc785\ub2c8\ub2e4.\\n\\n## \ube44\ub3d9\uae30 \ubcf5\uc81c\\n\ube44\ub3d9\uae30 \ubcf5\uc81c\ub294 \ub9d0\uadf8\ub300\ub85c \ube44\ub3d9\uae30\ub85c \ubcf5\uc81c\ud558\ub294 \uac83\uc785\ub2c8\ub2e4. \uc544\uc8fc \uac04\ub2e8\ud569\ub2c8\ub2e4. Source \uc11c\ubc84\uc5d0\uc11c \uc5b4\ub5a0\ud55c \uc774\ubca4\ud2b8\uac00 \ubc1c\uc0dd\ud560 \ub54c Replica \uc11c\ubc84\uc758 \ubc18\uc601\uacfc \uc0c1\uad00\uc5c6\uc774 \ub3d9\uc791\ud558\ub294 \uac83\uc785\ub2c8\ub2e4.\\n\uc18c\uc2a4 \uc11c\ubc84\uc5d0\uc11c \ucee4\ubc0b\ub41c \ud2b8\ub79c\uc7ad\uc158\uc740 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\uc5d0 \uae30\ub85d\ub418\uace0, \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc5d0\uc11c\ub294 \uc8fc\uae30\uc801\uc73c\ub85c \uc0c8\ub85c\uc6b4 \ud2b8\ub79c\uc7ad\uc158\uc5d0 \ub300\ud55c \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\ub97c \uc694\uccad\ud569\ub2c8\ub2e4. \uc774\ub7ec\ud55c \ubc29\uc2dd\uc740 \uc18c\uc2a4 \uc11c\ubc84\ub294 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \uc81c\ub300\ub85c \ubcc0\uacbd \ub418\uc5c8\ub294\uc9c0 \uc54c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc989 \ub370\uc774\ud130 \uc815\ud569\uc131\uc5d0 \ubb38\uc81c\uac00 \uc0dd\uae34\ub2e4\ub294 \ub2e8\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc774\ub7ec\ud55c \ubc29\uc2dd\uc740 \uc18c\uc2a4 \uc11c\ubc84\uac00 \uac01 \ud2b8\ub79c\uc7ad\uc158\uc5d0 \ub300\ud574 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\ub85c \uc804\uc1a1\ub418\ub294 \ubd80\ubd84\uc744 \uace0\ub824\ud558\uc9c0 \uc54a\ub294\ub2e4\ub294 \uc810\uc774 \uc18d\ub3c4 \uce21\uba74\uc5d0\uc11c \ube60\ub974\uace0, \ub610 \uc5ec\ub7ec \ub300\uc758 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\ub97c \uad6c\uc131\ud558\ub354\ub77c\ub3c4 \ud070 \uc131\ub2a5 \uc800\ud558\uac00 \uc5c6\ub2e4\ub294 \uc810\uc774\uc11c \uc7a5\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \ubc18\ub3d9\uae30 \ubcf5\uc81c\\n\ubc18\ub3d9\uae30 \ubcf5\uc81c\ub294 \ube44\ub3d9\uae30 \ubcf5\uc81c\ubcf4\ub2e4 \uc880 \ub354 \ub370\uc774\ud130 \uc815\ud569\uc131\uc774 \uc62c\ub77c\uac11\ub2c8\ub2e4. \uc18c\uc2a4 \uc11c\ubc84\ub294 \ubcc0\uacbd\ub41c \ud2b8\ub79c\uc7ad\uc158\uc774 \uc788\uc744 \ub54c \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \ub2e4 \uc804\uc1a1\uc774 \ub418\uc5c8\ub2e4\ub294 ACK \uc2e0\ud638\ub97c \ubc1b\uae30 \ub54c\ubb38\uc5d0 \ud655\uc2e4\ud788 \uc54c \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc804\uc1a1\uc5ec\ubd80\ub9cc \ud655\uc778\ud558\uae30 \ub54c\ubb38\uc5d0 \ud2b8\ub79c\uc7ad\uc158\uc774 \ubc18\uc601\uc774 \ub418\uc5c8\ub2e4\ub294 \ubcf4\uc7a5\uc740 \uc5c6\uc2b5\ub2c8\ub2e4. \ubc18\ub3d9\uae30 \ubcf5\uc81c \ubc29\uc2dd\uc740 2\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1. **After sync**: After Sync \ubc29\uc2dd\uc740 \uc18c\uc2a4 \uc11c\ubc84\uc5d0\uc11c \ud2b8\ub79c\uc7ad\uc158\uc744 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\uc5d0 \uae30\ub85d \ud6c4 Storage Engine\uc5d0 \ubc14\ub85c \ucee4\ubc0b\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\uc5d0 \uae30\ub85d \ud6c4 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc758 ACK \uc751\ub2f5\uc744 \uae30\ub2e4\ub9bd\ub2c8\ub2e4. \uadf8\ub9ac\uace0 ACK \uc751\ub2f5\uc774 \ub3c4\ucc29\ud558\uba74 \uadf8\uc81c\uc11c\uc57c \uc2a4\ud1a0\ub9ac\uc9c0 \uc5d4\uc9c4\uc744 \ucee4\ubc0b\ud558\uc5ec \ud2b8\ub79c\uc7ad\uc158\uc744 \ucc98\ub9ac\ud558\uace0 \uacb0\uacfc\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4.\\n2. **After commit**: After commit\uc740 \uc774\ub984 \uadf8\ub300\ub85c \ucee4\ubc0b\uc744 \uba3c\uc800 \ud558\ub294 \uac83\uc785\ub2c8\ub2e4. \ud2b8\ub79c\uc7ad\uc158\uc774 \uc0dd\uae30\uba74 \uba3c\uc800 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\uc5d0 \uae30\ub85d \ud6c4 \uc18c\uc2a4 \uc11c\ubc84 \uc2a4\ud1a0\ub9ac\uc9c0 \uc5d4\uc9c4\uc5d0 \ucee4\ubc0b\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc758 ACK \uc751\ub2f5\uc774 \ub0b4\ub824\uc624\uba74 \ud074\ub77c\uc774\uc5b8\ud2b8\ub294 \ucc98\ub9ac \uacb0\uacfc\ub97c \uc5bb\uace0 \ub2e4\uc74c \ucffc\ub9ac\ub97c \uc218\ud589\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 after commit \ubc29\uc2dd\uc740 \uc18c\uc2a4 \uc11c\ubc84\uc5d0 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c \ud32c\ud140 \ub9ac\ub4dc\uac00 \ubc1c\uc0dd\ud558\uac8c \ub429\ub2c8\ub2e4. \ud2b8\ub79c\uc7ad\uc158\uc774 \uc2a4\ud1a0\ub9ac\uc9c0 \uc5d4\uc9c4 \ucee4\ubc0b\uae4c\uc9c0\ub41c \ud6c4 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc758 \uc751\ub2f5\uc744 \uae30\ub2e4\ub9bd\ub2c8\ub2e4. \uc774\ucc98\ub7fc \uc2a4\ud1a0\ub9ac\uc9c0 \uc5d4\uc9c4 \ucee4\ubc0b\uae4c\uc9c0 \uc644\ub8cc\ub41c \ub370\uc774\ud130\ub294 \ub2e4\ub978 \uc138\uc158\uc5d0\uc11c\ub3c4 \uc870\ud68c\uac00 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud2b8\ub79c\uc7ad\uc158\uc774 \ucee4\ubc0b\ub418\uc5c8\uace0, \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\ub85c \uc544\uc9c1 \uc751\ub2f5\uc744 \uae30\ub2e4\ub9b4 \ub54c, \uc18c\uc2a4 \uc11c\ubc84\uc5d0 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud55c\ub2e4\uba74 \uc0c8\ub85c\uc6b4 \uc18c\uc2a4 \uc11c\ubc84\ub85c \uc2b9\uaca9\ub41c \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc5d0\uc11c \ub370\uc774\ud130\ub97c \uc870\ud68c\ud560 \ub54c \uc790\uc2e0\uc774 \uc774\uc804 \uc18c\uc2a4 \uc11c\ubc84\uc5d0\uc11c \uc870\ud68c\ud588\ub358 \ub370\uc774\ud130\ub97c \ubcf4\uc9c0 \ubabb\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc774\ucc98\ub7fc \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \uc2b9\uaca9\ub41c \uc0c1\ud669\uc5d0 \uc18c\uc2a4 \uc11c\ubc84\uc758 \uc7a5\uc560\uac00 \ubcf5\uad6c\ub418\uc5b4 \uc7ac\uc0ac\uc6a9\ud560 \uacbd\uc6b0 \uc774\ubbf8 \ucee4\ubc0b\ub41c \uadf8 \ud2b8\ub79c\uc7ad\uc158\uc744 \uc218\ub3d9\uc73c\ub85c \ub864\ubc31 \uc2dc\ucf1c\uc57c\ub9cc \ub370\uc774\ud130\uac00 \ub9de\ub294 \uc0c1\ud669\uc774 \uc0dd\uae41\ub2c8\ub2e4.\\n\\n### \uc800\ud76c \ud300\uc758 \ubcf5\uc81c \ub3d9\uae30\ud654 \ubc29\uc2dd\\n\uc774\ub7ec\ud55c \uc7a5\ub2e8\uc810\uc73c\ub85c \uc800\ud76c \ud300\uc740 \ub370\uc774\ud130 \ubb34\uacb0\uc131\uc774 \uc911\uc694\ud558\ub2e4 \ud310\ub2e8\ub418\uc5b4 \ubc18\ub3d9\uae30 \ubcf5\uc81c \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud558\uace0, After Sync \ubc29\uc2dd\uc744 \uc801\uc6a9\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n# \ubcf5\uc81c \ud1a0\ud3f4\ub9ac\uc9c0\\n\\n\ubcf5\uc81c \ud1a0\ud3f4\ub9ac\uc9c0\ub294 \uc5ec\ub7ec\uac00\uc9c0 \ubc29\uc2dd \uc911 \uc790\uc2e0\uc758 \uc0c1\ud669\uacfc \uac00\uc7a5 \ub9de\ub294 \ubc29\uc2dd\uc744 \uc0ac\uc6a9\ud558\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4. \uc800\ud76c \ud300\uc774 \uace0\ub824\ud574\uc57c\ud560 \ubb38\uc81c\ub294 \uba3c\uc800 \uc131\ub2a5\uc744 \uc62c\ub824\uc57c \ud588\uace0, \ub2e8\uc77c \uc7a5\uc560\ud3ec\uc778\ud2b8\ub97c \uac1c\uc120\ud574\uc57c\ud588\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uc11c\ubc84\ub294 2\ub300 \ubfd0\uc774\uc600\uc2b5\ub2c8\ub2e4. \uc774\ub7ec\ud55c \uc0c1\ud669\uc5d0\uc11c \uc5b4\ub5a4 \ubc29\uc2dd\uc744 \ud0dd\ud560 \uc218 \uc788\uc744\uae4c\uc694?\\n\\n## \uc2f1\uae00 \ub808\ud50c\ub9ac\uce74\\n```mermaid\\ngraph LR\\n A[Application Server] -- Read + Write --\x3e S[Source]\\n A -- Read --\x3e R[Replica]\\n S--\x3e R\\n```\\n\uac00\uc7a5 \uae30\ubcf8\uc801\uc774\uba70 \uac00\uc7a5 \ub9ce\uc774 \uc4f0\uc774\ub294 \ud615\ud0dc\uc785\ub2c8\ub2e4. \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc5d0\uc11c \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc5d0 \uc77d\uae30 \uc694\uccad\uc744 \uc804\ub2ec\ud558\uba74, \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc5d0 \ubb38\uc81c\uac00 \uc0dd\uacbc\uc744 \ub54c, \uc11c\ube44\uc2a4 \uc7a5\uc560 \uc0c1\ud669\uc774 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub7ec\ubbc0\ub85c \uc18c\uc2a4 \uc11c\ubc84\uc5d0\uc11c Read, Write\ub97c \ub458 \ub2e4 \ud558\uace0, \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\ub294 failover\ub97c \uc704\ud574 \ub300\uae30\ud558\ub294 \uc608\ube44\uc6a9 \uc11c\ubc84\ub85c \uad6c\uc131\ud569\ub2c8\ub2e4.\\n\uc18c\uc2a4 \uc11c\ubc84\uc5d0 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c \uc18c\uc2a4 \uc11c\ubc84\ub97c \ub300\uccb4\ud558\uac70\ub098 \ub370\uc774\ud130\ub97c \ubc31\uc5c5\ud558\ub294 \uc6a9\ub3c4\ub85c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\\n\\n## \uba40\ud2f0 \ub808\ud50c\ub9ac\uce74\\n\\n```mermaid\\ngraph LR\\n A[Application Server] -- Read + Write --\x3e S[Source]\\n A -- Read --\x3e R1[Replica1]\\n S --\x3e R1\\n S --\x3e R2[Replica2]\\n```\\n\uc2f1\uae00 \ub808\ud50c\ub9ac\uce74\uc640 \ube44\uc2b7\ud55c \uad6c\uc131\uc774\uc9c0\ub9cc \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \ud55c \ub300 \ub354 \ucd94\uac00\ub41c \uad6c\uc131\uc785\ub2c8\ub2e4. \ud574\ub2f9 \ubc29\uc2dd\uc740 SPOF \ubb38\uc81c\uac00 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84 \ud558\ub098\ub97c \uc77d\uae30 \uc804\uc6a9 \uc11c\ubc84\ub85c \ub458 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc77d\uae30 \uc791\uc5c5\uc744 \ubd84\uc0b0\ud568\uc73c\ub85c \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc758 \uc131\ub2a5\uc744 \ud5a5\uc0c1 \uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc544\uae4c \ub9d0\ud588\ub358 \uc7a5\uc560 \uc0c1\ud669\uc774 \ubc1c\uc0dd\ud558\uba74 \uc608\ube44\uc6a9 \uc11c\ubc84\uc778 Replica2 \uc11c\ubc84\ub97c Source \uc11c\ubc84 \ud639\uc740 Replica1(\uc77d\uae30 \uc804\uc6a9) \uc11c\ubc84\ub85c \ub300\uccb4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uccb4\uc778 \ubcf5\uc81c\\n\\n```mermaid\\ngraph LR\\n A[Application Server] -- Read + Write --\x3e S[Source1]\\n A -- Read --\x3e R1-1[Replica1-1]\\n S --\x3e R1-1\\n S --\x3e R1-2[Replica1-2]\\n S --\x3e R1-3[Replica1-3 / Source2]\\n R1-3 --\x3e R2-1[Replica2-1]\\n R1-3 --\x3e R2-2[Replica2-2]\\n B[Batch Server] --Read--\x3e R2-2\\n\\n```\\n\ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \ub9ce\uc544\uc838 \uc18c\uc2a4 \uc11c\ubc84\uc758 \ubc14\uc774\ub108\ub9ac \ub85c\uadf8\ub97c \uc77d\ub294 \ubd80\ud558\uac00 \ub9ce\uc544\uc9c8 \ub54c \ud560 \uc218 \uc788\ub294 \uad6c\uc131\uc785\ub2c8\ub2e4. \uc880 \uc804\uc5d0 \uc124\uba85\ub4dc\ub838\ub358 \uba40\ud2f0 \ub808\ud50c\ub9ac\uce74 \ubc29\uc2dd\uc5d0\uc11c \ub611\uac19\uc740 \uad6c\uc131\uc744 \ucd94\uac00\ud55c \ubc29\uc2dd\uc73c\ub85c \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. Source 1 \uc758 \uc815\ubcf4\ub97c \ubcf5\uc81c\ud55c Replica 1-1, 1-2 \uc11c\ubc84\ub294 \ube60\ub974\uac8c \ub370\uc774\ud130\uac00 \ubc18\uc601\ub418\uc9c0\ub9cc, Source1\uc758 \uc774\ubca4\ud2b8\ub97c \ubcf5\uc81c\ud55c Source2\ub97c \ubcf5\uc81c\ud55c Replica 2-1, 2-2 \uc11c\ubc84\ub294 \ub2f9\uc5f0\ud788 \ub2a6\uac8c \ubc18\uc601\ub418\uae30 \ub54c\ubb38\uc5d0 \ud574\ub2f9 \uadf8\ub8f9\uc740 \uc608\ube44\uc6a9\uc73c\ub85c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\\n\\n## \ub4c0\uc5bc \uc18c\uc2a4 \ubcf5\uc81c\\n\\n```mermaid\\ngraph LR\\n A[Application Server] -- Read + Write --\x3e S1[Source/Replica 1]\\n A -- Read + Write --\x3e S2[Source/Replica 2]\\n S1 <-- Replication --\x3e S2\\n```\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub458 \ub2e4 \uc18c\uc2a4 \uc11c\ubc84\uc774\uba74\uc11c \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uc778 \uacbd\uc6b0\uc785\ub2c8\ub2e4. \uc774 \uacbd\uc6b0\ub294 **Active-Active**\uad6c\uc131\uacfc **Active-Passive** \uad6c\uc131\uc73c\ub85c \ub098\ub269\ub2c8\ub2e4\\n\\nActive-Active\ub294 \uc11c\ubc84 \ub458 \ub2e4 \uc77d\uae30\uc640 \uc4f0\uae30\uac00 \uac00\ub2a5\ud55c \ud615\ud0dc\uc785\ub2c8\ub2e4. \uc989 \ubd80\ud558\ub97c \ubd84\uc0b0\uc2dc\ud0a4\uae30 \uc704\ud574 \uc11c\ubc84 \ubaa8\ub450 \uc77d\uace0 \uc4f0\ub294 \uc791\uc5c5\uc744 \ud558\ub294 \uac83\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc774\ub7ec\ud55c \ubc29\uc2dd\uc740 \ubed4\ud55c \ub2e8\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uc11c\ub85c\uc758 \uc774\ubca4\ud2b8\uac00 \ub3d9\uae30\ud654 \ub418\uae30 \uc804\uc5d0\ub294 \uc815\ud569\uc131\uc774 \uae68\uc9c8 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub610 \ub3d9\uc2dc\uc5d0 \uac19\uc740 \ub370\uc774\ud130\uc5d0 \ub300\ud574 \uc4f0\uae30 \uc791\uc5c5\uc744 \uc218\ud589\ud560 \ub54c, \ud558\ub098\uc758 \uc11c\ubc84\uc5d0\uc11c \uc4f0\uae30\uac00 \uc644\ub8cc\ub418\uc5c8\ub354\ub77c\ub3c4, \ub2e4\ub978 \ud558\ub098\uc758 \uc11c\ubc84\uc5d0 \ub2a6\uac8c \ub05d\ub09c \uc4f0\uae30\uac00 \uc788\ub2e4\uba74 \ub9c8\uc9c0\ub9c9 \ud2b8\ub79c\uc7ad\uc158\uc778 \ub2a6\uac8c \ub05d\ub09c \uc4f0\uae30 \uc791\uc5c5\uc774 \ubc18\uc601\ub418\uc5b4 \uc608\uc0c1\ud558\uc9c0 \ubabb\ud55c \uacb0\uacfc\uac00 \ub098\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub610 \ub2e4\ub978 \ubb38\uc81c\ub85c\ub294 Auto Increment\ub97c \uc0ac\uc6a9\ud560 \ub54c\uc785\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \ub370\uc774\ud130\uac00 \ub3d9\uc2dc\uc5d0 \uc0dd\uc131\ub420 \ub54c Auto Increment\uac00 \uc911\ubcf5\ub418\ub294 \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uae30 \ub54c\ubb38\uc5d0 \ud574\ub2f9 \ud1a0\ud3f4\ub85c\uc9c0\uc5d0\uc11c\ub294 ID\ub97c DB\uc5d0 \uc758\uc874\ud558\uc9c0 \uc54a\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4.\\n\\nActive-Passive \ubc29\uc2dd\uc740 \ud558\ub098\uc758 \uc11c\ubc84\ub9cc \uc77d\uae30\uc640 \uc4f0\uae30 \uc694\uccad\uc774 \ub418\uc9c0\ub9cc, \ub098\uba38\uc9c0 \uc11c\ubc84\ub294 \ub300\uae30\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \ub450 \uc11c\ubc84 \ubaa8\ub450 \uc5b8\uc81c\ub098 \uc4f0\uae30 \uc791\uc5c5\uc774 \uac00\ub2a5\ud55c \ud615\ud0dc\uc774\uae30 \ub54c\ubb38\uc5d0 \uc7a5\uc560 \ubc1c\uc0dd \uc2dc \ube60\ub974\uac8c Faliover\ud560 \uc218 \uc788\ub2e4\ub294 \uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uba40\ud2f0 \uc18c\uc2a4 \ubcf5\uc81c\\n\\n\\n```mermaid\\ngraph LR\\n A[Application Server] -- Read + Write --\x3e S1[Source 1]\\n A[Application Server] -- Read + Write --\x3e S2[Source 2]\\n A[Application Server] -- Read + Write --\x3e S3[Source 3]\\n A[Application Server] -- Read + Write --\x3e S4[Source 4]\\n S1 --\x3e R[Replica]\\n S2 --\x3e R[Replica]\\n S3 --\x3e R[Replica]\\n S4 --\x3e R[Replica]\\n```\\n\ud558\ub098\uc758 \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\uac00 \ub2e4\uc218\uc758 \uc18c\uc2a4 \uc11c\ubc84\ub97c \uac16\ub294 \uad6c\uc131\uc785\ub2c8\ub2e4. \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc0e4\ub529\uc744 \ud574\ub480\ub294\ub370, \ub2e4\uc2dc \ud558\ub098\uc758 \uc11c\ubc84\ub85c \ud1b5\ud569\ud558\uace0 \uc2f6\uc744 \ub54c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud639\uc740 \uc11c\ub85c \ub2e4\ub978 \ub370\uc774\ud130\ub97c \ud55c \uacf3\uc5d0 \ubc31\uc5c5\uc744 \ud560 \ub54c\ub3c4 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uc800\ud76c \ud300\uc758 \ud1a0\ud3f4\ub85c\uc9c0 \ubc29\uc2dd\\n\uadf8\ub7fc \uc774\ub807\uac8c\ub098 \ub9ce\uc740 \uad6c\uc131 \uc911\uc5d0 \uc800\ud76c \ud300\uc5d0\uc11c \ud0dd\ud560 \uc218 \uc788\ub294 \ud1a0\ud3f4\ub85c\uc9c0 \ubc29\uc2dd\uc740 \uc2f1\uae00 \ub808\ud50c\ub9ac\uce74 \ubc29\uc2dd\uacfc \ub4c0\uc5bc \uc18c\uc2a4 \ubcf5\uc81c \ubc29\uc2dd \ubc16\uc5d0 \uc5c6\uc2b5\ub2c8\ub2e4. \uc65c\ub0d0\ud558\uba74 \uc8fc\uc5b4\uc9c4 \uc11c\ubc84\uac00 2\ub300\ubfd0\uc774\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \ub4c0\uc5bc \uc18c\uc2a4 \ubc29\uc2dd\uc740 \uc801\uc6a9\ud558\ub294\ub370 \ubb34\ub9ac\uac00 \uc788\ub294 \ubd80\ubd84\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uc77c\ub2e8 \uc800\ud76c\uac00 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc801\uc6a9\ud558\ub824\ub294 \uac00\uc7a5 \ud070 \uc774\uc720\ub294 **\uc131\ub2a5** \uc774\uae30 \ub54c\ubb38\uc5d0 \uc131\ub2a5\uc774 \ubcc0\ud558\uc9c0 \uc54a\ub294 \ub4c0\uc5bc \uc18c\uc2a4\uc758 Active-Passive \ubc29\uc2dd\uc740 \uc81c\uc678\ud558\uaca0\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 Active-Active \ubc29\uc2dd\uc740 \ubd80\ud558\ub97c \ubd84\uc0b0\uc2dc\ud0ac \uc218 \uc788\ub2e4\ub294 \uc7a5\uc810\uc774 \uc788\uc9c0\ub9cc, \ub2e8\uc810\uc73c\ub85c\ub294 Auto Increment\ub97c \uc0ac\uc6a9\ud558\ub294\ub370\uc5d0 \uc704\ud5d8\uc774 \uc788\ub2e4\ub294 \uc810\uacfc, \ub370\uc774\ud130\uc758 \uc815\ud569\uc131 \ubb38\uc81c\uac00 \uc0dd\uae38 \uc218 \uc788\ub2e4\ub294 \uc810\uc5d0\uc11c \ub4c0\uc5bc \uc18c\uc2a4 \ubc29\uc2dd\uc740 \uc81c\uc678\ud558\ub3c4\ub85d \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \uc2f1\uae00 \ub808\ud50c\ub9ac\uce74 \ubc29\uc2dd\uc744 \uc801\uc6a9\ud560 \uc218 \ubc16\uc5d0 \uc5c6\ub294\ub370\uc694. \uc2f1\uae00 \ub808\ud50c\ub9ac\uce74\uc758 \ubc29\uc2dd\uc740 \uac00\uc6a9\uc131 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574 \ub9cc\ub4e4\uc5b4\uc9c4 \ubc29\uc2dd\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc800\ud76c \uc11c\ube44\uc2a4\ub294 \ud604\uc7ac \uac00\uc6a9\uc131\ubcf4\ub2e4 \uc131\ub2a5\uc744 \ub354 \uc2e0\uacbd\uc368\uc57c\ud558\ub294 \uc0c1\ud669\uc774\uae30\ub54c\ubb38\uc5d0 \uc2f1\uae00 \ub808\ud50c\ub9ac\uce74 \ud1a0\ud3f4\ub85c\uc9c0\ub97c \uad6c\uc131\ud558\uc9c0\ub9cc \ub808\ud50c\ub9ac\uce74 \uc11c\ubc84\ub97c \uc608\ube44\uc6a9\uc774 \uc544\ub2cc \uc77d\uae30 \uc804\uc6a9 \ubc29\uc2dd\uc73c\ub85c \uc0ac\uc6a9\ud558\ub3c4\ub85d \ud558\uace0, \uac00\uc6a9\uc131 \ubd80\ubd84\uc744 \ud3ec\uae30\ud558\uae30\ub85c \uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n# \ucf54\ub4dc\uc5d0 \uc801\uc6a9\ud558\uae30\\n[replication-datasource](https://github.com/kwon37xi/replication-datasource) Github \uc18c\uc2a4 \ucf54\ub4dc\ub97c \ucc38\uace0\ud558\uc2dc\uac70\ub098, [DB \ubcf5\uc81c, @Transactional\uc5d0 \ub530\ub77c \uc694\uccad \ubd84\ub9ac\ud574\ubcf4\uae30](https://greeng00se.github.io/db-replication) \uae00\uc744 \ucc38\uace0\ud558\uc5ec \ub530\ub77c\ud558\uba74 \uae08\ubc29\ud558\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4!\\n\\n## \uacb0\ub860\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158 \uc0dd\uac01\ubcf4\ub2e4 \uc5b4\ub835\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc7ac\ubc0c\uc2b5\ub2c8\ub2e4. \uc778\ud504\ub77c\ub3c4 \uc7ac\ubc0c\uc2b5\ub2c8\ub2e4.\\n\\n## \ucc38\uace0\\nReal Mysql 8.0"},{"id":"31","metadata":{"permalink":"/31","source":"@site/blog/2023-09-03-improved-query-performance.mdx","title":"\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","description":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4","date":"2023-09-03T00:00:00.000Z","formattedDate":"2023\ub144 9\uc6d4 3\uc77c","tags":[{"label":"mysql","permalink":"/tags/mysql"}],"readingTime":13.275,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"31","title":"\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","authors":["boxster"],"tags":["mysql"]},"prevItem":{"title":"\ub370\uc774\ud130\ubca0\uc774\uc2a4 \ub808\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/32"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \ud611\uc5c5 \uc77c\ud654","permalink":"/30"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4\\n## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\\n\uba3c\uc800 \uc774 \uae00\uc744 \uc4f0\uac8c \ub41c \uacc4\uae30\ub97c \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4. \uce74\ud398\uc778 \ud300 \ud504\ub85c\uc81d\ud2b8\uc5d0\ub294 \uc0ac\uc6a9\uc790\uac00 \ubcf4\uace0\uc788\ub294 \uc9c0\ub3c4\uc5d0 \ucda9\uc804\uc18c\ub97c \ubcf4\uc5ec\uc8fc\ub294 \uc870\ud68c \uae30\ub2a5\uc774 \uac00\uc7a5 \uc911\uc694\ud558\uace0, \uc81c\uc77c \uc694\uccad\uc774 \ub9ce\uc774 \ub4e4\uc5b4\uc635\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc870\ud68c \uc131\ub2a5\uc774 \uc88b\uc9c0 \uc54a\uc740 \uae4c\ub2ed\uc778\uc9c0 \uc5ec\ub7ec \uc0ac\uc6a9\uc790\uac00 \uc811\uc18d\ud558\uba74 \uc544\ub798\uc640 \uac19\uc774 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uac00 \uc2e4\ud589\ub418\uace0 \uc788\ub294 \uc11c\ubc84\uc758 cpu \uc0ac\uc6a9\ub960\uc774 100%\uac00 \ub418\ub294 \ubb38\uc81c\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n![cpu](https://github.com/drunkenhw/drunkenhw.github.io/assets/106640954/2330435f-17b4-4d38-b16b-c72fd7017969)\\n\\n## \uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30\\n\\n\uba3c\uc800 \uc81c\uac00 \uac1c\uc120\ud558\uae30 \uc704\ud574 \uc0ac\uc6a9\ud588\ub358 \ubc29\ubc95\ub4e4\uc5d0 \ub300\ud574 \uc801\uc5b4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n### DTO \uc774\uc6a9\ud558\uae30\\n\\n\ud604\uc7ac \uad6c\uc870\ub294 \uc544\ub798\uc758 JPA\ub97c \uc774\uc6a9\ud574 \uc544\ub798\uc640 \uac19\uc740 \ucffc\ub9ac\ub85c entity\ub85c \ub370\uc774\ud130\ub97c \uc870\ud68c\ud569\ub2c8\ub2e4.\\n\\n```sql\\n select distinct station.station_id,\\n charger.charger_id,\\n charger.station_id,\\n chargerStatus.charger_id,\\n chargerStatus.station_id,\\n station.created_at,\\n station.updated_at,\\n station.address,\\n station.company_name,\\n station.contact,\\n station.detail_location,\\n station.is_parking_free,\\n station.is_private,\\n station.latitude,\\n station.longitude,\\n station.operating_time,\\n station.private_reason,\\n station.station_name,\\n station.station_state,\\n charger.created_at,\\n charger.updated_at,\\n charger.capacity,\\n charger.method,\\n charger.price,\\n charger.type,\\n charger.station_id,\\n charger.charger_id,\\n chargerStatus.created_at,\\n chargerStatus.updated_at,\\n chargerStatus.charger_condition,\\n chargerStatus.latest_update_time\\n from charge_station station\\n inner join\\n charger charger on station.station_id = charger.station_id\\n inner join\\n charger_status chargerStatus on charger.charger_id = chargerStatus.charger_id\\n and charger.station_id = chargerStatus.station_id\\n where station.latitude >= 37.5019194727953082567\\n and station.latitude <= 37.5092305272047217433\\n and station.longitude >= 127.044542269049714936\\n and station.longitude <= 127.058071330950285064\\n\\n```\\n\\nJPA\ub97c \ud1b5\ud574 \uc774\ub7ec\ud55c \ubc29\uc2dd\uc73c\ub85c \uc870\ud68c\ud55c\ub2e4\uba74 \uc544\uc8fc \ud3b8\ud558\uac8c \uac12\uc744 \uac00\uc838\uc624\uace0, fetch join\uc744 \ud1b5\ud574 \ud558\uc704\uc758 entity\ub4e4\uc758 \uc815\ubcf4\ub3c4 \uae54\ub054\ud558\uac8c \uac00\uc838\uc635\ub2c8\ub2e4.\\n\\n\uac00\uc838\uc628 \uac12\uc73c\ub85c \ud544\uc694\ud55c \uc815\ubcf4\ub4e4\uc744 \ub9e4\ud551\ud558\uace0 \uac00\uacf5\ud558\uc5ec \uc751\ub2f5\uc744 \ub0b4\ub824\uc92c\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc870\ud68c\ub9cc\uc744 \uc704\ud574 JPA\uc758 entity\ub97c \uc870\ud68c\ud55c\ub2e4\ub294 \uac83\uc740 \uc5ec\ub7ec \ub2e8\uc810\uc774 \uc874\uc7ac\ud569\ub2c8\ub2e4.\\n\\n\uc81c\uc77c \uba3c\uc800 \uc751\ub2f5\uc744 \ub0b4\ub824\uc904 \ub54c \ubd88\ud544\uc694\ud55c \ub370\uc774\ud130\uae4c\uc9c0 \ubaa8\ub450 \uc870\ud68c\ub97c \ud55c\ub2e4\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4.\\n\uc774\ub807\uac8c \ub9ce\uc740 \ud544\ub4dc\ub4e4\uc774 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc751\ub2f5\uc5d0\uc11c\ub294 \ub300\ubd80\ubd84\uc758 \uacbd\uc6b0 \ubaa8\ub4e0 \uc815\ubcf4\uac00 \ud544\uc694\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ubaa8\ub4e0 \uc815\ubcf4\ub97c \ub2e4 \ubcf4\ub0b4\uc8fc\ub294 \uac83\ub3c4 \uc88b\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c \uc870\ud68c\ud560 \ub54c\uc758 \uc131\ub2a5\uc774 \uc544\uc8fc \ub098\ube60\uc9d1\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \ud544\uc694\ud55c \uce7c\ub7fc\ub9cc \uc870\ud68c\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ub610 \ub2e4\ub978 \ub2e8\uc810\uc73c\ub85c\ub294 JPA\ub85c entity\ub97c \uc870\ud68c\ud560 \ub54c Hibernate \uce90\uc2dc\uc5d0 \uc800\uc7a5\ud55c\ub2e4\ub358\uac00, One To One \uc5d0\uc11c N+1 \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud558\uae30 \ub54c\ubb38\uc5d0 \uc131\ub2a5\uc801\uc778 \uc774\uc288\uac00 \uc5ec\ub7ec\uac00\uc9c0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc870\ud68c\ub9cc \ud558\ub294 api\ub77c\uba74 DTO Projection\uc73c\ub85c \ud558\ub294 \uac83\uc774 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \uc544\ub798\uc640 \uac19\uc774 \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n```sql\\nSELECT s.station_id,\\n s.station_name,\\n s.latitude,\\n s.longitude,\\n s.is_parking_free,\\n s.is_private,\\n sum(case\\n when cs.charger_condition = \'STANDBY\' then 1\\n else 0\\n end),\\n sum(case\\n when c.capacity >= 50 then 1\\n else 0\\n end)\\nFROM charge_station s\\n inner join charger c on (c.station_id = s.station_id)\\n inner join charger_status cs on (c.charger_id = cs.charger_id and c.station_id = cs.station_id)\\nwhere s.station_id in (?, ?)\\ngroup by s.station_id;\\n```\\n\\n\uc774\ub807\uac8c \ud544\uc694\ud55c \uce7c\ub7fc\ub9cc \uc870\ud68c\ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \ubcc0\uacbd\ud558\uc5ec, \uc120\ub989\uc5ed \uadfc\ucc98\ub97c \uc870\ud68c\ud558\ub294 \uae30\uc900\uc73c\ub85c \uc57d 450ms -> 350ms\ub85c \uac1c\uc120\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc544\uc9c1\ub3c4 \ub108\ubb34 \ub290\ub9b0 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub798\uc11c \uc2e4\ud589 \uacc4\ud68d\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n### \uc2e4\ud589 \uacc4\ud68d \ud655\uc778\ud558\uae30\\n\\nsql\uc758 \uc2e4\ud589 \uacc4\ud68d\uc740 \uc544\uc8fc \uc911\uc694\ud558\uace0 \uc131\ub2a5\uc744 \uac1c\uc120\ud560 \ub54c \uc544\uc8fc \uc720\uc6a9\ud569\ub2c8\ub2e4.\\n\\n\uc2e4\ud589 \uacc4\ud68d\uc5d0\ub294 \uc5ec\ub7ec\uac00\uc9c0 \uc815\ubcf4\ub4e4\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n1. **ID**: \uc2e4\ud589 \uacc4\ud68d \ub0b4\uc5d0\uc11c \uac01 \uc791\uc5c5 \ub610\ub294 \ub2e8\uacc4\ub97c \uc2dd\ubcc4\ud558\ub294 \uc77c\ub828\ubc88\ud638\uc785\ub2c8\ub2e4. \uc2e4\ud589 \uacc4\ud68d\uc740 \uc5ec\ub7ec \ub2e8\uacc4\ub85c \ub098\ub258\uba70, ID\ub97c \ud1b5\ud574 \uc774\ub7ec\ud55c \ub2e8\uacc4\ub97c \uc2dd\ubcc4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n2. **Select Type**: \ucffc\ub9ac\uc758 \uac01 \ub2e8\uacc4(\uc608: SIMPLE, PRIMARY, SUBQUERY)\uc5d0 \ub300\ud55c \uc2e4\ud589 \uc720\ud615\uc744 \ub098\ud0c0\ub0c5\ub2c8\ub2e4. \uc774\ub294 MySQL\uc774 \ub370\uc774\ud130\ub97c \uc120\ud0dd\ud558\uace0 \ucc98\ub9ac\ud558\ub294 \ubc29\uc2dd\uc744 \ub098\ud0c0\ub0c5\ub2c8\ub2e4.\\n\\n3. **Table**: \uc2e4\ud589 \uacc4\ud68d\uc5d0 \ud3ec\ud568\ub41c \ud14c\uc774\ube14\uc758 \uc774\ub984 \ub610\ub294 \ubcc4\uce6d\uc785\ub2c8\ub2e4. \uc5b4\ub5a4 \ud14c\uc774\ube14\uc774 \uc0ac\uc6a9\ub418\ub294\uc9c0\ub97c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n4. **Type**: \ud14c\uc774\ube14 \uc811\uadfc \ubc29\uc2dd\uc744 \ub098\ud0c0\ub0c5\ub2c8\ub2e4. \uc774 \uac12\uc740 \uc778\ub371\uc2a4 \uc2a4\uce94, \ud480 \ud14c\uc774\ube14 \uc2a4\uce94 \ub4f1\uacfc \uac19\uc740 \uac12\uc77c \uc218 \uc788\uc73c\uba70, \uc131\ub2a5\uc5d0 \ud070 \uc601\ud5a5\uc744 \ubbf8\uce69\ub2c8\ub2e4.\\n\\n5. **Possible Keys**: \uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc778\ub371\uc2a4\ub97c \ub098\ud0c0\ub0c5\ub2c8\ub2e4. MySQL\uc774 \uc5b4\ub5a4 \uc778\ub371\uc2a4\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294\uc9c0 \uc54c\ub824\uc90d\ub2c8\ub2e4.\\n\\n6. **Key**: \uc2e4\uc81c\ub85c \uc120\ud0dd\ub41c \uc778\ub371\uc2a4\uc785\ub2c8\ub2e4. \uc774 \uac12\uc740 \uac00\ub2a5\ud55c \uc778\ub371\uc2a4 \uc911\uc5d0\uc11c \uc2e4\uc81c\ub85c \uc0ac\uc6a9\ub418\ub294 \uc778\ub371\uc2a4\ub97c \ub098\ud0c0\ub0c5\ub2c8\ub2e4.\\n\\n7. **Key Len**: \uc0ac\uc6a9\ub41c \uc778\ub371\uc2a4\uc758 \uae38\uc774\ub97c \ub098\ud0c0\ub0c5\ub2c8\ub2e4.\\n\\n8. **Ref**: \uc778\ub371\uc2a4\ub97c \uc0ac\uc6a9\ud558\uc5ec \ud14c\uc774\ube14 \uac04\uc758 \uc5f0\uacb0\uc744 \ub098\ud0c0\ub0b4\ub294 \uc5f4\uc785\ub2c8\ub2e4.\\n\\n9. **Rows**: \uac01 \ub2e8\uacc4\uc5d0\uc11c \uc608\uc0c1\ub418\ub294 \ud589\uc758 \uc218\uc785\ub2c8\ub2e4. \uc774 \uac12\uc740 \uc131\ub2a5 \ud3c9\uac00\uc5d0 \uc911\uc694\ud55c \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\n10. **Extra**: \uae30\ud0c0 \uc815\ubcf4\ub97c \uc81c\uacf5\ud569\ub2c8\ub2e4. \uc774 \uce7c\ub7fc\uc5d0\ub294 \ucd94\uac00 \uc815\ubcf4 \ubc0f \ud78c\ud2b8\uac00 \ud3ec\ud568\ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub807\uac8c \uc5ec\ub7ec \uce7c\ub7fc\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uadf8 \uc911 \uc131\ub2a5\uc5d0 \ud070 \uc601\ud5a5\uc744 \ubbf8\uce58\ub294 \uce7c\ub7fc \ub450 \uac00\uc9c0\ub9cc \uc790\uc138\ud788 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n### Type\\n1. **const** : \ucffc\ub9ac\uc5d0 Primary key \ud639\uc740 unique key \uce7c\ub7fc\uc744 \uc774\uc6a9\ud558\ub294 where \uc870\uac74\uc808\uc744 \uac00\uc9c0\uace0 \uc788\uace0, \ubc18\ub4dc\uc2dc \ud558\ub098\uc758 \ub370\uc774\ud130\ub97c \ubc18\ud658\ud558\ub294 \ubc29\uc2dd\uc774\ub2e4. (\uc635\ud2f0\ub9c8\uc774\uc800\uac00 \ud574\ub2f9 \ubd80\ubd84\uc740 \uc0c1\uc218\ub85c \ucc98\ub9ac\ud558\uae30 \ub54c\ubb38\uc5d0 const\ub77c\uace0 \ud55c\ub2e4.)\\n2. **eq_ref** : \uc870\uc778\uc5d0\uc11c Primary key \ud639\uc740 unique key \uce7c\ub7fc\uc744 \uc774\uc6a9\ud558\ub294 where \uc870\uac74\uc808\uc744 \uac00\uc9c0\uace0 \uc788\uace0, \ubc18\ub4dc\uc2dc \ud558\ub098\uc758 \ub370\uc774\ud130\ub97c \ubc18\ud658\ud558\ub294 \ubc29\uc2dd\uc774\ub2e4. (const\uc640 \ub2e4\ub978 \uc810\uc740 eq_ref\ub294 \uc870\uc778\uc5d0\uc11c \uc0ac\uc6a9\ub41c\ub2e4\ub294 \uc810\uc774\ub2e4.)\\n3. **ref** : eq_ref\uc640 \ub2e4\ub974\uac8c join\uc758 \uc21c\uc11c\uc640 \uad00\uacc4\uc5c6\uc774 \uc0ac\uc6a9\ub41c\ub2e4. \uadf8\ub9ac\uace0 primary key, unique key\ub3c4 \uad00\uacc4\uc5c6\ub2e4. \uadf8\ub0e5 \uc778\ub371\uc2a4\uc758 \uc885\ub958\uc640 \uad00\uacc4\uc5c6\uc774 `=` \uc870\uac74\uc73c\ub85c \uac80\uc0c9\ud560 \ub54c \uc0ac\uc6a9\ub41c\ub2e4\\n4. **fulltext**: mysql \uc804\ubb38 \uac80\uc0c9 \uc778\ub371\uc2a4\ub97c \uc0ac\uc6a9\ud574\uc11c \ub808\ucf54\ub4dc\uc5d0 \uc811\uadfc\ud558\ub294 \ubc29\ubc95, \uc804\ubb38 \uac80\uc0c9\ud560 \uceec\ub7fc\uc5d0 \uc778\ub371\uc2a4\uac00 \uc788\uc5b4\uc57c \ud55c\ub2e4. \\"MATCH ... AGAINST ...\\" \uad6c\ubb38\uc744 \uc0ac\uc6a9\ud574\uc11c \uc2e4\ud589\ub41c\ub2e4\\n5. **range**: \uc778\ub371\uc2a4\ub97c \uc774\uc6a9\ud574\uc11c \uac80\uc0c9\ud558\ub294\ub370, \uac80\uc0c9 \uc870\uac74\uc774 `>, >=, <, <=, BETWEEN, IN()` \ub4f1\uc758 \uc5f0\uc0b0\uc790\ub97c \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0\uc774\ub2e4. \ubcf4\ud1b5\uc758 \uc778\ub371\uc2a4 \uc2a4\uce94\uc774\ub77c\uace0 \ud558\uba74 range, const, ref\ub97c \uce6d\ud55c\ub2e4\\n6. **index**: \uc778\ub371\uc2a4 \ud480 \uc2a4\uce94\uc774\ub2e4. \uc778\ub371\uc2a4\ub97c \uc774\uc6a9\ud574\uc11c \ud14c\uc774\ube14\uc758 \ubaa8\ub4e0 \ub808\ucf54\ub4dc\ub97c \uc77d\ub294\ub2e4. \uc778\ub371\uc2a4\ub97c \uc774\uc6a9\ud574\uc11c \ud14c\uc774\ube14\uc744 \uc77d\ub294 \uac83\uc774\uae30 \ub54c\ubb38\uc5d0 all\ubcf4\ub2e4\ub294 \ube60\ub974\ub2e4.\\n7. **all**: \ud14c\uc774\ube14 \ud480 \uc2a4\uce94\uc774\ub2e4. \ud14c\uc774\ube14\uc758 \ubaa8\ub4e0 \ub808\ucf54\ub4dc\ub97c \uc77d\ub294\ub2e4. \uac00\uc7a5 \ub290\ub9b0 \ubc29\ubc95\uc774\ub2e4.\\n\\n\uc2e4\ud589 \uacc4\ud68d\uc5d0\uc11c \uc790\uc8fc \ubcf4\uc774\ub294 type\ub4e4\ub9cc **\uc131\ub2a5\uc774 \uc88b\uc740 \uc21c**\uc73c\ub85c \uc815\ub9ac\ud574\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n### Extra\\n1. **using filesort**: \uc815\ub82c\uc744 \uc704\ud574 \ubcc4\ub3c4\uc758 \ud30c\uc77c \uc815\ub82c\uc744 \uc218\ud589\ud55c\ub2e4. \uc774\ub294 \uc778\ub371\uc2a4\ub97c \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0 \uc815\ub82c\uc744 \uc218\ud589\ud55c\ub2e4\ub294 \uc758\ubbf8\uc774\ub2e4. \uc774\ub294 \uc131\ub2a5\uc5d0 \uc88b\uc9c0 \uc54a\ub2e4.\\n2. **using index**: \uc778\ub371\uc2a4\ub9cc\uc73c\ub85c \ucffc\ub9ac\ub97c \ucc98\ub9ac\ud55c\ub2e4. \uc774\ub294 \uc778\ub371\uc2a4\ub9cc\uc73c\ub85c \ucffc\ub9ac\ub97c \ucc98\ub9ac\ud558\uae30 \ub54c\ubb38\uc5d0 \uc131\ub2a5\uc774 \uc88b\ub2e4.\\n3. **using join** buffer: join\uc774 \ub418\ub294 \uce7c\ub7fc\uc740 \uc778\ub371\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4. \ud558\uc9c0\ub9cc driven table\uc5d0 \uc801\uc808\ud55c \uc778\ub371\uc2a4\uac00 \uc5c6\ub2e4\uba74 driving table\uc5d0 \uc788\ub294 \ubaa8\ub4e0 \ub808\ucf54\ub4dc\ub97c \uc77d\uc5b4\uc11c join\uc744 \uc218\ud589\ud55c\ub2e4. \uadf8\ub798\uc11c \uc774\uac78 \ubcf4\uc644\ud558\uae30 \uc704\ud574 driving table\uc5d0 \uc77d\uc740 \ub808\ucf54\ub4dc\ub97c \uc784\uc2dc \uacf5\uac04\uc5d0 \uc800\uc7a5\ud558\ub294\ub370 \uadf8 \uacf3\uc774 join buffer\uc774\ub2e4.\\n4. **using temporary**: \ucffc\ub9ac\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud574 \uc784\uc2dc \ud14c\uc774\ube14\uc744 \uc0dd\uc131\ud55c\ub2e4. \uc778\ub371\uc2a4\ub97c \uc0ac\uc6a9\ud558\uc9c0 \ubabb\ud558\ub294 group by \ucffc\ub9ac\uac00 \ub300\ud45c\uc801\uc778 \uc608\uc774\ub2e4.\\n5. **using where**: mysql \uc5d4\uc9c4\uc774 \ubcc4\ub3c4\uc758 \uac00\uacf5, \ud544\ud130\ub9c1 \uc791\uc5c5\uc744 \ucc98\ub9ac\ud55c \uacbd\uc6b0\uc77c \ub54c\ub9cc \ub098\ud0c0\ub09c\ub2e4. \ubc94\uc704 \uc870\uac74\uc740 \uc2a4\ud1a0\ub9ac\uc9c0 \uc5d4\uc9c4\uc5d0\uc11c \ucc98\ub9ac\ub418\uc5b4 \ub808\ucf54\ub4dc\ub97c \ub9ac\ud134\ud574\uc8fc\uc9c0\ub9cc, \uccb4\ud06c \uc870\uac74\uc740 mysql \uc5d4\uc9c4\uc5d0\uc11c \ucc98\ub9ac\ub41c\ub2e4.\\n\\n\\ntype\ubfd0\ub9cc \uc544\ub2c8\ub77c extra\ub3c4 \ucffc\ub9ac\uc758 \ubb38\uc81c\ub97c \ud30c\uc545\ud558\ub294\ub370 \uc544\uc8fc \ud070 \ub3c4\uc6c0\uc744 \uc90d\ub2c8\ub2e4. \uadf8 \uc911 \uc790\uc8fc \ubcf4\uc774\ub294 \uac83\ub4e4\uc5d0 \ub300\ud574\uc11c\ub9cc \uc815\ub9ac\ud574\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \uc544\uae4c \uc0dd\uc131\ud55c \ucffc\ub9ac\uc758 \uc2e4\ud589 \uacc4\ud68d\uc744 \ud655\uc778\ud574\ubd05\uc2dc\ub2e4.\\n```\\n+----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+\\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\\n+----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+\\n| 1 | SIMPLE | station | NULL | range | PRIMARY,idx_station_coordination | PRIMARY | 1022 | NULL | 2 | 100.00 | Using where; Using temporary |\\n| 1 | SIMPLE | charger | NULL | ALL | PRIMARY | NULL | NULL | NULL | 240340 | 10.00 | Using where; Using join buffer (hash join) |\\n| 1 | SIMPLE | chargersta | NULL | eq_ref | PRIMARY | PRIMARY | 2044 | charge.charger1_.charger_id,charge.station0_.station_id | 1 | 100.00 | NULL |\\n+----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+\\n```\\n\\nstation \ud14c\uc774\ube14\uc5d0 \ub300\ud574\uc11c\ub294 range \uc2a4\uce94, \uc784\uc2dc \ud14c\uc774\ube14\uc744 \uc0dd\uc131\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4, \uadf8\ub9ac\uace0 charger\uc5d0\uc11c\ub294 \ud14c\uc774\ube14 \ud480 \uc2a4\uce94, join buffer\uae4c\uc9c0 \uc0dd\uc131\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \ub2e4\ud589\ud788\ub3c4 chargersta \ud14c\uc774\ube14\uc5d0\uc11c\ub294 \uc801\ub2f9\ud55c \uc870\uac74\uc744 \uc0dd\uc131\ud55c \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e4\uc2dc \ud55c\ubc88 \ucffc\ub9ac\ub97c \ubcf4\uace0 \uc2e4\ud589 \uacc4\ud68d\uc774 \uc774\ub807\uac8c \ub098\uc628 \uc774\uc720\ub97c \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n```sql\\nSELECT\\n ...\\n FROM charge_station s\\n inner join charger c on (c.station_id = s.station_id)\\n inner join charger_status cs on (c.charger_id = cs.charger_id and c.station_id = cs.station_id)\\nwhere s.station_id in (?, ?)\\ngroup by s.station_id;\\n```\\n\\n\uc544\uae4c \uc598\uae30\ud588\ub358, using temporary\uc640 using join buffer\uac00 \ubc1c\uc0dd\ud558\ub294 \uc774\uc720\uc758 \uacf5\ud1b5\uc810\uc744 \ucc3e\uc544\ubcf4\uba74, \uc778\ub371\uc2a4\uac00 \ubb38\uc81c\uc778 \uac83\uc744 \uc720\ucd94\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\nstation\uacfc charger\ub97c join\ud560 \ub54c, driven table \uc989, charger \ud14c\uc774\ube14\uc5d0 \uc801\uc808\ud55c \uc778\ub371\uc2a4\uac00 \uc5c6\uc5b4 \uc131\ub2a5\uc774 \ub098\ube60\uc9c4 \uac83\uc774\ub77c \uc758\uc2ec\ud558\uc5ec, \uc778\ub371\uc2a4\ub97c \uc0dd\uc131\ud558\uace0 \ub2e4\uc2dc \ud55c\ubc88 \uc2e4\ud589 \uacc4\ud68d\uc744 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n```\\n+----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+\\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\\n+----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+\\n| 1 | SIMPLE | station | NULL | range | PRIMARY,idx_station_coordination | PRIMARY | 1022 | NULL | 2 | 100.00 | Using where |\\n| 1 | SIMPLE | charger | NULL | ref | PRIMARY,idx_station_id | idx_station_id | 1022 | charge.s.station_id | 3 | 100.00 | NULL |\\n| 1 | SIMPLE | chargersta | NULL | eq_ref | PRIMARY | PRIMARY | 2044 | charge.charger1_.charger_id,charge.station0_.station_id | 1 | 100.00 | NULL |\\n+----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+\\n```\\n\\n\uc774\ub807\uac8c charger \ud14c\uc774\ube14\uc5d0 \uc778\ub371\uc2a4\ub97c \uc0dd\uc131\ud55c \uac83\ub9cc\uc73c\ub85c\ub3c4 \uc2e4\ud589 \uacc4\ud68d\uc744 \uae54\ub054\ud558\uac8c \uac1c\uc120\ud588\uc2b5\ub2c8\ub2e4.\\n\\n### \uacb0\uacfc\\n\uc544\ub798\ub294 \uc778\ub371\uc2a4\ub97c \uc0dd\uc131\ud558\uae30 \uc804 \uc2e4\ud589 \uc18d\ub3c4\uc785\ub2c8\ub2e4.\\n\\n![\uac1c\uc120_\uc804](https://github.com/woowacourse-teams/2023-car-ffeine/assets/106640954/1130eee6-c2b9-4846-b294-73de78b0f070)\\n\\n\uc544\ub798\ub294 \uc778\ub371\uc2a4\ub97c \uc0dd\uc131\ud55c \ud6c4 \uc2e4\ud589 \uc18d\ub3c4\uc785\ub2c8\ub2e4.\\n\\n![\uac1c\uc120_\ud6c4](https://github.com/woowacourse-teams/2023-car-ffeine/assets/106640954/d024330a-c233-4e75-a28b-1b01b6ae3245)\\n\\n315ms -> 24ms \ub85c \uc57d 13\ubc30 \ube68\ub77c\uc9c4 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\\n**\uc2e4\ud589 \uacc4\ud68d \ud655\uc778\uc740 \ud544\uc218\uc785\ub2c8\ub2e4!**\\n\\n### \ucc38\uace0\\nreal mysql \ucc45"},{"id":"30","metadata":{"permalink":"/30","source":"@site/blog/2023-08-31-love-my-team.mdx","title":"\uce74\ud398\uc778 \ud300\uc758 \ud611\uc5c5 \uc77c\ud654","description":"\ub808\ubca83 \ub54c \ud504\ub85c\uc81d\ud2b8\ub97c \uc9c4\ud589\ud558\uba74\uc11c, \uc800\ud76c \ud300\uc740 \ub9ce\uc740 \ud611\uc5c5\uc744 \uc9c4\ud589\ud588\uc2b5\ub2c8\ub2e4.","date":"2023-08-31T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 31\uc77c","tags":[],"readingTime":2.895,"hasTruncateMarker":false,"authors":[],"frontMatter":{"slug":"30","title":"\uce74\ud398\uc778 \ud300\uc758 \ud611\uc5c5 \uc77c\ud654","authors":[],"tags":[]},"prevItem":{"title":"\uc870\ud68c \uc131\ub2a5 \uac1c\uc120\ud558\uae30","permalink":"/31"},"nextItem":{"title":"useSyncExternalStore\ub85c \ub9cc\ub4e4\uc5b4\ubcf4\ub294 \uc804\uc5ed\uc0c1\ud0dc\uad00\ub9ac \ub3c4\uad6c","permalink":"/29"}},"content":"\ub808\ubca83 \ub54c \ud504\ub85c\uc81d\ud2b8\ub97c \uc9c4\ud589\ud558\uba74\uc11c, \uc800\ud76c \ud300\uc740 \ub9ce\uc740 \ud611\uc5c5\uc744 \uc9c4\ud589\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ucc98\uc74c\uc5d0\ub294 \ud504\ub860\ud2b8\uc5d4\ub4dc, \ubc31\uc5d4\ub4dc \uc11c\ub85c \uac01\uac01\uc758 \ubd84\uc57c\ub9cc \uac1c\ubc1c\uc744 \ud574\uc654\uace0 \ud611\uc5c5\uc774 \uc775\uc219\ud558\uc9c0 \uc54a\uc544\uc11c \ub9ce\uc740 \ubd80\ubd84\uc5d0\uc11c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uace4 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uacfc\uc815\uc5d0\uc11c \uc800\ud76c \ud300\uc740 \uc5b4\ub5bb\uac8c \ub300\ucc98\ub97c \ud588\uc744\uae4c\uc694?\\n\\n\ud55c \uac00\uc9c0 \uc77c\ud654\ub85c \uc800\ud76c \ud300\uc758 \uc81c\uc774\uc640 \uc13c\ud2b8\uc758 \ud544\ud130 \uc801\uc6a9 \ubd80\ubd84\uc744 \uc124\uba85 \ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc870\ud68c \uc2dc\uc5d0 \ud544\ud130 \uc801\uc6a9 \ubd80\ubd84\uc744 \ub9cc\ub4e4 \ub54c \uae30\uc874\uc5d0 \uc791\uc131\ud574\ub454 API \uba85\uc138\ub300\ub85c \uc11c\ub85c \uc791\uc5c5\uc744 \uc9c4\ud589\ud558\uace0, \uc911\uac04\uc5d0 \uc0dd\uac01\ud558\uc9c0 \ubabb\ud55c \ubd80\ubd84\uc5d0 \ub300\ud574\uc11c\ub294 \uc11c\ub85c \ub300\ud654\ub97c \ub9ce\uc774 \ud588\uc2b5\ub2c8\ub2e4.\\n\ub300\ud654\ub97c \ud558\uba74\uc11c \uc9c4\ud589\uc744 \ud588\uc9c0\ub9cc \ubc1c\uacac\ud558\uc9c0 \ubabb\ud55c \ubb38\uc81c\uc810\uc774 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ubc14\ub85c \ucda9\uc804\uc18c \ud68c\uc0ac \uba85\uc5d0\uc11c key \uac12\uc744 \uc5b4\ub5bb\uac8c \ud558\ub0d0\uc5d0 \ubb38\uc81c\uc600\uc2b5\ub2c8\ub2e4.\\n\uc608\ub97c \ub4e4\uba74 \ucda9\uc804\uc18c \ud68c\uc0ac \uba85\uc5d0\uc11c `\uad11\uc8fc\uc2dc`\ub77c\ub294 \uc774\ub984\uc774 \uc788\uc5c8\ub294\ub370, \uc774 \ud544\ud130\ub294 \uc2e4\uc81c\ub85c \ub450 \uac00\uc9c0\uac00 \uc874\uc7ac\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\ub098\ub294 \uacbd\uae30\ub3c4 \uad11\uc8fc, \ud558\ub098\ub294 \uc804\ub77c\ub3c4 \uad11\uc8fc\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \ubd80\ubd84\uc5d0\uc11c \ubd88\ud544\uc694\ud55c \uc9c0\uc5ed\uc758 \ud544\ud130\uae4c\uc9c0 \uac78\ub9ac\uac8c \ub418\ub294 \ubb38\uc81c\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\ud611\uc5c5\ud558\ub294 \uacfc\uc815\uc5d0\uc11c \uc774\ub97c \ubc1c\uacac\ud588\uace0, \uc989\uac01 \uc870\uce58\ub97c \ucde8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc870\uce58\ub97c \ucde8\ud560 \ub54c \uc11c\ub85c\uc5d0\uac8c \uac01\uc790 \ud3b8\ud55c \ubc29\ubc95\uc774 \uc788\uc5c8\uc9c0\ub9cc,\\n\ub2e8\uc21c\ud788 \uc11c\ub85c\uc5d0\uac8c \ud3b8\ud55c \uc791\uc5c5\uc744 \ud558\uc9c0 \uc54a\uc558\uace0, \ud300\uc6d0\uacfc \uc0c1\uc758\ud558\uba74\uc11c \ucd94\ud6c4 \uc9c4\ud589\uc5d0 \ubb38\uc81c \uc5c6\ub294 \ubc29\ud5a5\uc744 \ucc3e\uace0 \uc9c4\ud589\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc9c0\uae08 \uc0dd\uac01\ud574\ubcf4\uba74 \ub9cc\uc57d \uac01\uc790\uc5d0\uac8c \ud3b8\ud55c \ubc29\uc2dd\uc73c\ub85c \ubb38\uc81c\ub97c \uc218\uc815\ud588\ub2e4\uba74, \ub2e4\ub978 \ud300\uc6d0\uc774 \ub2e4\ub978 \uc791\uc5c5\uc744 \ud560 \ub54c \uc9c0\uc7a5\uc774 \uac14\uc744 \uc218\ub3c4 \uc788\uace0 \ubd88\ud544\uc694\ud55c \uc791\uc5c5\uc744 \ud588\uc744 \uc218\ub3c4 \uc788\uc5c8\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uc2dc\uc810\uc744 \uacc4\uae30\ub85c \uc800\ud76c \ud300\ub07c\ub9ac \uc608\uc0c1\ud558\uc9c0 \ubabb\ud55c \ubb38\uc81c\ub97c \uc791\uc5c5 \uc911\uc5d0 \ubc1c\uacac\ud558\ub354\ub77c\ub3c4 \ub2e4\ub978 \ud300\uc6d0\uc5d0\uac8c \uacf5\uc720\ud558\uace0 \uc11c\ub85c \uc9e7\uc740 \ud68c\uc758\ub97c \ud1b5\ud574 \ubb38\uc81c \ud574\uacb0 \ubc29\uc548\uc744 \uac19\uc774 \ucc3e\ub294 \uac83\uc774 \uc790\uc5f0\uc2a4\ub7fd\uac8c \ud300\ubb38\ud654\ub85c \uc790\ub9ac \uc7a1\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4."},{"id":"29","metadata":{"permalink":"/29","source":"@site/blog/2023-08-25-external-state/index.mdx","title":"useSyncExternalStore\ub85c \ub9cc\ub4e4\uc5b4\ubcf4\ub294 \uc804\uc5ed\uc0c1\ud0dc\uad00\ub9ac \ub3c4\uad6c","description":"\uc800\ud76c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc9c0\ub3c4\uc640 React\ub97c \uacb0\ud569\uc744 \ud574\uc57c\ud588\uc2b5\ub2c8\ub2e4.","date":"2023-08-25T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 25\uc77c","tags":[{"label":"useSyncExternalStore","permalink":"/tags/use-sync-external-store"},{"label":"\uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac","permalink":"/tags/\uc804\uc5ed-\uc0c1\ud0dc-\uad00\ub9ac"},{"label":"\uc804\uc5ed\uc0c1\ud0dc","permalink":"/tags/\uc804\uc5ed\uc0c1\ud0dc"}],"readingTime":10.165,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"29","title":"useSyncExternalStore\ub85c \ub9cc\ub4e4\uc5b4\ubcf4\ub294 \uc804\uc5ed\uc0c1\ud0dc\uad00\ub9ac \ub3c4\uad6c","authors":["gabriel"],"tags":["useSyncExternalStore","\uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac","\uc804\uc5ed\uc0c1\ud0dc"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \ud611\uc5c5 \uc77c\ud654","permalink":"/30"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \uad00\ud558\uc5ec","permalink":"/28"}},"content":"\uc800\ud76c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc9c0\ub3c4\uc640 React\ub97c \uacb0\ud569\uc744 \ud574\uc57c\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud504\ub85c\uc81d\ud2b8 \ucd08\uae30\uc5d0\ub294 Google Maps API\ub97c React DOM\uc774 \uc544\ub2cc, \ubc14\ub2d0\ub77c JS\uc758 \uc601\uc5ed\uc5d0\uc11c \ub2e4\ub8e8\uae30\ub97c \ud76c\ub9dd\ud558\uc600\uace0, \uc5ec\ub7ec \ud14c\uc2a4\ud2b8 \uacb0\uacfc \ub450 \uc601\uc5ed\uc744 \ubd84\ub9ac\ud558\ub294 \uac83\uc740 \uc131\uacf5\uc801\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\nReact\ub294 \uadf8\uc800 \ubd80\ucc29 \ub2f9\ud560 DOM\uc744 \uc678\ubd80(Google Maps API)\ub85c \ub0b4\uc5b4\uc8fc\ub294 \uae30\ub2a5\uc5d0 \ubd88\uacfc\ud558\uc600\uace0, \uc9c0\ub3c4\uc640 React\uac00 \uc11c\ub85c \ud611\ub825 \ud574\uc57c\ud560 \ub54c\ub9cc \uc5f0\ub77d\uc744 \ud558\ub294 \uad6c\uc870\ub97c \ucde8\ud558\uace0\uc790 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uba74, React UI\ub294 UI\ub300\ub85c \ub3d9\uc791\ud558\uace0, \uc9c0\ub3c4\ub294 \uc9c0\ub3c4 \ub300\ub85c \ub3d9\uc791\ud558\ub2e4\uac00 \uc5b4\ub290 \uc21c\uac04\uc5d0\ub9cc \uc11c\ub85c\uac00 \uc11c\ub85c\ub97c \uc870\uc791\ud560 \uc218 \uc788\uc73c\uba74 \ub410\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \uac00\ub2a5\ud558\uac8c \ud558\ub294 \uae30\uc220\ub85c useSyncExternalStore\ub97c \uc120\uc815\ud558\uac8c \ub410\uc2b5\ub2c8\ub2e4. \uc774 \ud6c5\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\uc81c \ube14\ub85c\uadf8](https://leirbag.tistory.com/144)\ub098 [\uacf5\uc2dd\ubb38\uc11c](https://react.dev/reference/react/useSyncExternalStore)\uc5d0 \ub098\uc640\uc788\uc73c\ubbc0\ub85c \uc124\uba85\uc744 \uac04\ub7b5\ud788 \ud558\uc790\uba74 useSyncExternalStore\ub294 React DOM \ub0b4\ubd80\uac00 \uc544\ub2cc \uc678\ubd80 \uc800\uc7a5\uc18c(JS)\uc5d0\uc11c React DOM\uc744 \uc870\uc791\ud560 \uc218 \uc788\ub3c4\ub85d \ud558\ub294 \ucee4\uc2a4\ud140 \ud6c5\uc785\ub2c8\ub2e4.\\n\\n![no offset](./0-1.png)\\n\\n\uc774 \ud6c5\uc740 React 18\uc5d0 \ucd9c\uc2dc\ub418\uc5c8\uc73c\uba70, \uc678\ubd80 \uc800\uc7a5\uc18c\uc640 React\uc758 \uc18c\ud1b5\uc744 \uc6d0\ud65c\ud558\uac8c \ub3d5\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c \ud65c\uc6a9\ud558\uae30 \uc801\uc808\ud558\ub2e4\uace0 \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4. \uc774 \uae30\ub2a5\uc744 \uc5b4\ub5bb\uac8c \ud558\uba74 \ub354 \ud6a8\uc728\uc801\uc778 \ubc29\ubc95\uc73c\ub85c \uc7ac\uc0ac\uc6a9\ud560 \uc218 \uc788\uc744\uc9c0 \uace0\ubbfc\ud558\uc600\uace0, \uc5ec\ub7ec \ucd94\uc0c1\ud654 \ub2e8\uacc4\ub97c \uac70\uccd0 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc218\uc900\uc73c\ub85c \uc81c\uc791\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774\ud6c4\uc5d0 TanStack Query\ub97c \ub3c4\uc785\ud558\ub294 \uacfc\uc815\uc5d0\uc11c \uac01\uc885 \uae30\ub2a5\uc774 React Component \ub0b4\uc5d0\uc11c\ub9cc \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud558\ub3c4\ub85d \uac15\uc81c\ub418\uc5c8\uace0, \ub530\ub77c\uc11c \ub354\uc774\uc0c1 \uc9c0\ub3c4 API\ub97c \ubc14\ub2d0\ub77cJS \uc601\uc5ed\uc5d0\uc11c \ub2e4\ub8f0 \uc218 \uc5c6\uc5b4 React DOM\uc73c\ub85c \uc774\uc2dd \ud558\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./0-2.png)\\n\\n\uc774\ubbf8 \ub9cc\ub4e4\uc5b4 \ub454 \uae30\ub2a5\uc774 \ubd95 \ub5a0\ubc84\ub9b0 \uc0c1\ud669\uc774\uc5c8\uc9c0\ub9cc \uc5b4\ucc0c \ub410\ub4e0 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\uc5d0 \uc9c0\ub3c4 \uc778\uc2a4\ud134\uc2a4\ub97c \ub123\uc5b4\uc57c \ud558\ub294 \uc0c1\ud669\uc774\ub77c useSyncExternalStore\ub97c \ud504\ub85c\uc81d\ud2b8 \ub05d\uae4c\uc9c0 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc \uad00\ub9ac \ub3c4\uad6c\ub85c\uc368 \uc0ac\uc6a9\ud558\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc0c1\ud0dc \uad00\ub9ac \ud6c5\uc758 \ucd94\uc0c1\ud654 \uacfc\uc815\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n## **use-external-state \uad6c\uc131 \ubc0f \ub3d9\uc791 \uc6d0\ub9ac**\\n\\n**Store\ub294 \uc0c1\ud0dc \uad00\ub9ac \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4**\\n\\n\ubc14\uae65\uc5d0\uc11c \uc8fc\uc5b4\uc9c4 \ucd08\uae30 \uc0c1\ud0dc \uac12\uc740 StateManager\ub77c\ub294 \ud074\ub798\uc2a4\uc5d0 \uc804\ub2ec\ub429\ub2c8\ub2e4.\\n\\n![no offset](./1.png)\\n\\n```typescript\\nexport const store = (initialState: T) => {\\n const stateManager = new StateManager(initialState);\\n return stateManager;\\n};\\n```\\n\\n\ucd08\uae30 \uc0c1\ud0dc \uac12\uc744 \uc804\ub2ec\ubc1b\uc740 store \ud568\uc218\ub294 StateManager\ub77c\ub294 \uc5b4\ub5a4 \uc0c1\ud0dc \uad00\ub9ac \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.\\n\uc0dd\uc131\ub41c StateManager \uc778\uc2a4\ud134\uc2a4\uac00 \ubc18\ud658\ub418\uc5b4 store\uac00 \uace7 \ucd08\uae30 \uac12\uc744 \uac00\uc9c0\ub294 StateManager\uac00 \ub429\ub2c8\ub2e4.\\n\\n![no offset](./2.png)\\n\\n\uc608\ub97c \ub4e4\uc5b4, \ub2e4\uc74c\uacfc \uac19\uc740 \ucf54\ub4dc\uac00 \uc788\ub2e4\uace0 \ud560 \ub54c\\n\\n```typescript\\nexport const countStore = store(0);\\n```\\n\\ncountStore\ub294 \uace7 0\uc744 \ucd08\uae30\uac12\uc73c\ub85c \uac00\uc9c0\ub294 StateManager \uc778\uc2a4\ud134\uc2a4\uc774\uae30\ub3c4 \ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 StateManager\uc5d0 \ub300\ud574\uc11c \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n### StateManager\ub294 react \ubc14\uae65\uc5d0 \uc788\ub294 \uc5b4\ub5a4 \uc800\uc7a5\uc18c\uc774\ub2e4.\\n\\n(\uadfc\ub370 \uc774\uac8c \uadf8\ub0e5 \uc800\uc7a5\uc18c\ub294 \uc544\ub2c8\uace0 \uc880 \ud2b9\ubcc4\ud55c \uc800\uc7a5\uc18c\ub2e4.)\\n\\n```typescript\\nexport type SetStateCallbackType = (prevState: T) => T;\\n\\nexport interface DataObserver {\\n setState: (param: SetStateCallbackType | T) => void;\\n getState: () => T;\\n subscribe: (listener: () => void) => () => void;\\n emitChange: () => void;\\n}\\n\\nclass StateManager implements DataObserver {\\n public state: T;\\n private listeners: Array<() => void> = [];\\n\\n constructor(initialState: T) {\\n this.state = initialState;\\n }\\n\\n setState = (param: SetStateCallbackType | T) => {\\n if (param instanceof Function) {\\n const newState = param(this.state);\\n this.state = newState;\\n } else {\\n this.state = param;\\n }\\n\\n this.emitChange();\\n };\\n\\n getState = () => {\\n return this.state;\\n };\\n\\n subscribe = (listener: () => void) => {\\n this.listeners = [...this.listeners, listener];\\n\\n return () => {\\n this.listeners = this.listeners.filter((l) => l !== listener);\\n };\\n };\\n\\n emitChange = () => {\\n for (const listener of this.listeners) {\\n listener();\\n }\\n };\\n}\\n\\nexport default StateManager;\\n```\\n\\nStateManager \ud074\ub798\uc2a4\ub294 \uc678\ubd80\uc5d0\uc11c \ubc1b\uc544\uc628 \ucd08\uae30\uac12\uc744 \uc0c1\ud0dc\ub85c \uac00\uc9d1\ub2c8\ub2e4.\\nsetState, getState, subscribe, emitChange\ub97c \uba54\uc11c\ub4dc\ub85c \uac00\uc9d1\ub2c8\ub2e4.\\n\uc5ec\uae30\uc11c \uc791\uc131\ub41c \ucf54\ub4dc\ub4e4\uc740 react\uc5d0\uc11c \uc678\ubd80 \uc800\uc7a5\uc18c\uc640 \uc18c\ud1b5\ud558\uae30 \uc704\ud55c [\ucd5c\uc18c\ud55c\uc758 \uaddc\uaca9](https://react.dev/reference/react/useSyncExternalStore#subscribing-to-an-external-store)\uc785\ub2c8\ub2e4.\\n\\n- subscribe: \ub2e8\uc77c \ucf5c\ubc31 \uc778\uc218\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2a4\ud1a0\uc5b4\uc5d0 \uad6c\ub3c5\ud558\ub294 \ud568\uc218\uc785\ub2c8\ub2e4. \uc2a4\ud1a0\uc5b4\uac00 \ubcc0\uacbd\ub418\uba74 \uc81c\uacf5\ub41c \ucf5c\ubc31\uc744 \ud638\ucd9c\ud574\uc57c \ud569\ub2c8\ub2e4. \uadf8\ub7ec\uba74 \uad6c\uc131 \uc694\uc18c\uac00 \ub2e4\uc2dc \ub80c\ub354\ub9c1 \ub429\ub2c8\ub2e4. \uad6c\ub3c5 \uae30\ub2a5\uc740 \uad6c\ub3c5\uc744 \uc815\ub9ac\ud558\ub294 \uae30\ub2a5\uc744 \ubc18\ud658\ud574\uc57c \ud569\ub2c8\ub2e4. (\uad6c\ub3c5\uc5d0 \uad00\ub828\ub41c \ub370\uc774\ud130\ub294 \ub9ac\uc2a4\ub108 \ubc30\uc5f4 \ud544\ub4dc\uc5d0 \ub123\uc5b4\uc11c \uad00\ub9ac\ud569\ub2c8\ub2e4.)\\n\\n- emitChange: \ub9ac\uc2a4\ub108 \ubc30\uc5f4 \ud544\ub4dc\uc5d0 \ub2f4\uaca8\uc788\ub294 \ubaa8\ub4e0 \ub9ac\uc2a4\ub108\ub97c \uc2e4\ud589\ud569\ub2c8\ub2e4. \uc989, \uad6c\ub3c5\ub41c \uc5b4\ub5a4 \uac83\uc744 \uc21c\ucc28\uc801\uc73c\ub85c \uc2e4\ud589\ud558\uac8c \ud569\ub2c8\ub2e4. \uc774\ub294 \ub9ac\uc561\ud2b8 DOM\uc744 \uac15\uc81c\ub85c \uc77c\uae68\uc6cc\uc8fc\ub294 \uc635\uc800\ubc84 \ud328\ud134\uc758 \uc5ed\ud560\uc744 \ud558\uac8c \ub429\ub2c8\ub2e4. \uc774 \uacfc\uc815 \ub54c\ubb38\uc5d0 react DOM\uc774 \uc815\ud655\ud55c \uc7ac \ub80c\ub354\ub9c1 \uc9c0\uc810\uc744 \ud30c\uc545\ud560 \uc218 \uc788\uac8c\ub429\ub2c8\ub2e4. (\ucd5c\uc801\ud654 \ubb38\uc81c\uc5d0\uc11c \uc790\uc720\ub85c\uc6cc\uc9d0)\\n\\n- setState: \uc0c1\ud0dc\ub97c \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4. \ub2e4\ub9cc \uc0c1\ud0dc\uac00 \uc5c5\ub370\uc774\ud2b8 \ub410\uc74c\uc744 \uc54c\ub824\uc57c \ud558\ubbc0\ub85c emitChange\ub97c \uc2e4\ud589\uc2dc\ucf1c react DOM\uc744 \uac15\uc81c\ub85c \ub3d9\uae30\ud654\uc2dc\ud0b5\ub2c8\ub2e4.\\n\\n- getState: \ud638\ucd9c\ub418\ub294 \uc21c\uac04 \ud604\uc7ac \uc0c1\ud0dc \uac12\uc744 \uc77d\uc2b5\ub2c8\ub2e4.\\n\\n\uc880 \uc5b4\ub835\uc9c0\ub9cc \ub9ac\uc561\ud2b8\uc5d0\uc11c \uc774\ub7f0 \uaddc\uaca9\uc744 \uac00\uc838\uc57c useSyncExternalStore\ud6c5\uc744 \uc4f8 \uc218 \uc788\uac8c \ud574 \uc90d\ub2c8\ub2e4.\\n\uae30\uc874 \uc608\uc81c\uc5d0\uc11c\ub294 \ub2e8\uc21c\ud55c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8 \uac1d\uccb4\ub85c \uc9dc\uc5ec\uc788\uc5c8\uc9c0\ub9cc \uc778\uc2a4\ud134\uc2a4\ub97c \uc790\uc720\ub86d\uac8c \ucc0d\uc5b4\ub0bc \uc218 \uc788\ub294 class \uad6c\uc870\ub85c \uac1c\uc120\ud558\uace0 \ucd94\uc0c1\ud654\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc2e4 \uc5ec\uae30\uae4c\uc9c0\ub9cc \uad6c\ud604\ud574\ub3c4 useSyncExternalStore\ub97c \uc0ac\uc6a9\ud558\ub294\ub370 \uc9c0\uc7a5\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\uc55e\uc11c \uc120\uc5b8\ud55c store\uac1d\uccb4\uc5d0\uc11c subscribe\uc640 getState\ub97c \uaebc\ub0b4\uc11c \uc9c1\uc811 \uc804\ub2ec\ud574 \uc8fc\uba74 \uadf8\ub9cc\uc774\uae30 \ub54c\ubb38\uc774\uc8e0.\\n\\n\ud558\uc9c0\ub9cc \uacb0\uad6d \uc774 \uacfc\uc815 \uc790\uccb4\uac00 \ubc18\ubcf5\ub41c \uc791\uc5c5\uc744 \uc694\uad6c\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n### \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \uc27d\uac8c \uc811\uadfc\ud558\ub3c4\ub85d \ucd9c\uad6c\ub97c \uc5f4\uc5b4\uc8fc\uc790!\\n\\n\ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c\ub294 \ubc14\ub2d0\ub77c JS\ub85c \uc0c1\ud0dc\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\ub294 \uac83\ubcf4\ub2e4\ub294 useState\uc640 \ube44\uc2b7\ud55c \ud615\ud0dc\ub85c \ud6c5\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \ud6e8\uc52c \ubcf4\uae30 \uae54\ub054\ud560 \uac83\uc785\ub2c8\ub2e4.\\n\ub9e4\ubc88 \uc2a4\ud1a0\uc5b4\uc5d0\uc11c \ubb34\uc5b8\uac00\ub97c \uc9c1\uc811 \uaebc\ub0b4\uc9c0 \uc54a\ub3c4\ub85d \ud558\ub294 \uc911\uac04 \ucee4\uc2a4\ud140 \ud6c5\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n```typescript\\nexport const useExternalState = (\\n store: DataObserver\\n): [T, (param: SetStateCallbackType | T) => void] => {\\n const { subscribe, getState, setState } = store;\\n const state = useSyncExternalStore(subscribe, getState);\\n\\n return [state, setState];\\n};\\n```\\n\\n\uc774 \ud6c5\uc740, \ubc14\uae65\uc5d0\uc11c \ubc1b\uc544\uc628 store\ub97c \ud65c\uc6a9\ud558\uc5ec \uad6c\ub3c5/\uc5c5\ub370\uc774\ud2b8 \uae30\ub2a5\uc744 \ubc30\uc5f4\ub85c \ubc18\ud658\ud569\ub2c8\ub2e4.\\n\ubaa8\uc2dd\ub3c4\ub97c \uadf8\ub824\ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./3.png)\\n\\nReact \ucef4\ud3ec\ub10c\ud2b8\ub294 \uc5b4\ub514\uc120\uac00 \uc0dd\uc131\ub41c store() \uac1d\uccb4\ub97c useExternalStore\uc5d0 \ub118\uaca8\uc8fc\uace0, \\\\[\uc0c1\ud0dc, \uc0c1\ud0dc\uc5c5\ub370\uc774\ud2b8\ud568\uc218\\\\]\ub97c \ubc1b\uac8c \ub429\ub2c8\ub2e4.\\n\ub9c8\uce58 \uae30\uc874\uc758 useState\ub098 useRecoilState\ucc98\ub7fc \ub9d0\uc774\uc8e0.\\n\\n\uc815\ub9ac\ud558\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\ud478\ub978 \uc601\uc5ed\uc740 React DOM\\n\ub179\uc0c9 \uc601\uc5ed\uc740 \uc9c1\uc811 \ud638\ucd9c\ud574\uc57c \ud558\ub294 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc758 \uc601\uc5ed (\ud558\uc9c0\ub9cc \ucd5c\ub300\ud55c \ub2e8\uc21c\ud55c \ud615\ud0dc\ub85c \uad6c\uc131\ud574\uc11c \uac1c\ubc1c\uc790\uc758 \ubd80\ub2f4\uc744 \ub35c\uc5b4\uc8fc\ub294 \ud615\ud0dc)\\n\ube68\uac04\uc0c9\uc740 \uac1c\ubc1c\uc790\uac00 \uc9c1\uc811 \uac74\ub4e4\uc9c0 \ubabb\ud558\uc9c0\ub9cc \uac04\uc811\uc801\uc73c\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uc601\uc5ed\\n\ub178\ub780\uc0c9\uc740 React 18 \uc5d4\uc9c4\uc758 \uc601\uc5ed\uc785\ub2c8\ub2e4.\\n\\n\uc774\uc678\uc5d0 \uc81c\uacf5\ub418\ub294 \ub2e4\ub978 \ucee4\uc2a4\ud140 \ud6c5\ub4e4\ub3c4 \uac70\uc758 \ube44\uc2b7\ud55c \uad6c\uc870\ub97c \ub744\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```typescript\\n// \ucd94\uac00\ub85c \uad6c\ud604\ud560 \uc218 \uc788\ub294 \ud568\uc218\ub4e4\\n\\nexport const useSetExternalState = (store: DataObserver) => {\\n const { setState } = store;\\n\\n return setState;\\n};\\n\\nexport const useExternalValue = (store: DataObserver) => {\\n const { subscribe, getState } = store;\\n const state = useSyncExternalStore(subscribe, getState);\\n\\n return state;\\n};\\n\\n// \ubc14\ub2d0\ub77cJS \uc601\uc5ed\uc5d0\uc11c \uc790\uc5f0\uc2a4\ub7ec\uc6b4 \uc77d\uae30\ub97c \uc9c0\uc6d0\ud558\ub294 \ud568\uc218\\n\\nexport const getStoreSnapshot = (store: DataObserver) => {\\n return store.getState();\\n};\\n```\\n\\n\ub354 \ub2e4\uc591\ud55c \uc608\uc81c\ub294 [\uc5ec\uae30\uc5d0\uc11c \ud655\uc778](https://github.com/gabrielyoon7/external-state/tree/main/src/examples)\ud560 \uc218 \uc788\uace0\\n\uc791\uc131\ud55c \ub77c\uc774\ube0c\ub7ec\ub9ac \ucf54\ub4dc \uc804\ubb38\uc740 [\uc5ec\uae30\uc5d0\uc11c \ud655\uc778](https://github.com/gabrielyoon7/external-state/tree/main/src/lib/external-state)\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uaca8\uc6b0 \ud30c\uc77c \uc218\uc2ed \uc904\ub85c \ub9cc\ub4e0 \ucd08\uacbd\ub7c9 \uc0c1\ud0dc\uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\uc600\uc2b5\ub2c8\ub2e4"},{"id":"28","metadata":{"permalink":"/28","source":"@site/blog/2023-08-23-about-the-map-system-used-by-carffeine/index.mdx","title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \uad00\ud558\uc5ec","description":"\uc548\ub155\ud558\uc138\uc694? \uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \ub300\ud574\uc11c \uc18c\uac1c\ud558\ub824\uace0 \ud569\ub2c8\ub2e4.","date":"2023-08-23T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 23\uc77c","tags":[{"label":"google maps api","permalink":"/tags/google-maps-api"},{"label":"\uad6c\uae00 \uc9c0\ub3c4","permalink":"/tags/\uad6c\uae00-\uc9c0\ub3c4"}],"readingTime":17.43,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"28","title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \uad00\ud558\uc5ec","authors":["gabriel"],"tags":["google maps api","\uad6c\uae00 \uc9c0\ub3c4"]},"prevItem":{"title":"useSyncExternalStore\ub85c \ub9cc\ub4e4\uc5b4\ubcf4\ub294 \uc804\uc5ed\uc0c1\ud0dc\uad00\ub9ac \ub3c4\uad6c","permalink":"/29"},"nextItem":{"title":"EC2 \uc11c\ubc84 \ucd94\uac00\uc640 \ub3d9\uc2dc\uc5d0 Dev, Prod \ud658\uacbd \ubd84\ub9ac\ud558\uae30","permalink":"/27"}},"content":"\uc548\ub155\ud558\uc138\uc694? \uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \ub300\ud574\uc11c \uc18c\uac1c\ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\uc9c0\ub3c4 \uae30\ub2a5\uc5d0\uc11c \uac00\uc7a5 \ud575\uc2ec\uc778 \uae30\ub2a5 \ub450 \uac00\uc9c0\ub97c \ubf51\uc790\uba74, \uc9c0\ub3c4 \uadf8 \uc790\uccb4\uc640 \uc9c0\ub3c4 \uc704\uc5d0 \uadf8\ub824\uc9c0\ub294 \ub9c8\ucee4\ub97c \ubf51\uc744 \uc218 \uc788\uc744 \uac83\uc785\ub2c8\ub2e4. \uc9c0\ub3c4 \uc704\uc5d0 \ub9c8\ucee4\ub97c \uadf8\ub9ac\ub294 \uc77c\uc740 \uadf8\ub2e4\uc9c0 \uc5b4\ub835\uc9c0 \uc54a\uace0, documents \uc5d0 \uc788\ub294 \uc608\uc81c\ub4e4\uc744 \uc798 \ub530\ub77c\ud558\uba74 \ub204\uad6c\ub098 \ucda9\ubd84\ud788 \uad6c\ud604\ud560 \uc218 \uc788\uc744 \uac83\uc785\ub2c8\ub2e4.\\n\\n![no offset](./markers-on-map.png)\\n\\n\ud558\uc9c0\ub9cc \ub9c8\ucee4\uc758 \uac2f\uc218\uac00 \uacfc\ub3c4\ud558\uac8c \ub9ce\ub2e4\uba74 \uc5b4\ub5a4 \uc804\ub7b5\uc744 \uc138\uc6b8 \uc218 \uc788\uc744\uae4c\uc694?\\n\\n### \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294\uc694 ...\\n\\n\\n\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc5d0\uc11c \uc9c0\ub3c4\ub294 \uad49\uc7a5\ud788 \uc911\uc694\ud55c \uc694\uc18c \uc911 \ud558\ub098\uc600\uc2b5\ub2c8\ub2e4. \uc0ac\uc6a9\uc790\ub4e4\uc774 \uad81\uae08\ud55c \uc7a5\uc18c\uc758 \uc8fc\ubcc0\uc5d0 \uc788\ub294 \ucda9\uc804\uc18c\ub97c \uc2dc\uac01\uc801\uc73c\ub85c \uc81c\uacf5\ud574\uc8fc\uae30 \uc704\ud574\uc11c\ub294 \uc9c0\ub3c4\ub97c \uc798 \uc81c\uc5b4\ud560 \uc218 \uc788\uc5b4\uc57c \ud588\uc2b5\ub2c8\ub2e4. \ud2b9\ud788 \uc804\uad6d\uc5d0 \uc774\ubbf8 `\uc218\ub9cc \ub300\uc758 \ucda9\uc804\uc18c`\uac00 \ubcf4\uae09\uc774 \ub41c \uc0c1\ud669\uc5d0\uc11c \ucda9\uc804\uc18c \ub9c8\ucee4\ub97c \ubaa8\ub450 \uadf8\ub824\uc8fc\uae30 \uc704\ud574\uc11c\ub294 \ub9ce\uc740 \uc81c\uc57d\uc774 \uc788\uc5c8\uace0, \ub9c8\ucee4\ub97c \uc801\ub2f9\ud55c \uc218\uc900\uc73c\ub85c \ub80c\ub354\ub9c1 \ud558\ub824\uba74 \ud074\ub77c\uc774\uc5b8\ud2b8\uc640 \uc11c\ubc84 \uac04\uc5d0 \ud2b9\ubcc4\ud55c \uc791\uc5c5\uc774 \ud544\uc694\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc5b4\ub5a4 \uc804\ub7b5\uc744 \ud3bc\ucce4\ub294\uc9c0 \uc18c\uac1c\ud558\uae30\uc5d0 \uc55e\uc11c \ubbf8\ub9ac \ub9d0\uc500\ub4dc\ub9ac\uc9c0\ub9cc, \uc800\ud76c \ud300\uc5d0\uc11c \ucde8\ud55c \uc9c0\ub3c4 \uad00\ub9ac \uc804\ub7b5\uc740 \ubaa8\ub4e0 \ud504\ub85c\uc81d\ud2b8\uc5d0 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc744 \uac83\uc785\ub2c8\ub2e4. \uc9c0\ub3c4 \uc704\uc5d0 \ud55c\ubc88\uc5d0 \ud45c\ud604\ud560 \ub9c8\ucee4\uc758 \uac2f\uc218\uac00 \uc218\ubc31 \uac1c \uc774\ud558\ub77c\uba74, \uc11c\ubc84\uc5d0 \ub370\uc774\ud130\uac00 \uacfc\ub3c4\ud558\uac8c \ub9ce\uc740 \uac83\uc774 \uc544\ub2c8\ub77c\uba74 \uc624\ud788\ub824 \uc774\ub7ec\ud55c \uc804\ub7b5\uc774 \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc744 \ud574\uce60 \uc218 \uc788\uc744 \uac83\uc785\ub2c8\ub2e4. (\ud658\uacbd\uc774 \uc6d0\ud65c\ud558\ub2e4\uba74 \ub370\uc774\ud130\ub97c \uac00\ub2a5\ud55c \ub9ce\uc774 \ubcf4\uc5ec\uc8fc\ub294 \uac83\uc774 \uc88b\uc744\ud14c\ub2c8\uae50\uc694.)\\n\\n\ub610, \uc774 \uae00\uc5d0\uc11c\ub294 Google Maps API\ub97c \uae30\uc900\uc73c\ub85c \uc124\uba85\ud558\uace0 \uc788\uc9c0\ub9cc, \uc9c0\uc6d0\ud558\ub294 \uae30\ub2a5\uc774 \uc77c\ubd80 \ub2e4\ub974\ub354\ub77c\ub3c4 \ub300\ubd80\ubd84\uc758 \uc9c0\ub3c4 API\uc5d0\uc11c \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud55c \uc804\ub7b5\uc77c \uac83\uc785\ub2c8\ub2e4. \ucc38\uace0\ub85c \uac1c\uc778\uc801\uc73c\ub85c \uc0ac\uc6a9 \ud574\ubcf8 \uc5ec\ub7ec \ubca4\ub354 \uc0ac\uc758 \uc9c0\ub3c4 API\ub4e4\uc740 \ubaa8\ub450 \uc774\uc640 \uc720\uc0ac\ud55c \uae30\ub2a5\uc744 \uc81c\uacf5\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\\n### \uc88c\ud45c\ub780 \ubb34\uc5c7\uc77c\uae4c?\\n\\n\uc544\ub9c8 \uc5b4\ub9b0 \uc2dc\uc808\ubd80\ud130 \uc6b0\ub9ac\ub098\ub77c\uc5d0\ub294 \ud2b9\ubcc4\ud788 38\uc120\uc774\ub77c\ub294 \uac83\uc774 \uc874\uc7ac\ud55c\ub2e4\ub294 \uc0ac\uc2e4\uc744 \uad50\uc721\ubc1b\uae30\uc5d0 `\uc88c\ud45c\uacc4\ub77c\ub294 \uac83\uc774 \uc788\ub2e4\ub294 \uc0ac\uc2e4`\uc740 \ub204\uad6c\ub098 \uc54c \uac83\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \ub2f9\uc7a5 \uc704\ub3c4\uc640 \uacbd\ub3c4\ub97c \uad6c\ubd84\uc9c0\uc73c\ub77c\uace0 \ud558\uba74 \uc5b4\ub5a4 \uc120\uc774 \uc704\uc120\uc774\uace0 \uacbd\uc120\uc778\uc9c0 \ud5f7\uac08\ub9ac\uae30\uc5d0 \ucc0d\uc5b4\uc57c \ud560 \uac83\uc785\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc774 \uc120\uc774 \uc5b4\ub5a4 \uc120\uc778\uc9c0, \uc5b4\ub5a4 \uac12\uc744 \uc598\uae30\ud558\ub824\ub294 \uac83\uc778\uc9c0 \uc0ac\uc9c4\uacfc \ud568\uaed8 \uac04\ub2e8\ud788 \uc124\uba85\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./latlng.jpeg)\\n\\n\uc0ac\uc9c4\uc744 \ubcf4\uc2dc\uba74 \uc544\uc2dc\uaca0\uc9c0\ub9cc \uc704\ub3c4\ub780, \ub0a8\ubd81\uc758 \uc704\uce58\ub97c \ub098\ud0c0\ub0b4\ub294 \ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \uacbd\ub3c4\ub294 \ub3d9\uc11c\uc758 \uc704\uce58\ub97c \ub098\ud0c0\ub0b4\ub294 \ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \ub300\ubd80\ubd84\uc758 \uacf5\uc2dd \ubb38\uc11c\uac00 \uc601\uc5b4\ub85c \uc791\uc131\ub418\uc5b4\uc788\uace0, \ucf54\ub4dc\uc5d0\uc11c\ub3c4 \uc774\ub97c \ub098\ud0c0\ub0b4\ub294 \uac83\uc774 \uc911\uc694\ud558\uae30\uc5d0 \uc601\ubb38 \ud45c\uae30\ubc95\uae4c\uc9c0 \uc18c\uac1c\ub97c \ud558\uc790\uba74 \uc704\ub3c4\ub294 Latitude, \uacbd\ub3c4\ub294 Longitude\ub85c \ud45c\uae30\ud569\ub2c8\ub2e4. \uc774\uc720\ub294 \ubaa8\ub974\uaca0\uc9c0\ub9cc \uc81c\uacf5\ub418\ub294 \ubcc0\uc218\ub098 \uba54\uc11c\ub4dc \uba85\uc73c\ub85c lat, lng\ub77c\uace0 \uc904\uc5ec\uc11c \ud45c\uae30\ud558\uae30\ub3c4 \ud569\ub2c8\ub2e4.\\n\\n![no offset](./latlngeng.gif)\\n\\n\uc704\ub3c4\uc640 \uacbd\ub3c4\ub9cc \uc54c\uba74, \uc9c0\uad6c \uc704\uc758 \uc5b4\ub5a4 \uc704\uce58\ub97c \ub098\ud0c0\ub0bc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c, \uc5b4\ub5a4 \ub9c8\ucee4\ub97c \uc5b4\ub5a4 \uc704\uce58\uc5d0 \ucc0d\uc744 \uac83\uc778\uc9c0\ub294 \uc704\ub3c4\uc640 \uacbd\ub3c4 \uac12\uc73c\ub85c \uacb0\uc815\ud560 \uc218 \uc788\uac8c \ub418\uaca0\uc8e0?\\n\\n### \uc0ac\uc6a9\uc790\uac00 \uc5b4\ub51c \ubcf4\uace0 \uc788\uc744\uae4c?\\n\\n\uc9c0\ub3c4 api\uc5d0\uc11c \uc81c\uacf5\ud574\uc8fc\ub294 \uba54\uc11c\ub4dc\ub97c \ud65c\uc6a9\ud558\uba74 \uc0ac\uc6a9\uc790\uc758 \ub514\ubc14\uc774\uc2a4\uac00 \uc5b4\ub290 \uc704\uce58\ub97c \ubcf4\uace0 \uc788\ub294\uc9c0 \uc54c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```typescript\\nlet map = /* \uc5b4\ub514\uc120\uac00 \uc0dd\uc131\ub41c \uad6c\uae00 \ub9f5 \uac1d\uccb4 */\\nconst center = map.getCenter();\\nconsole.log(center.lng()); // \ub514\ubc14\uc774\uc2a4 \uc911\uc2ec\uc758 longitude\\nconsole.log(center.lat()); // \ub514\ubc14\uc774\uc2a4 \uc911\uc2ec\uc758 latitude\\n```\\n\\n\uc9c0\ub3c4 \uac1d\uccb4\ub85c \ubd80\ud130 \uc911\uc2ec\uc810\uc744 \uc54c\uac8c\ub418\uba74 \ud574\ub2f9 \ub514\ubc14\uc774\uc2a4\uc758 \uc911\uc2ec\uc758 \uc88c\ud45c\ub97c \uc54c\uc544\ub0bc \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\\n![no offset](./get-center.png)\\n\\n### \uc0ac\uc6a9\uc790\uc758 \ub514\ubc14\uc774\uc2a4\ub294 \uc5bc\ub9c8\ub098 \ub113\uac8c \ubcf4\uace0 \uc788\uc744\uae4c?\\n\\n\uc9c0\ub3c4 api\uc5d0\uc11c \uc81c\uacf5\ud574\uc8fc\ub294 \uba54\uc11c\ub4dc\ub97c \ud65c\uc6a9\ud558\uba74 \uc0ac\uc6a9\uc790\uc758 \ub514\ubc14\uc774\uc2a4\uac00 \uc5b4\ub5a4 \uc601\uc5ed\uc744 \ubcf4\uace0 \uc788\ub294\uc9c0\ub3c4 \uc54c\uac8c \ub429\ub2c8\ub2e4. \uc9c0\ub3c4 api \ub9c8\ub2e4 \uc81c\uacf5\ud558\ub294 \uc2a4\ud399\uc774 \ub2e4\ub974\uc9c0\ub9cc, \ub300\ubd80\ubd84\uc740 \uc5b4\ub5a4 \uc2dd\uc73c\ub85c\ub4e0 \uc54c\ub824\uc90d\ub2c8\ub2e4.\\n\\ngoogle maps API\uc5d0\uc11c\ub294 \ub514\uc2a4\ud50c\ub808\uc774\uc758 \ubd81\ub3d9\ucabd \ub05d \uc810\uc758 \uc88c\ud45c\uc640, \ub0a8\uc11c\ucabd \ub05d \uc810\uc758 \uc88c\ud45c\ub97c \uc81c\uacf5\ud574\uc90d\ub2c8\ub2e4.\\n\\n```typescript\\nconst map = /* \uc5b4\ub514\uc120\uac00 \uc0dd\uc131\ub41c \uad6c\uae00 \ub9f5 \uac1d\uccb4 */\\nconst bounds = map.getBounds();\\nconsole.log(bounds.getNorthEast().lng(), bounds.getNorthEast().lat()); // \ub514\ubc14\uc774\uc2a4 1\uc0ac\ubd84\uba74 \ub05d \uc810\uc758 longitude\uc640 latitude\\nconsole.log(bounds.getSouthWest().lng(), bounds.getSouthWest().lat()); // \ub514\ubc14\uc774\uc2a4 3\uc0ac\ubd84\uba74 \ub05d \uc810\uc758 longitude\uc640 latitude\\n```\\n\\n![no offset](./get-bounds.png)\\n\\n\ud3b8\uc758\uc0c1 \uc88c\ud45c\ub97c \ub2e4\uc74c\uacfc \uac19\uc774 \uc815\uc758\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n- \uc911\uc2ec \uc810 p0: (x0, y0)\\n- \ub514\ubc14\uc774\uc2a4\uc758 \uc81c 1\uc0ac\ubd84\uba74 \ub05d\uc810 p2: (x2, y2)\\n- \ub514\ubc14\uc774\uc2a4\uc758 \uc81c 3\uc0ac\ubd84\uba74 \ub05d\uc810 p1: (x1, y1)\\n\\n```\\n\uc704 \uc815\uc758\ub294 \uc544\ub798\uc5d0\uc11c\ub3c4 \uacc4\uc18d \uc124\uba85 \ub420 \uc810\uacfc \uc88c\ud45c \uc785\ub2c8\ub2e4.\\n```\\n\\n\uc774\ub807\uac8c \uc54c\uc544\ub0b8 \uac12\uc73c\ub85c \uc0ac\uc6a9\uc790 \ub514\ubc14\uc774\uc2a4\uc758 \uc601\uc5ed\uc744 \uc54c\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc774 \uac12\uc744 \uc880 \ub354 \ud6a8\uc728\uc801\uc73c\ub85c \ub2e4\ub8e8\uae30 \uc704\ud574 delta \uac1c\ub150\uc744 \ub3c4\uc785\ud588\uc2b5\ub2c8\ub2e4.\\n\\n### \ud654\uba74\uc5d0\uc11c \ubcf4\uace0 \uc788\ub294 \uc601\uc5ed\uc744 \ud655\ub300/\ucd95\uc18c \ud558\uba74 \uc5b4\ub5a4 \ud2b9\uc9d5\uc744 \ubcf4\uc77c\uae4c?\\n\\ndelta \uc124\uba85\uc744 \uc55e\uc11c, \uc0ac\uc6a9\uc790\uc758 \ub514\ubc14\uc774\uc2a4 \uc601\uc5ed\uacfc \ud655\ub300 \uc218\uc900\uc5d0 \ub530\ub978 \uc2e4\uc81c \uc88c\ud45c\uc5d0 \ub300\ud574 \uc54c\uc544\ubcf4\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790\uac00 \ud654\uba74\uc744 \uc5bc\ub9c8\ub098 \ub113\uac8c \ubcf4\uace0 \uc788\ub294\uc9c0\ub97c \uc27d\uac8c \uc54c\uae30 \uc704\ud574\uc11c\ub294 \ub05d\uc810\ub4e4\uc758 \uc218\uce58\ub97c \uacc4\uc0b0\ud574\uc904 \ud544\uc694\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc9c4\uc740 \uc0ac\uc6a9\uc790\uac00 \ub514\ubc14\uc774\uc2a4\ub97c \ud1b5\ud574 \ubc14\ub77c \ubcf4\uace0 \uc788\ub294 \uc911\uc2ec \uc88c\ud45c\uc640 \uadf8 \ub05d \uc810\uc744 \uc758\ubbf8\ud569\ub2c8\ub2e4.\\n\\n![no offset](./map-with-different-size.png)\\n\\n\\n\uc608\ub97c \ub4e4\uc5b4 \uc0ac\uc6a9\uc790\uac00 \uc9c0\ub3c4\ub97c \ub9ce\uc774 \ucd95\uc18c\ud55c \uacbd\uc6b0\uc5d0\ub294 \uc911\uc2ec \uc810 p0\uc740 \uadf8\ub300\ub85c\uc9c0\ub9cc \uc591 \ub05d\uc810 p1, p2\uc758 \uc704\uce58\uac00 \uc810\uc810 \uc911\uc2ec \uc810 p0\uc73c\ub85c \ubd80\ud130 \uba40\uc5b4\uc9c8 \uac83\uc785\ub2c8\ub2e4.\\n\\n\ubc18\uba74\uc5d0 \uc0ac\uc6a9\uc790\uac00 \uc9c0\ub3c4\ub97c \ub9ce\uc774 \ud655\ub300\ud55c \uacbd\uc6b0\uc5d0\ub294 \uc911\uc2ec \uc810 p0\uc740 \uadf8\ub300\ub85c\uc9c0\ub9cc \uc591 \ub05d\uc810 p1, p2\uc758 \uc704\uce58\uac00 \uc810\uc810 \uc911\uc2ec\uc810\uacfc \uac00\uae4c\uc6cc\uc9c8 \uac83\uc785\ub2c8\ub2e4.\\n\\n![no offset](./map-with-different-zoom.png)\\n\\n\uc591 \uc0ac\uc9c4 \ubaa8\ub450 \uc911\uc2ec \uc810 p0\ub294 \uadf8\ub300\ub85c\uc9c0\ub9cc, \ub514\ubc14\uc774\uc2a4\uc758 \ud655\ub300 \uc218\uc900\uc73c\ub85c \uc778\ud574 \uc591 \ub05d\uc810\uc778 p1\uacfc p2\uac00 \ub2ec\ub77c\uc9c4 \ubaa8\uc2b5\uc744 \ubcf4\uc778 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc989, \uc774\ub7f0 \uacb0\ub860\uc744 \ub0b4\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc591 \ub05d\uc810 p1, p2\uac00 \uc911\uc2ec \uc810 p0\uc73c\ub85c \ubd80\ud130 \uba40\uc5b4\uc9c8 \uc218\ub85d \uc9c0\ub3c4\ub97c \ucd95\uc18c\ud55c \uac83\uc774\ub2e4.\\n2. \uc591 \ub05d\uc810 p1, p2\uac00 \uc911\uc2ec \uc810 p0\uc73c\ub85c \ubd80\ud130 \uac00\uae4c\uc6cc \uc218\ub85d \uc9c0\ub3c4\ub97c \ud655\ub300\ud55c \uac83\uc774\ub2e4.\\n\\n\uc774 \ub54c \ub514\ubc14\uc774\uc2a4\uc758 \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc704\ub3c4 \uacbd\ub3c4 \uc0c1\uc73c\ub85c \uc5bc\ub9c8\ub098 \uba40\uc5b4\uc838\uc788\ub294\uc9c0\ub97c \uc218\uce58\ud654\ud558\uba74 \ud3b8\ud558\uac8c \ub2e4\ub8f0 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \ud655\ub300 \uc218\uc900\uc744 \uc218\uce58\ud654 \ud560 \uc218 \uc5c6\uc744\uae4c?\\n\\n\uc0ac\uc6a9\uc790\uc758 \ub514\uc2a4\ud50c\ub808\uc774\uc758 \uc911\uc2ec \uc810 p0\uc744 \uae30\uc900\uc73c\ub85c \ud558\uc5ec \uc591 \ub05d\uc810 p1, p2\uc774 \uc5bc\ub9c8\ub098 \uba40\uc5b4\uc838\uc788\ub294\uc9c0\uc5d0 \ub530\ub77c \uc9c0\ub3c4\uc758 \uc601\uc5ed \ubfd0\ub9cc \uc544\ub2c8\ub77c \uc5bc\ub9c8\ub098 \ub9ce\uc774 \ud655\ub300 \ub418\uc5c8\ub294\uc9c0 \uc5ec\ubd80\ub97c \uc54c\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub807\ub2e4\uba74 \uc774\ub97c \uc880 \ub354 \ud6a8\uc728\uc801\uc778 \ubc29\ubc95\uc73c\ub85c \ub098\ud0c0\ub0b4\ub824\uba74 \uc5b4\ub5a4 \uc804\ub7b5\uc744 \ucde8\ud560 \uc218 \uc788\uc744\uae4c\uc694?\\n\\n\uc0ac\uc6a9\uc790 \ub514\uc2a4\ud50c\ub808\uc774\ub97c \uc870\uae08 \ub354 \uc790\uc138\ud788 \uc0b4\ud3b4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./map-points.png)\\n\\n\uc911\ud559\uad50 \uc2dc\uc808 \ubc30\uc6e0\ub358 \uc88c\ud45c \ud3c9\uba74\uacc4\ub97c \ub5a0\uc62c\ub824\ubcf4\uba74 \ud654\uba74\uc5d0\uc11c \uc5bb\uc744 \uc218 \uc788\ub294 \uc88c\ud45c\ub4e4\uc740 \uc704\uc640 \uac19\uc2b5\ub2c8\ub2e4. \uc5ec\uae30\uc5d0\uc11c \uac01 \uc810\uc758 \uc218\uc9c1/\uc218\ud3c9\uc758 \ubcc0\ud654\ub7c9\uc778 delta\ub97c \uc54c\uc544\ubcf4\uba74 \uc5b4\ub5a8\uae4c\uc694?\\n\\n#### \uacbd\ub3c4 \ub378\ud0c0 (longitudeDelta)\\n\\np2\uc640 p0\uc758 \uacbd\ub3c4 \uac70\ub9ac, \uadf8\ub9ac\uace0 p1\uacfc p0\uc758 \uacbd\ub3c4 \uac70\ub9ac\ub294 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc989, `x2 - x0 === x0 - x1` \uc774\ub77c\ub294 \uacb0\ub860\uc744 \uc5bb\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c longitudeDelta\ub85c \uc815\uc758\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n#### \uc704\ub3c4 \ub378\ud0c0 (latitudeDelta)\\n\\np2\uc640 p0\uc758 \uc704\ub3c4 \uac70\ub9ac, \uadf8\ub9ac\uace0 p1\uacfc p0\uc758 \uc704\ub3c4 \uac70\ub9ac\ub294 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc989, `y2 - y0 === y0 - y1` \uc774\ub77c\ub294 \uacb0\ub860\uc744 \uc5bb\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c latitudeDelta\ub85c \uc815\uc758\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\\n![no offset](./delta.png)\\n\\n\ucf54\ub4dc\ub85c \uc54c\uc544\ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n```typescript\\nconst map = /* \uc5b4\ub514\uc120\uac00 \uc0dd\uc131\ub41c \uad6c\uae00 \ub9f5 \uac1d\uccb4 */\\nconst bounds = map.getBounds();\\nconst longitudeDelta = (bounds.getNorthEast().lng() - bounds.getSouthWest().lng()) / 2; // \uacbd\ub3c4 \ubcc0\ud654\ub7c9\\nconst latitudeDelta = (bounds.getNorthEast().lat() - bounds.getSouthWest().lat()) / 2; // \uc704\ub3c4 \ubcc0\ud654\ub7c9\\n```\\n\\n\ub4dc\ub514\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0\uc11c \ub378\ud0c0 \uac12\uc744 \uc0dd\uc131\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub807\ub2e4\uba74 \uc65c \uc774\ub807\uac8c \uad73\uc774 \ub378\ud0c0 \uac12\uc744 \uc0dd\uc131\ud55c \uac83\uc77c\uae4c\uc694?\\n\\n### delta\uc758 \uc720\uc6a9\ud55c \uc810 1: \uc6d0\ub798 \uc758\ub3c4\ud55c \uac12\uc744 \ubcf5\uc6d0\ud558\uae30 \uc27d\ub2e4.\\n\\n\uc11c\ubc84\uc758 \uc785\uc7a5\uc5d0\uc11c\ub294 \uc911\uc2ec \uc88c\ud45c\uc640 \ub378\ud0c0 \uac12\ub9cc \uc54c\uba74 \uc815\ud655\ud55c \uc601\uc5ed\ub9cc\ud07c \ub370\uc774\ud130\ub97c \ud638\ucd9c\ud560 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0\uc11c \uc11c\ubc84\ub85c \ub2e4\uc74c\uacfc \uac19\uc740 \ud30c\ub77c\ubbf8\ud130\ub97c \ub118\uaca8\uc92c\ub2e4\uace0 \uac00\uc815\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```json\\n{\\n \\"longitude\\": 127,\\n \\"latitude\\": 37,\\n \\"longitudeDelta\\": 0.1,\\n \\"longitudeDelta\\": 0.2,\\n}\\n```\\n\\n\uadf8\ub807\ub2e4\uba74 \uc11c\ubc84\uc5d0\uc11c\ub294 \ub2e4\uc74c\uacfc \uac19\uc774 \ud574\uc11d\ud560 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n```javascript\\nconst maxLongitude = longitude + longitudeDelta;\\nconst minLongitude = longitude - longitudeDelta;\\nconst maxLatitude = latitude + latitudeDelta;\\nconst minLatitude = latitude - latitudeDelta;\\n```\\n(javascript \uae30\uc900\uc73c\ub85c \uc791\uc131\ud588\uc2b5\ub2c8\ub2e4.)\\n\\n\uc774\ub807\uac8c \uc54c\uc544\ub0b8 \uacbd\uacc4 \uac12\uc744 \uac00\uc9c0\uace0 \ub2e4\uc74c\uacfc \uac19\uc740 sql\ubb38\uc744 \uc791\uc131\ud560 \uc218 \uc788\uac8c \ub420 \uac83\uc785\ub2c8\ub2e4.\\n\\n```sql\\nSELECT * FROM stations WHERE latitude >= :minLatitude AND latitude <= :maxLatitude AND longitude >= :minLongitude AND longitude <= :maxLongitude;\\n```\\n\\n![no offset](./find-within-range.png)\\n\\n\uc989, \uc704 \uadf8\ub9bc\ucc98\ub7fc, \uc6d0\ud558\ub294 \uc601\uc5ed\ub9cc\ud07c\ub9cc \uc815\ud655\ud558\uac8c \ub370\uc774\ud130\ub97c \ud638\ucd9c\ud560 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\\n### delta\uc758 \uc720\uc6a9\ud55c \uc810 2: \ub378\ud0c0\uac00 \ubb34\ubd84\ubcc4\ud558\uac8c \ucee4\uc9c0\ub294 \uac83\uc744 \ub9c9\uae30 \uc27d\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uc5b4 \uc0ac\uc6a9\uc790\uac00 \uc9c0\ub3c4\ub97c \ucd95\uc18c\ud558\uc5ec \ud55c\ubc18\ub3c4\ub97c \ub514\uc2a4\ud50c\ub808\uc774\uc5d0 \uac00\ub4dd \ucc44\uc6b4\ub2e4\uba74 \uc11c\ubc84\uac00 \uc5b4\ub5bb\uac8c \ub420\uae4c\uc694?\\n\\n\uc774\ub7ec\ud55c \ud589\uc704\ub97c \ub9c9\ub294 \uac00\uc7a5 \uc26c\uc6b4 \ubc29\ubc95\uc740 \uc9c0\ub3c4 api\uc5d0\uc11c \uc9c0\uc6d0\ud558\ub294 \uc90c \ub808\ubca8\uc744 \uc81c\ud55c \ud558\ub294 \uac83\uc785\ub2c8\ub2e4. \ud6c4\uc220\ud558\uaca0\uc9c0\ub9cc _\uc90c \ub808\ubca8\uc740 \ub514\uc2a4\ud50c\ub808\uc774\uc758 \ud574\uc0c1\ub3c4\ub97c \uace0\ub824\ud558\uc9c0 \ubabb\ud569\ub2c8\ub2e4._\\n\\n\ub530\ub77c\uc11c \uadfc\ubcf8\uc801\uc73c\ub85c \ub378\ud0c0\uac00 \uc77c\uc815 \uac12 \uc774\uc0c1 \uc694\uccad\ub418\uc9c0 \ubabb\ud558\ub3c4\ub85d, \ud639\uc740 \uc5f0\uc0b0\ub418\uc9c0 \ubabb\ud558\ub3c4\ub85d \ub9c9\uac8c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ubb3c\ub860 \ub378\ud0c0\uac00 \uc5c6\ub354\ub77c\ub3c4 \ub378\ud0c0 \uac12\uc744 \ucd94\uc815\ud558\uc5ec \uc5f0\uc0b0\ud560 \uc218 \uc788\uaca0\uc9c0\ub9cc, \uc774\ub97c _\uc218\uce58\ud654 \ud574\uc11c \uad00\ub9ac\ud55c\ub2e4\uba74 \ud074\ub77c\uc774\uc5b8\ud2b8\uc640 \uc11c\ubc84 \ubaa8\ub450 \uc9c0\ub3c4\ub97c \uc190\uc27d\uac8c \ud1b5\uc81c\ud558\ub294 \uac83\uc774 \uac00\ub2a5_\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uc5b4 \ub2e4\uc74c\uacfc \uac19\uc774 \ub378\ud0c0 \uac12\uc744 \uace0\uc815\ud558\uc5ec \uc694\uccad \uc601\uc5ed\uc744 \uc81c\ud55c\ud560(\uc694\uccad\uc744 \ubcf4\ub0b4\uc9c0 \uc54a\uac70\ub098 \uace0\uc815\ub41c \uc0ac\uc774\uc988\ub85c\ub9cc \uc694\uccad\uc744 \ubcf4\ub0bc) \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```json\\n{\\n longitude,\\n latitude,\\n longitudeDelta: longitudeDelta < 0.008 ? longitudeDelta : 0.008,\\n latitudeDelta: latitudeDelta < 0.004 ? latitudeDelta : 0.004,\\n}\\n```\\n\\n\ud2b9\uc815 \uc218\uce58\ub97c \ub118\uae30\uc9c0 \ubabb\ud558\uac8c \ucc98\ub9ac\ud560 \ub54c \ub208\uc5d0 \ubcf4\uc774\ub294 \ubcc0\uc218\ub85c \ucde8\uae09\ud558\uae30 \uc27d\uc2b5\ub2c8\ub2e4. (\uc989, \ub9e4\ubc88 \uacc4\uc0b0\ud558\uc9c0 \uc54a\uc544\ub3c4 \ub429\ub2c8\ub2e4.)\\n\\n\ub514\ubc14\uc774\uc2a4 \ud06c\uae30 \uad00\ub828 \ubb38\uc81c\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ubd84\uba85\ud788 \uac19\uc740 \uc90c \ub808\ubca8\uc774\uc9c0\ub9cc, \ub514\ubc14\uc774\uc2a4\uc758 \ud06c\uae30\ub098 \ud574\uc0c1\ub3c4\uc5d0 \ub530\ub77c \uc9c0\ub3c4\uac00 \ubcf4\uc5ec\uc9c0\ub294 \uc815\ub3c4\uac00 \ub2e4\ub985\ub2c8\ub2e4.\\n\\n![no offset](./different-device-size.png)\\n\\n\uc704 \uc0ac\uc9c4\uc740 \uad6c\uae00\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 zoom \ub808\ubca8\uc744 \ub3d9\uc77c\ud558\uac8c \ub9de\ucd98 \ud6c4, \uc5ec\ub7ec \ub514\ubc14\uc774\uc2a4\uc5d0\uc11c \ud638\ucd9c\ud55c \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc90c \ub808\ubca8\uc744 \ud1b5\ud574\uc11c \uc694\uccad\uc744 \uc81c\ud55c\ud558\ub2e4\ubcf4\uba74 \uc5ec\ub7ec \ud574\uc0c1\ub3c4\ub97c \uc81c\uc5b4\ud558\uae30 \uc5b4\ub835\uc2b5\ub2c8\ub2e4.\\n\\n![no offset](./too-big-screen.png)\\n\\n\uc2e4\uc81c\ub85c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uace0\ud574\uc0c1\ub3c4 \ubaa8\ub2c8\ud130\ub97c \ub300\uc751\ud558\uae30 \uc704\ud574 \ub378\ud0c0 \uac12\uc774 \ub108\ubb34 \ud06c\uac8c \ub418\uba74 \uc694\uccad\uc758 \uc81c\ud55c\uc744 \ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uc0ac\uc9c4\uc5d0\uc11c \ubcf4\uc2dc\ub2e4\uc2dc\ud53c \uace0\ud574\uc0c1\ub3c4 \ubaa8\ub2c8\ud130\uc758 \uacbd\uc6b0, \ub108\ubb34 \ub113\uc740 \ubc94\uc704\ub97c \uc694\uccad\ud55c\ub2e4 \uc2f6\uc73c\uba74 \uc911\uc2ec\uc810\uc73c\ub85c \ubd80\ud130 \uc77c\uc815 \uac70\ub9ac\ub9cc \ubcf4\uc5ec\uc8fc\ub3c4\ub85d \ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n(\ucc38\uace0\ub85c \uc90c \ub808\ubca8\uc5d0 \ub530\ub978 \uc694\uccad\ub3c4 \ub364\uc73c\ub85c \uc81c\ud55c\ud558\uace0 \uc788\uc5b4\uc11c \uba40\ub9ac\uc11c \ud638\ucd9c\ud558\ub294 \ud589\uc704\ub3c4 \uae08\uc9c0\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.)\\n\\n### delta\uc758 \uc720\uc6a9\ud55c \uc810 3: \uc801\ub2f9\ud55c \ubc94\uc704\ub97c \uc815\ud574\uc8fc\uae30 \ud3b8\ud558\ub2e4\\n\\n\uc704 \uc608\uc81c\uc5d0\uc11c\ub294 \uc815\ud655\ud55c \ubc94\uc704\ub9cc\ud07c \uc694\uccad\ud558\ub294 \uac83\uc744 \uc608\uc81c\ub85c \ud558\uc9c0\ub9cc, \ud504\ub85c\uc81d\ud2b8\uc5d0 \ub530\ub77c\uc11c \uc870\uae08 \ub354 \ub113\uc740 \uc601\uc5ed\uc744 \ud638\ucd9c\ud558\uace0 \uc2f6\uc744 \ub54c\uac00 \uc788\uc744 \uac83\uc785\ub2c8\ub2e4.\\n\\n![no offset](./bigger-than-delta.png)\\n\\n\uc608\ub97c \ub4e4\uc5b4 \ud604\uc7ac \uc0ac\uc6a9\uc790\uc758 \ub514\ubc14\uc774\uc2a4 \ud06c\uae30\ubcf4\ub2e4 \uc0b4\uc9dd \ud070 \ubc94\uc704\uc758 \ub370\uc774\ud130\ub97c \ubbf8\ub9ac \ub85c\ub4dc\ud574 \ub193\uc73c\uba74 \uc0ac\uc6a9\uc790\uac00 \uc881\uc740 \uc6c0\uc9c1\uc784\uc744 \ubcf4\uc77c \ub54c \ubd88\ud544\uc694\ud55c \uc7ac \ub80c\ub354\ub9c1\uc744 \uc904\uc5ec\uc11c \ub354 \ube60\ub978 \ub80c\ub354\ub9c1\uc774 \uac00\ub2a5\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uc0ac\uc2e4 \uc774 \uae30\ubc95\uc740 \ud504\ub85c\uc81d\ud2b8\ub9c8\ub2e4 \ub2e4\ub974\uaca0\uc9c0\ub9cc, \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ud55c\ubc88 \ubd88\ub7ec\uc628 \ub9c8\ucee4\ub97c \ub9e4\ubc88 \ud574\uc81c \ud558\uc9c0 \uc54a\uace0 **\uc774\uc804 \uc694\uccad \ub370\uc774\ud130\uc640 \ub2e4\uc74c \uc694\uccad \ub370\uc774\ud130\ub97c \ube44\uad50\ud558\uc5ec \ub2ec\ub77c\uc9c4 \ub9c8\ucee4\ub9cc\uc744 \uc815\ud655\ud558\uac8c \ud0c8\ubd80\ucc29\ud558\ub294 \uc791\uc5c5\uc744 \uc9c4\ud589**\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uae30\ubc95\uc744 \ud65c\uc6a9\ud558\uba74 \uc0ac\uc6a9\uc790\uac00 \uc881\uc740 \ubc94\uc704\uc5d0\uc11c \uc6c0\uc9c1\uc784\uc744 \ubcf4\uc600\uc744 \ub54c, \uae30\uc874\uc5d0 \ubd88\ub7ec\uc628 \ub9c8\ucee4\ub97c \uba54\ubaa8\ub9ac\uc5d0\uc11c \ud0c8\ub77d\uc2dc\ud0a4\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc744 \uac1c\uc120\ud560 \uc218\ub3c4 \uc788\uc744 \uac83\uc785\ub2c8\ub2e4.\\n\\n\ub9c8\ucee4\ub97c \uc0c1\ud0dc\uc5d0 \uc5f0\ub3d9\ud558\uc5ec \uc815\ud655\ud558\uac8c \uba54\ubaa8\ub9ac\uc5d0\uc11c \ud0c8\ubd80\ucc29 \uc2dc\ud0a4\ub294 \uc804\ub7b5\uc5d0 \ub300\ud55c \uae00\uc740 \uc774\ud6c4\uc5d0 \uc791\uc131\ud560 \uc608\uc815\uc785\ub2c8\ub2e4.\\n\\n\uae34 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4."},{"id":"27","metadata":{"permalink":"/27","source":"@site/blog/2023-08-17-given-ec2-prod-dev-sep.mdx","title":"EC2 \uc11c\ubc84 \ucd94\uac00\uc640 \ub3d9\uc2dc\uc5d0 Dev, Prod \ud658\uacbd \ubd84\ub9ac\ud558\uae30","description":"\uc548\ub155\ud558\uc138\uc694.","date":"2023-08-17T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 17\uc77c","tags":[{"label":"ec2","permalink":"/tags/ec-2"},{"label":"prod","permalink":"/tags/prod"},{"label":"dev","permalink":"/tags/dev"}],"readingTime":3,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"27","title":"EC2 \uc11c\ubc84 \ucd94\uac00\uc640 \ub3d9\uc2dc\uc5d0 Dev, Prod \ud658\uacbd \ubd84\ub9ac\ud558\uae30","authors":["jay"],"tags":["ec2","prod","dev"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud55c \uc9c0\ub3c4 \uc2dc\uc2a4\ud15c\uc5d0 \uad00\ud558\uc5ec","permalink":"/28"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300 \ud074\ub77c\uc774\uc5b8\ud2b8\uc758 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654","permalink":"/26"}},"content":"\uc548\ub155\ud558\uc138\uc694.\\n\uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\uc624\ub298\uc740 \uc800\ud76c\uac00 EC2 \uc778\uc2a4\ud134\uc2a4\ub97c \ubc1b\uc73c\uba74\uc11c, \uc5b4\ub5bb\uac8c dev, prod \ubc30\ud3ec \ud658\uacbd\uc744 \ubd84\ub9ac\ud588\ub294\uc9c0 \uc801\uc5b4\ubcf4\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\uae30\uc874 \uce74\ud398\uc778 \ud300\uc758 EC2 \uad6c\uc870\ub294 [\uc5ec\uae30\uc11c](https://blog.naver.com/sosow0212/223163203356) \ubcf4\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n## \uae30\uc874 \uc0c1\ud669\uacfc \ubb38\uc81c\uc810\\n\\n\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uae30\uc874\uc5d0 3\ub300\uc758 EC2 \uc778\uc2a4\ud134\uc2a4\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uac01\uac01 `infra, dev, db` \uc5ed\ud560\uc744 \ud558\ub294 \uc778\uc2a4\ud134\uc2a4\ub85c \uc874\uc7ac\ud558\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 release \ube0c\ub79c\uce58\ub97c \ud1b5\ud574 dev\uc11c\ubc84\uc5d0 \ubc30\ud3ec\ub97c \ud55c \ud6c4 \uac80\uc99d\uc774 \ub41c\ub2e4\uba74, \uc2e4\uc81c \uc0ac\uc6a9\uc790\ub4e4\uc774 \uc0ac\uc6a9\ud558\ub294 prod \uc11c\ubc84\uc5d0 \ubc30\ud3ec\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ubb38\uc81c\ub294 \uae30\uc874\uc758 3\ub300\uc758 \uc778\uc2a4\ud134\uc2a4 \uc911\uc5d0\uc11c dev \uc11c\ubc84\uc5d0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uae30\uc874 dev \uc11c\ubc84\ub294 \ucd1d 4\uac1c\uc758 \uc11c\ubc84\ub97c \ubc30\ud3ec\ud558\uace0 \uc788\uc5c8\uace0 \ubc30\ud3ec\ud558\ub294 \uc11c\ubc84\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4. `prod-BE, prod-FE, dev-BE, dev-FE`\\n\\n\uadf8\ub9ac\uace0, \uae30\uc874 dev \uc11c\ubc84\uc5d0\uc11c\ub294 \ud658\uacbd\uc744 \ubd84\ub9ac\ud574\uc8fc\uae30 \uc704\ud574\uc11c Nginx\ub97c \ud1b5\ud574\uc11c \ud3ec\ud2b8 \ud3ec\uc6cc\ub529\uc740 \ub2e4\uc74c\uacfc \uac19\uc774 \ud574\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n- prod-BE = 8080\\n- prod-FE = 3031\\n- dev-BE = 8081\\n- dev-FE = 3031\\n\\n\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 dev, prod \ud658\uacbd\uc774 \ubd84\ub9ac\ub418\uc9c0 \uc54a\uc544\uc11c \uc778\uc2a4\ud134\uc2a4\uc758 \uc0ac\uc6a9\ub7c9\uc774 \ub192\uc558\uace0, \uc774\uc5d0 \ub530\ub77c \ucd94\uac00\uc801\uc778 EC2 \uc778\uc2a4\ud134\uc2a4\uac00 \ud544\uc694\ud588\uc2b5\ub2c8\ub2e4.\\n\\n---\\n\\n## \ubb38\uc81c \ud574\uacb0\\n\ub2e4\ud589\ud788\ub3c4 \uce74\ud398\uc778 \ud300\uc5d0\uc11c \ucd94\uac00\uc801\uc778 EC2 \uc778\uc2a4\ud134\uc2a4\ub97c \ubc1b\uc558\uace0, \uc800\ud76c\ub294 \ubc30\ud3ec \ud658\uacbd\uc744 \ubd84\ub9ac\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![dev-prod-server](https://github.com/car-ffeine/car-ffeine.github.io/assets/63213487/52942893-3d8c-4c72-9972-278afa810d1d)\\n\\n\uc774\uc640 \uac19\uc774 \uae30\uc874 dev \uc11c\ubc84 \ud55c \uac1c\uac00 infra \uc11c\ubc84\uc640 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc5c8\ub294\ub370, \ub450 \uac08\ub798\ub85c \ub098\ub25c \uac83\uc744 \ud655\uc778\ud558\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 \ubc30\ud3ec\ub294 \ub2e4\uc74c\uacfc \uac19\uc774 \uc9c4\ud589\ub429\ub2c8\ub2e4.\\n\\n`release branch`\uc5d0 push\uac00 \uc77c\uc5b4\ub098\uba74 `dev\uc11c\ubc84\uc5d0 \ubc30\ud3ec \uc791\uc5c5`\uc774 \uc774\ub904\uc9d1\ub2c8\ub2e4.\\n`prod branch`\uc5d0 push\uac00 \uc77c\uc5b4\ub098\uba74 `prod\uc11c\ubc84\uc5d0 \ubc30\ud3ec \uc791\uc5c5`\uc774 \uc774\ub904\uc9d1\ub2c8\ub2e4.\\n\\n\ub610\ud55c \uae30\uc874 dev \uc11c\ubc84\uc5d0\uc11c 4\uac1c\uc758 \ud3ec\ud2b8\ud3ec\uc6cc\ub529 \ub610\ud55c \uad73\uc774 \uadf8\ub7f4 \ud544\uc694\uac00 \uc5c6\uc5b4\uc84c\uc2b5\ub2c8\ub2e4.\\n\uc0c8\ub85c\uc6b4 \uc11c\ubc84\uac00 \ucd94\uac00\ub428\uc5d0 \ub530\ub77c dev, prod \uc11c\ubc84 \uac01\uac01 Nginx\uc5d0\uc11c \ud3ec\ud2b8\ud3ec\uc6cc\ub529\uc744 \ub3d9\uc77c\ud558\uac8c `FE:3000, BE:8080` \uc73c\ub85c \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub807\uac8c \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 dev, prod \ud658\uacbd\uc744 \ubd84\ub9ac\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uac10\uc0ac\ud569\ub2c8\ub2e4!"},{"id":"26","metadata":{"permalink":"/26","source":"@site/blog/2023-08-16-how-fe-test.mdx","title":"\uce74\ud398\uc778 \ud300 \ud074\ub77c\uc774\uc5b8\ud2b8\uc758 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654","description":"\uc548\ub155\ud558\uc138\uc694, \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ud14c\uc2a4\ud2b8\ub97c \uc5b4\ub5bb\uac8c \ud558\uace0 \uc788\uc744\uae4c\uc694?","date":"2023-08-16T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 16\uc77c","tags":[{"label":"\ud14c\uc2a4\ud2b8","permalink":"/tags/\ud14c\uc2a4\ud2b8"},{"label":"test","permalink":"/tags/test"}],"readingTime":7.65,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"26","title":"\uce74\ud398\uc778 \ud300 \ud074\ub77c\uc774\uc5b8\ud2b8\uc758 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654","authors":["gabriel"],"tags":["\ud14c\uc2a4\ud2b8","test"]},"prevItem":{"title":"EC2 \uc11c\ubc84 \ucd94\uac00\uc640 \ub3d9\uc2dc\uc5d0 Dev, Prod \ud658\uacbd \ubd84\ub9ac\ud558\uae30","permalink":"/27"},"nextItem":{"title":"flyway\ub97c \uc774\uc81c\uc11c\uc57c \uc801\uc6a9\ud558\ub294 \uc774\uc720","permalink":"/25"}},"content":"\uc548\ub155\ud558\uc138\uc694, \uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ud14c\uc2a4\ud2b8\ub97c \uc5b4\ub5bb\uac8c \ud558\uace0 \uc788\uc744\uae4c\uc694?\\n\\n\uc77c\ubc18\uc801\uc73c\ub85c \uc18c\ud504\ud2b8\uc6e8\uc5b4 \ud14c\uc2a4\ud2b8\ub780 \ubc31\uc5d4\ub4dc\uc5d0\uc11c \uadf8 \uc911\uc694\uc131\uc774 \uac15\uc870\ub418\uace4 \ud558\uc9c0\ub9cc, \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0\uc11c\ub3c4 \uadf8\uc5d0 \ubabb\uc9c0 \uc54a\uac8c \uc911\uc694\ud55c \ubd80\ubd84\uc744 \ucc28\uc9c0\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc218\ub9ce\uc740 \ud234 \uc911\uc5d0\uc11c \uc5b4\ub5a4 \ud14c\uc2a4\ud2b8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub294\uc9c0 \uc18c\uac1c\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ub2e4\uc74c\uacfc \uac19\uc740 \ud504\ub860\ud2b8\uc5d4\ub4dc \ud14c\uc2a4\ud2b8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### Jest\\nJest\ub294 JavaScript\uc758 \ud14c\uc2a4\ud2b8\ub97c \uc704\ud55c \ub300\ud45c\uc801\uc778 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.\\n\uae30\ubcf8 \uc124\uc815\uc774 \uac04\ud3b8\ud558\uace0, \ube60\ub974\uac8c \ud14c\uc2a4\ud2b8\ub97c \uc2e4\ud589\ud560 \ub54c \uad49\uc7a5\ud788 \uc720\uc6a9\ud569\ub2c8\ub2e4.\\n\ud568\uc218\ub97c mocking\ud558\uc5ec \uc758\uc874\uc131\uc774 \uac15\ud55c \ud568\uc218\ub97c \uc81c\uac70\ud558\uc5ec \uc6d0\ud558\ub294 \ud14c\uc2a4\ud2b8\ub97c \uc27d\uac8c \uad6c\uc131\ud560 \uc218 \uc788\ub2e4\ub294 \ud2b9\uc9d5\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\\n### React Testing Library\\nReact Testing Library\ub294 \ub9ac\uc561\ud2b8 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc758 UI\ub97c \ud14c\uc2a4\ud2b8\ud558\uae30 \uc704\ud55c \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.\\nReact \ucef4\ud3ec\ub10c\ud2b8\ub97c \ud638\ucd9c\ud558\uc5ec, \uc0ac\uc6a9\uc790\uc758 \uc758\ub3c4\ub300\ub85c \uc870\uc791\ud560 \uc218 \uc788\ub294 \ud589\uc704\ub97c \uc815\uc758\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\uc0ac\uc6a9\uc790 \uc785\uc7a5\uc5d0\uc11c \uc0c1\ud638\uc791\uc6a9 \ud560 \uc218 \uc788\ub294 \ubd80\ubd84\uc744 \uc2a4\ud06c\ub9bd\ud2b8\ub85c \uc791\uc131\ud558\uc5ec \ucef4\ud3ec\ub10c\ud2b8\uac00 \uc5b4\ub5bb\uac8c \ubcc0\ud654\ud558\ub294\uc9c0\ub97c \ud14c\uc2a4\ud2b8 \ud560 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\uac00\ub839, \uc5b4\ub5a4 \uc0ac\uc6a9\uc790\uac00 \uc5b4\ub5a4 \ud3fc\uc5d0 \uc5b4\ub5a4 \uac12\uc744 \uc785\ub825\ud588\uc744 \ub54c\uc758 \uc608\uc0c1\ub418\ub294 \uacb0\uacfc\ub97c \uc791\uc131\ud574\ub450\uba74 \uc774\ud6c4\uc5d0 \ucf54\ub4dc \uc791\uc5c5 \uc911 \ubc84\uadf8\uac00 \ubc1c\uc0dd\ud55c\ub2e4\uba74 \ud574\ub2f9 \uc704\uce58\uc5d0\uc11c \ud14c\uc2a4\ud2b8\uac00 \uc2e4\ud328\ud560 \uac83\uc785\ub2c8\ub2e4.\\n\\n### Storybook\\nStorybook\uc740 UI\ub97c \ucef4\ud3ec\ub10c\ud2b8 \ub2e8\uc704\ub85c \uac1c\ubc1c\ud558\uace0 \uadf8 \uc989\uc2dc \uc2dc\uac01\ud654 \ud560 \uc218 \uc788\ub3c4\ub85d \ub3d5\ub294 \ud14c\uc2a4\ud305 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.\\n\ucef4\ud3ec\ub10c\ud2b8\ub97c \ub208 \uc55e\uc5d0 \ubc14\ub85c \ubcf4\uc5ec\uc8fc\uace0 \uc2e4\uc81c \ub9ac\uc561\ud2b8\uc5d0\uc11c \ub3d9\uc791\ud558\ub294 \uac83 \ucc98\ub7fc \ucef4\ud3ec\ub10c\ud2b8 \ub2e8\uc704\ub85c \uac1c\ubc1c\uc744 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. CDD\ub97c \uc9c0\ud5a5\ud55c\ub2e4\uba74 \uad49\uc7a5\ud788 \uc720\uc6a9\ud55c \uae30\ub2a5\uc774\uba70, \uac1c\ubc1c\uc790\uac00 \uc544\ub2cc \ud611\uc5c5\uc790\uc5d0\uac8c\ub3c4 \uc6d0\ud65c\ud55c \ucee4\ubba4\ub2c8\ucf00\uc774\uc158\uc744 \ub3c4\uc640\uc90d\ub2c8\ub2e4.\\n\ucef4\ud3ec\ub10c\ud2b8 \ub2e8\uc704\ub85c \uac1c\ubc1c\ud558\uae30 \ub54c\ubb38\uc5d0 \uac1c\ubcc4 \ucef4\ud3ec\ub10c\ud2b8\uac00 \uc5b4\ub5bb\uac8c \ub3d9\uc791\ud558\ub294\uc9c0 \ud655\uc778\ud560 \uc218 \uc788\ub2e4\ub294 \uac83 \uc790\uccb4\uac00 \uad49\uc7a5\ud55c \uc774\uc810\uc73c\ub85c \uc791\uc6a9\ud569\ub2c8\ub2e4.\\n\uc608\ub97c \ub4e4\uc5b4 \uc5b4\ub5a4 \ucef4\ud3ec\ub10c\ud2b8\uac00 \ud2b9\uc815 \uba54\ub274 \uc548\uc5d0 \uc874\uc7ac\ud574\uc57c \ud55c\ub2e4\uba74, \uc774\uac83\uc744 \ud655\uc778\ud558\uae30 \uc704\ud574 \ud574\ub2f9 \uba54\ub274\uae4c\uc9c0 \uc811\uadfc\ud574\uc57c \ud560 \uac83\uc785\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc Storybook\uc744 \uc774\uc6a9\ud558\uba74 \ud2b9\uc815 \ucef4\ud3ec\ub10c\ud2b8\ub97c Storybook \uc704\uc5d0 \uc62c\ub824\ub193\uace0 \ud14c\uc2a4\ud2b8\ub97c \ud560 \uc218 \uc788\uc5b4 \ube60\ub974\uac8c \uc791\uc5c5\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n\uc778\ud130\ub809\uc158\uc774\ub098 \uc6f9\uc811\uadfc\uc131\uc744 \ud655\uc778\ud574\uc8fc\ub294 \ud50c\ub7ec\uadf8\uc778\ub3c4 \uc874\uc7ac\ud558\uc5ec \ud504\ub860\ud2b8\uc5d4\ub4dc \uac1c\ubc1c\uc5d0\uc11c \uad49\uc7a5\ud788 \uc911\uc694\ud55c \uc5ed\ud560\ub85c \ubd80\uc0c1\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uc800\ud76c \ud300\uc740 \uc774\uc678\uc5d0 Cypress\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\ub3c4 \uace0\ub824\ud558\uc600\uc73c\ub098, \uc9c0\ub3c4\uc640 \uacb0\ud569\ub41c \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \ud14c\uc2a4\ud2b8\ud558\uae30\uc5d0 \ub2e4\uc18c \uc5b4\ub824\uc6c0\uc774 \uc788\uc5b4 \uc704 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc744 \uac1c\ubc1c\uc5d0 \ud65c\uc6a9\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uc800\ud76c\ub294 \uc704 \ud14c\uc2a4\ud305 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc744 \uc6d0\ud65c\ud788 \ud65c\uc6a9\ud558\uae30 \uc704\ud574 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654\ub97c \uad6c\ucd95\ud588\uc2b5\ub2c8\ub2e4.\\n\\n## Jest\uc640 React Testing Library \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654\\n\\n```yaml\\nname: frontend-test\\n\\non:\\n pull_request:\\n branches:\\n - main\\n - develop\\n paths:\\n - frontend/**\\n - .github/**\\n\\npermissions:\\n contents: read\\n\\njobs:\\n test:\\n name: test-when-pull-request\\n runs-on: ubuntu-latest\\n environment: test\\n defaults:\\n run:\\n working-directory: ./frontend\\n steps:\\n - name: Checkout PR\\n uses: actions/checkout@v2\\n - name: Install dependencies\\n run: npm install\\n - name: Test\\n run: npm run test\\n```\\n\\n\\n#### \uc774\ubca4\ud2b8 \ud2b8\ub9ac\uac70 \uc124\uc815\\npull_request \uc774\ubca4\ud2b8\uac00 \ubc1c\uc0dd\ud558\uc600\uc744 \ub54c, \ud574\ub2f9 \uc774\ubca4\ud2b8\uac00 main \ube0c\ub79c\uce58\uc640 develop \ube0c\ub79c\uce58\uc5d0\uc11c\ub9cc \ub3d9\uc791\ud569\ub2c8\ub2e4.\\n\\n#### \ubcc0\uacbd \uc0ac\ud56d \uacbd\ub85c \uc81c\ud55c\\n\ud14c\uc2a4\ud2b8\ub97c \uc2e4\ud589\ud560 \ub54c\ub294 frontend \ub514\ub809\ud1a0\ub9ac\uc640 .github \ub514\ub809\ud1a0\ub9ac \ub0b4\uc758 \ud30c\uc77c\ub4e4\uc744 \uace0\ub824\ud558\ub3c4\ub85d \ud588\uc2b5\ub2c8\ub2e4. \ubc31\uc5d4\ub4dc\uc640\uc758 \ud658\uacbd \ubd84\ub9ac\ub97c \uc704\ud574 \uc774\ub7ec\ud55c \uc811\uadfc \uc81c\ud55c\uc744 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n#### \uad8c\ud55c \uc124\uc815\\npermissions\uc740 \uc77d\uae30 \uad8c\ud55c\ub9cc \uc124\uc815\ub418\uc5b4 \uc788\uc5b4 \ucf54\ub4dc\ub098 \ud30c\uc77c\uc744 \ubcc0\uacbd\uc744 \ubc29\uc9c0\ud569\ub2c8\ub2e4.\\n\\n#### \uc791\uc5c5(Job) \uc124\uc815\\ntest\ub77c\ub294 \uc774\ub984\uc758 \uc791\uc5c5\uc744 \uc815\uc758\ud558\uc600\uace0, \uc774 \uc791\uc5c5\uc5d0\uc11c\ub294 Ubuntu \ud658\uacbd\uc5d0\uc11c \ud14c\uc2a4\ud2b8\ub97c \uc2e4\ud589\ud569\ub2c8\ub2e4. test\ub77c\ub294 \uc774\ub984\uc758 \ud658\uacbd \ubcc0\uc218\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4. \ud14c\uc2a4\ud2b8\ub294 (\uce74\ud398\uc778 \ud300 \ub808\ud3ec\uc9c0\ud1a0\ub9ac\uc758) frontend \ub514\ub809\ud1a0\ub9ac\uc5d0\uc11c \uc791\uc5c5\ud558\ub3c4\ub85d \ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n#### \uc2a4\ud15d(Step) \uc124\uc815\\n\ucf54\ub4dc\ub97c \uccb4\ud06c\uc544\uc6c3\ud558\uace0, \uc758\uc874\uc131\uc744 \uc124\uce58\ud558\uba70, \ud14c\uc2a4\ud2b8\ub97c \uc2e4\ud589\ud558\ub294 \uc138 \uac00\uc9c0 \ub2e8\uacc4\ub85c \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\\n\\n\uc774\ub7ec\ud55c \uc124\uc815\uc744 \ud1b5\ud574 PR\uc5d0 \ucf54\ub4dc\uac00 \uc62c\ub77c\uc62c \ub54c \uc790\ub3d9\uc73c\ub85c \ud504\ub860\ud2b8\uc5d4\ub4dc \ud14c\uc2a4\ud2b8\uac00 \uc2e4\ud589\ub429\ub2c8\ub2e4.\\n\\n\uc774\ub7ec\ud55c \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654 \uc804\ub7b5\uc740 \ud504\ub860\ud2b8\uc5d4\ub4dc \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc548\uc815\uc801\uc774\uac8c \uac1c\ubc1c\ud558\uace0 \uc720\uc9c0\ud560 \uc218 \uc788\ub3c4\ub85d \ub3c4\uc640\uc90d\ub2c8\ub2e4.\\n\\n## Storybook\uc758 \ube4c\ub4dc \uc790\ub3d9\ud654\\n\\n```yaml\\nname: storybook-deploy\\n\\non:\\n pull_request:\\n branches:\\n - develop\\n paths:\\n - frontend/**\\n - .github/**\\n\\njobs:\\n build:\\n runs-on: ubuntu-22.04\\n defaults:\\n run:\\n working-directory: ./frontend\\n steps:\\n - name: Setup Repository\\n uses: actions/checkout@v3\\n\\n - name: Set up Node\\n uses: actions/setup-node@v3\\n with:\\n node-version: 18.16.0\\n\\n - name: Install dependencies\\n run: npm install\\n\\n - name: Cache node_modules\\n id: cache\\n uses: actions/cache@v3\\n with:\\n path: \'**/node_modules\'\\n key: ${{ runner.os }}-node-${{ hashFiles(\'**/package-lock.json\') }}\\n restore-keys: |\\n ${{ runner.os }}-node-\\n\\n - name: storybook build\\n run: npm run build-storybook\\n\\n - name: Upload storybook build files to temp artifact\\n uses: actions/upload-artifact@v3\\n with:\\n name: Storybook\\n path: frontend/storybook-static\\n deploy:\\n needs: build\\n runs-on: self-hosted\\n steps:\\n - name: Remove previous version app\\n working-directory: .\\n run: rm -rf dist\\n\\n - name: Download the built file to AWS\\n uses: actions/download-artifact@v3\\n with:\\n name: Storybook\\n path: frontend/dev/dist\\n\\n - name: Move folder\\n working-directory: frontend/dev/\\n run: |\\n rm -rf /home/ubuntu/dist/*\\n cp -r ./dist /home/ubuntu\\n\\n - name: comment PR\\n uses: thollander/actions-comment-pull-request@v1\\n env:\\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\\n with:\\n message: \'\ud83d\ude80storybook: https://storybook.carffe.in/\'\\n```\\n\\n\ube44\uc2b7\ud55c \ucf54\ub4dc\uc774\uc9c0\ub9cc, \ub9e4\ubc88 PR\uc774 \uc5f4\ub9b4 \ub54c \ub9c8\ub2e4 \uc2a4\ud1a0\ub9ac\ubd81\uc774 \uc790\ub3d9\uc73c\ub85c \ube4c\ub4dc \ubc0f \ubc30\ud3ec\ub429\ub2c8\ub2e4.\\n\ubc30\ud3ec\uac00 \uc644\ub8cc\ub418\uba74 \ubc30\ud3ec\ub41c URL\uc744 \uc54c\ub824 \ucf54\ub4dc \ub9ac\ubdf0\ud560 \ub54c \ucc38\uace0\ud560 \uc218 \uc788\ub3c4\ub85d \ub3d5\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc0c1 \uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud558\uace0 \uc788\ub294 \ud14c\uc2a4\ud305 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc640 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654 \ubc29\ubc95\uc744 \uc54c\uc544\ubd24\uc2b5\ub2c8\ub2e4."},{"id":"25","metadata":{"permalink":"/25","source":"@site/blog/2023-08-15-flyway.mdx","title":"flyway\ub97c \uc774\uc81c\uc11c\uc57c \uc801\uc6a9\ud558\ub294 \uc774\uc720","description":"\uc548\ub155\ud558\uc138\uc694","date":"2023-08-15T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 15\uc77c","tags":[{"label":"hello","permalink":"/tags/hello"},{"label":"world","permalink":"/tags/world"}],"readingTime":7.585,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"25","title":"flyway\ub97c \uc774\uc81c\uc11c\uc57c \uc801\uc6a9\ud558\ub294 \uc774\uc720","authors":["boxster"],"tags":["hello","world"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300 \ud074\ub77c\uc774\uc5b8\ud2b8\uc758 \ud14c\uc2a4\ud2b8 \uc790\ub3d9\ud654","permalink":"/26"},"nextItem":{"title":"Out of memory trouble shooting","permalink":"/24"}},"content":"\uc548\ub155\ud558\uc138\uc694\\n\\n## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\\n\uc800\ud76c \ud300\uc740 flyway\ub97c \uc801\uc6a9\ud588\uc2b5\ub2c8\ub2e4. \uac00\uc7a5 \ud070 \uc774\uc720\ub294 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \ub370\uc774\ud130\ub97c drop \ud560 \uc218 \uc5c6\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\\n\ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c drop\ud558\ub294 \uac83\uacfc flyway\uac00 \ubb34\uc2a8 \uc0c1\uad00\uc774 \uc788\uae38\ub798 \uc801\uc6a9\ud560\uae4c\uc694.\\n\\n### \uc608\uc2dc \uc0c1\ud669\\n\\n\uc81c\uac00 \uc544\ub798\uc640 \uac19\uc774 Member\ub77c\ub294 entity\ub97c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n```java\\nclass Member {\\n\\n private Long id;\\n private String name;\\n}\\n```\\n\uc9c0\uae08\uc758 entity\ub294 \ub450\uac1c\uc758 \ud544\ub4dc \ubc16\uc5d0 \uc5c6\uc2b5\ub2c8\ub2e4. \uc5b4\ub290 \ub0a0\ubd80\ud130 Member\uc5d0 email\uc774\ub77c\ub294 \uc815\ubcf4\uac00 \uc788\uc5b4\uc57c\ud55c\ub2e4\ub294 \uc694\uad6c\uc0ac\ud56d\uc774 \uc0dd\uae41\ub2c8\ub2e4.\\n\uadf8\ub798\uc11c \uc800\ud76c\ub294 \uc544\ub798\uc640 \uac19\uc774 email\uc744 \ucd94\uac00\ud569\ub2c8\ub2e4.\\n```java\\nclass Member {\\n\\n private Long id;\\n private String name;\\n private String email;\\n}\\n```\\n\uadf8\ub9ac\uace0 \ub2e4\uc2dc jpa\uc758 ddl-auto \uc18d\uc131 \uc911 create\ub97c \uc0ac\uc6a9\ud574\uc11c \uc0c8\ub85c\uc6b4 \ud14c\uc774\ube14\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4. \uae30\uc874\uc758 \ud14c\uc774\ube14\uc744 \ub2e4 \ub0a0\ub9ac\uba74\uc11c\uc694.\\n\\n\ud558\uc9c0\ub9cc \uc800\ud76c\uc758 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc758 \ub370\uc774\ud130\ub4e4\uc744 \uadf8\ub0e5 drop\ud574\ub3c4 \ub418\ub294 \uac83\uc77c\uae4c\uc694?\\n\uac1c\ubc1c \uc11c\ubc84\ub77c\ub3c4 \ud798\ub4e4\uac8c \uc313\uc740 \ub370\uc774\ud130\ub4e4\uc744 \ud14c\uc774\ube14\uc774 \uc870\uae08 \ubcc0\uacbd\ub418\uc5c8\ub2e4\uace0 \ub0a0\ub824\ubc84\ub9ac\ub294 \uac83\uc740 \ubc14\ubcf4\uac19\uc740 \uc77c\uc774\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub7ec\uba74 ddl-auto\uc758 \ub2e4\ub978 \uc870\uac74\uc778 update\ub97c \uc0ac\uc6a9\ud558\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4. \uadf8\ub7ac\ub354\ub2c8 jpa\uac00 \uc544\ub798\uc640 \uac19\uc774 \ucffc\ub9ac\ub97c \uc774\uc058\uac8c \ub9cc\ub4e4\uc5b4 \uc92c\uc2b5\ub2c8\ub2e4.\\n```sql\\nALTER TABLE member\\n ADD COLUMN email varchar(255);\\n```\\nupdate\ub97c \uc0ac\uc6a9\ud558\ub2c8 \uc544\uc8fc \ud3b8\ud558\uac8c \uce7c\ub7fc\uc774 \ucd94\uac00\ub418\ub294 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc5ec\uae30\uc11c \ub610 \uc544\ub798\uc640 \uac19\uc740 \uc694\uad6c\uc0ac\ud56d\uc774 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\nemail\uc758 \uc81c\uc57d\uc870\uac74\uc73c\ub85c null\uc774 \ub418\uba74 \uc548\ub418\uace0, \uae38\uc774\ub294 20\uc790\uac00 \ub418\uc5b4\uc57c\ud569\ub2c8\ub2e4.\\n\uadf8\ub7ec\uba74 \uc5b4\ub178\ud14c\uc774\uc158\uc744 \uc0ac\uc6a9\ud558\uc5ec \ubcc0\uacbd\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n```java\\nclass Member {\\n\\n private Long id;\\n private String name;\\n @Column(nullable= false, length = 20)\\n private String email;\\n}\\n```\\n\uc774\ub807\uac8c \ud558\uace0 \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc7ac\uc2dc\uc791 \ud588\uc2b5\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uc544\ubb34\ub7f0 ddl\uc774 \ubc1c\uc0dd\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc65c\ub0d0\uba74 Jpa\uc758 ddl-auto: update\uc758 \uc18d\uc131\uc740 \uc81c\uc57d\uc870\uac74\uc774 \ubcc0\uacbd\ub41c \uac83\uc740 \ubc18\uc601\ud574\uc8fc\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ub9cc\uc57d \uc774 \uc804\uc758 \ud68c\uc6d0\ub4e4\uc758 email\uc774 null\uc778 row\ub3c4 \uc788\ub2e4\uba74 \uc5b4\ub5bb\uac8c \ub420\uae4c\uc694? \uc81c\uc57d\uc870\uac74\uc744 \ubc18\uc601\ud560 \uc218 \uc5c6\uc744 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uc2dd\uc73c\ub85c \uc6b4\uc601 \ub3c4\uc911 table\uc758 \uce7c\ub7fc\ub4e4\uc774 \ucd94\uac00\ub418\uac70\ub098, \uc0ad\uc81c\ub418\uac70\ub098, \ud639\uc740 \uc81c\uc57d\uc870\uac74\uc774 \ubcc0\uacbd\ub420 \ub54c update \uc18d\uc131\ub9cc\uc73c\ub85c\ub294 \ubc18\uc601\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n## flyway\\n\\n\uadf8\ub798\uc11c flyway\ub97c \uc0ac\uc6a9\ud588\uc2b5\ub2c8\ub2e4.\\n\ubb3c\ub860 flyway \uc5c6\uc774\ub3c4 \uc774\ub7f0 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc29\ubc95\uc740 \uac04\ub2e8\ud569\ub2c8\ub2e4. \ub370\uc774\ud130\ubca0\uc774\uc2a4\uac00 \uc788\ub294 \uc11c\ubc84\uc5d0 \uc9c1\uc811 \uc811\uc18d\ud558\uc5ec ddl\uc744 \uc9c1\uc811 \ud558\ub098 \ud558\ub098 \ub2e4 \uc791\uc131\ud558\uba74 \ub429\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774\ub7f0 \ubc29\uc2dd\uc5d0\ub294 \ub2e8\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4. \ud558\ub098 \ud558\ub098 \uc9c1\uc811 \uc785\ub825\ud558\ub2e4\ubcf4\ub2c8 \ud734\uba3c \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud560 \uc218\ub3c4 \uc788\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ub9e4\ubc88 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc11c\ubc84\uc5d0 \uc811\uc18d\ud574\uc57c\ud55c\ub2e4\ub294 \ub2e8\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\uc774\ub807\uac8c \ub9e4\ubc88 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \uc811\uc18d\uc744 \ud574\uc57c\ud55c\ub2e4\uba74 cd\ub97c \ud558\ub294 \uc774\uc720\uac00 \uc788\uc744\uae4c\uc694?\\n\\n\ud558\uc9c0\ub9cc flyway\ub97c \uc0ac\uc6a9\ud558\uba74 \ud3b8\ud558\uac8c \ubcc0\uacbd\ub41c schema\ub97c \uad00\ub9ac\ud560 \uc218 \uc788\uace0 \uc5b8\uc81c \ubc14\ub00c\uc5c8\ub294\uc9c0 \uc5b4\ub5bb\uac8c \ubc14\ub00c\uc5c8\ub294\uc9c0 \ud655\uc778\ub3c4 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uae00\ub85c\ub294 \uc798 \uc640\ub2ff\uc9c0 \uc54a\uc744 \uc218\ub3c4 \uc788\uc73c\ub2c8 \uc0ac\uc6a9\ubc95\uc744 \ud655\uc778\ud558\uba74\uc11c \uc5b4\ub5a4 \uc7a5\uc810\uc774 \uc788\ub294\uc9c0 \ud655\uc778\ud574\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 flyway \uc758\uc874\uc131\uc744 \ucd94\uac00\ud558\uace0 `resources/db/migration` \ud328\ud0a4\uc9c0\ub97c \ub9cc\ub4ed\ub2c8\ub2e4.\\n\uac70\uae30\uc5d0 file\uc744 \ub9cc\ub4ed\ub2c8\ub2e4. \ud30c\uc77c \uc774\ub984\uc774 \uc911\uc694\ud55c\ub370\uc694 `V1__init.sql` \uc774\ub7ec\ud55c \ubc29\uc2dd\uc73c\ub85c `V{version \uc22b\uc790}__{\uc5b4\ub5a0\ud55c \ud30c\uc77c\uc778\uc9c0\uc5d0 \ub300\ud55c \uc774\ub984}.sql` \uc5b8\ub354\uc2a4\ucf54\uc5b4 2\uac1c\ub294 \ud544\uc218\ub85c \uc791\uc131\ud574\uc57c\ud569\ub2c8\ub2e4.\\n\\n```sql\\ncreate table member(\\n id bigint auto_increment primary key,\\n name varchar(255) null,\\n);\\n```\\n\uc774\ub807\uac8c `V1__init.sql`\uc5d0 \ub300\ud55c \ud30c\uc77c\uc744 \uc791\uc131\ud588\uc2b5\ub2c8\ub2e4. \uc774\uc81c\ub294 email\uc744 \ucd94\uac00\ud55c\ub2e4\ub294 \uc694\uad6c\uc0ac\ud56d\uc744 \ubc18\uc601\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```sql\\nALTER TABLE member\\n ADD COLUMN email varchar(255);\\n```\\n\uc774\ub807\uac8c \uc0c8\ub85c\uc6b4 \ud30c\uc77c\uc744 \ub9cc\ub4e4\uc5b4\uc11c \ud574\ub2f9 \uc2a4\ud06c\ub9bd\ud2b8\ub97c \uc791\uc131\ud588\uc2b5\ub2c8\ub2e4. \ud30c\uc77c\uba85\uc774 \uc911\uc694\ud55c\ub370\uc694, \uc774\uc804 \ud30c\uc77c\uc758 \uc22b\uc790\ubcf4\ub2e4 +1 \uc774 \ub418\ub294 \uc22b\uc790\ub97c V \ub4a4\uc5d0 \ubd99\uc785\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc774\ubc88 \ud30c\uc77c\uc740 `V2__add_column_email.sql` \uc774\ub77c\uace0 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \uc774\uc81c \ub610 \uc2dc\uac04\uc774 \uc9c0\ub098 \ud68c\uc6d0\uc774 \ub9ce\uc544\uc84c\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc email\uc774 \uc5c6\ub294 \uc0ac\uc6a9\uc790\ub3c4 \ub9ce\uc2b5\ub2c8\ub2e4. \uc774 \uc0c1\ud669\uc5d0\uc11c email\uc744 not null\ub85c \ubcc0\uacbd\ud574\uc57c\ud55c\ub2e4\ub294 \uc694\uad6c\uc0ac\ud56d\uc774 \uc0dd\uacbc\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 \uc544\ub798\uc640 \uac19\uc774 \ubc18\uc601\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n```sql\\nALTER TABLE member\\n MODIFY email VARCHAR(20) NOT NULL default \'default\'\\n```\\n\uc774\ub807\uac8c `V3__add_constraints.sql` \ud30c\uc77c\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4. \uadf8\ub7ec\uba74 null\uc774 \uc788\ub358 row\ub4e4\uc740 email\uc774 default\uac00 \ub418\uace0 not null \uc81c\uc57d\uc870\uac74\uc774 \ud65c\uc131\ud654 \ub41c \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 \uc8fc\uc5b4\uc9c4 \uc694\uad6c\uc0ac\ud56d\uc740 \ubaa8\ub450 \ub9cc\uc871\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uac70\uae30\uc5d0\ub2e4 v1, v2, v3 \uac00 \ub098\ub258\uc5b4\uc838\uc788\uc5b4\uc11c \uc5b4\ub290 \ucee4\ubc0b\ubd80\ud130 \ud574\ub2f9 sql\uc774 \ucd94\uac00\ub418\uc5c8\ub294\uc9c0\ub3c4 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 ddl-auto update\ub97c \uc0ac\uc6a9\ud558\uba74 \ubc18\uc601\ub418\uc9c0 \uc54a\uc558\ub358 \uc81c\uc57d\uc870\uac74\uc758 \ucd94\uac00\ub3c4 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub7ec\uba74 ddl-auto\uc758 \uc18d\uc131\uc744 validate\ub85c \ubcc0\uacbd\ud558\uc5ec, db schema\uc640 entity\uc758 \ud544\ub4dc\uac00 \ub2e4\ub974\uba74\\n\uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc774 \uc2e4\ud589\ub418\uc9c0 \uc54a\ub3c4\ub85d \ud574\uc11c \uc880 \ub354 \uc548\uc804\ud55c \uac1c\ubc1c\uc744 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\\nflyway\ub294 roll back\uc744 \ud558\ub294 \uac83\uc774 \uc720\ub8cc\ub77c\uc11c, production \uc11c\ubc84\uc5d0\uc11c \ud639\uc740 \ub864\ubc31\uc744 \ud574\uc57c\ud558\ub294 \uc77c\uc774 \uc788\ub294 \uc11c\ubc84\uc5d0\uc11c\ub294 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc9c0 \uc54a\uc9c0\ub9cc,\\n\uc774\uc640 \uac19\uc774 \ub370\uc774\ud130\ub97c drop \ud560 \uc218 \uc5c6\ub294 \uc0c1\ud669\uc774\ub77c\uba74, \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc744 \uc774\uc720\uac00 \uc5c6\uc5b4\ubcf4\uc774\ub294 \uc88b\uc740 \ub3c4\uad6c\uc785\ub2c8\ub2e4.\\n\\n\uc9e7\uc740 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4."},{"id":"24","metadata":{"permalink":"/24","source":"@site/blog/2023-08-06-out-of-memory-trouble-shooting/index.mdx","title":"Out of memory trouble shooting","description":"\uc548\ub155\ud558\uc138\uc694 \ubd80\ub989\ubd80\ub989 \ud5c8\ub9ac\ucf00\uc778 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4.","date":"2023-08-06T00:00:00.000Z","formattedDate":"2023\ub144 8\uc6d4 6\uc77c","tags":[{"label":"OOM","permalink":"/tags/oom"},{"label":"java","permalink":"/tags/java"},{"label":"trouble-shooting","permalink":"/tags/trouble-shooting"}],"readingTime":15.43,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"24","title":"Out of memory trouble shooting","authors":["boxster"],"tags":["OOM","java","trouble-shooting"]},"prevItem":{"title":"flyway\ub97c \uc774\uc81c\uc11c\uc57c \uc801\uc6a9\ud558\ub294 \uc774\uc720","permalink":"/25"},"nextItem":{"title":"Deadlock trouble shooting","permalink":"/23"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubd80\ub989\ubd80\ub989 \ud5c8\ub9ac\ucf00\uc778 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4.\\n## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\uba3c\uc800 \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\ub294 \uc800\ud76c \uce74\ud398\uc778 \ud300\uc758 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub4e4\uc758 \uc0c8\ub85c\uc6b4 \uc815\ubcf4\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\uac70\ub098, \uc800\uc7a5\ud558\ub294 \ub85c\uc9c1\uc5d0\uc11c \uc544\ub798\uc640 \uac19\uc774 OOM(Out of memory)\uac00 \ubc1c\uc0dd\ud588\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n![error-log](./error-log.png)\\n\\n### \uc65c \ubc1c\uc0dd\ud588\uc744\uae4c\\n\\n\uba3c\uc800 \uac04\ub2e8\ud788 \uc800\ud76c\uac00 \ucc98\ud55c \uc0c1\ud669\uc5d0 \ub300\ud574 \uc124\uba85\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\ucc98\uc74c \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc2e4\ud589\ud558\uba74 \uacf5\uacf5 API\ub97c \ud638\ucd9c\ud558\uc5ec \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\uc5d0 \ub300\ud55c \ubaa8\ub4e0 \uc815\ubcf4\ub4e4\uc744 \uac00\uc838\uc640 \uc800\uc7a5\ud569\ub2c8\ub2e4. (\ucda9\uc804\uc18c \uc57d 6\ub9cc \uacf3 + \ucda9\uc804\uae30 \uc57d 23\ub9cc \uae30)\\n\\n\ud558\uc9c0\ub9cc \uc774\ub7ec\ud55c \uc815\ubcf4\ub4e4\uc740 \uc218\uc815\uc774 \ub420 \uc218 \uc788\uace0, \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\uac00 \ucd94\uac00\ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\ubbc0\ub85c \uc815\ud655\ud55c \uc815\ubcf4\uac00 \uc0ac\uc6a9\uc790\uc5d0\uac8c \uac00\uc7a5 \uc911\uc694\uc2dc\ub418\ub294 \uc11c\ube44\uc2a4\uc5d0\uc11c \uc774\ub7ec\ud55c \uc815\ubcf4\ub4e4\uc774 \ub2a6\uac8c \ubc18\uc601\uc774 \ub41c\ub2e4\uac70\ub098, \ubc18\uc601\uc774 \ub418\uc9c0 \uc54a\ub294\ub2e4\uba74 \uc800\ud76c \uc11c\ube44\uc2a4\ub97c \uc0ac\uc6a9\ud560 \uc0ac\uc6a9\uc790\uac00 \uc5c6\uc744 \uac83\uc774\ub77c \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \ud558\ub8e8\uc5d0 \ud55c \ubc88 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub4e4\uc758 \uc815\ubcf4\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\uace0, \ucd94\uac00\ub41c \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub97c \uc800\uc7a5\ud558\ub294 \ub85c\uc9c1\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub300\ub7b5\uc801\uc778 \ub85c\uc9c1\uc740 \uc544\ub798\uc640 \uac19\uc2b5\ub2c8\ub2e4.\\n```java\\n public void updatePeriodicStations() {\\n List stations = requestStations();\\n stationUpdateService.updateStations(stations);\\n }\\n\\n public void updateStations(List updatedStations) {\\n List stations = stationRepository.findAllFetch();\\n\\n Map savedStationsByStationId = stations.stream()\\n .collect(Collectors.toMap(Station::getStationId, Function.identity()));\\n\\n // \uc800\uc7a5\ub41c \uc815\ubcf4\uc640 \ube44\uad50\ud558\uc5ec \uc0c8\ub85c\uc6b4 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub97c \ucc3e\ub294 \ub85c\uc9c1\\n ...\\n\\n saveAllStations(toSaveStations);\\n updateAllStations(toUpdateStations);\\n\\n saveAllChargers(toSaveChargers);\\n updateAllChargers(toUpdateChargers);\\n }\\n\\n```\\n\uac04\ub2e8\ud558\uac8c \ub9d0\uc500\ub4dc\ub9ac\uba74 `requestStations()` \uba54\uc11c\ub4dc\ub294 \uacf5\uacf5 API\uc5d0\uc11c \ubaa8\ub4e0 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub97c \uc694\uccad\ud558\uace0 \ubc1b\uc544\uc624\ub294 \uba54\uc11c\ub4dc\uc785\ub2c8\ub2e4. 23\ub9cc + 6\ub9cc\uac1c\uc758 \uc815\ubcf4\ub97c \ubc1b\uc544\uc624\ub294 \uac83\uc785\ub2c8\ub2e4.\\n\uc774\ub807\uac8c \ub9ce\uc740 \uc815\ubcf4\ub97c \ubc1b\uc544\uc624\uace0 \uba54\ubaa8\ub9ac\uc5d0 \uc62c\ub9b0\ub2e4\ub294 \uac83\uc740 \ub204\uac00\ubd10\ub3c4 \ube44\ud6a8\uc728\uc801\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc774\ub7ec\ud55c \uc120\ud0dd\uc744 \ud55c \uc774\uc720\ub294 \uacf5\uacf5 API\ub294 \uc800\ud76c\uac00 \uc5b4\ub5a4 \ubc29\uc2dd\uc73c\ub85c \ubcf4\ub0b4\uc904 \uc9c0 \ubaa8\ub978\ub2e4\ub294 \uac83\uc774\uc600\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub798\uc11c \uc5b4\uca54 \uc218 \uc5c6\uc774 23\ub9cc\uac74\uc744 \ubaa8\ub450 \uc694\uccad\ud574\uc57c\ud55c\ub2e4\ub294 \ubd80\ubd84\uc740 \ubc14\uafc0 \uc218 \uc5c6\ub294 \ud55c\uacc4\uc785\ub2c8\ub2e4.\\n\\n\uadf8 \ub2e4\uc74c\uc73c\ub85c\ub294 \uc694\uccad\ud574\uc11c \ubc1b\uc544\uc628 \ub370\uc774\ud130\ub4e4\uacfc \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \uc800\uc7a5\ub418\uc5b4 \uc788\ub358 \ub370\uc774\ud130\ub4e4\uc744 `findAll()`\uc744 \ud1b5\ud574 \ube44\uad50\ud558\uace0 \uc0c8\ub85c\uc6b4 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub294 \uc800\uc7a5\ud558\uace0, \uc5c5\ub370\uc774\ud2b8\ub41c \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub294 \uc218\uc815\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \ub85c\uc9c1\uc740 \ucd1d (23 + 6) * 2 \ub9cc\uac74\uc758 \uac1d\uccb4 \uc57d 58\ub9cc\uac1c\ub97c Heap \uba54\ubaa8\ub9ac\uc5d0 \uc801\uc7ac\ud569\ub2c8\ub2e4. \ub9ce\ub2e4\uace0\ub294 \uc0dd\uac01\ud588\uc9c0\ub9cc, \uc77c\ub2e8 \uc81c \ub85c\uceec\ud658\uacbd\uc5d0\uc11c\ub294 \uc798 \uc791\ub3d9\ud588\uace0, \uae30\ub2a5 \uad6c\ud604\uc774 \uc6b0\uc120\uc774\uae30 \ub54c\ubb38\uc5d0 \ucd94\ud6c4\uc5d0 \uac1c\uc120\uc744 \ud558\uae30\ub85c \ud558\uace0 \ub118\uc5b4\uac14\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uac1c\ubc1c \uc11c\ubc84 \ubc30\ud3ec\ub97c \ud558\uace0 \ub2e4\uc74c\ub0a0 \uc11c\ubc84\uac00 \uc811\uc18d\uc774 \ub418\uc9c0 \uc54a\ub294 \uac83\uc744 \ud655\uc778\ud588\uace0, \ub85c\uadf8\ub97c \ubcf4\ub2c8 \uc704\uc758 \uc0ac\uc9c4\uacfc \uac19\uc774 OOM\uc774 \ubc1c\uc0dd\ud55c \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \ud574\uacb0 \ubc29\uc548\\n\\n\\n### Heap size \uc870\uc808\ud558\uae30\\n\uc77c\ub2e8 \uc784\uc2dc \ubc29\ud3b8\uc73c\ub85c Heap memory\uc758 \ucd5c\ub300 \ud06c\uae30\ub97c \ub298\ub9ac\ub294 \ubc95\uc774\uc600\uc2b5\ub2c8\ub2e4. JVM\uc740 \uc2e4\ud589\ub418\ub294 \ud658\uacbd\uc5d0 \ub530\ub77c \ud799 \uba54\ubaa8\ub9ac\uc758 \ucd5c\ub300 \uc0ac\uc774\uc988\ub97c \uc815\ud569\ub2c8\ub2e4. \ud799 \uba54\ubaa8\ub9ac\ub294 \uc124\uc815\ud558\uc9c0 \uc54a\uc73c\uba74 \ud574\ub2f9 \ud658\uacbd\uc758 \uba54\ubaa8\ub9ac 1/4\ub85c \uc124\uc815\ud569\ub2c8\ub2e4.\\n\uadf8\ub798\uc11c \uc800\ud76c EC2 \uc778\uc2a4\ud134\uc2a4\uc758 \uba54\ubaa8\ub9ac\ub294 \uc57d 2\uae30\uac00\ub85c, \uc57d 500MB\uac00 \ud560\ub2f9\ub418\uc5b4 \uc788\uc5c8\uc2b5\ub2c8\ub2e4. \uadf8\ub798\uc11c \uc800\ud76c\ub294 \uba54\ubaa8\ub9ac\ub97c \uc870\uae08\uc529 \ub298\ub824\uac00\uba70 \uc870\uc815\ud558\uc5ec \uc57d 1\uae30\uac00\ub85c \ud799 \uba54\ubaa8\ub9ac\uc758 \ucd5c\ub300 \uc0ac\uc774\uc988\ub97c \uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\ud799 \uba54\ubaa8\ub9ac\uc758 \uc124\uc815\uc744 \ud558\ub294 \ubc29\ubc95\uc740 \uac04\ub2e8\ud569\ub2c8\ub2e4.\\n```shell\\njava -Xms512m -Xmx1024m boxster.jar\\n```\\n\uc2e4\ud589\ud560 \ub54c \uc774\ub7ec\ud55c \ubc29\uc2dd\uc73c\ub85c \ud558\uba74 \ucd5c\uc18c \ud799 \uba54\ubaa8\ub9ac \uc0ac\uc774\uc988\ub294 512MB, \ucd5c\ub300 1024MB\ub85c \uc124\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \ud398\uc774\uc9d5\ud574\uc11c \uac00\uc838\uc624\uae30\\n\ud799 \uba54\ubaa8\ub9ac\uc758 \uc0ac\uc774\uc988\ub97c \uc870\uc808\ud574\uc11c \ud574\uacb0\ud55c\ub2e4\ub294 \ubd80\ubd84\uc740 \uc784\uc2dc \ubc29\ud3b8\uc774\uc9c0 \ub9cc\uc57d \uc800\ud76c EC2 \ud658\uacbd\uc774 \ub2e4\uc6b4\uadf8\ub808\uc774\ub4dc \ub418\uac70\ub098 \ud55c\ub2e4\uba74 \ub610 OOM\uc774 \ubc1c\uc0dd\ud560 \uac83\uc774 \ubed4\ud569\ub2c8\ub2e4. \uadf8\ub798\uc11c \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158 \ub808\ubca8\uc5d0\uc11c \uc880 \ub354 \ud574\uacb0\ud560 \ubc29\uc548\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n\\nAPI\uc758 \uc694\uccad\uc5d0 \ub300\ud55c \ubd80\ubd84\uc740 \uc694\uccad\ubcf4\ub0b4\ub294 \ud68c\uc0ac\uc758 \uc815\ucc45\uc774 \ubc14\ub00c\uc9c0 \uc54a\ub294 \uc774\uc0c1 \uc800\ud76c\ub294 23\ub9cc\uac74\uc744 \ubaa8\ub450 \ub85c\ub529\ud574\uc57c\ud55c\ub2e4\ub294 \uc810\uc740 \uc5b4\uca54 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uadf8\ub807\ub2e4\uba74 \uc800\ud76c\uac00 \uc81c\uc5b4\ud560 \uc218 \uc788\ub294 \uc720\uc77c\ud55c \ubd80\ubd84\uc740 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \ub370\uc774\ud130\ub97c \uaebc\ub0b4\uc624\ub294 \ubd80\ubd84 \ubc16\uc5d0 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub807\ub2e4\uba74 \uc774\uac83\uc744 \uc5b4\ub5bb\uac8c \uc870\uc808\ud560 \uc218 \uc788\uc744\uae4c\uc694.\\n\\n\uc5ec\ub7ec \ubc29\ubc95\uc744 \ucc3e\uc544\ubcf4\ub358 \uc911 `No Offset`\ubc29\uc2dd\uc73c\ub85c \ub370\uc774\ud130\ub97c \ud398\uc774\uc9d5\ud55c\ub2e4\ub294 \uae00\uc744 \uc77d\uc5c8\uc2b5\ub2c8\ub2e4. \ud398\uc774\uc9d5\uc744 \ud558\uae30\uc704\ud574\uc11c\ub294 \uc5b4\ub514\uc11c\ubd80\ud130 \uc2dc\uc791\ud558\uace0 \uc5b4\ub514\uae4c\uc9c0 \uac00\uc838\uc62c \uac83\uc778\uc9c0 \uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \uadf8 \uc911 \uba3c\uc800 \uc81c\uc77c \uc790\uc8fc \uc0ac\uc6a9\ub418\ub294 Offset \ubc29\uc2dd\uc5d0 \ub300\ud574 \uac04\ub2e8\ud788 \uc124\uba85\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\ud574\ub2f9 \ubc29\uc2dd\uc740\\n```sql\\nSELECT *\\nFROM station\\nORDER BY id DESC\\nOFFSET 20000\\nLIMIT 10000\\n```\\n\uc774\ub7ec\ud55c \ucffc\ub9ac\ub97c \ub9cc\ub4e4\uc5b4 \uc694\uccad\ud569\ub2c8\ub2e4. station \ud14c\uc774\ube14\uc758 20001\ubc88\uc9f8 \ub808\ucf54\ub4dc\ubd80\ud130 10000\uac1c\uc758 \ub370\uc774\ud130\ub97c \uc694\uccad\ud558\ub294 \ubc29\uc2dd\uc785\ub2c8\ub2e4. \uc774\ub7ec\ud55c \ucffc\ub9ac\ub3c4 \ub098\uc058\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\uc7a5\uc810\uc73c\ub85c\ub294 \uc5b8\uc81c\ub4e0 \ud574\ub2f9 \ud398\uc774\uc9c0\ub85c \uc774\ub3d9\ud560 \uc218 \uc788\ub2e4\ub294 \uc810\uc785\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774 \ucffc\ub9ac\uc5d0\ub294 \ub2e8\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4. \ub4a4\ub85c \uac08\uc218\ub85d \uc131\ub2a5\uc774 \ub098\ube60\uc9c4\ub2e4\ub294 \uc810\uc785\ub2c8\ub2e4. 20001\ubc88\uc9f8 \ub808\ucf54\ub4dc\ubd80\ud130 10000\uac1c\ub97c \uc694\uccad\ud55c\ub2e4\uba74 \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub294 \uc5b4\uca54 \uc218 \uc5c6\uc774 20001\ubc88\uc9f8 \ub808\ucf54\ub4dc\ub97c \ucc3e\uae30 \uc704\ud574\\n\uc815\ub82c\uc744 \ud558\uace0, \uc815\ub82c\ud55c \ud6c4\uc5d0 20001\ubc88\uc9f8\uae4c\uc9c0 \uc138\uc5b4\uac00\uba70 \uc77d\uace0, \uac70\uae30\uc11c\ubd80\ud130 10000\uac1c\uc758 \ub808\ucf54\ub4dc\ub97c \ubc18\ud658\ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n![offset](./offset.png)\\n\\n\ud55c \ubb38\uc7a5\uc73c\ub85c \uc815\uc758\ud558\uba74, \uc21c\uc11c\ub97c \uc54c\uc544\uc57c\ud558\uae30 \ub54c\ubb38\uc5d0 \ub0b4\uac00 \ud544\uc694\ud558\uc9c0 \uc54a\ub294 \ub808\ucf54\ub4dc\ub3c4 \uc77d\uc5b4\uc57c \ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\\n#### No Offset\\n\uadf8\ub7fc No offset \ubc29\uc2dd\uc5d0 \ub300\ud574 \uc124\uba85\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc2e4 \uc774\ub984\ub9cc \ub4e4\uc73c\uba74 \uc5b4\ub824\uc6b8 \uac83 \uac19\uc9c0\ub9cc \uadf8\ub0e5 offset\uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0 \ud398\uc774\uc9d5\ud558\ub294 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc2a4\ud06c\ub864\uc744 \ub0b4\ub9ac\uba74\uc11c \uc790\ub3d9\uc73c\ub85c \ub9c8\uc9c0\ub9c9\uc758 \ub370\uc774\ud130\ub97c \uae30\uc900\uc73c\ub85c \ub2e4\uc74c \uba87\uac1c\uc758 \ub808\ucf54\ub4dc\ub97c \ubd88\ub7ec\uc624\ub294 \ubc29\uc2dd\uc774\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\ud574\ub2f9 \ubc29\uc2dd\uc740\\n```sql\\nselect *\\nfrom station\\nwhere id < \ub9c8\uc9c0\ub9c9\uc73c\ub85c \ubcf4\ub0b8 id\\norder by id desc\\nlimit 10000;\\n```\\n\uc774\ub7ec\ud55c \ucffc\ub9ac\ub85c \uc791\ub3d9\ud569\ub2c8\ub2e4. \uc544\uae4c\uc640\ub294 \ub2e4\ub978 \ubd80\ubd84\uc740 where \uc808\uc5d0 `\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ubcf4\ub0b8 id`\ub77c\ub294 \uc815\ubcf4\uac00 \ud544\uc694\ud558\ub2e4\ub294 \ubd80\ubd84\uacfc, offset\uc774 \uc0ac\ub77c\uc9c4 \ubd80\ubd84\uc785\ub2c8\ub2e4.\\n\\n\uac19\uc740 \uacb0\uacfc\ub97c \ub9cc\ub4e4\uc5b4\ub0b4\ub294 \ucffc\ub9ac\uc9c0\ub9cc, \ud558\ub098\uac00 \ucd94\uac00\ub418\uace0 \ud558\ub098\uac00 \uc0ac\ub77c\uc84c\ub2e4\ub294 \uac83\uc740 \ucd94\uac00\ub41c \ubd80\ubd84\uc774 \uc0ac\ub77c\uc9c4 \ubd80\ubd84\uc744 \ub300\uc2e0\ud55c\ub2e4\ub294 \ub73b\uc774\uaca0\uc8e0.\\n\\n\uc774 \uc774\ub7ec\ud55c \ubc29\uc2dd\uc758 \ub2e8\uc810\uc740 offset\uc744 \uc774\uc6a9\ud55c \ubc29\uc2dd\uacfc\ub294 \ub2e4\ub974\uac8c page\ub97c \uc9c0\uc815\ud574\uc11c \ub3cc\uc544\uac00\uae30\ub294 \ud798\ub4ed\ub2c8\ub2e4.\\n\\n![no offset](./no-offset.png)\\n\\n\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ubcf4\ub0b8 id\ub97c \ubc1b\uc544 \uc778\ub371\uc2a4\ub97c \uc774\uc6a9\ud574 \ud574\ub2f9 id\uc5d0\uc11c\ubd80\ud130 \ub808\ucf54\ub4dc\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4. \uad73\uc774 \ud544\uc694\uc5c6\ub294 \ub808\ucf54\ub4dc\ub97c \uc77d\uc744 \ud544\uc694 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \uc131\ub2a5\uc774 \uc88b\uc544\uc84c\uc744 \uac83\uc774\ub77c \uc608\uc0c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uc131\ub2a5 \ucc28\uc774\\n\\n\ubc14\ub85c \ud55c\ubc88 \ub450 \uac1c\uc758 \ucffc\ub9ac\ub97c \uc2e4\ud589\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n![test](./test.png)\\n\\n\uc704\uc758 \ucffc\ub9ac\ub294 no offset, \uc544\ub798\ub294 offset \ubc29\uc2dd\uc785\ub2c8\ub2e4. \ud604\uc7ac \ub370\uc774\ud130\uac00 6\ub9cc\uac74 \ub4e4\uc5b4\uc788\ub294 \ud14c\uc774\ube14\uc758 \uc870\ud68c \uae30\uc900\uc73c\ub85c \uc57d 10\ubc30 \uac00\ub7c9 \uc131\ub2a5\uc774 \ucc28\uc774\ub098\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \uc2e4\ud589 \uacc4\ud68d\ub3c4 \uac04\ub2e8\ud788 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 offset \ubc29\uc2dd\uc758 \uc2e4\ud589 \uacc4\ud68d\uc785\ub2c8\ub2e4.\\n![offset explain](./offset-explain.png)\\n\\n type \uce7c\ub7fc\uc744 \ubcf4\uc2dc\uba74 `index`\ub77c\uace0 \ub418\uc5b4 \uc788\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc5ec\uae30\uc11c index \uc811\uadfc \ubc29\ubc95\uc740\\n \uc778\ub371\uc2a4\ub97c \ud6a8\uc728\uc801\uc73c\ub85c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc544\ub2cc \uc778\ub371\uc2a4\ub97c \ucc98\uc74c\ubd80\ud130 \ub05d\uae4c\uc9c0 \uc77d\ub294 full scan\uc744 \ub73b\ud569\ub2c8\ub2e4. \uadf8\ub798\uc11c \uadf8\ub2e4\uc9c0 \ud6a8\uc728\uc801\uc774\uc9c0 \ubabb\ud55c \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 rows \uce7c\ub7fc\uc5d0\ub294 `40010`\uc774\ub77c\uace0 \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ubd80\ubd84\uc740 \uc81c\uac00 offset\uc744 40000, limit\uc744 10\uc73c\ub85c \ub450\uc5c8\uae30 \ub54c\ubb38\uc5d0 40010d\uc758 row\ub97c\\n\uc77d\uc5b4\uc57c\ud55c\ub2e4\uace0 \uc608\uc0c1 \uac12\uc744 \ub098\ud0c0\ub0b8 \uac83\uc785\ub2c8\ub2e4.\\n\\n\ub2e4\uc74c\uc740 no offset \ubc29\uc2dd\uc758 \uc2e4\ud589 \uacc4\ud68d\uc785\ub2c8\ub2e4.\\n![no offset explain](./no-offset-explain.png)\\n\uc544\uae4c\uc640\ub294 \ub2e4\ub974\uac8c type \uce7c\ub7fc\uc740 `range`\uc785\ub2c8\ub2e4. range \uc811\uadfc \ubc29\uc2dd\uc740 \uc778\ub371\uc2a4\ub97c \ud558\ub098\uc758 \uac12\uc774 \uc544\ub2c8\ub77c \ubc94\uc704\ub85c \uac80\uc0c9\ud558\ub294 \uacbd\uc6b0\ub97c \uc758\ubbf8\ud569\ub2c8\ub2e4.\\n\uc880 \uc804\uc758 index \uc811\uadfc \ubc29\uc2dd\uacfc\ub294 \ub2e4\ub974\uac8c \ud6e8\uc52c \ud6a8\uc728\uc801\uc778 \uc811\uadfc \ubc29\uc2dd\uc785\ub2c8\ub2e4. \uadf8\ub9ac\uace0 rows\ub3c4 \ub2ec\ub77c\uc9c4 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uc9c4\uc9dc \ud574\uacb0\ud558\uae30\\n\uc774\uc81c \uc5f4\uc2ec\ud788 \ud398\uc774\uc9d5 \ucc98\ub9ac\ub97c \ud588\uc73c\ub2c8 \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc5d0\uc11c \ud574\uacb0\uc744 \ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5b4\uc57c\ud569\ub2c8\ub2e4.\\n\\n\uc800\ud76c \ud300\uc740 \ub3d9\uc801 \ucffc\ub9ac \uc0dd\uc131\uc744 \ub3c4\uc640\uc8fc\ub294 Query DSL\uc744 \ub3c4\uc785\ud558\uc9c0 \uc54a\uc558\uace0 \uc544\uc9c1\uae4c\uc9c4 \uad73\uc774 \ud544\uc694\ud558\uc9c0 \uc54a\uc544\uc11c no offset \ubc29\uc2dd\uc744 jpa\uc758 jpql\uc744 \ud1b5\ud574 \uad6c\ud604\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 \uccab \ud398\uc774\uc9c0\ub294 id\uc758 \uad00\uacc4\uc5c6\uc774 \uc6d0\ud558\ub294 \uac2f\uc218\ub9cc\ud07c\ub9cc \uac00\uc838\uc624\uba74 \ub429\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ub450\ubc88\uc9f8 \ud398\uc774\uc9c0\ubd80\ud130\ub294 id\ub97c \ubc1b\uc544 \uadf8 \ub2e4\uc74c\ubd80\ud130 \ubc18\ud658\ud558\uba74 \ub429\ub2c8\ub2e4.\\n```java\\npublic interface StationRepository extends Repository {\\n\\n @Query(\\"SELECT s FROM Station s INNER JOIN FETCH s.chargers ORDER BY s.stationId\\")\\n List findAllByOrder(Pageable pageable);\\n\\n @Query(\\"SELECT s FROM Station s INNER JOIN FETCH s.chargers WHERE s.stationId > :stationId ORDER BY s.stationId\\")\\n List findAllByPaging(@Param(\\"stationId\\") String stationId, Pageable pageable);\\n}\\n```\\n\uadf8\ub7fc \uc544\uae4c update\ub97c \ud574\uc8fc\ub358 \uba54\uc11c\ub4dc\uc5d0\uc11c \uc870\uae08 \uc218\uc815\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n```java\\n public void updatePeriodicStations() {\\n List stations = getStations();\\n // \ucc98\uc74c\uc5d0\ub294 station\uc758 id\uac00 null\\n String lastStationId = null;\\n for (int i = 0; i < stations.size() / LIMIT + 1; i++) {\\n // \ub9c8\uc9c0\ub9c9 id\ub97c \uba54\uc11c\ub4dc \uc2e4\ud589\ud560 \ub54c\ub9c8\ub2e4 \ubcc0\uacbd\ud574\uc900\ub2e4.\\n lastStationId = stationUpdateService.updateStations(stations, lastStationId, LIMIT);\\n }\\n }\\n\\n public String updateStations(List updatedStations, String lastStationId, int limit) {\\n List savedStations = getStations(lastStationId, limit);\\n\\n Map savedStationsByStationId = stations.stream()\\n .collect(Collectors.toMap(Station::getStationId, Function.identity()));\\n\\n // \uc800\uc7a5\ub41c \uc815\ubcf4\uc640 \ube44\uad50\ud558\uc5ec \uc0c8\ub85c\uc6b4 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub97c \ucc3e\ub294 \ub85c\uc9c1\\n ...\\n\\n saveAllStations(toSaveStations);\\n updateAllStations(toUpdateStations);\\n\\n saveAllChargers(toSaveChargers);\\n updateAllChargers(toUpdateChargers);\\n // \uac00\uc838\uc628 list\uc5d0\uc11c \uc81c\uc77c \ub9c8\uc9c0\ub9c9 station\uc758 id\ub97c \ubc18\ud658\\n return getLastStationId(savedStations);\\n }\\n // \ud398\uc774\uc9d5 \ucc98\ub9ac\\n private List getStations(String stationId, int limit) {\\n // Id \uac00 null \uc774\ub77c\uba74 \uccab \ud398\uc774\uc9c0\uc774\uae30 \ub54c\ubb38\uc5d0 limit \uc0ac\uc774\uc988\ub9cc\ud07c select\\n if (stationId == null) {\\n return stationRepository.findAllByOrder(Pageable.ofSize(limit));\\n }\\n // \uc544\ub2c8\ub77c\uba74 station Id \ubd80\ud130 limit \ub9cc\ud07c\\n return stationRepository.findAllByPaging(stationId, Pageable.ofSize(limit));\\n }\\n```\\n\uc774\ub807\uac8c \ub418\uba74 \uc6d0\ub798 23\ub9cc\uac1c\ub97c \ud55c\uaebc\ubc88\uc5d0 \uac00\uc838\uc624\ub358 \ub85c\uc9c1\uc744 \ub098\ub20c \uc218 \uc788\uae30 \ub54c\ubb38\uc5d0 Heap \uba54\ubaa8\ub9ac\uc758 \uc5ec\uc720\uac00 \uc0dd\uae38 \uac83\uc785\ub2c8\ub2e4.\\n\\n### \uc9c4\uc9dc \ud655\uc778\ud574\ubcf4\uae30\\n\\n\ubb3c\ub860 GC\uc758 \ub3d9\uc791\uc774 \uc5b4\ub5a8\uc9c0 \ubaa8\ub974\uaca0\uc9c0\ub9cc 23\ub9cc\uac1c \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uac83\ubcf4\ub2e4 5000\uac1c \ud639\uc740 \ub354 \uc801\uac8c \uc0dd\uc131\ud558\ub294 \uac83\uc774 Heap \uba54\ubaa8\ub9ac\ub97c \uc801\uac8c \uc0ac\uc6a9\ud560 \uac83\uc784\uc744 \uc720\ucd94\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uc9c1\uc811 \ud655\uc778\ud574\ubcf4\uae30 \uc804\uae4c\uc9c0\ub294 \ud655\uc2e0\ud560 \uc218 \uc5c6\uc73c\ub2c8 \uac04\ub2e8\ud788 `Runtime` \ud074\ub798\uc2a4\uc5d0\uc11c \uc81c\uacf5\ud574\uc8fc\ub294 `totalMemory()`, `freeMemory()` \uba54\uc11c\ub4dc\ub97c \ud1b5\ud574 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n @Test\\n void \ud398\uc774\uc9d5\uc744_\uc0ac\uc6a9\ud55c_\uc870\ud68c() {\\n List stations = stationRepository.findAllByOrder(Pageable.ofSize(1000));\\n\\n long total = Runtime.getRuntime().totalMemory();\\n long free = Runtime.getRuntime().freeMemory();\\n System.out.println(\\"paging \uc0ac\uc6a9 \uc911\uc778 \uba54\ubaa8\ub9ac: \\" + ((total - free) / 1024 / 1024) + \\"MB\\");\\n }\\n\\n @Test\\n void \ud398\uc774\uc9d5\uc744_\uc0ac\uc6a9\ud558\uc9c0_\uc54a\uace0_\uc870\ud68c() {\\n List stations = stationRepository.findAllFetch();\\n\\n long total = Runtime.getRuntime().totalMemory();\\n long free = Runtime.getRuntime().freeMemory();\\n\\n System.out.println(\\"findAll() \uc0ac\uc6a9 \uc911\uc778 \uba54\ubaa8\ub9ac: \\" + ((total - free) / 1024 / 1024) + \\"MB\\");\\n }\\n```\\n\\n![findAll](./findAll.png)\\n![paging](./paging.png)\\n\ud655\uc5f0\ud788 \ucc28\uc774\uac00 \ub098\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ubb3c\ub860 \ud14c\uc2a4\ud2b8\ucf54\ub4dc\uc5d0\uc11c\ub294 23\ub9cc\uac74\uc758 API \uc694\uccad\uc740 \uac19\uc740 \uc870\uac74\uc774\ub2c8 \ubc30\uc81c\ud558\uace0 \ud655\uc778\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub85c\uc368 \ud558\ub098\uc758 \ubb38\uc81c\uac00 \ub610 \ud574\uacb0\ub41c \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\uc9c1 \ubc30\uc6b0\ub294 \ub2e8\uacc4\ub77c \ud639\uc2dc \ud2c0\ub9b0 \uc810\uc774 \uc788\ub2e4\uba74 \uc9c0\uc801 \ubd80\ud0c1\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## Reference\\n\\n- \ub9ac\uc5bc \ub9c8\uc774 \uc5d0\uc2a4\ud050\uc5d8 8.0\\n- https://jojoldu.tistory.com/528"},{"id":"23","metadata":{"permalink":"/23","source":"@site/blog/2023-07-31-deadlokc-trouble-shooting.mdx","title":"Deadlock trouble shooting","description":"\uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720","date":"2023-07-31T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 31\uc77c","tags":[{"label":"deadlock","permalink":"/tags/deadlock"},{"label":"trouble-shooting","permalink":"/tags/trouble-shooting"}],"readingTime":12.565,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"23","title":"Deadlock trouble shooting","authors":["boxster"],"tags":["deadlock","trouble-shooting"]},"prevItem":{"title":"Out of memory trouble shooting","permalink":"/24"},"nextItem":{"title":"\ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604\uacfc \uc778\ub371\uc2a4 \uc774\uc6a9\ud55c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\ud558\uae30","permalink":"/22"}},"content":"## \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\\n\uba3c\uc800 \uc774 \uae00\uc744 \uc4f0\ub294 \uc774\uc720\ub294 \uc800\ud76c \uce74\ud398\uc778 \ud300\uc758 \ud63c\uc7a1\ub3c4 \uc800\uc7a5 \ubc0f \ucda9\uc804\uae30\uc758 \uc0c1\ud0dc\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\ub294 \ub85c\uc9c1\uc5d0\uc11c dead Lock\uc774 \ubc1c\uc0dd\ud558\uc5ec mysql\uacfc connection\uc744 \uc783\ub294 \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\\n```shell\\n------------------------\\nLATEST DETECTED DEADLock\\n------------------------\\n2023-07-21 01:49:54 281472560787424\\n*** (1) TRANSACTION:\\nTRANSACTION 1000560, ACTIVE 373 sec inserting\\nmysql tables in use 1, Locked 1\\nLock WAIT 3 Lock struct(s), heap size 1128, 9 row Lock(s), undo log entries 328\\nMySQL thread id 860, OS thread handle 281472107409376, query id 2958720 update\\nINSERT INTO charger_status (station_id, charger_id, latest_update_time, charger_state) VALUES (\'ST414511\', \'01\', \'2023-07-21 08:27:43\', \'CHARGING_IN_PROGRESS\') ON DUPLICATE KEY UPDATE latest_update_time = \'2023-07-21 08:27:43\', charger_state = \'CHARGING_IN_PROGRESS\'\\n\\n*** (1) HOLDS THE Lock(S):\\nRECORD LockS space id 64 page no 742 n bits 424 index PRIMARY of table `charge`.`charger_status` trx id 1000560 Lock_mode X Locks rec but not gap\\n\\n*** (1) WAITING FOR THIS Lock TO BE GRANTED:\\nRECORD LockS space id 64 page no 718 n bits 280 index PRIMARY of table `charge`.`charger_status` trx id 1000560 Lock_mode X Locks rec but not gap waiting\\n\\n*** (2) TRANSACTION:\\nTRANSACTION 946331, ACTIVE 507 sec inserting\\nmysql tables in use 1, Locked 1\\nLock WAIT 4 Lock struct(s), heap size 1323, 12 row Lock(s), undo log entries 432\\nMySQL thread id 859, OS thread handle 281472629186528, query id 3017483 update\\nINSERT INTO charger_status (station_id, charger_id, latest_update_time, charger_state) VALUES (\'ST412801\', \'11\', \'2023-07-21 10:48:20\', \'CHARGING_IN_PROGRESS\') ON DUPLICATE KEY UPDATE latest_update_time = \'2023-07-21 10:48:20\', charger_state = \'CHARGING_IN_PROGRESS\'\\n\\n*** (2) HOLDS THE Lock(S):\\nRECORD LockS space id 64 page no 718 n bits 208 index PRIMARY of table `charge`.`charger_status` trx id 946331 Lock_mode X Locks rec but not gap\\n\\n*** (2) WAITING FOR THIS Lock TO BE GRANTED:\\nRECORD LockS space id 64 page no 742 n bits 424 index PRIMARY of table `charge`.`charger_status` trx id 946331 Lock_mode X Locks rec but not gap waiting\\n\\n\\n```\\n\uc2e4\uc81c **\uac1c\ubc1c \uc11c\ubc84**\uc5d0\uc11c \ubc1c\uc0dd\ud55c \ub370\ub4dc\ub77d\uc758 \ub85c\uadf8\uc785\ub2c8\ub2e4. \ud574\ub2f9 \ub85c\uadf8\ub294 charger_status\uc5d0 \uc800\uc7a5 \uc2dc \uc11c\ub85c XLock\uc744 \ud68d\ub4dd\ud558\uc9c0 \ubabb\ud558\uc5ec \uc0dd\uae30\ub294 \uc5d0\ub7ec\uc785\ub2c8\ub2e4.\\n\\n## Mysql Dead Lock\uc774\ub780\\n\\n\uadf8\ub7fc Dead Lock\uc740 \uc65c \uc0dd\uae30\uace0 \uc5b8\uc81c \uc0dd\uae38\uae4c\uc694?\\n\uc800\ub294 \uc774 Log\ub97c \uc9c1\uc811 \ub9c8\uc8fc\ud558\uae30 \uc804\uae4c\uc9c0\ub294 Dead Lock\uc774 \uadf8\ub0e5 Lock\uc758 \uc2dc\uac04\uc774 \uc624\ub798 \uac78\ub9b4 \ub54c \uc0dd\uae30\ub294 \uc904 \uc54c\uc558\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uadf8\ub807\uac8c \uac04\ub2e8\ud558\uac8c \ubc1c\uc0dd\ud558\ub294 \uac83\uc740 \uc544\ub2c8\uc600\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc0c1\ud638 \ubc30\uc81c(Mutual Exclusion): MySQL\uc740 \uae30\ubcf8\uc801\uc73c\ub85c \ud2b8\ub79c\uc7ad\uc158 \ub0b4\uc5d0\uc11c \uc7a0\uae08(Lock)\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub370\uc774\ud130\uc758 \uc0c1\ud638 \ubc30\uc81c\ub97c \uc81c\uc5b4\ud569\ub2c8\ub2e4. \ub530\ub77c\uc11c \ub450 \uac1c \uc774\uc0c1\uc758 \ud2b8\ub79c\uc7ad\uc158\uc774 \uac19\uc740 \ub370\uc774\ud130\ub97c \ub3d9\uc2dc\uc5d0 \ubcc0\uacbd\ud558\ub824\uace0 \ud560 \ub54c, \ud574\ub2f9 \ub370\uc774\ud130\uc5d0 \ub300\ud55c \uc7a0\uae08\uc774 \uc124\uc815\ub418\uc5b4 \uc0c1\ud638 \ubc30\uc81c \uc870\uac74\uc774 \ub9cc\uc871\ub429\ub2c8\ub2e4.\\n\\n2. \uc810\uc720\uc640 \ub300\uae30(Hold and Wait): \ud2b8\ub79c\uc7ad\uc158\uc774 \uc774\ubbf8 \ud558\ub098 \uc774\uc0c1\uc758 \ub370\uc774\ud130\ub97c \uc7a0\uadfc \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ub370\uc774\ud130\uc758 \uc7a0\uae08\uc744 \uc5bb\uae30 \uc704\ud574 \ub300\uae30\ud558\uace0 \uc788\ub294 \uacbd\uc6b0 \uc810\uc720\uc640 \ub300\uae30 \uc870\uac74\uc774 \ub9cc\uc871\ub429\ub2c8\ub2e4. \uc989, \ud2b8\ub79c\uc7ad\uc158\uc774 \uc790\uc2e0\uc774 \uc810\uc720\ud55c \ub370\uc774\ud130\ub97c \uc720\uc9c0\ud55c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ub370\uc774\ud130\uc5d0 \ub300\ud55c \uc7a0\uae08\uc744 \uae30\ub2e4\ub9ac\uace0 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n\\n3. \ube44\uc120\uc810(Non-Preemption): MySQL\uc5d0\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud2b8\ub79c\uc7ad\uc158\uc774 \ub2e4\ub978 \ud2b8\ub79c\uc7ad\uc158\uc774 \uc810\uc720\ud55c \ub370\uc774\ud130\uc758 \uc7a0\uae08\uc744 \uac15\uc81c\ub85c \ud574\uc81c\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \ube44\uc120\uc810 \uc870\uac74\uc774 \ub9cc\uc871\ub429\ub2c8\ub2e4.\\n\\n4. \uc21c\ud658 \ub300\uae30(Circular Wait): \ub450 \uac1c \uc774\uc0c1\uc758 \ud2b8\ub79c\uc7ad\uc158\uc774 \uac01\uac01 \uc11c\ub85c\uac00 \uae30\ub2e4\ub9ac\ub294 \ub370\uc774\ud130\uc758 \uc7a0\uae08\uc744 \ubcf4\uc720\ud574\uc57c \uc21c\ud658 \ub300\uae30 \uc870\uac74\uc774 \ub9cc\uc871\ub429\ub2c8\ub2e4. \uc608\ub97c \ub4e4\uba74, \ud2b8\ub79c\uc7ad\uc158 A\uac00 \ub370\uc774\ud130 X\uc758 \uc7a0\uae08\uc744 \uae30\ub2e4\ub9ac\uace0, \ud2b8\ub79c\uc7ad\uc158 B\ub294 \ub370\uc774\ud130 Y\uc758 \uc7a0\uae08\uc744 \uae30\ub2e4\ub9ac\uba70, \ud2b8\ub79c\uc7ad\uc158 C\ub294 \ub370\uc774\ud130 Z\uc758 \uc7a0\uae08\uc744 \uae30\ub2e4\ub9ac\ub294 \uc0c1\ud0dc\uac00 \ubc1c\uc0dd\ud55c\ub2e4\uba74 \uc21c\ud658 \ub300\uae30 \uc870\uac74\uc774 \uc131\ub9bd\ud569\ub2c8\ub2e4.\\n\\n\uc0ac\uc2e4 \uae30\ubcf8 \ucef4\ud4e8\ud130 \uc2dc\uc2a4\ud15c\uc758 dead Lock\uacfc \uc720\uc0ac\ud55c \uc870\uac74\uc785\ub2c8\ub2e4. \uc774 \ubd80\ubd84\uc744 \ubaa8\ub450 \ub9cc\uc871\ud574\uc57c \ub370\ub4dc\ub77d\uc774 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\ud558\ub098\uc529 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \uac1c\ubc1c \uc11c\ubc84\uc5d0\uc11c \ubc1c\uc0dd\ud55c \ub370\ub4dc\ub77d\uc73c\ub85c \uc0b4\ud3b4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n```shell\\n*** (1) TRANSACTION:\\nTRANSACTION 1000560, ACTIVE 373 sec inserting\\nmysql tables in use 1, Locked 1\\nLock WAIT 3 Lock struct(s), heap size 1128, 9 row Lock(s), undo log entries 328\\nMySQL thread id 860, OS thread handle 281472107409376, query id 2958720 update\\nINSERT INTO charger_status (station_id, charger_id, latest_update_time, charger_state) VALUES (\'ST414511\', \'01\', \'2023-07-21 08:27:43\', \'CHARGING_IN_PROGRESS\') ON DUPLICATE KEY UPDATE latest_update_time = \'2023-07-21 08:27:43\', charger_state = \'CHARGING_IN_PROGRESS\'\\n\\n*** (1) HOLDS THE Lock(S):\\nRECORD LockS space id 64 page no 742 n bits 424 index PRIMARY of table `charge`.`charger_status` trx id 1000560 Lock_mode X Locks rec but not gap\\n\\n\\n-------------------------------------------------------------------------\\n*** (2) TRANSACTION:\\nTRANSACTION 946331, ACTIVE 507 sec inserting\\nmysql tables in use 1, Locked 1\\nLock WAIT 4 Lock struct(s), heap size 1323, 12 row Lock(s), undo log entries 432\\nMySQL thread id 859, OS thread handle 281472629186528, query id 3017483 update\\nINSERT INTO charger_status (station_id, charger_id, latest_update_time, charger_state) VALUES (\'ST412801\', \'11\', \'2023-07-21 10:48:20\', \'CHARGING_IN_PROGRESS\') ON DUPLICATE KEY UPDATE latest_update_time = \'2023-07-21 10:48:20\', charger_state = \'CHARGING_IN_PROGRESS\'\\n\\n*** (2) HOLDS THE Lock(S):\\nRECORD LockS space id 64 page no 718 n bits 208 index PRIMARY of table `charge`.`charger_status` trx id 946331 Lock_mode X Locks rec but not gap\\n```\\n\\n1\ubc88 \ud2b8\ub79c\uc7ad\uc158 1000560\uc774 charge_status \ud14c\uc774\ube14\uc5d0 insert ~~~ on duplicate key update ~~~ \ucffc\ub9ac\ub97c \ubc1c\uc0dd\uc2dc\ud0a4\uae30 \uc704\ud574 `space id 64 page no 742 n bits 424 index PRIMARY of table` \uc5d0 X Lock\uc744 \uac00\uc9c0\uace0 \uc788\uc2b5\ub2c8\ub2e4\\n\uadf8\ub9ac\uace0 2\ubc88 \ud2b8\ub79c\uc7ad\uc158 946331 \ub3c4 \ub611\uac19\uc740 \ud14c\uc774\ube14\uc5d0 \ube44\uc2b7\ud55c \ucffc\ub9ac\ub97c \ubc1c\uc0dd\uc2dc\ud0a4\ub824\uace0 \ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ud574\ub2f9 \ud2b8\ub79c\uc7ad\uc158\ub3c4 X Lock\uc744 \uac00\uc9c0\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uc800\ud76c \ud300\uc5d0 \ub370\ub4dc\ub77d\uc774 \ubc1c\uc0dd\ud55c \uc774\uc720\\n\uba3c\uc800 \uc800\ud76c \ud300\uc740 \uacf5\uacf5 API\ub97c \ud1b5\ud574 \uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c cron\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \ucda9\uc804\uae30\uc758 \uc0c1\ud0dc\ub3c4 \uc8fc\uae30\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uac31\uc2e0\ud560 \uacbd\uc6b0 \uc0c8\ub85c \uc0dd\uae34 \ucda9\uc804\uc18c\uac00 \uc788\ub2e4\uba74 \ucd94\uac00\ud574\uc918\uc57c\ud558\uace0, \uc0c8\ub85c \uc0dd\uae34 \ucda9\uc804\uc18c\uac00 \uc788\ub2e4\uba74 \uc0c8\ub85c\uc6b4 \ucda9\uc804\uae30\uac00 \uc788\uc744\ud14c\uace0, \uadf8\uc5d0\ub530\ub978 \ucda9\uc804\uae30 \uc0c1\ud0dc\ub3c4 \ucd94\uac00\ud574\uc918\uc57c\ud569\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \uc6d0\ub798 \uc788\ub358 \ucda9\uc804\uc18c\uc758 \uc815\ubcf4\uac00 \ubc14\ub00c\ub294 \uac83\uc5d0 \ub300\ud574\uc11c\ub3c4 \uc5c5\ub370\uc774\ud2b8 \ud574\uc918\uc57c\ud569\ub2c8\ub2e4. \uc774\ub807\uac8c \ub41c\ub2e4\uba74 \uc5ec\ub7ec\ubc88\uc758 \ubd84\uae30 \ucc98\ub9ac\ub85c application \ub808\ubca8\uc5d0\uc11c \ud574\uacb0\ud558\ub294 \uac83\uc774 \ub098\uc744\uc9c0 \ud639\uc740 mysql\uc758 \ubb38\ubc95 \uc911 `INSERT ~~ ON DUPLICATE KEY UPDATE ~~`\uc744 \uc774\uc6a9\ud560\uae4c \uace0\ubbfc\uc744 \ud588\uc5c8\ub294\ub370\uc694.\\n\\n\uadf8 \uc911 \ud6c4\uc790\ub97c \ud0dd\ud55c \uc774\uc720\ub97c \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4. \ucda9\uc804\uae30\uc758 \uc815\ubcf4\ub294 \ud558\ub8e8\uc5d0 \uacf5\uacf5 API\ub97c \uc694\uccad\ud560 \uc218 \uc788\ub294 key \uc81c\ud55c\uc774 \uc788\uace0 \uc9c0\ub3c4 \uae30\ubc18\uc73c\ub85c \uac80\uc0c9\ud560 \uc218 \uc788\ub294 \uc870\uac74\uc774 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \uc2e4\uc2dc\uac04\uc73c\ub85c \uc694\uccad\ud560 \uc218 \uc5c6\ub294 \uad6c\uc870\uc785\ub2c8\ub2e4.\\n\uadf8\ub798\uc11c \uc694\uccad key \uc81c\ud55c\uc744 \ub118\uc9c0 \uc54a\ub294 \uc120\uc5d0\uc11c \uc790\uc8fc \uc694\uccad\uc744 \ud574\uc57c\ud569\ub2c8\ub2e4. \uadf8\ub807\uac8c\ud574\uc57c \uc0ac\uc6a9\uc790\uc5d0\uac8c \uc815\ubcf4\uc5d0 \ub300\ud55c \uc2e0\ub8b0\ub97c \uc904 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc790\uc8fc \uc694\uccad\ub418\ub294 \uc791\uc5c5\uc5d0 Application \ub808\ubca8\uc5d0\uc11c \uad6c\ud604\ud55c\ub2e4\uba74, findAll() \uba54\uc11c\ub4dc\ub97c \ud1b5\ud574 23\ub9cc\uac74\uc758 \ucda9\uc804\uae30 \uc0c1\ud0dc \uc815\ubcf4\ub97c \uba54\ubaa8\ub9ac\uc5d0 \ub85c\ub529\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 api\uc758 \uc694\uccad\uc744 \ud1b5\ud574 \uc5bb\uc740 23\ub9cc+n\uac74\uc758 \ucda9\uc804\uae30 \uc0c1\ud0dc \uc815\ubcf4\ub97c \uba54\ubaa8\ub9ac\uc5d0 \uc62c\ub9bd\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ud574\ub2f9 \uc815\ubcf4\uac00 \uc788\ub294\uc9c0 \ube44\uad50\ud569\ub2c8\ub2e4. \ud574\ub2f9 \uc815\ubcf4\uac00 findAll()\ub85c \ucc3e\uc544\uc628 list\uc5d0 \uc5c6\uc73c\uba74 Insert, \ud574\ub2f9 \uc815\ubcf4\uac00 \uc788\ub2e4\uba74 update\ub97c \ud1b5\ud574 \uac31\uc2e0\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub807\uac8c \ud55c\ub2e4\uba74 \ucd1d batch Insert, Update\ub97c \ud1b5\ud574 2\ubc88\uc758 \ucffc\ub9ac + 46\ub9cc n\uac74\uc758 \ucda9\uc804\uae30 \uc815\ubcf4\ub97c \uba54\ubaa8\ub9ac\uc5d0 \uc62c\ub824 \ube44\uad50\ud558\ub294 \uc5f0\uc0b0\uc774 \uc0dd\uae38 \uac83 \uc785\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc `INSERT ~~ ON DUPLICATE KEY UPDATE ~~` \ub97c \uc0ac\uc6a9\ud55c\ub2e4\uba74 batch insert\ub97c \ud1b5\ud574 1\ubc88\uc758 \ucffc\ub9ac\ub85c \ud574\uacb0 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uba54\ubaa8\ub9ac\uc5d0 \ub370\uc774\ud130\ub3c4 \uc801\uc7ac\ud558\uc9c0 \uc54a\uace0 \ub9d0\uc774\uc8e0.\\n\\n\ud558\uc9c0\ub9cc \ubcf4\uc168\ub358 \uac83\uacfc \uac19\uc774 \ub370\ub4dc\ub77d\uc774 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc774\uc720\ub294 `INSERT ~~ ON DUPLICATE KEY UPDATE ~~` \uc758 \ud2b9\uc218\ud55c Lock mechanism \ub54c\ubb38\uc785\ub2c8\ub2e4. \ud574\ub2f9 \ucffc\ub9ac\ub294\\n1. \uba3c\uc800 \uc0bd\uc785\ud558\ub824\ub294 \ud589\uc774 \ud14c\uc774\ube14\uc5d0 \uc874\uc7ac\ud558\ub294\uc9c0 \ud655\uc778\ud569\ub2c8\ub2e4.\\n2. \uadf8\ub9ac\uace0 \uc77d\uc740 record\uc5d0 \ub300\ud574 S Lock\uc744 \uc124\uc815\ud569\ub2c8\ub2e4.\\n3. \uadf8\ub9ac\uace0 \ud574\ub2f9 record\uac00 duplicate key\ub77c\ub294 \uc870\uac74\uc774\ub77c\uba74 X Lock\uc744 \uc124\uc815\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \ubc29\uc2dd\uc73c\ub85c \uc791\ub3d9\ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\uc774\ub7f0 \ubc29\uc2dd\uc774 \uc65c \ubb38\uc81c\uac00 \ub420 \uc218 \uc788\ub294\uc9c0 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\uba3c\uc800 \uc790\uc2e0\uc774 \uc77d\uc740 record\uc5d0 S Lock\uc744 \uac01\uac01\uc758 \ud2b8\ub79c\uc7ad\uc158\uc774 \uc124\uc815\ud569\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \uc5c5\ub370\uc774\ud2b8\ub97c \ud558\uae30\uc704\ud574 record\uc5d0 X Lock\uc744 \uc124\uc815\ud558\ub824\uace0 \ud569\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uac01\uac01\uc758 \ud2b8\ub79c\uc7ad\uc158\uc774 \uc11c\ub85c S Lock\uc744 \uc124\uc815\ud588\uae30 \ub54c\ubb38\uc5d0 S Lock\uc744 \ubc18\ub0a9\ud558\uace0 X Lock\uc744 \uc124\uc815\ud558\ub824\uace0 \ud574\ub3c4 \ub450 \ud2b8\ub79c\uc7ad\uc158 \ubaa8\ub450 \uae30\ub2e4\ub9ac\ub294 \ub370\ub4dc\ub77d \uc0c1\ud669\uc774 \ubc1c\uc0dd\ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4. \uc774\ub7f0 \uc0c1\ud669\uc774 \ub418\uba74 \uc544\uae4c \uc704\uc5d0\uc11c \ub9d0\uc500\ub4dc\ub838\ub358 \ub370\ub4dc\ub77d\uc758 \uc870\uac74 4\uac00\uc9c0\uac00 \ub2e4 \ub9cc\uc871\ud558\uace0 \ub370\ub4dc\ub77d\uc774 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\\n### \ud574\uacb0 \ubc29\uc548\\n\\n\ud574\uacb0 \ubc29\uc548\uc740 \uc5ec\ub7ec\uac00\uc9c0\uac00 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\uadf8 \uc911 \uc81c \uc218\uc900\uc5d0\uc11c \uc0dd\uac01\ud560 \uc218 \uc788\ub294 \ubc29\ubc95\uc740 2\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1. \ud2b8\ub79c\uc7ad\uc158\uc744 \uc791\uac8c \ubd84\ub9ac\\n\ud2b8\ub79c\uc7ad\uc158\uc744 \uc624\ub798 \uac00\uc9c0\uace0 \uc788\uc73c\uba74 Lock\uc744 \uac00\uc9c0\uace0 \uc788\ub294 \uc2dc\uac04\uc774 \uc624\ub798\uac78\ub9bd\ub2c8\ub2e4.\\n\uadf8\ub798\uc11c \ud2b8\ub79c\uc7ad\uc158\uc744 \uc791\uac8c \ubd84\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud398\uc774\uc9d5\uc744 \ud1b5\ud574 \ud2b8\ub79c\uc7ad\uc158\uc744 \uc791\uac8c \ubd84\ub9ac\ud558\ub2e4\ubcf4\uba74 \ucffc\ub9ac\uac00 \uc5ec\ub7ec\ubc88 \ub098\uac00 \uc131\ub2a5\uc0c1 \ubb38\uc81c\uac00 \uc0dd\uae38 \uc218 \uc788\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n2. `INSERT ~~ ON DUPLICATE KEY UPDATE ~~` \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uae30\\n\ud574\ub2f9 sql\uc774 \uc544\ub2cc `INSERT IGNORE`\uc744 \uc0ac\uc6a9\ud558\uc5ec \ucd94\uac00\ub41c \uc815\ubcf4\ub9cc \ub123\uace0, update\ub294 \ub2e4\ub978 \uc791\uc5c5\uc73c\ub85c \ubd84\ub9ac\ud558\uae30\\n\\n\uc774\ub7f0 \ubc29\ubc95\ub4e4\uc744 \uc0ac\uc6a9\ud558\uba74 \ub420 \uac83 \uac19\uc558\uc2b5\ub2c8\ub2e4. \uadf8 \uc911 \uc800\ub294 \ud604\uc7ac\ub294 \uac04\ub2e8\ud558\uac8c 2\ubc88\uc9f8 \ubc29\ubc95\uc774 \uc81c\uc77c \ub098\uc744 \uac83 \uac19\ub2e4\ub294 \uc0dd\uac01\uc5d0 \ucffc\ub9ac\ub97c \uc218\uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ubb38\uc81c\ub97c \ud574\uacb0\ud588\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uac8c \ub418\uc5b4 \uc880 \ub354 \uc7ac\ubc0c\ub294 \uac83\ub4e4\uc744 \uace0\ubbfc\ud558\uace0 \uacf5\ubd80\ud560 \uc218 \uc788\ub294 \uc800\ud76c \ud300\uc5d0\uac8c \uac10\uc0ac\ud558\uace0 \ubaa8\ub974\ub294 \ud0a4\uc6cc\ub4dc\ub97c \ub9ce\uc774 \uc54c\ub824\uc900 \ub204\ub204\uc5d0\uac8c \uac10\uc0ac\ud569\ub2c8\ub2e4.\\n\\n\uc544\uc9c1 \ubc30\uc6b0\ub294 \ub2e8\uacc4\ub77c \uc815\ud655\ud55c \uc815\ubcf4\uac00 \uc544\ub2d0 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubd80\uc871\ud55c \ubd80\ubd84\uc5d0 \ub300\ud574 \ub9ce\uc740 \uc9c0\uc801 \ubd80\ud0c1\ub4dc\ub9bd\ub2c8\ub2e4."},{"id":"22","metadata":{"permalink":"/22","source":"@site/blog/2023-07-27-filtering-and-index.mdx","title":"\ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604\uacfc \uc778\ub371\uc2a4 \uc774\uc6a9\ud55c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\ud558\uae30","description":"\uc548\ub155\ud558\uc138\uc694~","date":"2023-07-27T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 27\uc77c","tags":[{"label":"filter","permalink":"/tags/filter"},{"label":"index","permalink":"/tags/index"}],"readingTime":10.86,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"22","title":"\ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604\uacfc \uc778\ub371\uc2a4 \uc774\uc6a9\ud55c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\ud558\uae30","authors":["jay"],"tags":["filter","index"]},"prevItem":{"title":"Deadlock trouble shooting","permalink":"/23"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc774 styled-components\ub97c \uc120\ud0dd\ud55c \uc774\uc720","permalink":"/20"}},"content":"\uc548\ub155\ud558\uc138\uc694~\\n\\n\uc6b0\ud14c\ucf54 \uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\uc624\ub298\uc740 \ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604 \ubc0f \uc778\ub371\uc2a4\ub97c \uc774\uc6a9\ud55c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\ud558\ub294 \uc791\uc5c5\uc744 \uc9c4\ud589\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\\n## \uc694\uad6c \uc0ac\ud56d\uacfc \uae30\ub2a5 \uad6c\ud604 \ubaa9\ub85d\\n\uce74\ud398\uc778 \ud300\uc740 \uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc870\ud68c \ubc0f \ud1b5\uacc4 \ub370\uc774\ud130\ub97c \uc81c\uacf5\ud574\uc8fc\ub294 \uc11c\ube44\uc2a4\uc785\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790 \uc785\uc7a5\uc5d0\uc11c \uc804\uae30\ucc28 \ucda9\uc804\uc18c\ub97c \uc870\ud68c\ud560 \ub54c \ubcf8\uc778 \ucc28\uc5d0 \ub9de\ub294 \ucda9\uc804\uae30 \ud0c0\uc785\uacfc, \uc18d\ub3c4, \ub9c8\uc9c0\ub9c9\uc73c\ub85c \ucda9\uc804\uae30\ub97c \uc81c\uacf5\ud558\ub294 \ud68c\uc0ac\uba85 \uc694\uae08\uacfc \uad00\ub828\ub3c4 \ub418\uc5b4 \uc788\uc5b4\uc11c \uc911\uc694\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \ubb34\uc218\ud788 \ub9ce\uc740 \ucda9\uc804\uc18c\ub97c \ubcf4\ub294 \uac83\uc774 \uc544\ub2cc \uc790\uc2e0\uc5d0\uac8c \ud544\uc694\ud55c \uac83\ub9cc \ubcf4\ub294 \uac83\uc774 \uc0ac\uc6a9\uc790 \uacbd\ud5d8\uc5d0 \uc788\uc5b4\uc11c\ub294 \ub354 \uc911\uc694\ud55c\ub370\uc694.\\n\\n\uc800\ud76c \ud300\uc740 \uc774\ub97c \uc704\ud574 \ud544\ud130\ub9c1 \uae30\ub2a5\uc744 \ub3c4\uc785\ud558\uace0\uc790 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ub610\ud55c \uc870\ud68c\uac00 \ub9ce\uc740 \uc11c\ube44\uc2a4\uc778\ub9cc\ud07c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\uc744 \uc704\ud574 \uc778\ub371\uc2a4\ub97c \uc801\uc6a9\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud544\ud130\ub9c1 \ubfd0\ub9cc \uc544\ub2c8\ub77c \ud574\ub2f9 \uc791\uc5c5\uc744 \ud558\uba74\uc11c \uc5b4\ub5a4 \uace0\ubbfc\uc744 \ud588\uace0 \uc5b4\ub5a4 \uac83\uc744 \ud588\ub294\uc9c0 \uc801\uc5b4\ubcf4\uace0\uc790 \ud569\ub2c8\ub2e4.\\n\\n\\n## \ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604\ud558\uae30\\n\uc800\ud76c \ud300\uc740 \ube60\ub974\uac8c \uae30\ub2a5\uc744 \uad6c\ud604\ud558\ub294 \ub2e8\uacc4\uc5d0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc77c\ub2e8 3\uac1c\uc758 \ud544\ud130\ub9cc \ub3c4\uc785\ud588\uace0, \ud544\ud130\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4. [\ucda9\uc804\uc18c \uc6b4\uc601 \ud68c\uc0ac \uc774\ub984, \ucda9\uc804 \ud0c0\uc785, \ucda9\uc804 \uc18d\ub3c4]\\n\\n\uc0ac\uc6a9\uc790\ub294 \ud544\ud130\ub97c \ud074\ub9ad\ud558\uba74 \ud604\uc7ac \uc704\uce58\ub97c \uae30\uc900\uc73c\ub85c \uc8fc\ubcc0\uc5d0 \ud574\ub2f9 \ud544\ud130\uac00 \uc801\uc6a9\ub41c \ucda9\uc804\uc18c\ub97c \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n3\uac1c\uc758 \ud544\ud130 \uc911\uc5d0\uc11c \ubaa8\ub450 \uc801\uc6a9\ub420 \uc218\ub3c4 \uc788\uace0, \ubaa8\ub450 \uc801\uc6a9\ub418\uc9c0 \uc54a\uc744 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c 2^3 = 8\uac00\uc9c0\uc758 \uacbd\uc6b0\ub97c \uc0dd\uac01\ud574\uc57c \ud588\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uadf8\ub798\uc11c \ucc98\uc74c\uc5d0 \ud544\ud130\ub97c \uc801\uc6a9\ud558\uae30 \uc704\ud574\uc11c \ub2e4\uc74c\uacfc \uac19\uc740 \ubc29\ubc95\ub4e4\uc744 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n1. JPQL + \ud544\ud130\uc758 \uc870\ud569 (2^3)\ub9cc\ud07c if\ubb38 \uc0ac\uc6a9\ud558\uae30\\n\\n2. \uae30\uc874 \uc88c\ud45c\ub85c \uc870\ud68c\ud558\ub294 findAllByLatitudeBetweenAndLongitudeBetween() \uba54\uc11c\ub4dc\ub97c \uc0ac\uc6a9 \ud6c4 Stream\uc744 \uc774\uc6a9\ud574 \uc790\ubc14 \ucf54\ub4dc\ub85c \ud544\ud130\ub9c1\ud558\uae30\\n\\n\\n\uc774\ub807\uac8c \ub450 \uac00\uc9c0 \ubc29\ubc95\uc774 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n1\ubc88\uc758 \uacbd\uc6b0 \uc6b0\ud14c\ucf54 \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c Querydsl\uc744 \uc0ac\uc6a9\ud574\ub3c4 \ub418\ub294\uc9c0 \ud655\uc2e4\ud558\uc9c0 \uc54a\uc558\uace0 \uc815\ud655\ud55c \ud544\ud130 \uba85\uc138\uac00 \uc544\uc9c1\uc740 \uc5c6\uace0 3\uac00\uc9c0\ub9cc \uc77c\ub2e8 \ub3c4\uc785\ud558\uace0\uc790 \ud574\uc11c JPQL\uc744 \uc774\uc6a9\ud574\uc11c \uc0c1\ud669\ub9c8\ub2e4 if\ubb38\uc73c\ub85c \ud574\ub2f9 \uba54\uc11c\ub4dc\ub97c \uc2e4\ud589\uc2dc\ucf1c\uc8fc\ub294 \ubc29\ubc95\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n```java\\n// 1. fetch join + \ud68c\uc0ac \uc774\ub984\ub9cc \uc870\ud68c\\n @Query(\\"SELECT DISTINCT s FROM Station s \\" +\\n \\"LEFT JOIN FETCH s.chargers c \\" +\\n \\"LEFT JOIN FETCH c.chargerStatus \\" +\\n \\"WHERE s.latitude.value BETWEEN :minLatitude AND :maxLatitude \\" +\\n \\"AND s.longitude.value BETWEEN :minLongitude AND :maxLongitude \\" +\\n \\"AND s.companyName IN :companyNames\\")\\n List findAllByFilteringBeingCompanyNames(@Param(\\"minLatitude\\") BigDecimal minLatitude,\\n @Param(\\"maxLatitude\\") BigDecimal maxLatitude,\\n @Param(\\"minLongitude\\") BigDecimal minLongitude,\\n @Param(\\"maxLongitude\\") BigDecimal maxLongitude,\\n @Param(\\"companyNames\\") List companyNames);\\n\\n // 2. fetch join + \ucda9\uc804 \ud0c0\uc785\\n @Query(\\"SELECT DISTINCT s FROM Station s \\" +\\n \\"LEFT JOIN FETCH s.chargers c \\" +\\n \\"LEFT JOIN FETCH c.chargerStatus \\" +\\n \\"WHERE s.latitude.value BETWEEN :minLatitude AND :maxLatitude \\" +\\n \\"AND s.longitude.value BETWEEN :minLongitude AND :maxLongitude \\" +\\n \\"AND c.type IN :types\\")\\n List findAllByFilteringBeingTypes(@Param(\\"minLatitude\\") BigDecimal minLatitude,\\n @Param(\\"maxLatitude\\") BigDecimal maxLatitude,\\n @Param(\\"minLongitude\\") BigDecimal minLongitude,\\n @Param(\\"maxLongitude\\") BigDecimal maxLongitude,\\n @Param(\\"types\\") List types);\\n\\n```\\n\\n\uc9c4\ud589 \ud588\ub2e4\uba74 \uc774\ub7f0 \ub290\ub08c\uc774\uc5c8\uaca0\ub124\uc694!\\n\\n\\n2\ubc88\uc758 \uacbd\uc6b0 \ubaa8\ub450 \uc870\ud68c\ub97c\ud558\uace0 \uc790\ubc14 \ucf54\ub4dc\ub97c \uc774\uc6a9\ud574\uc11c \ud544\ud130\ub9c1 \ud574\uc8fc\ub294 \ubc29\ubc95\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ud604\uc7ac \uc800\ud76c \uc11c\ube44\uc2a4\ub294 \uc88c\ud45c\ub97c \uc911\uc2ec\uc73c\ub85c \uc8fc\ubcc0 \ucda9\uc804\uc18c\ub97c \uc870\ud68c\ud569\ub2c8\ub2e4.\\n\\n\uc5b4\ucc28\ud53c \uc0ac\uc6a9\uc790\uac00 \ud654\uba74\uc744 \ucd95\uc18c\ud574\uc11c \ud070 \ubc94\uc704\uc758 \uc9c0\ub3c4\ub97c \ubcf4\ub294 \uac83\uc740 \uc5b4\ucc28\ud53c \ub9c9\ud790\ud14c\ub2c8 \uc0ac\uc6a9\uc790\ub294 \uc791\uc740 \ubc94\uc704\uc5d0 \ub300\ud574\uc11c \uc870\ud68c\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \ud558\ub098\uc758 \ucffc\ub9ac\ub97c \uc774\uc6a9\ud574\uc11c \uc790\ubc14 \ucf54\ub4dc\ub85c \ud544\ud130\ub9c1 \ud574\uc8fc\ub294 \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\\n\\n\\n\uc774\ub807\uac8c\ub9cc \ubd24\uc744 \ub550 1\ubc88 \ubc29\uc2dd\uc778 \ud544\ud130 \ubcc4\ub85c \uc870\ud68c\ud574\uc8fc\ub294 \uac83\uc740 \uc870\ud68c \ud6a8\uc728\uc740 \ub354 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc 1\ubc88\uc758 \ubc29\ubc95\uc740 \'\ud604\uc7ac \uad6c\uc870\'\uc5d0\uc11c\ub294 \ub9ce\uc740 \ucffc\ub9ac\ubb38\uacfc \uba54\uc11c\ub4dc\ub97c \uc791\uc131\ud574\uc57c\ud558\uace0, if\ubb38 \ubc94\ubc85\uc73c\ub85c \ubcf4\uae30 \uc88b\uc9c0 \uc54a\uc740 \ucf54\ub4dc\uac00 \uc644\uc131 \ub410\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uacb0\uad6d 2\ubc88 \ubc29\uc2dd\uc778 [\uc804\uccb4 \uc870\ud68c + \ucf54\ub4dc\ub85c \ud544\ud130\ub9c1] \ubc29\uc2dd\uc744 \uc120\ud0dd\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc5b4\ucc28\ud53c \uc0ac\uc6a9\uc790\ub294 \uc791\uc740 \ubc94\uc704\uc5d0\uc11c \uc870\ud68c\ub97c \ud55c\ub2e4.\\n2. \uc778\ub371\uc2a4\ub97c \uac78\uc5c8\uc744 \ub54c \uac00\uc7a5 \ud6a8\uc728\uc801\uc774\ub2e4.\\n\\n1\ubc88\uc758 \uc774\uc720\ub294 \uc704\uc5d0\uc11c \ub9d0\ud588\uace0, 2\ubc88\uc5d0 \ub300\ud574 \uac04\ub2e8\ud558\uac8c \uc124\uba85 \ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uc800\ud76c \uc11c\ube44\uc2a4\ub294 \uc870\ud68c\uac00 \uad49\uc7a5\ud788 \ub9ce\uc9c0\ub9cc, \ucda9\uc804\uc18c\uc758 \uc8fc\uae30\uc801\uc778 \uc5c5\ub370\uc774\ud2b8\ub97c \uc704\ud574 \ub370\uc774\ud130 \uc5c5\ub370\uc774\ud2b8\uac00 \uad49\uc7a5\ud788 \ube48\ubc88\ud558\uac8c \uc77c\uc5b4\ub0a9\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c \ub9ce\uc9c0\ub294 \uc54a\uc9c0\ub9cc \ub370\uc774\ud130 \uc0bd\uc785\ub3c4 \ubc1c\uc0dd\ud558\uace0, \ub370\uc774\ud130 \uc5c5\ub370\uc774\ud2b8\ub3c4 \ub9ce\uc544\uc9d1\ub2c8\ub2e4.\\n\\nJPQL\ub85c \uc870\uac74\uc744 \ub098\ub220\uc11c \uc870\ud68c\ud574\uc900\ub2e4\uba74 \ud574\ub2f9\ud558\ub294 \ubaa8\ub4e0 \ud544\ud130\uc5d0 \uc778\ub371\uc2a4\ub97c \uac78\uc5b4\uc57c\ud560\uae4c\uc694?\\n\\n\uadf8\ub7f4 \uc21c \uc5c6\uc5c8\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uac00\uc7a5 \ud6a8\uc728\uc801\uc778 Column\uc5d0 \uc778\ub371\uc2a4\ub97c \uac78\uc5c8\uaca0\uc8e0, \uadf8\ub807\ub2e4\uba74 \uc870\ud68c\ub9c8\ub2e4 \uc18d\ub3c4\ub3c4 \ub2ec\ub77c\uc84c\uc744 \uac83\uc774\uace0 \uac00\ub839 \ud574\ub2f9\ud558\ub294 \ubaa8\ub4e0 Column\uc5d0 \uc778\ub371\uc2a4\ub97c \uc124\uc815\ud574\ub194\ub3c4 \uc5c5\ub370\uc774\ud2b8\uc640 \uc0bd\uc785\uc774 \ub290\ub824\uc84c\uc744 \uac83\uc785\ub2c8\ub2e4.\\n\\n\uc774\ub294 7\ubd84\ub9c8\ub2e4 \ub370\uc774\ud130\ub97c \uc5c5\ub370\uc774\ud2b8 \ud558\ub294 \uc800\ud76c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \uc801\uc808\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\ubc18\uba74\uc5d0 \ud55c \uac1c\uc758 \ucffc\ub9ac\ub85c \uc8fc\ubcc0\uc744 \ubaa8\ub450 \uc870\ud68c\ud558\uace0 \uc774\ub97c \uc790\ubc14 \ucf54\ub4dc\ub85c \ubc14\uafb8\ub294 \ubc29\ubc95\uc740 \ub354 \uc26c\uc6e0\uc2b5\ub2c8\ub2e4.\\n\\n\uc5b4\ucc28\ud53c \ub9ce\uc9c0 \uc54a\uc740 \uc591\uc758 \ub370\uc774\ud130\ub97c \uc870\ud68c\ud558\uace0 \ud544\ud130\ub9c1 \ud558\uae30 \ub54c\ubb38\uc5d0 \uc18d\ub3c4 \uba74\uc5d0\uc11c\ub3c4 \ud070 \ucc28\uc774\uac00 \ub098\uc9c0 \uc54a\uc558\uace0, \uc778\ub371\uc2a4 \uc124\uc815\uc5d0\ub3c4 \uc720\ub9ac\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc870\ud68c\uc2dc \uc774\uc6a9\ud558\ub294 latitude\uc640 longitude\ub9cc \uc124\uc815\ud574\uc8fc\uba74 \uc5b4\ub5a4 \uacbd\uc6b0\ub4e0 \ube60\ub974\uac8c \uc870\ud68c\ub97c \ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\n## \uc778\ub371\uc2a4 \uc801\uc6a9\uc73c\ub85c \uc870\ud68c \uc18d\ub3c4 \ud5a5\uc0c1\uc2dc\ud0a4\uae30\\n\\n\uba3c\uc800 \uc77c\ub2e8 \ud604\uc7ac \ucf54\ub4dc\uc5d0\uc11c \uc870\ud68c\uc2dc \ub2e4\uc74c\uacfc \uac19\uc740 \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n```sql\\nHibernate:\\n select\\n station0_.station_id as station_1_0_0_,\\n ...\\n ...\\n ...\\n chargersta2_.latest_update_time as latest_u4_2_2_\\n from\\n charge_station station0_\\n left outer join\\n charger chargers1_\\n on station0_.station_id=chargers1_.station_id\\n left outer join\\n charger_status chargersta2_\\n on chargers1_.charger_id=chargersta2_.charger_id\\n and chargers1_.station_id=chargersta2_.station_id\\n where\\n (\\n station0_.latitude between ? and ?\\n )\\n and (\\n station0_.longitude between ? and ?\\n )\\n```\\n\\nwhere \uc808\uc5d0\uc11c \uc704\ub3c4 \uacbd\ub3c4\ub97c \ubc14\ud0d5\uc73c\ub85c \uc8fc\ubcc0\ub9cc \uac00\uc838\uc624\uac8c \ub429\ub2c8\ub2e4. \uae30\uc874\uc5d0 N+1 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud574\uc11c EntityGraph\ub85c \ubc14\uafe8\uace0 \uc2e4\ud589\uc2dc \ucffc\ub9ac\uc785\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uc544\ub798 \uae00\uc744 \uc77d\uace0 BETWEEN \ucffc\ub9ac\uc5d0\uc11c \ubd80\ub4f1\ud638\ub97c \uc774\uc6a9\ud558\ub294 \ucffc\ub9ac\ub85c \ubcc0\uacbd\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n[Mysql Query Between \uacfc >=, <= \uc131\ub2a5 \ucc28\uc774 \ube44\uad50 ( \ub354\ubbf8\ub370\uc774\ud130 50\ub9cc )\\n](https://velog.io/@ggomjae/Mysql-Query-Between-%EA%B3%BC-%EC%84%B1%EB%8A%A5-%EC%B0%A8%EC%9D%B4-%EB%B9%84%EA%B5%90-%EB%8D%94%EB%AF%B8%EB%8D%B0%EC%9D%B4%ED%84%B0-50%EB%A7%8C)\\n\\n\\n```java\\n@Query(\\"SELECT DISTINCT s FROM Station s \\" +\\n \\"LEFT JOIN FETCH s.chargers c \\" +\\n \\"LEFT JOIN FETCH c.chargerStatus \\" +\\n \\"WHERE s.latitude.value >= :minLatitude AND s.latitude.value <= :maxLatitude \\" +\\n \\"AND s.longitude.value >= :minLongitude AND s.longitude.value <= :maxLongitude\\")\\n List findAllByLatitudeBetweenAndLongitudeBetweenWithFetch(@Param(\\"minLatitude\\") BigDecimal minLatitude,\\n @Param(\\"maxLatitude\\") BigDecimal maxLatitude,\\n @Param(\\"minLongitude\\") BigDecimal minLongitude,\\n @Param(\\"maxLongitude\\") BigDecimal maxLongitude);\\n\\n```\\n\uc704\uc640 \uac19\uc774 \uc870\ud68c\ud574\uc8fc\ub294 \ucffc\ub9ac\ub97c \ub9cc\ub4e4\uc5c8\uace0, \uc778\ub371\uc2a4\ub97c \ub9cc\ub4e4\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc778\ub371\uc2a4 \uc124\uc815 \uae30\uc900\uc740 [\uc778\ub371\uc2a4 \uc815\ub9ac \ubc0f \ud301](https://jojoldu.tistory.com/243)\\n\uc704\uc5d0 \ub9c1\ud06c\uc640 \uac19\uc774 \ub3d9\uc6b1\ub2d8\uc758 \ube14\ub85c\uadf8\ub97c \ucc38\uc870\ud574\uc11c \uae30\uc900\uc744 \uc138\uc6e0\uc2b5\ub2c8\ub2e4.\\n\\n\ubb34\uc870\uac74 \uce74\ub514\ub110\ub9ac\ud2f0\uac00 \ub192\uc740 \uac83\uc744 \uc124\uc815\ud560 \uc21c \uc5c6\uc5c8\uae30 \ub54c\ubb38\uc5d0 (\uc5c5\ub370\uc774\ud2b8\uc640 \uc0bd\uc785 \uc791\uc5c5\uc774 \ub9ce\uae30 \ub54c\ubb38\uc5d0) \ucffc\ub9ac\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 column\uacfc update \uc791\uc5c5\uc744 \uace0\ub824\ud558\uace0 \uc131\ub2a5\uc744 \ube44\uad50\ud574\uac00\uba74\uc11c \uac00\uc7a5 \ud6a8\uc728\uc801\uc778 \uac83\uc744 \uc124\uc815\ud574\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc18d\ub3c4\ub97c \ube44\uad50\ud574\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n
    \\n
    \\n\\n\u200b\\n\\n\uba3c\uc800 \uc18d\ub3c4 \ube44\uad50\ub97c \uc704\ud574\uc11c \ub370\uc774\ud130 \uc14b\uc740 \ub2e4\uc74c\uacfc \uac19\uc774 \uc9c4\ud589\ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\\n- Charger (23\ub9cc \uac74)\\n- Station (6\ub9cc \uac74)\\n- ChargerStatus(23\ub9cc \uac74)\\n- \uc120\ub989\uc5ed \uadfc\ucc98 \uc870\ud68c\\n\\n\\n### Ver1. \uc778\ub371\uc2a4 \uc801\uc6a9\uc744 \ud558\uc9c0 \uc54a\uace0 \uc870\ud68c \ubc0f \ud544\ud130\ub9c1 \ud588\uc744 \ub54c \uc18d\ub3c4 (0.84\ucd08)\\n![\uc774\ubbf8\uc9c0](https://postfiles.pstatic.net/MjAyMzA3MjdfMTYy/MDAxNjkwNDQwMDA0ODEw.vaeA83AD9ycHa26YN58rqzPV3XdX2zTvIZgKM6YKXWEg.Qqkkdr_lEJeGbYPpWji0E-IusfGpqMpZHKWZM4AyRrUg.PNG.sosow0212/image.png?type=w773)\\n\ud3c9\uade0\uc801\uc73c\ub85c 0.84\ucd08\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\\n### Ver2. \uc778\ub371\uc2a4 \uc801\uc6a9 \ubc0f \uc870\ud68c \ubc0f \ud544\ud130\ub9c1 \ud588\uc744 \ub54c \uc18d\ub3c4 (0.63\ucd08)\\n![\uc774\ubbf8\uc9c0](https://postfiles.pstatic.net/MjAyMzA3MjdfNTUg/MDAxNjkwNDQwMTUyMDcx.F3sSiDgLp3O2Rn1waqh31vC6yv1Uk0zZkRzjyuDQEM4g.eziRKLCmUbzW88ueQRozZcYvhsH10C17w-IDRLh0cJ4g.PNG.sosow0212/SE-48b3f814-3306-4add-ab95-381186bab6ca.png?type=w773)\\n\ud3c9\uade0\uc801\uc73c\ub85c 0.63\ucd08\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\uc57d 25 ~ 30%\uc758 \uc870\ud68c \uc18d\ub3c4\uac00 \uac1c\uc120\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\uc9c1 \uc774 \ubd80\ubd84\uc740 \uac1c\uc120\uc774 \ub354 \ud544\uc694\ud574\ubcf4\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub798\ub3c4 \uac1c\uc120\uc774 \ub410\uace0, \uc0bd\uc785\uacfc \uac31\uc2e0\uc5d0\ub294 \ud070 \uc9c0\uc7a5\uc774 \uc5c6\uc5b4\uc11c \uc77c\ub2e8 \uc774\uc815\ub3c4\ub85c \ub9c8\ubb34\ub9ac \ud558\uace0, \ucd94\ud6c4\uc5d0 \uac1c\uc120\uc744 \ud574\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\\n![\uc774\ubbf8\uc9c0](https://postfiles.pstatic.net/MjAyMzA3MjdfNzMg/MDAxNjkwNDQwODA1NzAy.b5gZPjl_E1x3wbjSMNcmfQDKB-hB9p8FEbIJqs5Kl4Qg.ZBq0-GmXJruPO7ejA_zq7RfaBaC17doHJUT19wje1Qkg.PNG.sosow0212/SE-f5396915-60ef-4293-a457-e30e8f5a2794.png?type=w773)\\n\ucd94\uac00\uc801\uc73c\ub85c \ucda9\uc804\uae30 \uc870\ud68c\ub294 \uad49\uc7a5\ud788 \ube68\ub77c\uc84c\uc2b5\ub2c8\ub2e4!\\n\\n\\n\ubc30\uc6b0\ub294 \ub2e8\uacc4\uc774\ub2e4\ubcf4\ub2c8 \ubbf8\uc219\ud558\uace0 \ud2c0\ub9b0 \ubd80\ubd84\uc774 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uae34 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4 :)"},{"id":"20","metadata":{"permalink":"/20","source":"@site/blog/2023-07-26-why-styled-components.mdx","title":"\uce74\ud398\uc778 \ud300\uc774 styled-components\ub97c \uc120\ud0dd\ud55c \uc774\uc720","description":"\uc65c styled-components\uc778\uac00?","date":"2023-07-26T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 26\uc77c","tags":[{"label":"styled-components","permalink":"/tags/styled-components"},{"label":"css","permalink":"/tags/css"},{"label":"css in js","permalink":"/tags/css-in-js"}],"readingTime":1.495,"hasTruncateMarker":false,"authors":[{"name":"\uc57c\ubbf8","title":"Frontend","url":"https://github.com/feb-dain","imageURL":"https://github.com/feb-dain.png","key":"yummy"}],"frontMatter":{"slug":"20","title":"\uce74\ud398\uc778 \ud300\uc774 styled-components\ub97c \uc120\ud0dd\ud55c \uc774\uc720","authors":["yummy"],"tags":["styled-components","css","css in js"]},"prevItem":{"title":"\ud544\ud130\ub9c1 \uae30\ub2a5 \uad6c\ud604\uacfc \uc778\ub371\uc2a4 \uc774\uc6a9\ud55c \uc870\ud68c \uc18d\ub3c4 \uac1c\uc120\ud558\uae30","permalink":"/22"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \uc0c1\ud0dc\uad00\ub9ac \uc804\ub7b5 (\uc65c Tanstack Query\uc5ec\uc57c \ud558\ub294\uac00?)","permalink":"/21"}},"content":"## \uc65c styled-components\uc778\uac00?\\n\\n
    \\n\\n\uc5ec\ub7ec `CSS-in-JS` \uc911 styled-components\ub97c \uc120\ud0dd\ud55c \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n1. \ucef4\ud3ec\ub10c\ud2b8 \uc548\uc5d0 \uad00\ub828 CSS\ub97c \uc791\uc131\ud560 \uc218 \uc788\uc5b4 \ucef4\ud3ec\ub10c\ud2b8\ubcc4 \ub514\uc790\uc778 \ucf54\ub4dc \ud655\uc778 \ubc0f \uc218\uc815\uc774 \uc6a9\uc774\ud558\ub2e4.\\n\\n2. \ud639\uc790\ub294 \ucf54\ub4dc \uac00\ub3c5\uc131\uc774 \uc548 \uc88b\uc544\uc9c4\ub2e4\uace0\ub3c4 \ud558\uc9c0\ub9cc, \uac1c\uc778\uc801\uc73c\ub85c\ub294 \ud0dc\uadf8\ub97c \ub354 \uc2dc\ub9e8\ud2f1 \ud558\uac8c \uc791\uc131\ud560 \uc218 \uc788\uc5b4\uc11c \uc88b\ub2e4\uace0 \ub290\uaf08\ub2e4.\\n\\n3. \ud300\uc6d0\ub4e4 \ubaa8\ub450 styled-components\uac00 \uc775\uc219\ud558\ub2e4.\\n\\n4. \uc9c0\uae08\uae4c\uc9c0 \uc0ac\uc6a9\ud558\uba74\uc11c \ubd88\ud3b8\ud55c \uc810\uc744 \ubabb \ub290\uaf08\ub2e4.\\n\\n
    \\n\\nstyled-components\uc640 emotion\uc740 \uae30\ub2a5\ub3c4, \uc791\uc131\ubc95\ub3c4 \uc0c1\ub2f9\ud788 \uc720\uc0ac\ud558\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc774\ubc88\uc5d0\ub294 styled-components \ub300\uc2e0 emotion\uc744 \uc368\ubcfc\uae4c\ub3c4 \uc0dd\uac01\ud588\uc5c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc emotion\uc5d0\uc11c\ub9cc \uc0ac\uc6a9 \uac00\ub2a5\ud558\ub358 \\\\*CSS Props\ub77c\ub294 \ud3b8\ub9ac\ud55c \uae30\ub2a5\uc744\\n\\nstyled-components(v5.2.0 \uc774\uc0c1)\uc5d0\uc11c \uc4f8 \uc218 \uc788\uac8c \ub418\uae30\ub3c4 \ud588\uace0,\\n\\n\'\uc0c8\ub85c\uc6b4 \uae30\uc220 \uacf5\ubd80\ub97c \ud574\ubcf4\uba74 \uc88b\uc744 \uac83 \uac19\ub2e4\'\ub294 \uc774\uc720\ub97c \uc81c\uc678\ud558\uace0\ub294\\n\\n\ub531\ud788 emotion\uc744 \uc0ac\uc6a9\ud560 \ud544\uc694\uc131\uc744 \ubabb \ub290\uaef4 styled-components\ub97c \ucc44\ud0dd\ud588\ub2e4.\\n\\n```typescript\\n// *CSS Props \uc608\uc2dc\\n\\nconst buttonStyle = css`\\n font-size: 18px;\\n color: white;\\n background: black;\\n`;\\n\\nconst ClickButton = styled.button<{ css: CSSProp }>`\\n width: 100px;\\n\\n ${({ css }) => css}\\n`;\\n\\nClick me!;\\n```"},{"id":"21","metadata":{"permalink":"/21","source":"@site/blog/2023-07-26-why-tanstack-query-is-good.mdx","title":"\uce74\ud398\uc778 \ud300\uc758 \uc0c1\ud0dc\uad00\ub9ac \uc804\ub7b5 (\uc65c Tanstack Query\uc5ec\uc57c \ud558\ub294\uac00?)","description":"\uc548\ub155\ud558\uc138\uc694? \uce74\ud398\uc778 \ud300 FE\uc5d0\uc11c \uc0c1\ud0dc\uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc5b4\ub5bb\uac8c \ud574\uc57c\ud560 \uc9c0 \uace0\ubbfc \ub05d\uc5d0 \uc11c\ub4dc\ud30c\ud2f0 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \ud544\uc694\ud558\uac8c \ub418\uc5b4 \uae00\uc744 \uc791\uc131\ud558\uac8c\ub410\uc2b5\ub2c8\ub2e4.","date":"2023-07-26T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 26\uc77c","tags":[{"label":"tanstack query","permalink":"/tags/tanstack-query"},{"label":"react state management","permalink":"/tags/react-state-management"}],"readingTime":8.695,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"21","title":"\uce74\ud398\uc778 \ud300\uc758 \uc0c1\ud0dc\uad00\ub9ac \uc804\ub7b5 (\uc65c Tanstack Query\uc5ec\uc57c \ud558\ub294\uac00?)","authors":["gabriel"],"tags":["tanstack query","react state management"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc774 styled-components\ub97c \uc120\ud0dd\ud55c \uc774\uc720","permalink":"/20"},"nextItem":{"title":"OAuth 2.0\uc758 \ud750\ub984\uacfc \uc124\uc815 \ud574\ubcf4\uae30","permalink":"/19"}},"content":"\uc548\ub155\ud558\uc138\uc694? \uce74\ud398\uc778 \ud300 FE\uc5d0\uc11c \uc0c1\ud0dc\uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc5b4\ub5bb\uac8c \ud574\uc57c\ud560 \uc9c0 \uace0\ubbfc \ub05d\uc5d0 \uc11c\ub4dc\ud30c\ud2f0 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \ud544\uc694\ud558\uac8c \ub418\uc5b4 \uae00\uc744 \uc791\uc131\ud558\uac8c\ub410\uc2b5\ub2c8\ub2e4.\\n\\n# \uc11c\ubc84 \uc0c1\ud0dc\uc640 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\uc758 \uad6c\ubd84\\n\\n\uc11c\ubc84\uc0c1\ud0dc\uc640 UI\uc0c1\ud0dc\ub97c \uc774\ud574\ud558\ub294 \uac83\uc740 \uad49\uc7a5\ud788 \uc911\uc694\ud588\uc2b5\ub2c8\ub2e4. \ub370\uc774\ud130\ub97c \uc1a1\uc218\uc2e0\ud558\ub294 \uc791\uc5c5\uacfc \uc0c1\ud0dc\ub97c \uad00\ub9ac\ud558\ub294 \uc791\uc5c5\uc740 \uc720\uae30\uc801\uc73c\ub85c \ub3d9\uc791\ud574\uc57c\ud588\uc2b5\ub2c8\ub2e4. \uae30\uc874\uc5d0\ub294 `\uc0c1\ud0dc\uc640 \ub370\uc774\ud130 \uc1a1\uc218\uc2e0 \uacfc\uc815\uc744 \ubd84\ub9ac\ud574\uc11c \uc0dd\uac01`\ud588\ub2e4\uba74, \ud604\ub300\uc758 react \ud504\ub85c\uc81d\ud2b8\ub4e4\uc740 `\uc11c\ubc84\uc640 \ub3d9\uae30\ud654\ub97c \ud574\uc57c\ud560 \uc0c1\ud0dc`\uc640 `\uadf8\ub807\uc9c0 \uc54a\uc740 \uc0c1\ud0dc`\ub85c \ubd84\ub9ac\ud574\uc11c \uc0dd\uac01\ud574\uc57c \ud569\ub2c8\ub2e4.\\n\\n`React\uc5d0\uc11c \uc5b4\ub5a4 \ub370\uc774\ud130\ub97c \uc0c1\ud0dc\ub85c \ub2e4\ub904\uc57c \ud558\ub294\uac00`\uc5d0 \ub300\ud574\uc11c\ub294 \uc5ec\ub7ec \uc758\uacac\uc774 \ub098\uc62c \uc218 \uc788\ub2e4\uace0 \uc0dd\uac01\ud558\uc9c0\ub9cc `\uc0c1\ud0dc\uac00 \ud2b9\uc131\uc744 \uac00\uc9c0\uace0 \uc788\ub294\uac00`\uc5d0 \ub300\ud574\uc11c\ub294 \ub300\ubd80\ubd84 \ud2b9\uc131\uc774 \uc788\ub2e4\uace0 \ub3d9\uc758\ud560 \uac83\uc785\ub2c8\ub2e4. \uc774 \uae00\uc5d0\uc11c\ub294 React\uc758 \uc0c1\ud0dc\ub780 \ubb34\uc5c7\uc778\uac00?\uc5d0 \ub300\ud574\uc11c \ub2e4\ub8e8\uc9c0 \uc54a\uace0 React\uc758 \uc0c1\ud0dc\uc758 \ud2b9\uc131\uc5d0 \ub300\ud574\uc11c\ub9cc \uc5b8\uae09\uc744 \ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\uc0c1\ud0dc\uc758 \ud2b9\uc131\uc73c\ub85c\ub294 \ud06c\uac8c \ub450 \uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\\n\\n\ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\ub294 \ucef4\ud3ec\ub10c\ud2b8\ub4e4 \uac04\uc5d0 \uc5b4\ub5a4 \uac12\uc744 \uacf5\uc720\ud574\uc57c\ud558\uba74\uc11c `\uc624\ub85c\uc9c0 React DOM \ub0b4\ubd80\uc5d0\uc11c\ub9cc CRUD\uac00 \uc77c\uc5b4\ub098\ub294 \uc0c1\ud0dc\ub97c \uc758\ubbf8`\ud569\ub2c8\ub2e4. \uc774 \uc0c1\ud0dc\ub4e4\uc740 React DOM \uc678\ubd80 \uc138\uacc4\uc640 \ud06c\uac8c \uad00\ub828\uc774 \uc5c6\uc73c\uba70 `\ub3d9\uae30\uc801\uc73c\ub85c \ubc18\uc601`\ub429\ub2c8\ub2e4. \ub300\ud45c\uc801\uc73c\ub85c\ub294 UI\ub97c \uc870\uc791\ud558\ub294 \uc0c1\ud0dc\ub4e4\uc774 \ub420 \uac83\uc785\ub2c8\ub2e4. \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\ub4e4\uc740 \ub300\ubd80\ubd84 \uc7a5\uae30\uc801\uc73c\ub85c \uc720\uc9c0\ub420 \ud544\uc694\uac00 \uc5c6\uae30\uc5d0 \ud654\uba74\uc744 \ubc97\uc5b4\ub098\uac70\ub098 \uc138\uc158\uc774 \ub04a\uae30\ub294 \uacbd\uc6b0 \uc0ac\ub77c\uc838\ub3c4 \uad1c\ucc2e\uc740 \uacbd\uc6b0\uac00 \ub9ce\uc2b5\ub2c8\ub2e4.\\n\\n### \uc11c\ubc84 \uc0c1\ud0dc\\n\\n\uc11c\ubc84 \uc0c1\ud0dc\ub294 React\uc758 \ubc14\uae65 \uc138\uc0c1(\uc11c\ubc84)\uc5d0 \uc874\uc7ac\ud558\ub294 `\ub370\uc774\ud130\uac00 React\uc758 \uc0c1\ud0dc \uad00\ub9ac\uc640 \ube44\ub3d9\uae30\uc801\uc73c\ub85c \ub3d9\uae30\ud654 \ub41c \uac83`\uc744 \uc758\ubbf8\ud569\ub2c8\ub2e4. \uc5b4\ub5a4 \uc0c1\ud0dc\uac00 `\uc678\ubd80\uc5d0\uc11c \uad00\ub9ac\ub418\ub294 \ub370\uc774\ud130\uc640 \ubc18\ub4dc\uc2dc \uc5f0\ub3d9`\ub418\uc5b4\uc57c \ud55c\ub2e4\uba74 \uc774\ub294 \uace7 \uc11c\ubc84 \uc0c1\ud0dc\uc784\uc744 \uc758\ubbf8\ud569\ub2c8\ub2e4. React\uc758 \uc0c1\ud0dc\ub97c CRUD \ud558\ub294 \uac83 \ubfd0\ub9cc \uc544\ub2cc, \uc11c\ubc84\uc5d0\uc11c\ub3c4 \ud56d\uc0c1 \uac19\uc740 \uc77c\uc774 \uc77c\uc5b4\ub098\uc57c \ud569\ub2c8\ub2e4. \uc11c\ubc84 \uc0c1\ud0dc\ub294 \uc7a5\uae30\uc801\uc73c\ub85c \uc720\uc9c0\ub418\uc5b4\uc57c \ud558\uba70, \uc138\uc158\uc5d0\uc11c \ubc97\uc5b4\ub098\ub354\ub77c\ub3c4 \uc11c\ubc84\ub85c \ubd80\ud130 \ubcf5\uad6c\ub97c \ud574\uc57c \ud569\ub2c8\ub2e4.\\n\\n\uae30\uc874\uc758 \uc0c1\ud0dc \uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc740 \ub9ac\uc561\ud2b8\uc758 \uc804\uc5ed\uc5d0\uc11c \uc0c1\ud0dc\ub97c \uc870\uc791\ud558\ub294 \uac83\uc5d0 \ud2b9\ud654\ub418\uc5b4\uc788\uace0, \ube44\ub3d9\uae30\uc801\uc778 \uc0c1\ud0dc \uad00\ub9ac\ub3c4 \uc9c0\uc6d0\ud558\uc5ec \uc11c\ubc84\uc640\uc758 \ud1b5\uc2e0\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \ub300\ubd80\ubd84\uc758 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc740 `\ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\ub97c \uc870\uc791\ud558\ub294 \uac83\uc5d0 \ucd08\uc810`\uc774 \ub9de\ucdb0\uc838\uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub354\uad70\ub2e4\ub098 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\uc640 \uc11c\ubc84 \uc0c1\ud0dc\uac00 \ud558\ub294 \uc77c\uc774 \uba85\ud655\ud558\uac8c \ub2e4\ub978 \uc0c1\ud669\uc5d0\uc11c \uc774 \ub458\uc744 \ud55c \uacf3\uc5d0\uc11c \uad00\ub9ac\ud558\ub294 \uac83 \ubcf4\ub2e4\ub294 \uc644\ubcbd\ud558\uac8c \ubd84\ub9ac\ud558\ub294 \uac83\uc774 \ub354 \ub098\uc744 \uac83\uc785\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc11c\ubc84 \uc0c1\ud0dc\ub97c \uad00\ub9ac\ud558\ub294 \uac83\uc5d0 \uc911\uc810\uc744 \ub454 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc774 \ub4f1\uc7a5\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub300\ud45c\uc801\uc778 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c\ub294 RTK Query, Tanstack Query, SWR \ub4f1\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n# \uc65c Tanstack Query\uc600\ub098?\\n\\n### vs RTK Query\\n\\nRTK Query\ub294 RTK\ub97c \ubc18\ub4dc\uc2dc \uc0ac\uc6a9\ud574\uc57c \ud558\ub294 \uac83\uc740 \uc544\ub2c8\uc9c0\ub9cc RTK\ub97c \ud0c0\uac9f\uc73c\ub85c \ub098\uc628 \uc11c\ubc84 \uc0c1\ud0dc \uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4. `\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc0c1\ud0dc\ub97c \uad00\ub9ac\ud558\uae30 \uc704\ud574 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.` \ub354\uc6b1\uc774 Redux\uc758 \ubcf5\uc7a1\ud55c \ucf54\ub4dc \uad6c\uc131\uacfc \ubc29\ub300\ud55c \ubcf4\uc77c\ub7ec \ud50c\ub808\uc774\ud2b8\ub294 \ub9e4\ub825\uc801\uc774\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. tanstack query\uc5d0\uc11c\ub294 \ubb34\ud55c \ub370\uc774\ud130 \ud398\uce6d\uc744 \uc9c0\uc6d0\ud558\uae30 \uc704\ud574 **Infinite Queries**\uac00 \uc788\uc9c0\ub9cc RTK Query\ub294 \uadf8\ub807\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\\n### vs SWR\\n\\nSWR\ub3c4 \ud558\ub098\uc758 \uc88b\uc740 \uc120\ud0dd\uc9c0\uc600\uc9c0\ub9cc, \uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc774 \ubc94\uc6a9\uc801\uc73c\ub85c \uc9c0\uc6d0\ud558\ub294 \uc140\ub809\ud130 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ub610, \uac00\ube44\uc9c0 \uceec\ub809\ud130\uc758 \ubd80\uc7ac\ub3c4 \uc544\uc26c\uc6e0\uc2b5\ub2c8\ub2e4. \uc7ac\uc694\uccad\uc744 \ud558\uae30 \uc704\ud55c stale time \uc124\uc815\uc774\ub098 \ucffc\ub9ac \ucde8\uc18c \uae30\ub2a5\uc774 \uc5c6\ub294 \uc810\ub3c4 \ub9e4\ub825\uc801\uc774\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.\\n\\n# \uce74\ud398\uc778 \ud300\uc5d0\uc11c \ud558\ub824\ub294 \uc77c\uc740\uc694\u2026\\n\\n\uc800\ud76c \uce74\ud398\uc778 \ud300\uc758 \ud504\ub85c\uc81d\ud2b8\ub294 `\uc2e4\uc2dc\uac04 \uc804\uae30\uc790\ub3d9\ucc28 \ucda9\uc804\uc18c \uc9c0\ub3c4 \ubc0f \uc0ac\uc6a9 \ud1b5\uacc4 \uc870\ud68c \uc11c\ube44\uc2a4` \ub85c \uc9c0\ub3c4 \uae30\ubc18\uc758 \ud504\ub85c\uc81d\ud2b8\uc785\ub2c8\ub2e4. \uc11c\ubc84 \uc0c1\ud0dc\ub97c \uc801\uadf9\uc801\uc73c\ub85c \ub2e4\ub904\uc57c \ud558\ub294 \uc0c1\ud669\uc5d0\uc11c Tanstack Query\ub97c \uc11c\ubc84 \uc0c1\ud0dc \uad00\ub9ac \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c \uc120\uc815\ud558\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n\uba54\uc778 \uae30\ub2a5 \uc911 Tanstack Query\uac00 \ud575\uc2ec\uc73c\ub85c \uc0ac\uc6a9\ub420 \uac83 \uac19\uc740 \uae30\ub2a5\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n- \uc9c0\ub3c4\uc5d0\uc11c \ucda9\uc804\uc18c \uc870\ud68c\\n - \ud604\uc7ac \uc811\uc18d\ud55c \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0 \ub80c\ub354\ub9c1 \ub41c \uc9c0\ub3c4 \ud654\uba74(\ub514\uc2a4\ud50c\ub808\uc774)\uc758 \ud06c\uae30\uc5d0 \ub530\ub978 GPS\uc88c\ud45c\ub97c \uc54c\uc544\ub0b4\uc5b4 \uc11c\ubc84\ub85c \ubd80\ud130 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uc218\uc2e0 \ubc1b\uc2b5\ub2c8\ub2e4. \uc989, \ud654\uba74\uc774 \uc774\ub3d9\ud558\uac8c \ub418\uba74 \uc0ac\uc6a9\uc790\uac00 \ubc14\ub77c\ubcf4\uace0 \uc788\ub294 \uc601\uc5ed\uc774 \ubcc0\ud558\ubbc0\ub85c \uc0c8\ub85c\uc6b4 \uc694\uccad\uc744 \ubcf4\ub0b4\uac8c \ub429\ub2c8\ub2e4.\\n - \uc11c\ubc84\uc5d0\uc11c \uc218\uc2e0\ud55c \ucda9\uc804\uc18c \uc815\ubcf4\ub294 \uc2e4\uc2dc\uac04 \uc0ac\uc6a9 \ud604\ud669\ub3c4 \ubc18\uc601\ub418\uc5b4\uc788\uc73c\ubbc0\ub85c `\uc8fc\uae30\uc801\uc778 \uc5c5\ub370\uc774\ud2b8`\ub3c4 \ud544\uc694\ud569\ub2c8\ub2e4.\\n - \ube48\ubc88\ud55c \ub370\uc774\ud130\uc758 \ubcc0\ud654\uac00 \ud544\uc694\ud558\uba70 \uadf8\ub9cc\ud07c \ud1b5\uc2e0 \uc2e4\ud328 \ub4f1 `\uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud560 \uac00\ub2a5\uc131\ub3c4 \ub9ce\uc544\uc9c0\uac8c` \ub429\ub2c8\ub2e4.\\n - \uc0ac\uc6a9\uc790\uc758 \ube60\ub978 \uc9c0\ub3c4 \uc774\ub3d9\uc774 \ubc1c\uc0dd\ud558\ub294 \uacbd\uc6b0\ub97c \ub300\uc751\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n- \uc804\uad6d \ucda9\uc804\uc18c \uac80\uc0c9\uae30\\n - \uc6d0\ud558\ub294 \ucda9\uc804\uc18c \uac80\uc0c9\uc744 \ud558\ub294 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud569\ub2c8\ub2e4. \uc804\uad6d \ub2e8\uc704\ub85c \uac80\uc0c9 \uacb0\uacfc\ub97c \uc218\uc2e0\ud558\ub294 \uae30\ub2a5\uc785\ub2c8\ub2e4.\\n - \ub124\uc774\ubc84\uc640 \uad6c\uae00 \uac80\uc0c9\ucc3d \ucc98\ub7fc \uc0ac\uc6a9\uc790\uac00 input \ucc3d\uc5d0 \uac80\uc0c9\uc5b4\ub97c \uc785\ub825\ud560 \ub54c \ub9c8\ub2e4 \uac80\uc0c9 \uacb0\uacfc\uac00 \ub3d9\uc801\uc73c\ub85c \ud45c\uc2dc\ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n - \ube48\ubc88\ud55c \ub370\uc774\ud130\uc758 \ubcc0\ud654\uac00 \ud544\uc694\ud558\uace0, `\uc0ac\uc6a9\uc790\uc758 \ube60\ub978 \ud0c0\uc774\ud551\uc73c\ub85c \uc778\ud574 \uc7a6\uc740 \uac80\uc0c9\uc774 \ubc1c\uc0dd\ud558\ub294 \uacbd\uc6b0\ub97c \ub300\uc751\ud560 \uc218 \uc788\uc5b4\uc57c` \ud569\ub2c8\ub2e4.\\n - \uc774\ub97c \uc704\ud574 \ub370\uc774\ud130\ub97c \uce90\uc2f1\ud560 \ud544\uc694\ub3c4 \uc788\ub2e4\uace0 \uc0dd\uac01\ud569\ub2c8\ub2e4.\\n\\n\ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c \ud074\ub77c\uc774\uc5b8\ud2b8\uc640 \uc11c\ubc84\uc640\uc758 \ud1b5\uc2e0\uc774 \uc5b4\uca4c\ub2e4 \ud55c\ubc88 \uc77c\uc5b4\ub09c\ub2e4\uba74 \uad73\uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \ud544\uc694\uac00 \uc5c6\uaca0\uc9c0\ub9cc, \uc11c\ubc84\uc758 \ub370\uc774\ud130 \uc804\uc801\uc73c\ub85c \uc758\uc874\ud574\uc57c \ud558\ub294 \uc800\ud76c \ud504\ub85c\uc81d\ud2b8 \ud2b9\uc131\uc0c1 Tanstack Query\uc758 \uc5ec\ub7ec \uae30\ub2a5\uc774 \uc0dd\uc0b0\uc131\uc5d0 \ub9ce\uc740 \ub3c4\uc6c0\uc774 \ub420 \uac83\uc73c\ub85c \uae30\ub300\ud569\ub2c8\ub2e4."},{"id":"19","metadata":{"permalink":"/19","source":"@site/blog/2023-07-23-oauth.mdx","title":"OAuth 2.0\uc758 \ud750\ub984\uacfc \uc124\uc815 \ud574\ubcf4\uae30","description":"OAuth 2.0 ?","date":"2023-07-23T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 23\uc77c","tags":[{"label":"oauth","permalink":"/tags/oauth"},{"label":"login","permalink":"/tags/login"}],"readingTime":12.57,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"19","title":"OAuth 2.0\uc758 \ud750\ub984\uacfc \uc124\uc815 \ud574\ubcf4\uae30","authors":["boxster"],"tags":["oauth","login"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 \uc0c1\ud0dc\uad00\ub9ac \uc804\ub7b5 (\uc65c Tanstack Query\uc5ec\uc57c \ud558\ub294\uac00?)","permalink":"/21"},"nextItem":{"title":"private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\ub97c \uc678\ubd80\uc640 \uc5f0\uacb0\ud560 \ub54c, public ip? private ip?","permalink":"/18"}},"content":"## OAuth 2.0 ?\\n\\n\\n> OAuth(\\"Open Authorization\\")\ub294 \uc778\ud130\ub137 \uc0ac\uc6a9\uc790\ub4e4\uc774 \ube44\ubc00\ubc88\ud638\ub97c \uc81c\uacf5\ud558\uc9c0 \uc54a\uace0 \ub2e4\ub978 \uc6f9\uc0ac\uc774\ud2b8 \uc0c1\uc758 \uc790\uc2e0\ub4e4\uc758 \uc815\ubcf4\uc5d0 \ub300\ud574 \uc6f9\uc0ac\uc774\ud2b8\ub098 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc758 \uc811\uadfc \uad8c\ud55c\uc744 \ubd80\uc5ec\ud560 \uc218 \uc788\ub294 \uacf5\ud1b5\uc801\uc778 \uc218\ub2e8\\n\\n\uc704\ud0a4 \ubc31\uacfc\uc5d0\uc11c\ub294 \uc704\uc640 \uac19\uc774 \uc124\uba85\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uc6b0\ub9ac\uac00 google\uacfc \uac19\uc740 \uc6f9 \uc0ac\uc774\ud2b8\uc5d0 \ud68c\uc6d0\uac00\uc785\uc744 \ud558\uace0 \uc800\uc7a5\ud574\ub454 \uc774\ub984, \uc774\uba54\uc77c, \ud504\ub85c\ud544 \uc774\ubbf8\uc9c0 \uac19\uc740 \uc815\ubcf4\ub97c\\n\uad73\uc774 \ud55c\ubc88 \ub354 \uc785\ub825\ud558\uc9c0 \uc54a\uace0\ub3c4 \ub2e4\ub978 \uc6f9 \uc0ac\uc774\ud2b8\uc5d0\uc11c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uac83 \uc785\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ub2e4\ub978 \uc6f9 \uc0ac\uc774\ud2b8\ub97c \uc0ac\uc6a9\ud558\ub354\ub77c\ub3c4 google\uc5d0\uc11c \ub85c\uadf8\uc778\uc744 \ud558\ub294 \uacfc\uc815\uc744 \uac70\uce58\uae30 \ub54c\ubb38\uc5d0, \uc0ac\uc6a9\uc790\ub294\\n\ube44\ubc00\ubc88\ud638\ub098, critical\ud55c \uac1c\uc778\uc815\ubcf4 \uac19\uc740 \uac83\uc744 \ud55c \uacf3\uc5d0\uc11c \uad00\ub9ac\ud560 \uc218 \uc788\ub2e4\ub294 \uc7a5\uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub2e4\uc2dc \ud55c\ubc88 \uc815\ub9ac\ud558\uc790\uba74 \uc6b0\ub9ac \uc6f9 \uc0ac\uc774\ud2b8\uc758 \uc0ac\uc6a9\uc790\uac00 \uc774\uc6a9\ud558\ub294 \ub2e4\ub978 \uc6f9 \uc0ac\uc774\ud2b8\uc758 \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\uac8c\ub054 \ub2e4\ub978 \uc6f9 \uc0ac\uc774\ud2b8\uc5d0\uc11c \uad8c\ud55c\uc744 \uc704\uc784 \ubc1b\ub294 \uac83 \uc785\ub2c8\ub2e4.\\n\\n### OAuth flow\\n\\n\\nOAuth Flow\ub97c \uc124\uba85\ud558\uae30 \uc804\uc5d0 \uc5ec\uae30\uc11c \ubaa8\ub974\ub294 \ub2e8\uc5b4\ub4e4\uc774 \ub9ce\uc2b5\ub2c8\ub2e4.\\n\ud574\ub2f9 [\ub9c1\ud06c](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-16#section-1.1)\uc5d0\uc11c \ub354 \uc790\uc138\ud558\uac8c \uc815\ub9ac \ub418\uc5b4\uc788\uc9c0\ub9cc \uc124\uba85\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n#### Resource Owner\\nResource Owner\ub294 \ub9d0 \uadf8\ub300\ub85c \ub9ac\uc18c\uc2a4 \uc18c\uc720\uc790\uc774\uace0, \uad6c\uae00\uacfc \uac19\uc740 \ud50c\ub7ab\ud3fc\uc5d0 \ud68c\uc6d0\uac00\uc785\uc774 \ub418\uc5b4\uc788\ub294, \uc989 \uad6c\uae00\uc5d0 \uc790\uc2e0\uc758 \uc815\ubcf4\ub4e4\uc774 \uc788\ub294 \uc0ac\uc6a9\uc790\uc785\ub2c8\ub2e4.\\n\\n#### Client\\nClient\ub3c4 \ub9d0 \uadf8\ub300\ub85c \uace0\uac1d\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \uc5b4\ub5a4 \uad00\uc810\uc5d0\uc11c \ubcf4\ub290\ub0d0 \uace0\uac1d\uc774\ub780 \ub73b\uc740 \ub2ec\ub77c\uc9d1\ub2c8\ub2e4. \uc5ec\uae30\uc11c\ub294 Google\uacfc \uac19\uc740 \ud50c\ub7ab\ud3fc\uc5d0\uc11c \uc81c\uacf5\ubc1b\uc740 \ub9ac\uc18c\uc2a4\ub97c \uc0ac\uc6a9\ud558\ub294 \uace0\uac1d\uc785\ub2c8\ub2e4.\\n\uc989 \uc6b0\ub9ac\uc758 \uc11c\ube44\uc2a4\uac00 Client\uac00 \ub418\ub294 \uac83\uc785\ub2c8\ub2e4. \uc65c\ub0d0\uba74 \uc6b0\ub9ac\ub294 \uad6c\uae00\uc5d0 \uc815\ubcf4\ub97c \uc694\uccad\ud558\uace0 \uc6b0\ub9ac\uc758 \uc11c\ube44\uc2a4\uc5d0\uc11c \uc0ac\uc6a9\ud558\uae30 \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\\n#### Authorization Server\\n\uc5ec\uae30\ub3c4 \ub9d0 \uadf8\ub300\ub85c \uc778\uc99d \uc11c\ubc84\uc785\ub2c8\ub2e4. Resource Owner\uac00 \uc62c\ubc14\ub978 \uc815\ubcf4\ub97c \uc785\ub825\ud588\ub294\uc9c0 \uac80\uc99d\ud558\uace0, \ubc1c\uae09 \ubc1b\uc740 Code\uc640 Token\uc774 \uc62c\ubc14\ub978 \uac83\uc778\uc9c0 \uac80\uc99d\ud569\ub2c8\ub2e4.\\n\\n#### Resource Server\\nResource Owner\uc758 \uc815\ubcf4\ub4e4\uc744 \uac00\uc9c0\uace0 \uc788\ub294 \uc11c\ubc84\uc785\ub2c8\ub2e4. \uc778\uc99d \uc11c\ubc84\uc5d0\uc11c \uc778\uc99d\uc744 \ub9c8\uce58\uace0 \ub09c \ub4a4 \uc6b0\ub9ac\ub294 Resource\ub97c \ubc1b\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc5ec\uae30\uc11c Authorization Server \uc640 Resource Server\uac00 \ub098\ub258\uc5b4\uc9c4 \uc774\uc720\ub294 \ub531\ud788 \uc5c6\uc2b5\ub2c8\ub2e4. **\ud574\ub2f9 \ud50c\ub7ab\ud3fc\uc758 \uc11c\ubc84 \uad6c\uc131\uc5d0 \ub530\ub77c \ub2e4\ub97c \uc218 \uc788\uc2b5\ub2c8\ub2e4.**\\n\\n\uc911\uc694\ud55c \uac83\uc740 **Authorization Server**\uc640 **Resource Server**\uac00 \uac19\uc740 \ubb36\uc74c\uc774\ub77c\ub294 \uac83 \uc785\ub2c8\ub2e4.\\n\\n```mermaid\\nsequenceDiagram\\n actor RO as Resource Owner (\ubc15\uc2a4\ud130)\\n participant C as Client (\uce74\ud398\uc778)\\n participant AS as Authorization Server (\uad6c\uae00)\\n participant RS as Resource Server (\uad6c\uae00)\\n\\n RO->>+C: 1. \ub85c\uadf8\uc778 \uc694\uccad\\n C--\x3e>-AS: 2. \ub85c\uadf8\uc778 \uc694\uccad\\n AS ->>+ RO: 3. \ub85c\uadf8\uc778 \ud398\uc774\uc9c0 \uc81c\uacf5\\n RO ->>+ AS: 4. ID/PW \uc785\ub825\\n AS ->> RO: 5. Authorization Code \ubc1c\uae09\\n RO ->> C: 6. Redirect URI\ub85c \uc774\ub3d9\\n C ->>+ AS: 7. \ubc1c\uae09 \ubc1b\uc740 Authorization Code\ub85c Token \uc694\uccad\\n AS ->> C: 8. Access Token \ubc1c\uae09\\n C ->> RO: 9. \ub85c\uadf8\uc778 \uc131\uacf5\\n RO ->> C: 10. \uc11c\ube44\uc2a4 \uc694\uccad\\n C ->> RS: 11. \ubc1c\uae09 \ubc1b\uc740 Token\uc73c\ub85c \uc815\ubcf4 \ud638\ucd9c\\n RS ->> C: 12. \uc815\ubcf4 \uc81c\uacf5\\n```\\n\\n\uac04\ub2e8\ud558\uac8c flow\ub97c \ub3c4\uc2dd\ud654 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n1. \uba3c\uc800 Resource Owner\ub294 \ub85c\uadf8\uc778\uc744 \ud558\uace0 \uc2f6\ub2e4\uba74 Client\uac00 \uc81c\uacf5\ud558\ub294 \ud574\ub2f9 Resource platform\uc758 URI\ub97c \ud074\ub9ad\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc778\uc99d\uc11c\ubc84\uc5d0\uc11c \ub85c\uadf8\uc778 \ud398\uc774\uc9c0\ub97c \uc81c\uacf5 \ubc1b\uc2b5\ub2c8\ub2e4.\\n2. \uadf8\ub9ac\uace0 Resource Owner\ub294 ID/PW\ub97c \uc785\ub825\ud558\uace0 Authorization Code\ub97c \ubc1c\uae09 \ubc1b\uc2b5\ub2c8\ub2e4. \ub3d9\uc2dc\uc5d0 Client\uc5d0\uc11c \ub4f1\ub85d\ud574\ub193\uc740 Redirect URI\ub85c code\uc640 \ud568\uaed8 \uc774\ub3d9\ud569\ub2c8\ub2e4.\\n3. Client\ub294 Resource Owner\uc5d0\uac8c \ubc1b\uc740 Code\ub97c \uac00\uc9c0\uace0 Authorization Server\uc5d0 \ud1a0\ud070\uc744 \uc694\uccad\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ubc1b\uc740 \ud1a0\ud070\uc744 \uc800\uc7a5\ud569\ub2c8\ub2e4.\\n4. \uadf8\ub9ac\uace0 Client\ub294 \ub85c\uadf8\uc778\uc744 \uc131\uacf5\ud558\uace0 \uc774\ud6c4 \ub2e4\ub978 platform\uc5d0\uc11c \uc815\ubcf4\ub97c \ud544\uc694\ud558\uac8c \ub41c\ub2e4\uba74 \uc800\uc7a5\ud55c Access Token\uc744 \ud1b5\ud574 Resource Server\uc5d0\uc11c \uc815\ubcf4\ub97c \uac00\uc838\uc635\ub2c8\ub2e4.\\n\\n\uadfc\ub370 \uc5ec\uae30\uc11c \uc774\uc0c1\ud55c \uc810\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uc774\uc0c1\ud558\ub2e4\uae30\ubcf4\ub2e8 \uc65c \uc774\ub807\uac8c \ubcf5\uc7a1\ud55c\uac00 \ub77c\ub294 \uc758\ubb38\uc744 \uac00\uc9c8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\\n\uad73\uc774 Authorization code\ub97c \ubc1b\uc544 \ub2e4\uc2dc \ud55c\ubc88 \ub354 Access Token\uc744 \ubc1b\uc544\uc57c \ud55c\ub2e4\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4. \ubc14\ub85c **Client\uc5d0\uac8c Access Token\uc744 \uc900\ub2e4\uba74 \ud1b5\uc2e0\uc774 \ud55c\ubc88 \uc904\uc5b4\ub4e4 \uc218 \uc788\uc9c0 \uc54a\uc744\uae4c??**\\n\\n\ubcf4\uc548\ubb38\uc81c \ub54c\ubb38\uc785\ub2c8\ub2e4.\\n\ub9cc\uc57d \ubc14\ub85c Access Token\uc744 \uc900\ub2e4\uba74 \uadf8 Access Token\uc774 \ud0c8\ucde8 \ub2f9\ud558\uba74 \ud574\ub2f9 Resource Owner\uc758 \ubaa8\ub4e0 \uc815\ubcf4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\nCode\ub294 Secret Key\uc640 \uac19\uc774 \uc804\ub2ec\ud574\uc57c Access Token\uc744 \ubc1c\uae09 \ubc1b\uc744 \uc218 \uc788\uae30 \ub54c\ubb38\uc5d0 \ud0c8\ucde8\ub418\uc5b4\ub3c4 \ub354 \uc548\uc804\ud569\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \ub2e4\ub978 \ud50c\ub7ab\ud3fc\uc5d0\uc11c Code\ub098 Token\uc774\ub098 \ud574\ub2f9 \uc815\ubcf4\ub97c \uc804\ub2ec\ud560 \ubc29\ubc95\uc740 URI\uc5d0 \uc804\ub2ec\ud558\ub294 \ubc29\ubc95\ubfd0 \uc785\ub2c8\ub2e4.\\n\uadf8\ub807\uae30 \ub54c\ubb38\uc5d0 Redircet URI\uc5d0 Access Token\uc744 \ub2f4\ub294\ub2e4\uba74 \ud0c8\ucde8 \uac00\ub2a5\uc131\uc774 \ucee4\uc9c0\uae30 \ub54c\ubb38\uc5d0 \ubcf4\uc548\ubb38\uc81c\uac00 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\\n\\n### \ubc31\uc5d4\ub4dc\uc640 \ud504\ub860\ud2b8\uc5d4\ub4dc\uc758 flow\\n\\n```mermaid\\n\\nsequenceDiagram\\n actor RO as Resource Owner\\n participant F as Frontend\\n participant B as Backend\\n participant AS as Authorization Server\\n participant RS as Resource Server\\n\\n RO->>+F: 1. \ub85c\uadf8\uc778 \uc694\uccad\\n F--\x3e>-B: 2. \ub85c\uadf8\uc778 \uc694\uccad\\n B->>+F: 3. \ud574\ub2f9 \ud50c\ub7ab\ud3fc \ub85c\uadf8\uc778 URI \uc81c\uacf5\\n F--\x3e>-RO: 4. \ud574\ub2f9 \ud50c\ub7ab\ud3fc \ub85c\uadf8\uc778 URI \uc81c\uacf5\\n AS ->>+ RO: 5. \ub85c\uadf8\uc778 \ud398\uc774\uc9c0 \uc81c\uacf5\\n RO ->>+ AS: 6. ID/PW \uc785\ub825\\n AS ->> RO: 7. Authorization Code \ubc1c\uae09\\n RO ->> F: 8. Redirect URI\ub85c \uc774\ub3d9 (w. Authorization Code)\\n F ->>+ B: 9. \ubc1c\uae09 \ubc1b\uc740 Authorization Code \uc804\ub2ec\\n B ->>+ AS: 10. \uc804\ub2ec \ubc1b\uc740 Authorization Code\ub85c Token \uc694\uccad\\n AS ->> B: 11. Access Token \ubc1c\uae09\\n B ->>+ F: 12. \ub85c\uadf8\uc778 \uc131\uacf5\\n F --\x3e>- RO: 13. \ub85c\uadf8\uc778 \uc131\uacf5\\n RO ->>+ F: 14. \uc11c\ube44\uc2a4 \uc694\uccad\\n F --\x3e>- B: 15. \uc11c\ube44\uc2a4 \uc694\uccad\\n B ->> RS: 16. \ubc1c\uae09 \ubc1b\uc740 Token\uc73c\ub85c \uc815\ubcf4 \ud638\ucd9c\\n RS ->> B: 17. \uc815\ubcf4 \uc81c\uacf5\\n\\n```\\n\uc544\uae4c Client \ubd80\ubd84\uc744 \uc880 \ub354 Frontend, Backend\ub85c \uad6c\ubd84\uc9c0\uc5b4 \uc138\ubd84\ud654 \ud574\ubd24\uc2b5\ub2c8\ub2e4. \ubcf5\uc7a1\ud574\ubcf4\uc774\uc9c0\ub9cc, \uc804\ud600 \uc5b4\ub835\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc544\uae4c \uc124\uba85\ud588\ub358 \ud750\ub984\uacfc \ub2e4\ub978 \ubd80\ubd84\uc740 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\\n\ub610 \uc5ec\uae30\uc11c\ub294 \uad73\uc774 \uc81c\uac00 Authorization Server\uc5d0\uc11c Code\ub97c \ubc1b\uc544\uc62c \ub54c Redirect URI\ub97c \ubc31\uc5d4\ub4dc \uc11c\ubc84\ub85c \ud558\uc9c0\uc54a\uace0 \ud504\ub860\ud2b8\uc5d4\ub4dc \uc11c\ubc84\ub85c \ud558\ub824\ub294 \uc774\uc720\ub294 Resource Owner\uac00 \ub2e4\ub978 platform\uacfc \uc778\uc99d\ud558\ub294 \ubd80\ubd84\uc740 \ubc31\uc5d4\ub4dc\uc758 \uc5ed\ud560\uc774 \uc544\ub2c8\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub9ac\uace0 \ubc31\uc5d4\ub4dc\ub294 Resource Owner\uac00 \uac00\uc838\uc628 code\ub97c \ud504\ub860\ud2b8\uc5d4\ub4dc\uc5d0\uc11c \uc804\ub2ec \ubc1b\uc544 Resource Server\uc5d0 \uc815\ubcf4\ub97c \uc694\uccad\ud558\ub294 \uac83\uc774\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n***(\ubb3c\ub860 \uc81c \uac1c\uc778\uc801\uc778 \uc758\uacac\uc774\ub77c \uc815\ub2f5\uc740 \uc544\ub2d9\ub2c8\ub2e4.)***\\n\\n\\n## OAuth \uad6c\ud604\ud574\ubcf4\uae30\\n\\n\uac04\ub2e8\ud788 Spring Security \uc5c6\uc774 OAuth \uc778\uc99d\uc744 \uad6c\ud604\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc81c\uc77c \uba3c\uc800 \uad6c\uae00 \ud639\uc740 \ub2e4\ub978 \ud50c\ub7ab\ud3fc\uc5d0\uc11c \uc124\uc815\ud55c id, secret key \ub4f1\ub4f1\uc758 \uc815\ubcf4\ub97c yml\uc5d0 \uc791\uc131\ud588\uc2b5\ub2c8\ub2e4.\\n```yml title=\\"application-oauth.yml\\"\\noauth2:\\n provider:\\n google:\\n id: google-id\\n secret: google-secret-key\\n redirect-url: http://localhost:8080/login/oauth2/code/google\\n token-url: https://www.googleapis.com/oauth2/v4/token\\n info-url: https://www.googleapis.com/oauth2/v2/userinfo\\n```\\n\uadf8\ub9ac\uace0 OAuth\ub294 \uc5b4\ub290 \ud50c\ub7ab\ud3fc\uc774 \ub420 \uc9c0 \ubaa8\ub974\uace0, \ud655\uc7a5\uc131 \uc788\uac8c \uad6c\uc131\ud558\ub294 \uac83\uc774 \uc88b\uc744 \uac83 \uac19\uc544 \uc778\ud130\ud398\uc774\uc2a4\ub85c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n```java title=\\"OAuthMember.java\\"\\npublic interface OAuthMember {\\n String id();\\n String email();\\n String nickname();\\n String imageUrl();\\n}\\n```\\n\uc774\ub7ec\ud55c \ud074\ub798\uc2a4\ub4e4\uc744 \uad00\ub9ac\ud558\uae30 \uc27d\uac8c Enum\uc744 \ucd94\uac00\ud569\ub2c8\ub2e4.\\n\\n```java title=\\"Provider.java\\"\\npublic enum Provider {\\n\\n GOOGLE(\\"google\\", GoogleMember::new),\\n ;\\n\\n private final String providerName;\\n private final Function, OAuthMember> function;\\n\\n Provider(String providerName, Function, OAuthMember> function) {\\n this.providerName = providerName;\\n this.function = function;\\n }\\n\\n public static Provider from(String name) {\\n return Arrays.stream(values())\\n .filter(it -> it.providerName.equals(name))\\n .findFirst()\\n .orElseThrow(() -> new RuntimeExceptin());\\n }\\n\\n public OAuthMember getOAuthProvider(Map body) {\\n return function.apply(body);\\n }\\n}\\n```\\n\ud574\ub2f9 Enum\uc740 \ub450\uac1c\uc758 \ud544\ub4dc\ub97c \uac00\uc9c0\uace0 \uc788\uc2b5\ub2c8\ub2e4. \ud558\ub098\ub294 \ud574\ub2f9 \ud50c\ub7ab\ud3fc\uc758 \uc774\ub984, \uadf8\ub9ac\uace0 `Map`\ub97c \uc544\uae4c \ub9cc\ub4e4\uc5c8\ub358 \uc778\ud130\ud398\uc774\uc2a4\ub85c \ubc18\ud658\ud558\ub294 Function \uc5ec\uae30\uc11c\\n`Map`\ub85c \uc9c0\uc815\ud574\uc900 \uc774\uc720\ub294, \ud50c\ub7ab\ud3fc\ub9c8\ub2e4 \ubc18\ud658\ub418\ub294 JSON \ud0c0\uc785\uc774 \ub2e4\ub974\uae30 \ub54c\ubb38\uc5d0 \uadf8\ub7f0 \ubd80\ubd84\uc5d0 \ub300\ud574 \uc911\ubcf5\uc744 \uc81c\uac70\ud558\uae30 \uc704\ud574 \uc774\ub7ec\ud55c \ud615\ud0dc\ub85c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc544\uae4c yml\uc5d0 \uc791\uc131\ud588\ub358 \uc815\ubcf4\ub4e4\uc744 \uac00\uc838\uc640\uc57c\ud569\ub2c8\ub2e4. `@Value` \uc5b4\ub178\ud14c\uc774\uc158\uc73c\ub85c\ub3c4 \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n```java\\n @Value(\\"oauth.provider.google.id\\")\\n private String id;\\n @Value(\\"oauth.provider.google.secret\\")\\n private String secret;\\n\\n ...\\n```\\n\ud558\uc9c0\ub9cc \uc774\ub807\uac8c \uacc4\uc18d binding\uc744 \ud574\uc918\uc57c\ud55c\ub2e4\ub294 \uc810\uc774 \uc544\uc8fc \uadc0\ucc2e\uace0 \ubcf4\uae30\ub3c4 \uc548\uc88b\uc2b5\ub2c8\ub2e4.\\n```groovy title=\\"build.gradle\\"\\nannotationProcessor \\"org.springframework.boot:spring-boot-configuration-processor\\"\\n```\\n\ud558\uc9c0\ub9cc \uc704\uc758 \uc758\uc874\uc131\uc744 \ucd94\uac00\ud574\uc900\ub2e4\uba74 \uc544\uc8fc \ud3b8\ud558\uac8c property\ub97c \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```java title=\\"OAuthProviderProperties.java\\"\\n@Component\\n@ConfigurationProperties(prefix = \\"oauth2\\")\\npublic class OAuthProviderProperties {\\n // prefix oauth2 \uae30\uc900\uc73c\ub85c \uc54c\uc544\uc11c google\uc774 \uc774\ub984\uc778 Provider Enum\uc744 \ucc3e\uc544\uc11c Key\ub85c \ubc14\uc778\ub529\\n private final Map provider = new EnumMap<>(Provider.class);\\n\\n public OAuthProviderProperty getProviderProperties(Provider provider) {\\n return this.provider.get(provider);\\n }\\n\\n @Getter\\n @Setter\\n public static class OAuthProviderProperty {\\n // \uadf8\ub9ac\uace0 provider \ud558\uc704 \uc815\ubcf4\ub4e4\uc740 \uc544\ub798\uc758 \ud544\ub4dc\uc5d0 \ubc14\uc778\ub529\\n private String id;\\n private String secret;\\n private String redirectUrl;\\n private String tokenUrl;\\n private String infoUrl;\\n }\\n}\\n```\\n\uc774\ub807\uac8c \ub418\uba74 \uad6c\uc870\uc801\uc778 \uc900\ube44\ub294 \ub05d\ub0ac\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc81c\ub294 \ud574\ub2f9 \ud50c\ub7ab\ud3fc\uc5d0 \uc815\ubcf4\ub97c \uc694\uccad\ud558\ub294 \uc791\uc5c5\ub9cc \ud558\uba74 \ub429\ub2c8\ub2e4.\\n\uadf8\ub7fc \uc544\uae4c \ub9d0\uc500\ub4dc\ub838\ub358 \uc21c\uc11c\ub85c \uc694\uccad\uc744 \ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java title=\\"RestTemplateOAuthRequester.java\\"\\npublic class RestTemplateOAuthRequester implements OAuthRequester {\\n\\n @Override\\n public OAuthMember login(OAuthLoginRequest request) {\\n // frontend\uc5d0\uc11c \ubc1b\uc544\uc628 \ub85c\uadf8\uc778 platform\\n Provider provider = Provider.from(request.provider());\\n // \ud574\ub2f9 Platform\uc5d0 \ub9de\ub294 \uc815\ubcf4 \ucc3e\uc74c\\n OAuthProviderProperty property = oAuthProviderProperties.getProviderProperties(provider);\\n // frontend\uc5d0\uc11c \ubc1b\uc544\uc628 code\uc640 \ub4f1\ub85d\ud574\ub193\uc740 property\ub85c Access Token \uc694\uccad\\n OAuthTokenResponse token = requestAccessToken(property, requet.getCode());\\n // \ubc1b\uc544\uc628 Token\uc73c\ub85c \ud574\ub2f9 Resource Owner\uc758 \uc815\ubcf4 \uc694\uccad\\n Map userAttributes = getUserAttributes(property, token);\\n return provider.getOAuthProvider(userAttributes);\\n }\\n\\n private OAuthTokenResponse requestAccessToken(OAuthProviderProperty property, String code) {\\n HttpHeaders headers = new HttpHeaders();\\n headers.setBasicAuth(property.getId(), property.getSecret());\\n headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);\\n\\n HttpEntity> request = new HttpEntity<>(headers);\\n URI tokenUri = getTokenUri(property, code);\\n return restTemplate.postForEntity(tokenUri, request, OAuthTokenResponse.class).getBody();\\n }\\n\\n private URI getTokenUri(OAuthProviderProperty property, String code) {\\n return UriComponentsBuilder.fromUriString(property.getTokenUrl())\\n .queryParam(CODE, URLDecoder.decode(code, StandardCharsets.UTF_8))\\n .queryParam(GRANT_TYPE, AUTHORIZATION_CODE)\\n .queryParam(REDIRECT_URI, property.getRedirectUrl())\\n .build()\\n .toUri();\\n }\\n\\n private Map getUserAttributes(OAuthProviderProperty property, OAuthTokenResponse tokenResponse) {\\n HttpHeaders headers = new HttpHeaders();\\n headers.setBearerAuth(tokenResponse.accessToken());\\n headers.setContentType(MediaType.APPLICATION_JSON);\\n URI uri = URI.create(property.getInfoUrl());\\n RequestEntity requestEntity = new RequestEntity<>(headers, HttpMethod.GET, uri);\\n ResponseEntity> responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<>() {\\n });\\n return responseEntity.getBody();\\n }\\n}\\n```\\n\\n\uc774\ub807\uac8c\ub9cc \ud55c\ub2e4\uba74 \uadf8 \uc5b4\ub824\uc6cc \ubcf4\uc774\ub358 OAuth \uc778\uc99d\ub3c4 \uac04\ub2e8\ud558\uac8c \ud574\uacb0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n***(\ubb3c\ub860 \uc81c \ucf54\ub4dc\uac00 \uc815\ub2f5\uc774 \uc544\ub2d9\ub2c8\ub2e4)***\\n\\n\\n### Reference\\nhttps://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-16\\n\\nhttps://developers.google.com/identity/protocols/oauth2?hl=ko"},{"id":"18","metadata":{"permalink":"/18","source":"@site/blog/2023-07-23-why-private-ip-is-required-for-instance.mdx","title":"private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\ub97c \uc678\ubd80\uc640 \uc5f0\uacb0\ud560 \ub54c, public ip? private ip?","description":"\uc5b4\ub5a4 \ubb38\uc81c\uac00 \uc788\uc5c8\ub098\uc694?","date":"2023-07-23T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 23\uc77c","tags":[{"label":"aws","permalink":"/tags/aws"},{"label":"vpc","permalink":"/tags/vpc"},{"label":"subnet","permalink":"/tags/subnet"},{"label":"ip","permalink":"/tags/ip"}],"readingTime":10.365,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"18","title":"private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\ub97c \uc678\ubd80\uc640 \uc5f0\uacb0\ud560 \ub54c, public ip? private ip?","authors":["nunu"],"tags":["aws","vpc","subnet","ip"]},"prevItem":{"title":"OAuth 2.0\uc758 \ud750\ub984\uacfc \uc124\uc815 \ud574\ubcf4\uae30","permalink":"/19"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 CI/CD","permalink":"/17"}},"content":"## \uc5b4\ub5a4 \ubb38\uc81c\uac00 \uc788\uc5c8\ub098\uc694?\\n\\n\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4\uc5d0\uc11c private \uc11c\ube0c\ub137\uc5d0 db \uc778\uc2a4\ud134\uc2a4\ub97c \ub450\uace0, \ubcf4\uc548\uc744 \uc704\ud574 \uc678\ubd80\uc5d0\uc11c \uc811\uc18d\uc744 \ucc28\ub2e8\ud558\ub824\uace0 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c \ucd1d 2\uac00\uc9c0\uc758 \ubb38\uc81c\uc810\uc774 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n1. private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\uac00 \uc778\ud130\ub137\uc5d0\uc11c mysql\uc744 \uc124\uce58\ud560 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n2. public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0\uc11c private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc774 \uc548\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \ubd80\ubd84\uc744 \uc5b4\ub5bb\uac8c \ud574\uacb0\ud588\ub294\uc9c0 \uc54c\uc544\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\ub798\uc758 \ubaa8\ub4e0 \uc124\uba85\uc740 AWS \ub97c \uae30\uc900\uc73c\ub85c \ud569\ub2c8\ub2e4.\\n\\n## private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\uac00 \uc778\ud130\ub137\uc5d0\uc11c mysql\uc744 \uc124\uce58\ud560 \uc218 \uc5c6\uc5c8\ub2e4.\\n\\n### \ud574\uacb0 \ubc29\ubc95\\n\\npublic ip \uc790\ub3d9\ud560\ub2f9\uc744 \ud574\uc8fc\uc9c0 \uc54a\uc544\uc11c, \uc778\ud130\ub137\uc5d0 \uc5f0\uacb0\uc774 \uc548 \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574 public ip \uc790\ub3d9\ud560\ub2f9\uc744 \ud574\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc65c public ip\ub97c \ud560\ub2f9\ud588\ub354\ub2c8 \ubb38\uc81c\uac00 \ud574\uacb0\ub418\uc5c8\uc744\uae4c\uc694?\\n\\n## private \uc11c\ube0c\ub137\uc774\ub780?\\n\\n\uc815\ub9d0 \uac04\ub2e8\ud558\uac8c \uc124\uba85\ud588\uc744 \ub54c\\n\\nprivate \uc11c\ube0c\ub137\uc740 \uc778\ud130\ub137\uc5d0 \uc5f0\uacb0\ub418\uc9c0 \uc54a\uc740 \uc11c\ube0c\ub137\uc785\ub2c8\ub2e4.\\n\\n\uc870\uae08 \uc790\uc138\ud558\uac8c \ub4e4\uc5b4\uac00 \ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\nprivate \uc11c\ube0c\ub137\uc740 \uc778\ud130\ub137 \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc5f0\uacb0\ub418\uc9c0 \uc54a\uc740 \uc11c\ube0c\ub137\uc785\ub2c8\ub2e4.\\n\\naws \uacf5\uc2dd\ubb38\uc11c\uc5d0\uc11c \uc0ac\uc9c4\uc744 \ud1b5\ud574 \ubcf4\uba74 \uc544\ub798\uc640 \uac19\uc774 \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4\\n\\n![private subnet](https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/images/internet-gateway-basics.png)\\n\\npublic \uc11c\ube0c\ub137\uc5d0\ub9cc \uc778\ud130\ub137 \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc5f0\uacb0\ub418\uc5b4 \uc788\uace0, private \uc11c\ube0c\ub137\uc5d0\ub294 \uc778\ud130\ub137 \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc5f0\uacb0\ub418\uc5b4\uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\nprivate \uc11c\ube0c\ub137\uc5d0 \uc778\ud130\ub137 \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc9c0 \uc54a\ub2e4\uace0 \ud588\uc744 \ub54c, \uae30\ubcf8\uc801\uc73c\ub85c \uc778\ud130\ub137\uc5d0 \uc811\uc18d\uc774 \uc548\ub429\ub2c8\ub2e4.\\n\\nmysql\uc744 \uc124\uce58\ud560 \ub54c\ub3c4, \uc778\ud130\ub137\uc5d0 \uc811\uc18d\uc744 \ud574\uc57c\ud558\ub294\ub370, \uc778\ud130\ub137\uc5d0 \uc811\uc18d\uc774 \uc548\ub418\ub2c8 \uc124\uce58\uac00 \uc548\ub418\ub294 \uac83\uc785\ub2c8\ub2e4.\\n\\n### \uc5b4? \uc778\ud130\ub137 \uc790\uccb4\uac00 \uc811\uadfc\uc774 \uc548\ub418\uba74 \uc5b4\ub5bb\uac8c \uc124\uce58\ud558\ub098\uc694?\\n\\n\uc815\ub9d0 \uc6d0\uc2dc\uc801\uc73c\ub85c \ud574\uacb0\ud558\uae30 \uc704\ud574\uc11c\ub294 public \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\ub97c \ud558\ub098 \ub354 \ub9cc\ub4e4\uc5b4\uc11c, mysql \uc744 \uc555\ucd95\ud574\uc11c scp\ub97c \ud1b5\ud574 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc804\uc1a1\ud558\uace0, \uc555\ucd95\uc744 \ud480\uc5b4\uc11c \uc124\uce58\ud558\ub294 \ubc29\ubc95\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774 \ubc29\ubc95\uc740 \ub108\ubb34 \uc6d0\uc2dc\uc801\uc774\uace0, \ube44\ud6a8\uc728\uc801\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc778\ud130\ub137\uc73c\ub85c \uc694\uccad\uc744 \ubcf4\ub0bc \uc218 \uc788\ub3c4\ub85d \ub9cc\ub4dc\ub294 \uacfc\uc815\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n### \uc778\ud130\ub137\uc73c\ub85c \uc694\uccad\uc744 \ubcf4\ub0bc \uc218 \uc788\ub3c4\ub85d \ub9cc\ub4dc\ub294 \uacfc\uc815\\n\\n\uc778\ud130\ub137\uc73c\ub85c \uc694\uccad\uc744 \ubcf4\ub0bc \uc218 \uc788\ub3c4\ub85d \ub9cc\ub4dc\ub294 \uacfc\uc815\uc740 \ud06c\uac8c 2\uac00\uc9c0\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### private \uc11c\ube0c\ub137\uc744 public \uc11c\ube0c\ub137\uc73c\ub85c \ubc14\uafb8\uae30\\n\\n\ubcf4\uc548\uc744 \uc704\ud574\uc11c private \uc11c\ube0c\ub137\uc5d0 \ub450\ub824\uace0 \ud588\ub358 \uac83\uc744 public \uc11c\ube0c\ub137\uc73c\ub85c \ubc14\uafbc\ub2e4\ub294 \ubd80\ubd84\uc740 \ub9e4\uc6b0 \uc704\ud5d8\ud569\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc774 \ubc29\ubc95\uc740 \ubcf4\ud1b5 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n### NAT \uc778\uc2a4\ud134\uc2a4(Gateway) \ub9cc\ub4e4\uae30\\n\\nNAT \uc778\uc2a4\ud134\uc2a4\ub294 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 \uc778\ud130\ub137\uc5d0 \uc811\uc18d\ud560 \uc218 \uc788\ub3c4\ub85d \ub9cc\ub4e4\uc5b4\uc8fc\ub294 \uc778\uc2a4\ud134\uc2a4\uc785\ub2c8\ub2e4.\\n\\n\uc778\ud130\ub137\uc5d0 \uc811\uc18d\uc744 \ud558\uae30 \uc704\ud574\uc11c\ub294 public ip \uac00 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c NAT \uc778\uc2a4\ud134\uc2a4, NAT \uac8c\uc774\ud2b8\uc6e8\uc774\ub294 public \uc11c\ube0c\ub137\uc5d0 \uc874\uc7ac\ud574\uc57c \ud569\ub2c8\ub2e4.\\n\\n\uc5b4? NAT \uc778\uc2a4\ud134\uc2a4\ub97c \ud1b5\ud574\uc11c \ubc14\ub85c \ud1b5\uc2e0\uc774 \uac00\ub2a5\ud558\uba74 \uc65c private \uc11c\ube0c\ub137\uc774 \ud544\uc694\ud55c\uac00\uc694? \uadf8\ub0e5 \ub2e4 public \uc11c\ube0c\ub137\uc5d0 \ub450\uba74 \ub418\uc9c0 \uc54a\ub098\uc694?\\n\\nNAT \uc778\uc2a4\ud134\uc2a4, NAT Gateway\ub294 \ub0b4\ubd80\uc5d0\uc11c \ucd9c\ubc1c\ud55c \ud2b8\ub798\ud53d\ub9cc \ud1b5\uacfc\ud560 \uc218 \uc788\ub3c4\ub85d \uc124\uc815\uc774 \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uba74 private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\ud574\uc11c \uc9c1\uc811 mysql download \uc694\uccad\uc744 \ud588\uc744 \ub54c\ub9cc \ud5c8\uc6a9\uc774 \ub429\ub2c8\ub2e4.\\n\\n\uc678\ubd80\uc5d0\uc11c \ubc14\ub85c private \uc778\uc2a4\ud134\uc2a4\ub85c \uc811\uadfc\ud560 \uc218\ub294 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\nNAT \uc778\uc2a4\ud134\uc2a4\ub9cc \uc124\uc815\uc744 \ud558\uba74 \ubc14\ub85c \uc5f0\uacb0\uc774 \ub418\ub098\uc694?\\n\\npublic ip\ub3c4 \uc790\ub3d9 \ud560\ub2f9\uc744 \ud574\uc918\uc57c \ud569\ub2c8\ub2e4\\n\\n### public ip \uac00 \ud544\uc694\ud55c \uc774\uc720\\n\\nNAT \uc778\uc2a4\ud134\uc2a4\ub97c \ud1b5\ud574\uc11c private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 \uc778\ud130\ub137\uc5d0 \uc811\uc18d\ud560 \uc218 \uc788\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\ub294\ub370, \uc65c public ip \uac00 \ud544\uc694\ud560\uae4c\uc694?\\n\\n\uc678\ubd80 \uc778\ud130\ub137\uacfc \ud1b5\uc2e0\uc744 \ud560 \ub54c public ip \uac00 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\nNAT \uc778\uc2a4\ud134\uc2a4 \ud639\uc740 NAT \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc778\ud130\ub137\uacfc \ud1b5\uc2e0\ud560 \ub54c, NAT \uc778\uc2a4\ud134\uc2a4\uc758 public ip + private ip\ub97c \ud1b5\ud574\uc11c \ud1b5\uc2e0\uc744 \ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\ub0b4\ubd80 \uc778\uc2a4\ud134\uc2a4\uc758 public ip \ub97c \ud1b5\ud574\uc11c \ud1b5\uc2e0\uc744 \ud558\uac8c \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c NAT \uc778\uc2a4\ud134\uc2a4\uc640 \ub0b4\ubd80 \uc778\uc2a4\ud134\uc2a4 \ubaa8\ub450 public ip \uac00 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c 1\ubc88 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc81c 2\ubc88\uc9f8 \ubb38\uc81c\ub97c \ud574\uacb0\ud574 \ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0\uc11c private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc774 \uc548 \ub418\ub294 \ubb38\uc81c\\n\\npublic \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc11c\ubc84\uac00 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc11c\ubc84\uc5d0 \uc811\uc18d\uc744 \ud558\ub824\uace0 \ud588\ub294\ub370, \uc811\uc18d\uc774 \uc548 \ub418\ub294 \ubb38\uc81c\uac00 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n### \ud574\uacb0 \ubc29\ubc95\\n\\n\ud574\uacb0 \ubc29\ubc95\uc5d0\ub294 2\uac00\uc9c0 \uacfc\uc815\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc5d0 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc744 \ucd94\uac00\ud574 \uc8fc\uae30\\n\\n\uae30\ubcf8\uc801\uc73c\ub85c public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc5d0\ub294 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc774 \ucd94\uac00\ub418\uc5b4\uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc5d0 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc744 \ucd94\uac00\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n\\n### private ip\ub97c \ud1b5\ud574\uc11c \uc811\uc18d\ud558\uae30\\n\\npublic \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\ud560 \ub54c, public ip \ub97c \ud1b5\ud574\uc11c \uc811\uc18d\uc744 \ud558\uba74 \uc548 \ub429\ub2c8\ub2e4.\\n\\npublic ip\ub97c \ud1b5\ud574\uc11c \uc811\uc18d\ud558\ub294 \uacfc\uc815\uc744 \uc790\uc138\ud558\uac8c \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n1. public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 public ip \ub97c \ud1b5\ud574\uc11c private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc744 \uc2dc\ub3c4\ud569\ub2c8\ub2e4.\\n2. \ub77c\uc6b0\ud305 \ud14c\uc774\ube14\uc5d0\uc11c public ip \uc77c \uacbd\uc6b0\uc5d0 \uc5b4\ub5bb\uac8c \ucc98\ub9ac\ud560\uc9c0\uc5d0 \ub300\ud55c \uc815\ubcf4\ub97c \ucc3e\uc2b5\ub2c8\ub2e4.\\n3. \ub77c\uc6b0\ud130\ub97c \ud1b5\ud574\uc11c \uc678\ubd80 \uc778\ud130\ub137\uc73c\ub85c \ub098\uac00\uac8c \ub429\ub2c8\ub2e4.\\n4. \ud2b8\ub798\ud53d\uc774 NAT \uc778\uc2a4\ud134\uc2a4\uc5d0 \ub3c4\ucc29\ud569\ub2c8\ub2e4.\\n5. NAT \uc778\uc2a4\ud134\uc2a4\ub294 \ub0b4\ubd80\uc5d0\uc11c \ucd9c\ubc1c\ud55c \ud2b8\ub798\ud53d\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0, \ud2b8\ub798\ud53d\uc744 \uac70\ubd80\ud569\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc774 \uc77c\uc5b4\ub098\uae30\uc5d0, public ip \ub97c \ud1b5\ud574\uc11c \uc811\uc18d\uc744 \ud558\uba74 \uc548 \ub429\ub2c8\ub2e4.\\n\\nprivate ip\ub97c \ud1b5\ud574\uc11c \uc811\uadfc\ud558\uba74 \uc5b4\ub5bb\uac8c \ub418\ub294\uc9c0 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4\\n\\n1. public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 private ip \ub97c \ud1b5\ud574\uc11c private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc744 \uc2dc\ub3c4\ud569\ub2c8\ub2e4.\\n2. \ub77c\uc6b0\ud305 \ud14c\uc774\ube14\uc5d0\uc11c private ip \uc77c \uacbd\uc6b0\uc5d0 \uc5b4\ub5bb\uac8c \ucc98\ub9ac\ud560\uc9c0\uc5d0 \ub300\ud55c \uc815\ubcf4\ub97c \ucc3e\uc2b5\ub2c8\ub2e4.\\n3. \ub77c\uc6b0\ud130\ub97c \uac70\uccd0\uc11c private \uc11c\ube0c\ub137\uc758 \ub77c\uc6b0\ud130\ub85c \uc774\ub3d9\ud569\ub2c8\ub2e4.\\n4. private \uc11c\ube0c\ub137\uc758 \ub77c\uc6b0\ud130\ub294 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0\uac8c \ud2b8\ub798\ud53d\uc744 \uc804\ub2ec\ud569\ub2c8\ub2e4.\\n5. private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub294 \ud2b8\ub798\ud53d\uc744 \ubc1b\uc544\uc11c \ucc98\ub9ac\ud569\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c 2\ubc88 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \uc694\uc57d\\n\\n1. private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 \uc778\ud130\ub137\uc5d0 \uc811\uc18d\uc744 \ud558\ub824\uba74 NAT \uc778\uc2a4\ud134\uc2a4 \ud639\uc740 NAT \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \ud544\uc694\ud569\ub2c8\ub2e4.\\n2. private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub3c4 public ip \uac00 \ud544\uc694\ud569\ub2c8\ub2e4.\\n3. public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc744 \ud558\ub824\uba74 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc758 \ubcf4\uc548 \uadf8\ub8f9\uc744 \ucd94\uac00\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n4. public \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uac00 private \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uc18d\uc744 \ud560 \ub54c, private ip \ub97c \ud1b5\ud574\uc11c \uc811\uc18d\uc744 \ud574\uc57c \ud569\ub2c8\ub2e4."},{"id":"17","metadata":{"permalink":"/17","source":"@site/blog/2023-07-22-ci-cd.mdx","title":"\uce74\ud398\uc778 \ud300\uc758 CI/CD","description":"\uc548\ub155\ud558\uc138\uc694. \uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.","date":"2023-07-22T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 22\uc77c","tags":[{"label":"CI","permalink":"/tags/ci"},{"label":"CD","permalink":"/tags/cd"}],"readingTime":7.735,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"17","title":"\uce74\ud398\uc778 \ud300\uc758 CI/CD","authors":["jay"],"tags":["CI","CD"]},"prevItem":{"title":"private \uc11c\ube0c\ub137\uc5d0 \uc778\uc2a4\ud134\uc2a4\ub97c \uc678\ubd80\uc640 \uc5f0\uacb0\ud560 \ub54c, public ip? private ip?","permalink":"/18"},"nextItem":{"title":"JPA\uc5d0\uc11c ID\uac00 \uc788\ub294 Entity\uc5d0 \ub300\ud574 save \uc2dc\uc5d0 select \ucffc\ub9ac\uac00 \ub098\uac00\ub294 \uc774\uc720","permalink":"/16"}},"content":"\uc548\ub155\ud558\uc138\uc694. \uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\uc800\ud76c \ud300\uc5d0\uc11c CI/CD\ub294 \uc5b4\ub5bb\uac8c \uc9c4\ud589\ub418\ub294\uc9c0 \uc791\uc131\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## CI (\uc9c0\uc18d\uc801 \ud1b5\ud569)\\n![ci](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/ci.png?raw=true)\\n\\n\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc9c0\uc18d\uc801 \ud1b5\ud569 \uc989 CI\ub97c \uc9c4\ud589\ud558\uae30 \uc704\ud574\uc11c \uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 Github Actions\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\\n\\nmain, develop \ube0c\ub79c\uce58\uc5d0 Push, Pull Request \uc694\uccad\uc774 \ub4e4\uc5b4\uac04\ub2e4\uba74 \uc774\ubca4\ud2b8\uac00 \ubc1c\uc0dd\ud558\uace0, Github Actions\ub97c \ud1b5\ud574 \uc800\ud76c\uac00 \uc791\uc131\ud574\ub454 \uc2a4\ud06c\ub9bd\ud2b8\uac00 \uc2e4\ud589 \ub429\ub2c8\ub2e4.\\n\\n\uc774 \uc2a4\ud06c\ub9bd\ud2b8\uc5d0 \uc5ec\ub7ec\uac00\uc9c0\ub97c \ub4f1\ub85d\ud560 \uc21c \uc788\uc9c0\ub9cc, \uc800\ud76c\ub294 \uc790\ub3d9\uc73c\ub85c \ud14c\uc2a4\ud2b8\ub97c \uc9c4\ud589\ud558\ub3c4\ub85d \ud558\uc600\uc2b5\ub2c8\ub2e4.\\n\uc790\ub3d9\uc73c\ub85c \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub9ac\uba74\uc11c \ud14c\uc2a4\ud2b8\uac00 \ud1b5\uacfc\ub97c \ud574\uc57c\uc9c0\ub9cc Merge\ub97c \uc9c4\ud589\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \ud1b5\ud574 \uac1c\ubc1c\uc790\uc758 \uc2e4\uc218\ub97c \uc904\uc77c \uc218 \uc788\uace0 \uc548\uc815\uc801\uc73c\ub85c \uc9c0\uc18d\uc801 \ud1b5\ud569\uc744 \uc774\ub8f0 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\\n\\n
    \\n\\n## CD (\uc9c0\uc18d\uc801 \ubc30\ud3ec)\\n![cd](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/cd.png?raw=true)\\n\\n\uc800\ud76c\uc758 \uc9c0\uc18d\uc801 \ubc30\ud3ec \uc544\ud0a4\ud14d\ucc98\uc785\ub2c8\ub2e4.\\n\\n\uc21c\uc11c\ub97c \uc694\uc57d\ud558\uc790\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n1. Release \ube0c\ub79c\uce58\uc5d0 Push\ub97c \ud55c\ub2e4.\\n2. Github Actions\ub97c \ud1b5\ud574 Docker Hub\uc5d0 \ub808\ud3ec\uc9c0\ud1a0\ub9ac\uc758 \uc18c\uc2a4\ucf54\ub4dc\ub97c Docker Image\ub85c \ube4c\ub4dc\ud574\uc11c Push \ud55c\ub2e4.\\n3. \uc778\ud504\ub77c \uc11c\ubc84\uc5d0\uc11c Self Hosted Runner\uac00 \uc791\ub3d9\ud55c\ub2e4.\\n4. \uc778\ud504\ub77c \uc11c\ubc84\uc5d0\uc11c \ubc30\ud3ec \uc11c\ubc84\ub85c \ub4e4\uc5b4\uac04\ub2e4.\\n5. \ubc30\ud3ec \uc11c\ubc84 \uc548\uc5d0\uc11c Docker Hub\uc5d0 \ubbf8\ub9ac \uc5c5\ub85c\ub4dc\ud55c Docker Image\ub97c Pull \ud574\uc628\ub2e4.\\n6. \ubc30\ud3ec \uc11c\ubc84 \uc548\uc5d0\uc11c Docker Image\ub97c \ucee8\ud14c\uc774\ub108\uc5d0 \ub744\uc6b4\ub2e4.\\n\\n\\n
    \\n\\n### \ubc30\ud3ec \uc790\ub3d9\ud654 \ud234 \uc120\ud0dd\ud558\uae30\\n\uba3c\uc800 \ubc30\ud3ec \uc790\ub3d9\ud654 \uacfc\uc815\uc744 \uad6c\ucd95\ud558\uae30 \uc704\ud574\uc11c \uc5ec\ub7ec\uac00\uc9c0 \ud234\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\nTravis, Jenkins, Github Actions \ub4f1\ub4f1 \uc5ec\ub7ec\uac00\uc9c0\uac00 \uc788\ub294\ub370\uc694.\\n\uc800\ud76c \ud300\uc740 `Github Actions`\ub97c \uc120\ud0dd\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \uc120\ud0dd\ud55c \uc5ec\ub7ec\uac00\uc9c0 \uc774\uc720\uac00 \uc788\uc5c8\uc9c0\ub9cc\\n\uc800\ud76c \ud300 \ub204\ub204\ub97c \uc81c\uc678\ud558\uace0 CI/CD \uacbd\ud5d8\uc774 \ubd80\uc871\ud574\uc11c \ube44\uad50\uc801 \uc27d\uace0 \uc124\uce58 \ubc0f \ud070 \uc138\ud305\uc774 \uc5c6\ub294 \uc810\uc774 \uc800\ud76c\ud55c\ud14c\ub294 \ub9e4\ub825\uc801\uc73c\ub85c \ub2e4\uac00\uc654\uc2b5\ub2c8\ub2e4.\\n\\n\ub610\ud55c Docker\ub97c \uc0ac\uc6a9\ud558\ub294\ub370, \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n1. JDK \ud639\uc740 Node \ubc84\uc804\uc744 \uad00\ub9ac\ud560 \uc218 \uc788\ub2e4.\\n2. Docker Image\ub97c \ube4c\ub4dc\ud55c \ud6c4 \ubc30\ud3ec\ud558\uae30 \ub54c\ubb38\uc5d0 \uc11c\ubc84 \ud658\uacbd \ucc28\uc774\ub85c \ubc1c\uc0dd\ud558\ub294 \ubb38\uc81c\ub97c \ucd5c\uc18c\ud654\ud560 \uc218 \uc788\ub2e4.\\n3. \ubc30\ud3ec \uc11c\ubc84\uc5d0\uc11c Docker\ub9cc \uc124\uce58\ud558\uace0 Image\ub97c \ubc1b\uace0 \uc2e4\ud589\uc2dc\ud0a4\uba74 \ub3fc\uc11c \ube60\ub974\uace0 \uc27d\uac8c \ubc30\ud3ec \ud658\uacbd\uc744 \uad6c\ucd95\ud560 \uc218 \uc788\ub2e4.\\n\\n
    \\n\\n### \uacfc\uc815\\n\ubcf8\uaca9\uc801\uc73c\ub85c \uc800\ud76c\uc758 \ubc30\ud3ec \uc790\ub3d9\ud654\ub97c \uad6c\ucd95\ud558\ub294 \uacfc\uc815\uc744 \uc124\uba85\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n
    \\n\\n1. Github Actions\uc5d0 Runners \ub4f1\ub85d\\n\\n![runner](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/selfHosted.png?raw=true)\\n\uba3c\uc800 Self Hosted Runner\ub97c \uc774\uc6a9\ud558\uae30 \ub54c\ubb38\uc5d0 \uc800\ud76c\ub294 \uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 Runners\ub97c \ub4f1\ub85d\uc744 \ud574\uc92c\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub97c \ub4f1\ub85d\uc744 \ud560 \ub54c \uc81c\uacf5\ud574\uc8fc\ub294 \uc124\uc815 \ucf54\ub4dc\uac00 \ub098\uc624\ub294\ub370\uc694.\\n\uc774 \ucf54\ub4dc\ub4e4\uc744 infra \uc11c\ubc84\uc5d0 \ubaa8\ub450 \uc785\ub825\uc744 \ud574\uc8fc\uba74\uc11c \uc124\uc815\uc744 \ud574\uc8fc\uc2dc\uba74 \ub429\ub2c8\ub2e4.\\n\\n
    \\n\\n2. Github workflow \ub9cc\ub4e4\uae30\\n\ub2e4\uc74c\uc73c\ub85c\ub294 \uc800\ud76c\uac00 \uc218\ud589\ud558\uace0\uc790 \ud558\ub294 Task\ub97c \ub4f1\ub85d\ud574\uc8fc\uae30 \uc704\ud574\uc11c yml \ud30c\uc77c\uc744 \ub9cc\ub4e4\uc5b4\uc90d\ub2c8\ub2e4.\\n\\nyml \ud30c\uc77c\uc758 \uacbd\ub85c\ub294 `./github/workflows/` \uc548\uc5d0 \ub9cc\ub4e4\uc5b4\uc8fc\uba74 \ub429\ub2c8\ub2e4.\\n```yaml\\nname: deploy\\n\\n# release/backend push \ud560 \ub54c\\non:\\n push:\\n branches:\\n - release/backend\\n\\njobs:\\n # Docker\\n docker-build:\\n runs-on: ubuntu-latest\\n defaults:\\n run:\\n working-directory: ./backend\\n steps:\\n\\t\\t# Docker Hub \ub85c\uadf8\uc778\\n - name: Log in to Docker Hub\\n uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a\\n with:\\n username: ${{ secrets.DOCKERHUB_USERNAME }}\\n password: ${{ secrets.DOCKERHUB_PASSWORD }}\\n - uses: actions/checkout@v3\\n\\n - name: Set up JDK 17\\n uses: actions/setup-java@v3\\n with:\\n java-version: \'17\'\\n distribution: \'adopt\'\\n\\n - name: Gradle Caching\\n uses: actions/cache@v3\\n with:\\n path: |\\n ~/.gradle/caches\\n ~/.gradle/wrapper\\n key: ${{ runner.os }}-gradle-${{ hashFiles(\'**/*.gradle*\', \'**/gradle-wrapper.properties\') }}\\n restore-keys: |\\n ${{ runner.os }}-gradle-\\n\\n - name: Grant execute permission for gradlew\\n run: chmod +x gradlew\\n\\n - name: Build with Gradle\\n run: ./gradlew bootjar\\n\\n - name: Extract metadata (tags, labels) for Docker\\n id: meta\\n uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7\\n with:\\n images: Docker Hub \uc0ac\uc6a9\uc790\uba85/\uc774\ubbf8\uc9c0 \uc774\ub984\\n\\n\\t # Build \ubc0f Docker image\ub97c Docker Hub\uc5d0 push\\n - name: Build and push Docker image\\n uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671\\n with:\\n context: .\\n file: ./backend/Dockerfile\\n push: true\\n platforms: linux/arm64\\n tags: woowacarffeine/backend:latest\\n labels: ${{ steps.meta.outputs.labels }}\\n\\n deploy:\\n runs-on: self-hosted\\n if: ${{ needs.docker-build.result == \'success\' }}\\n needs: [ docker-build ]\\n steps:\\n\\t\\t# EC2 \ubc30\ud3ec \uc11c\ubc84\ub85c \uc811\uc18d\\n - name: Join EC2 dev server\\n uses: appleboy/ssh-action@master\\n env:\\n JASYPT_KEY: ${{ secrets.JASYPT_KEY }}\\n with:\\n host: ${{ secrets.SERVER_HOST }}\\n username: ${{ secrets.SERVER_USERNAME }}\\n key: ${{ secrets.SERVER_KEY }}\\n port: ${{ secrets.SERVER_PORT }}\\n envs: JASYPT_KEY\\n\\n # 1. \ub3c4\ucee4 \uc774\ubbf8\uc9c0 \ubc1b\uae30\\n # 2. \uae30\uc874\uc5d0 \ucf1c\uc9c4 \ubc31\uc5d4\ub4dc \uc11c\ubc84(\ub3c4\ucee4 \uc774\ubbf8\uc9c0) stop\\n # 3. \ucd5c\uc2e0 \ubc31\uc5d4\ub4dc \uc11c\ubc84 run\\n # 4. \uc0ac\uc6a9\ud558\uc9c0 \uc54a\ub294 \uc774\ubbf8\uc9c0\uc640 \ucee8\ud14c\uc774\ub108 \uc0ad\uc81c\\n script: |\\n sudo docker pull woowacarffeine/backend:latest\\n sudo docker stop backend || true\\n sudo docker run -d --rm -p 8080:8080 \\\\\\n -e \\"ENCRYPT_KEY=${{secrets.JASYPT_KEY}}\\" \\\\\\n --name backend \\\\\\n Docker Hub \uc0ac\uc6a9\uc790\uba85/\uc774\ubbf8\uc9c0 \uc774\ub984:latest\\n\\n sudo docker image prune -f\\n```\\n\\n\uc800\ud76c \ud300\uc740 \uc704\uc640 \uac19\uc774 backend-deploy.yml \ud30c\uc77c\uc744 \ub9cc\ub4e4\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc704\uc5d0 yml\uc5d0\uc11c \uc800\ud76c\ub294 \ud0a4\ub97c \uc228\uacbc\ub294\ub370\uc694.\\n\\n![img](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/selfHostedKeys.png?raw=true)\\n\uc704\uc5d0 \uc0ac\uc9c4\uacfc \uac19\uc774 \uc124\uc815\uc744 \ud574\uc8fc\uc2dc\uba74 \ub429\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc774\ub97c yml\uc5d0\uc11c \uc0ac\uc6a9\ud558\uae30 \uc704\ud574\uc120 `secrets.Key\uc774\ub984`\uc73c\ub85c \uc0ac\uc6a9\ud574\uc8fc\uc2dc\uba74 \ub429\ub2c8\ub2e4.\\n\\n
    \\n\\n\uc774\uc81c \ub9c8\uc9c0\ub9c9\uc73c\ub85c `Dockerfile`\uc744 \ub9cc\ub4e4\uc5b4\uc90d\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 `/backend/` \uacbd\ub85c\uc5d0 \ub9cc\ub4e4\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n```Dockerfile\\nFROM amazoncorretto:17-alpine-jdk\\nARG JAR_FILE=./backend/build/libs/carffeine-0.0.1-SNAPSHOT.jar\\nCOPY ${JAR_FILE} app.jar\\nENTRYPOINT [\\"java\\", \\"-Dspring.profiles.active=dev\\", \\"-jar\\",\\"/app.jar\\"]\\n```\\n\\n\uc800\ud76c\ub294 \uc704\ucc98\ub7fc \uc808\ub300 \uacbd\ub85c\ub97c \uae30\uc900\uc73c\ub85c JAR_FILE \uc704\uce58\ub97c \uc9c0\uc815\ud558\uace0, profiles\ub294 dev\ub85c \uc124\uc815\ud574\uc11c \ub9cc\ub4e4\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n
    \\n\\n3. \ubc30\ud3ec\ud558\uae30\\n\\n\ud2b8\ub9ac\uac70\ub97c \uc791\ub3d9\uc2dc\ucf1c\uc11c \uc800\ud76c\uac00 yml \ud30c\uc77c\uc5d0\uc11c \uc9c0\uc815\ud574\uc900 \uac83\ub4e4\uc774 \uc798 \uc791\ub3d9\ud558\ub294\uc9c0 \ud655\uc778\ud569\ub2c8\ub2e4.\\n\\n![jobSuccess](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/jobsSuccess.png?raw=true)\\n\uc704\uc5d0 \uc0ac\uc9c4\ucc98\ub7fc \ubaa8\ub4e0 Job\uc774 \uc131\uacf5\uc801\uc73c\ub85c \ud1b5\uacfc\ud558\ub294 \uac83\uc744 \ubcf4\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![dockerPs](https://github.com/car-ffeine/car-ffeine.github.io/blob/main/ci-cd/success.png?raw=true)\\n\uc774\ub807\uac8c \uc778\ud504\ub77c \uc11c\ubc84\uc5d0\uc11c \ubc30\ud3ec \uc11c\ubc84\ub85c \ub4e4\uc5b4\uac00\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uc11c\ubc84\ub97c \ub3c4\ucee4\ub85c \ub744\uc6b4 \uac83\uc744 \ubcf4\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\nEC2 \ubc30\ud3ec \uc11c\ubc84\uc5d0\uc11c `docker ps`\ub97c \uc785\ub825\ud588\uc744 \ub54c\uc5d0\ub3c4 \uc798 \uc2e4\ud589\uc774 \ub418\ub124\uc694!\\n\\n
    \\n\\n### CD \ubc30\ud3ec \uacfc\uc815 \uc694\uc57d\\n\uc9c0\uc18d\uc801 \ubc30\ud3ec \uacfc\uc815\uc744 \uc694\uc57d \ud558\uc790\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n1. Self Hosted Runner\ub97c EC2 \uc778\ud504\ub77c \uc11c\ubc84\uc5d0 \ub4f1\ub85d\ud574\uc900\ub2e4.\\n2. yml \ud30c\uc77c\uacfc Dockerfile\uc744 \ub9cc\ub4e4\uc5b4\uc900\ub2e4.\\n3. \ud2b8\ub9ac\uac70\ub97c \uc791\ub3d9\uc2dc\ucf1c\uc11c Github Actions\uc758 \ud0dc\uc2a4\ud06c\uac00 \ubaa8\ub450 \uc798 \ub418\ub294\uc9c0 \ud655\uc778\ud55c\ub2e4.\\n4. \uc798 \ub410\ub2e4\uba74 EC2 \ubc30\ud3ec \uc11c\ubc84\uc5d0 Docker image\uac00 \uc131\uacf5\uc801\uc73c\ub85c \ub744\uc6cc\uc9c4\ub2e4."},{"id":"16","metadata":{"permalink":"/16","source":"@site/blog/2023-07-15-jpa-create-select-query-when-id-is-not-null-.mdx","title":"JPA\uc5d0\uc11c ID\uac00 \uc788\ub294 Entity\uc5d0 \ub300\ud574 save \uc2dc\uc5d0 select \ucffc\ub9ac\uac00 \ub098\uac00\ub294 \uc774\uc720","description":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130 \uc785\ub2c8\ub2e4.","date":"2023-07-15T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 15\uc77c","tags":[{"label":"jpa","permalink":"/tags/jpa"}],"readingTime":9.97,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"16","title":"JPA\uc5d0\uc11c ID\uac00 \uc788\ub294 Entity\uc5d0 \ub300\ud574 save \uc2dc\uc5d0 select \ucffc\ub9ac\uac00 \ub098\uac00\ub294 \uc774\uc720","authors":["boxster"],"tags":["jpa"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc758 CI/CD","permalink":"/17"},"nextItem":{"title":"\uc8fc\uae30\uc801\uc778 \ub370\uc774\ud130 \uc694\uccad\uc73c\ub85c \ubc1b\uc740 \ub370\uc774\ud130\ub97c \ud6a8\uc728\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ubc0f \uc0bd\uc785\ud558\uae30 (with. \ubc15\uc2a4\ud130)","permalink":"/15"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130 \uc785\ub2c8\ub2e4.\\n\\n\uba3c\uc800 \uc774\ubc88\uc5d0 \uae00\uc744 \uc4f0\uac8c\ub41c \uacc4\uae30\ub97c \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4. \uc800\ud76c \ud300\uc740 \uacf5\uacf5 \ub370\uc774\ud130 API\uc5d0\uc11c \ubc1b\uc544\uc628 \ucda9\uc804\uc18c\uc640, \ucda9\uc804\uae30\ub4e4\uc758 ID\ub97c \uadf8\ub300\ub85c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\ubb3c\ub860 \ub2e4\ub978 API, \uc81c\uac00 \uc81c\uc5b4\ud560 \uc218 \uc5c6\ub294 \uacf3\uc5d0 \uc758\uc874\ud558\ub294 \uac83\uc740 \uc88b\uc9c0 \uc54a\ub2e4\uace0 \uc0dd\uac01\ud569\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \ub370\uc774\ud130\ub97c \ubc1b\uc544\uc624\ub294 \uacfc\uc815\uc5d0\uc11c \ub9c8\uc8fc\ud55c \uc131\ub2a5\uc801\uc778 \ubb38\uc81c \ub54c\ubb38\uc5d0 \uadf8\ub300\ub85c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uc804\uad6d\uc758 \ucda9\uc804\uc18c\ub294 6\ub9cc\uac1c, \ucda9\uc804\uc18c \uc548\uc5d0 \uc874\uc7ac\ud558\ub294 \ucda9\uc804\uae30\ub294 23\ub9cc\uae30\uc785\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uacf5\uacf5 \ub370\uc774\ud130\ub294 \ucda9\uc804\uc18c\uc640, \ucda9\uc804\uae30\uc758 \uc815\ubcf4\ub97c \ub530\ub85c \uc81c\uacf5\ud558\ub294 \uac83\uc774 \uc544\ub2cc \uc911\ubcf5\ub41c \ucda9\uc804\uc18c\ub97c \ud3ec\ud568\ud55c \ub370\uc774\ud130\ub97c \ucda9\uc804\uae30 \uac1c\uc218\ub9cc\ud07c\uc778 23\ub9cc\uac1c\uc758 row\ub85c \uc81c\uacf5\ud569\ub2c8\ub2e4.\\n\\n\\n\ub530\ub77c\uc11c \uc800\ud76c\uac00 ID\ub97c \ub530\ub85c \ubd80\uc5ec\ud558\uac8c \ub41c\ub2e4\uba74, \ucda9\uc804\uc18c\ub97c \uc800\uc7a5\ud558\ub294 \uacfc\uc815\uc5d0\uc11c \ubc1b\uc544\uc624\ub294 ID\ub85c \ucda9\uc804\uae30\ub97c \uc5f0\uacb0\ud574\uc918\uc57c\ud558\ub294\ub370 \uadf8\ub807\uac8c \ub41c\ub2e4\uba74 \uc140 \uc218 \uc5c6\uc774 \ub9ce\uc740 \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\\n\uc7a0\uae50 \uc0dd\uac01\ud574\ubcf8\ub2e4\uba74\\n1. \ucda9\uc804\uc18c\ub97c \uac01\uac01 \uc800\uc7a5\ud558\uace0 ID\ub97c \ubd80\uc5ec\ubc1b\ub294 \ucffc\ub9ac `6\ub9cc\ubc88` (ID\ub97c \uc54c\uc544\uc640\uc57c\ud558\uae30 \ub54c\ubb38\uc5d0 batch\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.)\\n2. \ucda9\uc804\uc18c\uc5d0\uc11c \ubc1b\uc544\uc628 ID\ub97c \ucda9\uc804\uae30\uc5d0 \ub9e4\ud551\ud558\uace0 \uc800\uc7a5\ud558\ub294 \ucffc\ub9ac `\ucd5c\uc18c 1\ubc88` (\ub9cc\uc57d batch\ub85c 23\ub9cc\uac74\uc744 \ud55c\ubc88\uc5d0 \uc800\uc7a5\ud55c\ub2e4\ub294 \uac00\uc815)\\n\\n\ud558\uc9c0\ub9cc ID\ub97c \uadf8\ub300\ub85c \uc0ac\uc6a9\ud558\uac8c \ub41c\ub2e4\uba74,\\n1. \ucda9\uc804\uc18c\ub97c \uc800\uc7a5\ud558\ub294 \ucffc\ub9ac `\ucd5c\uc18c 1\ubc88` (\ub9cc\uc57d batch\ub85c 6\ub9cc\uac74\uc744 \ud55c\ubc88\uc5d0 \uc800\uc7a5\ud55c\ub2e4\ub294 \uac00\uc815)\\n2. \ucda9\uc804\uae30\ub97c \uc800\uc7a5\ud558\ub294 \ucffc\ub9ac `\ucd5c\uc18c 1\ubc88` (\ub9cc\uc57d batch\ub85c 23\ub9cc\uac74\uc744 \ud55c\ubc88\uc5d0 \uc800\uc7a5\ud55c\ub2e4\ub294 \uac00\uc815)\\n\\n23\ub9cc\uac74\uc774 \ub118\ub294 \uc815\ubcf4\ub97c \ud655\uc778\ud588\uc744 \ub54c, ID\ub294 \uc911\ubcf5\ub418\uc9c0 \uc54a\uc558\uace0, \uc911\ubcf5\ud558\uc9c0 \uc54a\uc744 \uac83\uc774\ub77c \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4. \uadf8 \ubfd0\ub9cc \uc544\ub2c8\ub77c \ucc98\uc74c \ud55c\ubc88\ub9cc \uc800\uc7a5\ud558\ub294 \uac83\uc774 \uc544\ub2cc \uc8fc\uae30\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\ub41c \uc815\ubcf4\ub97c\\n\ubc18\uc601\ud574\uc8fc\uace0 `update` or `save` \ud574\uc8fc\uc5b4\uc57c\ud558\uae30 \ub54c\ubb38\uc5d0, ID\ub97c \uadf8\ub300\ub85c \uac00\uc9c0\uace0 \uc788\ub294 \uac83\uc774 \ud6e8\uc52c \ud6a8\uc728\uc801\uc774\ub77c \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc871\uc774 \uae38\uc5c8\uc2b5\ub2c8\ub2e4. \uac01\uc124\ud558\uace0 \uc774\ub7f0 \ubc29\uc2dd\uc73c\ub85c ID\ub97c \uc9c1\uc811 \ub123\uc5b4\uc8fc\ub294 \uacbd\uc6b0 \ubc1c\uc0dd\ud558\ub294 \ubb38\uc81c\uc5d0 \ub300\ud574 \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## ID\ub97c \uc9c1\uc811 \ub123\uc5b4\uc900 Entity\ub97c \uc800\uc7a5\ud560 \ub54c\\n\\n\uba3c\uc800 \uac04\ub2e8\ud55c \uc608\uc81c Entity\ub85c \uc124\uba85\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Entity\\npublic class ChargeStation {\\n\\n @Id\\n private String stationId;\\n\\n private String stationName;\\n\\n ...\\n}\\n\\n```\\n\ubcf4\ud1b5\uc758 Entity\uc640 \ub2e4\ub978 \ubd80\ubd84\uc740 Id\ub97c \uc9c1\uc811 \ud560\ub2f9\ud558\uae30 \ub54c\ubb38\uc5d0 `@GeneratedValue(strategy = GenerationType.IDENTITY)` \uc774\ub7ec\ud55c ID \uc0dd\uc131 \uc804\ub7b5\uc5d0 \ub300\ud55c \uc815\ubcf4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 `save()` \ucf54\ub4dc\ub97c \ud638\ucd9c\ud558\uba74 \uc5b4\ub5a4 \ucffc\ub9ac\uac00 \ub098\uac00\ub294\uc9c0 \ud655\uc778\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc640 \uac19\uc774 \uc544\uc8fc \uac04\ub2e8\ud55c \uc120\ub989\uc5ed \ucda9\uc804\uc18c\ub97c \uc800\uc7a5\ud558\ub294 \ud14c\uc2a4\ud2b8\ub97c \uc2e4\ud589\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n```java\\n@DataJpaTest\\nclass ChargeStationRepositoryTest {\\n\\n @Autowired\\n private ChargeStationRepository chargeStationRepository;\\n\\n @Test\\n void \ucda9\uc804\uc18c\ub97c_\uc800\uc7a5\ud55c\ub2e4() {\\n ChargeStation station = ChargeStationFixture.\uc120\ub989\uc5ed_\ucda9\uc804\uc18c_\ucda9\uc804\uae30_2\uac1c_\uc0ac\uc6a9\uac00\ub2a5_1\uac1c;\\n\\n chargeStationRepository.save(station);\\n\\n ChargeStation expect = chargeStationRepository.findByStationId(station.getStationId()).get();\\n assertThat(expect).isEqualTo(station);\\n }\\n}\\n```\\n\\n\uba3c\uc800 \ucf54\ub4dc\ub9cc \ubcf4\uba74 \uba3c\uc800 `chargeStationRepository.save()` \ud638\ucd9c\uacfc \ud568\uaed8 insert \ucffc\ub9ac 1\ubc88, \uadf8\ub9ac\uace0 `chargeStationRepository.findByStationId()`\uc5d0\uc11c select \ucffc\ub9ac 1\ubc88\\n\ucd1d 2\ubc88 \ubc1c\uc0dd\ud560 \uac83\uc774\ub77c\uace0 \uc720\ucd94\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![query-three-times](https://github.com/car-ffeine/design-system/assets/106640954/f48b7f0f-3f39-41ce-8fcd-94b995e95fae)\\n\\n\ud558\uc9c0\ub9cc \uc608\uc0c1\uacfc \ub2e4\ub974\uac8c \uc704\uc758 \uc0ac\uc9c4\uacfc \uac19\uc774 \ucffc\ub9ac\uac00 \ucd1d 3\ubc88 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uccab\ubc88\uc9f8\ub294 \ud638\ucd9c\ud558\uc9c0 \uc54a\uc740 station id\ub85c station\uc744 \uc870\ud68c\ud558\ub294 \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc720\ub97c \ucc3e\uae30 \uc704\ud574 `save()` \uba54\uc11c\ub4dc\ub97c \ub514\ubc84\uae45 \ud574\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n### save \uc2dc SELECT \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud558\ub294 \uc774\uc720\\n\\n\\n![save-method](https://github.com/car-ffeine/design-system/assets/106640954/b1db00b7-d7fb-4647-912c-6f8e2fe44974)\\n\ub85c\uc9c1\uc740 \uac04\ub2e8\ud574\ubcf4\uc785\ub2c8\ub2e4. `isNew()` \ub97c \ud1b5\ud574 \uc0c8\ub85c\uc6b4 Entity\uc778\uc9c0 \ud655\uc778\ud55c \ud6c4, \uc0c8\ub85c\uc6b4 Entity\ub77c\uba74 `persist()`, \uc544\ub2c8\ub77c\uba74 `merge()`\ub97c \ud638\ucd9c\ud569\ub2c8\ub2e4.\\n\\n\uc5ec\uae30\uc11c `EntityManager#persist()` \uba54\uc11c\ub4dc\ub97c \uac04\ub2e8\ud788 \ub9d0\uc500\ub4dc\ub9ac\uba74, \uc0c8\ub85c\uc6b4 Entity\ub97c \uc601\uc18d\ud654\ud558\ub294 \uba54\uc11c\ub4dc\ub85c \ud2b8\ub79c\uc7ad\uc158\uc774 \ucee4\ubc0b\ub420 \ub54c \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \uc800\uc7a5\ud569\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 `EntityManager#merge()` \uba54\uc11c\ub4dc\ub294 \uc900\uc601\uc18d \uc0c1\ud0dc\uc758 Entity\ub97c \uc601\uc18d \uc0c1\ud0dc\ub85c \ubcc0\uacbd\ud558\ub294\ub370 \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\\n\ud558\uc9c0\ub9cc \uc774\ub54c \uc601\uc18d\uc131 \ucee8\ud14d\uc2a4\ud2b8\uc5d0 \uc874\uc7ac\ud558\uc9c0 \uc54a\ub294 \uac1d\uccb4\ub77c\uba74 \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0\uc11c \uc870\ud68c \ud6c4 \uc601\uc18d\ud654\ud558\ub294 \uc791\uc5c5\uc744 \uc218\ud589\ud569\ub2c8\ub2e4.\\n\\n`merge()`\ub97c \ud638\ucd9c\ud558\uae30 \ub54c\ubb38\uc5d0 SELECT \ucffc\ub9ac\uac00 \ubc1c\uc0dd\ud558\uace0, \uc601\uc18d\ud654\ud558\ub294 \uc791\uc5c5\uc744 \uc218\ud589\ud558\ub294 \uac83 \uc785\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc81c\uac00 \uc800\uc7a5\ud55c \uac1d\uccb4\ub294 \ud655\uc2e4\ud788 \uc0c8\ub85c\uc6b4 Entity\uac00 \ub9de\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc `entityInformation.isNew()` \uba54\uc11c\ub4dc\ub294 false\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c \uc5b4\ub5a4 \uac83\uc744 \uae30\uc900\uc73c\ub85c \uc0c8\ub85c\uc6b4 Entity\uc778 \uac83\uc744 \uad6c\ubd84\ud558\ub294\uc9c0 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n### \uc0c8\ub85c\uc6b4 Entity\ub97c \uad6c\ubd84\ud558\ub294 \uae30\uc900\\n\\n\uc77c\ub2e8, \ub514\ubc84\uae45\uc744 \ud1b5\ud574 isNew \uba54\uc11c\ub4dc\ub97c \ud655\uc778\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n![is-new](https://github.com/car-ffeine/design-system/assets/106640954/e4a56694-c623-46d8-badd-3345d557e29f)\\n\\n\uac04\ub2e8\ud569\ub2c8\ub2e4. \uba3c\uc800 Entity\uc5d0 ID\ub97c \uac00\uc838\uc635\ub2c8\ub2e4. \uadf8\ub9ac\uace0 id\uac00 `primitive` \ud0c0\uc785\uc778\uc9c0 \ud655\uc778 \ud6c4, \uc544\ub2d0\uacbd\uc6b0 id\uac00 null \uc774\uba74 \uc0c8\ub85c\uc6b4 Entity, \uc544\ub2d0\uacbd\uc6b0 false\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub54c, `primitve` \ud0c0\uc785\uc774\ub77c\uba74, id\uac00 \uc22b\uc790\uc778\uc9c0 \ud655\uc778 \ud6c4 id\uac00 0\uc774\uba74 \uc0c8\ub85c\uc6b4 Entity, \uc544\ub2d0\uacbd\uc6b0 false\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4.\\n\\n## ID\ub97c \uc9c1\uc811 \ub123\uc5b4\uc8fc\ub294 \uac1d\uccb4\ub294 JPA \uc0ac\uc6a9\uc744 \ud3ec\uae30\ud574\uc57c\ud560\uae4c?\\n\\n\uacb0\ub860\ubd80\ud130 \ub9d0\uc500\ub4dc\ub9ac\uba74 \uc544\ub2d9\ub2c8\ub2e4. \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c \uc0c8\ub85c\uc6b4 Entity \uc784\uc744 \uc99d\uba85\ud560 \uc218 \uc788\ub2e4\uba74 `merge()`\uac00 \uc544\ub2cc `persist()`\ub97c \ud638\ucd9c\ud558\ub3c4\ub85d \ub9cc\ub4e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \uadf8\ub7fc \uc5b4\ub5bb\uac8c?\\n\uba3c\uc800 save() \uba54\uc11c\ub4dc\uc758 \ud544\ub4dc \uc911 `JpaEntityInformation`\uc774\ub77c\ub294 \ud544\ub4dc\ub97c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![entity-info](https://github.com/car-ffeine/design-system/assets/106640954/d9956fe6-07c7-41a9-9d6b-7c01b5f31c5d)\\n\\n\uc774 \uc778\ud130\ud398\uc774\uc2a4\ub294 Entity\uc758 \ucd94\uac00 \uc815\ubcf4\ub97c \uc54c\uae30 \uc704\ud574 \ud544\ub4dc\uc5d0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud574\ub2f9 \uc778\ud130\ud398\uc774\uc2a4\uc758 \uad6c\ud604\uccb4\ub294 `JpaEntityInformationSupport`, `JpaMetamodelEntityInformation`, `JpaPersistableEntityInformation` \uc774\ub807\uac8c 3\uac1c\uc758 \ud074\ub798\uc2a4\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c\ub3c4 `isNew()`\uac00 \uad6c\ud604\ub418\uc5b4 \uc788\uc744\uac70\ub77c \ucd94\uce21\uc744 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub514\ubc84\uae45\uc744 \ud1b5\ud574 \uc54c\uc544\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\uae4c \uc704\uc758 \uc0ac\uc9c4\uc73c\ub85c \ubcf4\uace0 \uc2e4\uc81c\ub85c \uc2e4\ud589\ub410\ub358 `isNew()` \uba54\uc11c\ub4dc\uc758 \uc8fc\uc778\uc740 `JpaMetamodelEntityInformation` \ud074\ub798\uc2a4\uc600\uc2b5\ub2c8\ub2e4. \uadf8\ub798\uc11c \ud574\ub2f9 \ud074\ub798\uc2a4\ub294 \uc81c\uc678\ud558\uace0 \ub2e4\ub978 \ud074\ub798\uc2a4\ub97c \ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uba3c\uc800 `JpaPersistableEntityInformation` \ud074\ub798\uc2a4\uc785\ub2c8\ub2e4.\\n![is-new-persistable](https://github.com/car-ffeine/design-system/assets/106640954/dc2293c3-2854-4619-9ef6-d08e55b4581b)\\n\uc544\uc8fc \uac04\ub2e8\ud558\uac8c entity\uc758 `isNew()`\ub97c \ud638\ucd9c\ud55c\ub2e4\uace0 \uc801\ud600\uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc `Persistable` \uc778\ud130\ud398\uc774\uc2a4\ub97c \uad6c\ud604\ud55c Entity\uc758 `isNew()` \ub97c \ud638\ucd9c\ud558\ub294 \uac83 \uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \ub0a8\uc740 \ud558\ub098\uc758 \ud074\ub798\uc2a4\ub97c \ud655\uc778\ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![info-support](https://github.com/car-ffeine/design-system/assets/106640954/f1d654c0-e741-4db7-8e7f-4e758c36133a)\\n\\n\uc704 \uc0ac\uc9c4\ucc98\ub7fc \uc774 \ud074\ub798\uc2a4\uac00 Entity \ub9c8\ub2e4 `Persistable` \uad6c\ud604 \uc720\ubb34\uc5d0 \ub530\ub77c \ub3d9\uc801\uc73c\ub85c \uad6c\ud604\uccb4\ub97c \ubcc0\uacbd\ud574\uc8fc\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \ub2f5\uc774 \ub098\uc628 \uac83 \uac19\uc2b5\ub2c8\ub2e4. ID\ub97c \uc9c1\uc811 \ud560\ub2f9\ud558\ub294 Entity\uc5d0 `Persistable`\uc744 \uad6c\ud604\ud574\uc8fc\uba74 \ub429\ub2c8\ub2e4.\\n\\n### Persistable \uad6c\ud604\ud558\uae30\\n```java\\n@Entity\\npublic class ChargeStation implements Pesistable{\\n\\n @Id\\n private String stationId;\\n\\n private String stationName;\\n\\n @CreatedDate\\n private LocalDateTime createdTime;\\n\\n ...\\n\\n @Override\\n public Object getId() {\\n return getStationId();\\n }\\n\\n @Override\\n public boolean isNew() {\\n return createdTime == null;\\n }\\n}\\n```\\n\\n\uac04\ub2e8\ud788 \ub9cc\ub4e4\uc5b4\ubd24\uc2b5\ub2c8\ub2e4. `@CreatedDate`\ub294 Entity\uac00 \ucc98\uc74c \uc601\uc18d\ud654\ub420 \ub54c \ub3d9\uc791\ud558\uae30 \ub54c\ubb38\uc5d0 \uc774 Entity\uc758 CreateTime \ud544\ub4dc\uac00 null \uc774\uba74 \uc0c8\ub85c\uc6b4 Entity\ub77c\uace0 \ud655\uc2e0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\uadf8\ub7fc \uc774\ub807\uac8c \uc778\ud130\ud398\uc774\uc2a4\ub97c \uad6c\ud604\ud558\uace0 \uc544\uae4c \uc2e4\ud589\ud588\ub358 \ud14c\uc2a4\ud2b8\ub97c \ub2e4\uc2dc \uc2e4\ud589\ud574\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![solved](https://github.com/car-ffeine/design-system/assets/106640954/ea5db719-9919-42f4-b431-00e14d6fea5e)\\n\\n\uae54\ub054\ud558\uac8c \uad6c\ud604\ub41c \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4. \uc6d0\ud558\ub358\ub300\ub85c \ucffc\ub9ac\uac00 2\ubc88 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\uc774\ub7f0 `Persistable`\uc744 `@MappedSuperClass`\ub97c \ud1b5\ud574 \ub354 \uae54\ub054\ud558\uac8c \uad6c\ud604\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud558\uc9c0\ub9cc \ub530\ub85c \uc124\uba85\ub4dc\ub9ac\uc9c0\ub294 \uc54a\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n#### \uacb0\ub860\\nJPA\ub294 \ub9ce\uc740 \ud3b8\uc758 \uae30\ub2a5\uc744 \uc81c\uacf5\ud574\uc8fc\ub294 \uac83 \uac19\uc544\ubcf4\uc785\ub2c8\ub2e4. \ucac4\uc9c0\ub9d9\uc2dc\ub2e4."},{"id":"15","metadata":{"permalink":"/15","source":"@site/blog/2023-07-14-data-update-process-with-boxter.mdx","title":"\uc8fc\uae30\uc801\uc778 \ub370\uc774\ud130 \uc694\uccad\uc73c\ub85c \ubc1b\uc740 \ub370\uc774\ud130\ub97c \ud6a8\uc728\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ubc0f \uc0bd\uc785\ud558\uae30 (with. \ubc15\uc2a4\ud130)","description":"\uc548\ub155\ud558\uc138\uc694~","date":"2023-07-14T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 14\uc77c","tags":[{"label":"\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4","permalink":"/tags/\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4"},{"label":"\uc11c\ubc84","permalink":"/tags/\uc11c\ubc84"}],"readingTime":9.215,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"},{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"15","title":"\uc8fc\uae30\uc801\uc778 \ub370\uc774\ud130 \uc694\uccad\uc73c\ub85c \ubc1b\uc740 \ub370\uc774\ud130\ub97c \ud6a8\uc728\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ubc0f \uc0bd\uc785\ud558\uae30 (with. \ubc15\uc2a4\ud130)","authors":["jay","boxster"],"tags":["\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4","\uc11c\ubc84"]},"prevItem":{"title":"JPA\uc5d0\uc11c ID\uac00 \uc788\ub294 Entity\uc5d0 \ub300\ud574 save \uc2dc\uc5d0 select \ucffc\ub9ac\uac00 \ub098\uac00\ub294 \uc774\uc720","permalink":"/16"},"nextItem":{"title":"\uce74\ud398\uc778\ud300 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98\ub97c \uc124\uba85\ud574\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4","permalink":"/14"}},"content":"\uc548\ub155\ud558\uc138\uc694~\\n\uc6b0\ud14c\ucf54 \uce74\ud398\uc778 \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\uc624\ub298\uc740 \uce74\ud398\uc778 \ud300\uc758 \ud504\ub85c\uc81d\ud2b8\ub97c \uc9c4\ud589\ud558\uba74\uc11c \'\ubc15\uc2a4\ud130\'\uc640 \ud568\uaed8 \uc5b4\ub5a4 \ubb38\uc81c\ub97c \uacaa\uace0 \ud574\uacb0\ud588\ub294\uc9c0 \uc801\uc5b4\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n* \ubc30\uc6b0\ub294 \ub2e8\uacc4\uc774\ub2e4 \ubcf4\ub2c8 \ud2c0\ub9b0 \ubd80\ubd84\uc774 \uc788\uc744 \uc218 \uc788\ub294\ub370, \ud53c\ub4dc\ubc31 \ubd80\ud0c1\ub4dc\ub9bd\ub2c8\ub2e4 :)\\n\\n\uba3c\uc800 \uae00\uc744 \uc4f0\uae30 \uc804\uc5d0 \ubb38\uc81c \uc0c1\ud669\uc5d0 \ub300\ud574 \uac04\ub2e8\ud558\uac8c \ub9d0\uc500\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\\n## \ubb38\uc81c \uc0c1\ud669\\n\\n\uce74\ud398\uc778 \ud300\uc5d0\uc11c\ub294 \uc804\uae30\ucc28 \ucda9\uc804\uc18c \uacf5\uacf5 API\ub97c \ud65c\uc6a9\ud558\uc5ec \ucda9\uc804\uc18c\uc758 \ud63c\uc7a1\ub3c4 \uc81c\uacf5 \ubc0f \uc5ec\ub7ec \uc11c\ube44\uc2a4\ub97c \uc81c\uacf5\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uc11c\ube44\uc2a4\ub97c \uc0ac\uc6a9\uc790\ub4e4\uc5d0\uac8c \uc81c\uacf5\ud558\uae30 \uc704\ud574\uc11c \ub2e4\uc74c\uacfc \uac19\uc740 \uc791\uc5c5\ub4e4\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.\\n\\n1. \uccab \uc2e4\ud589\uc2dc \uacf5\uacf5 API \ub370\uc774\ud130\ub97c \ubaa8\ub450 \ubd88\ub7ec\uc11c \ub370\uc774\ud130\ubca0\uc774\uc2a4\uc5d0 \uc0bd\uc785\ud569\ub2c8\ub2e4.\\n2. \ud63c\uc7a1\ub3c4\ub97c \uc81c\uacf5\ud558\uae30 \uc704\ud574\uc11c \uc8fc\uae30\uc801\uc778 \uc2dc\uac04 (\uc544\uc9c1 \uc815\ud558\uc9c4 \uc54a\uc558\uc9c0\ub9cc ex.12\uc2dc\uac04) \ub2e8\uc704\ub85c \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\uc758 \uc0c1\ud0dc\ub97c \uc5c5\ub370\uc774\ud2b8 \ud558\uae30 \uc704\ud574\uc11c \ub2e4\uc2dc \ub370\uc774\ud130\ub97c \uc694\uccad\uc744 \ud569\ub2c8\ub2e4.\\n3. \uc0c8\ub86d\uac8c \ucd94\uac00\ub41c \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub294 \ubaa8\ub450 Insert\ud574\uc8fc\uace0, \uae30\uc874\uc5d0 \uc788\ub358 \ucda9\uc804\uc18c \ud639\uc740 \ucda9\uc804\uae30\uac00 \uc5c5\ub370\uc774\ud2b8 \ub410\ub2e4\uba74 \ubcc0\uacbd\ub41c \ub370\uc774\ud130\ub85c \uc5c5\ub370\uc774\ud2b8 \ud574\uc90d\ub2c8\ub2e4.\\n\\n\\n\uc800\ub791 \ubc15\uc2a4\ud130\ub294 2~3\ubc88 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub294 \uc5ed\ud560\uc744 \ub9e1\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\ud14c\uc774\ube14\uc758 \uad00\uacc4\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n```\\ncharge_station <---1------N---\x3e charger\\n charger <---1------1---\x3e charger_status\\n```\\n\\n\uc800\ud76c\ub294 \uc774 \ubb38\uc81c\ub97c \uc5b4\ub5bb\uac8c \ud574\uacb0 \ud588\ub294\uc9c0 \ubcf4\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\\n## \ubb38\uc81c \ud574\uacb0 \uacfc\uc815\\n\\n\uc804\uc81c\uc870\uac74\\n\\n- \uccab \uc2e4\ud589 \ubaa8\ub4e0 \ud14c\uc774\ube14\uc740 \ucd08\uae30\ud654 \uc0c1\ud0dc\uc774\ub2e4.\\n- \ub370\uc774\ud130\ub294 9999\uac74\uc744 \uae30\uc900\uc73c\ub85c \ud55c\ub2e4.\\n- \uba54\uc11c\ub4dc \uccab \uc2dc\ud589\uc5d0\uc11c\ub294 \ubaa8\ub4e0 \ub370\uc774\ud130\uac00 \uc0c8\ub86d\uac8c insert \ub418\uace0\\n- \uadf8 \ub2e4\uc74c \uba54\uc11c\ub4dc \uc2dc\ud589\uc5d0\uc11c\ub294 \uc77c\ubd80 \ub370\uc774\ud130\ub294 \ucd94\uac00\ub418\uace0, \uc77c\ubd80\ub294 \uc5c5\ub370\uc774\ud2b8 \ub41c\ub2e4.\\n\\n\\n## Ver1. findAll() \uc870\ud68c \ud6c4 \uac01\uac01 save() \ud574\uc8fc\uae30 (\uc57d14\ucd08)\\n\\n\uc800\ud76c\uac00 \ucc98\uc74c\uc5d0 \uc0dd\uac01\ud55c \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\uc54c\uc544\uc11c \ubc14\ub010 \uac83\ub4e4\uc740 \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uace0, \uc0c8\ub85c\uc6b4 \uac74 \uc800\uc7a5\ud574\uc8fc\uae30 \ub54c\ubb38\uc5d0 \uac04\ub2e8\ud55c \ubc29\ubc95\uc73c\ub85c \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc2e4\uc81c\ub85c \ud574\ubcf8 \uacb0\uacfc, \uc0bd\uc785\uc758 \uacbd\uc6b0\ub294 SELECT \ucffc\ub9ac\ubb38 \uc2e4\ud589 \ud6c4 INSERT \ucffc\ub9ac\ubb38\uc744 \ubc1c\uc0dd \uc2dc\ucf30\uace0,\\n\uc5c5\ub370\uc774\ud2b8 \uc2dc\uc5d0\ub3c4 SELECT \ud6c4 UPDATE \ud639\uc740 INSERT\ub97c \ubc1c\uc0dd \uc2dc\ucf30\uc2b5\ub2c8\ub2e4. (\ubcc0\uacbd \uc0ac\ud56d \uc5c6\uc73c\uba74 SELECT\ub9cc)\\n\\n\uc774\ub294 \uc2dd\ubcc4\uc790\uc5d0 \ub530\ub978 JPA \uc791\ub3d9 \ubc29\uc2dd \ub54c\ubb38\uc778\ub370\uc694.\\n\uc774 \ubc29\ubc95\uc758 \uacb0\uacfc\ub294 \uc57d 14\ucd08\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\ub294 \uc774\ub807\uac8c \ubd88\ud544\uc694\ud55c SELECT \uc791\uc5c5\uc744 \ub9c9\uc544\ubcf4\uace0\uc790 \ub2e4\ub978 \ubc29\ubc95\uc744 \uad6c\uc0c1\ud574\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n\uae30\ubcf8\uc801\uc73c\ub85c Jdbc\ub97c \uc774\uc6a9\ud574\uc11c Batch Insert\uc640 Batch Update\ub97c \uc0ac\uc6a9\ud558\uae30\ub85c \ud588\uace0, \uc774 \uc791\uc5c5\uc744 \uc704\ud574\uc11c \ubcc0\uacbd \ud639\uc740 \uc0bd\uc785\ub420 \ub370\uc774\ud130\ub4e4\uc744 \uc9c1\uc811 \ucc3e\ub294 \uacfc\uc815\uc774 \uc911\uc694\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\\n## Ver2. \ubcc0\uacbd \uac10\uc9c0\ub97c \uc9c1\uc811 \ud574\uc8fc\uace0, \uc790\ub8cc\uad6c\uc870\ub85c \ubc30\uce58 \ub370\uc774\ud130 \ubaa8\uc73c\uae30 : O(n^2) (\uc57d 11\ucd08)\\n\\n\ub450 \ubc88\uc9f8\ub85c \uc800\ud76c\uac00 \uc0dd\uac01\ud55c \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\uba3c\uc800 \ub370\uc774\ud130 \ucd94\uac00 \ubc0f \ubcc0\uacbd \uac10\uc9c0 \ubd80\ubd84\uc785\ub2c8\ub2e4.\\n\\n\uae30\uc874 \uc5c5\ub370\uc774\ud2b8 \uc2dc\uc5d0 SELECT\uc640 UPDATE(or INSERT) \ub450\ubc88\uc758 \ucffc\ub9ac\uac00 \ub098\uac00\ub294 \uac83\uc774 \ub9d8\uc5d0 \ub4e4\uc9c0 \uc54a\uc544\uc11c\\n\ubcc0\uacbd \uac10\uc9c0\ub97c \uc9c1\uc811 \ud574\uc8fc\ub824\uace0 \uba54\uc11c\ub4dc\ub97c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc800\ud76c\uac00 \uc0dd\uac01\ud55c \ubcc0\uacbd \uac10\uc9c0\ub294 \uc0dd\uac01\ubcf4\ub2e4 \uac04\ub2e8\ud55c\ub370\uc694.\\n\ub3c4\uba54\uc778\uc5d0 \uba54\uc11c\ub4dc\ub97c \ub9cc\ub4e4\uc5b4\uc11c \ud544\ub4dc\ub97c if\ubb38\uc73c\ub85c \ud558\ub098\uc529 \ube44\uad50\ud574\uc92c\uc2b5\ub2c8\ub2e4.\\n\\n\ucda9\uc804\uc18c\uc758 \ub370\uc774\ud130 \ud2b9\uc9d5\uc0c1 \ub370\uc774\ud130\uac00 \uc790\uc8fc \ubc14\ub00c\ub294 \ub370\uc774\ud130\ub294 \ube44\uad50\uc801 \ucd08\ubc18\uc5d0 \ube44\uad50\ud558\ub3c4\ub85d \uad6c\ud604\ud558\uace0, \uc790\uc8fc \ubc14\ub00c\uc9c0 \uc54a\ub294 \ub370\uc774\ud130\ub294 \ud6c4\uc5d0 \ube44\uad50\ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \ub370\uc774\ud130 \uc800\uc7a5 \ubc0f \uc5c5\ub370\uc774\ud2b8 \ubd80\ubd84\uc785\ub2c8\ub2e4.\\n\uba3c\uc800 findAll()\ub85c \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30 \ub4f1 \uad00\ub828\ub41c \ubaa8\ub4e0 \ub370\uc774\ud130\ub97c Map\uc5d0 \ub123\uc5c8\uc2b5\ub2c8\ub2e4.\\nMap\uc758 \uad6c\uc870\ub85c \uae30\uc874\uc5d0 \ud14c\uc774\ube14\uc5d0 \uc800\uc7a5\ub41c \ubaa8\ub4e0 \ub370\uc774\ud130\ub97c \uc790\ub8cc\uad6c\uc870\uc5d0 \ub123\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uacf5\uacf5 API\ub97c \ubd88\ub7ec\uc640\uc11c, \ub611\uac19\uc774 Map\uc758 \uad6c\uc870\ub85c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n(Station \uc548\uc5d0\ub294 `List`\uac00 \uc874\uc7ac)\\n\\n\uc800\ud76c\ub294 \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uc18c\uc5d0 \ud574\ub2f9\ud558\ub294 \ubaa8\ub4e0 \ucda9\uc804\uae30\ub4e4\uc744 \ube44\uad50\ud558\uba74\uc11c \ubcc0\uacbd \uac10\uc9c0\ub97c \ud574\uc918\uc57c\ud558\uae30 \ub54c\ubb38\uc5d0\\n\uac01\uac01\uc758 Map.values()\uc778 `List : \uae30\uc874 \ucda9\uc804\uc18c`\uc640 `List : \uc5c5\ub370\uc774\ud2b8\ub41c \ucda9\uc804\uc18c`\ub97c \ube44\uad50\ud574\uc92c\uc2b5\ub2c8\ub2e4.\\n\\n\ube44\uad50\ub97c \ud558\uba74\uc11c \uc0c8\ub85c \uc0bd\uc785\ub41c \ucda9\uc804\uc18c\uc640 \uc5c5\ub370\uc774\ud2b8 \ub41c \ucda9\uc804\uc18c\ub97c \uac01\uac01 \ucc98\ub9ac\ud574\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ucda9\uc804\uc18c\uc758 \ubcc0\uacbd \uac10\uc9c0\ub97c \uc704\ud574\uc11c \ucda9\uc804\uae30\ub4e4\uc740 \ucda9\uc804\uc18c \uc548\uc5d0 List\ub85c \uc18d\ud574\uc788\uae30 \ub54c\ubb38\uc5d0 O(n^2)\uc758 \uc2dc\uac04 \ubcf5\uc7a1\ub3c4\ub97c \uac00\uc9c0\uace0 \uc804\uccb4 \ub370\uc774\ud130\ub4e4\uc740 \uc57d 23\ub9cc \uac74\uc774\ubbc0\ub85c, \uc804\uccb4 \ub370\uc774\ud130\ub97c \ub300\uc0c1\uc73c\ub85c \ud55c\ub2e4\uba74 \uc57d 530\uc5b5\ubc88\uc758 \uc5f0\uc0b0\uc774 \uc774\ub904\uc84c\uaca0\ub124\uc694.\\n\\nVer1\uc5d0 \ube44\ud574\uc11c\ub294 \ud06c\uc9c0\ub294 \uc54a\uc9c0\ub9cc \ud3c9\uade0\uc801\uc73c\ub85c \uc57d 2\ucd08 \uc815\ub3c4 \uc904\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\n\\n## Ver3. \ubcc0\uacbd \uac10\uc9c0\ub97c \uc9c1\uc811 \ud574\uc8fc\uace0, \uc790\ub8cc\uad6c\uc870\ub85c \ubc30\uce58 \ub370\uc774\ud130 \ubaa8\uc73c\uae30 : O(1) (\uc57d 10\ucd08)\\nVer2\uc640 \uac70\uc758 \uc720\uc0ac\ud55c \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\\n\ucc28\uc774\uc810\uc740 Map \uc790\ub8cc \uad6c\uc870 \uc0ac\uc6a9\ubc29\ubc95\uc744 \ubcc0\uacbd\ud588\uc2b5\ub2c8\ub2e4.\\n\uae30\uc874 2\uc911 for\ubb38\uc5d0\uc11c, 1\uc911 for\ubb38\uc744 \ub3cc\uba74\uc11c \ud0a4 \uac12\uc744 \ud1b5\ud574\uc11c \uc2e0\uaddc \ub370\uc774\ud130\uc640 \uc5c5\ub370\uc774\ud2b8 \ub420 \ub370\uc774\ud130\ub4e4\uc744 \ubd84\ub958\ud558\uace0, \uc774\ub4e4\uc744 \uac01\uac01 List\uc5d0 \ub123\uc5b4\uc8fc\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uc774\ub97c \ud1b5\ud574\uc11c Ver2\uc5d0 \ube44\ud574\uc11c 1\ucd08\uc815\ub3c4 \uc904\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\n\\n## Ver4. \uc774\uc804 \ubc29\uc2dd + Fetch Join \uc0ac\uc6a9\ud558\uae30 (\uc57d 6\ucd08)\\n\ub9c8\uc9c0\ub9c9 \ubc29\ubc95\uc740 \uc870\ud68c \uacfc\uc815\uc758 \uc2dc\uac04 \ub2e8\ucd95\uc785\ub2c8\ub2e4.\\n\\n\ucc98\uc74c\uc5d0 Stations\ub97c findAll()\ud558\ub294 \ucffc\ub9ac\ub97c \ud655\uc778\ud574\ubcf4\ub2c8 N+1 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\uadf8 \uc774\uc720\ub294 Station\uc5d0\uc11c Chargers\ub97c \uc9c0\uc5f0\ub85c\ub529\uc73c\ub85c \uc124\uc815 \ud588\ub294\ub370, \uc774\ub97c \uadf8\ub300\ub85c get \uba54\uc11c\ub4dc\ub97c \ud1b5\ud574 \uc870\ud68c\ud574\uc11c \ud574\ub2f9 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\\n\\n```java\\nList findAll(); // \uae30\uc874\\n\\n@Query(\\"SELECT DISTINCT c FROM ChargeStation c JOIN FETCH c.chargers\\"); // Fetch Join \uc801\uc6a9\\nList findAll();\\n```\\n\\n\ub530\ub77c\uc11c \uc704\uc5d0 \ucf54\ub4dc\uc640 \uac19\uc774 Fetch Join\uc744 \uc774\uc6a9\ud574\uc11c \ucc98\uc74c\uc5d0 \ub370\uc774\ud130\ub97c \uac00\uc838\uc654\uc2b5\ub2c8\ub2e4.\\n\uc774\ub807\uac8c \ud6a8\uc728\uc801\uc778 \uc870\ud68c\ub85c \ubcc0\uacbd\ud558\uba74\uc11c \uc2dc\uac04\uc744 \ub9ce\uc774 \uc904\uc77c \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\\n### \uc9c0\uae08\uae4c\uc9c0\uc758 \ubc29\ubc95\uc744 \uc815\ub9ac\ub97c \ud558\uc790\uba74\\nVer1 \uacfc \uac19\uc740 \ubc29\uc2dd\uc5d0\uc11c\ub294 \uc5c5\ub370\uc774\ud2b8 \uacfc\uc815\uc5d0\uc11c JPA\uc758 \uc2dd\ubcc4\uc790\uc5d0 \ub530\ub978 \ucc98\ub9ac \ubc29\uc2dd\uc73c\ub85c \uc778\ud574 [SELECT + UPDATE] or [SELECT + INSERT] \uc640 \uac19\uc774 \ucffc\ub9ac\uac00 \ub450 \ubc88\uc529 \ub098\uac14\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c Ver3\uae4c\uc9c0 \uac1c\uc120\uc744 \ud558\uae30 \uc704\ud574\uc11c \uc800\uc7a5\uacfc \uc5c5\ub370\uc774\ud2b8\ub97c \ud55c \ubc88\uc5d0 JDBC\ub97c \uc774\uc6a9\ud574\uc11c Batch\ub85c \ucc98\ub9ac\ud574\uc8fc\ub294 \ubc29\uc2dd\uc744 \uc120\ud0dd\ud588\uace0,\\n\\n\ubcc0\uacbd \uac10\uc9c0 + \ubc30\uce58 \ub370\uc774\ud130\ub97c \ubaa8\uc73c\uae30 \uc704\ud574\uc11c \uc790\ub8cc\uad6c\uc870\ub97c \uc774\uc6a9\ud574\uc11c \uc2dc\uac04\uc744 \uc870\uae08\uc529 \ub2e8\ucd95 \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ub9c8\uc9c0\ub9c9\uc73c\ub85c Ver4\uc5d0\uc11c\ub294 findAll()\uc5d0\uc11c \ubc1c\uc0dd\ud558\ub294 N+1\uc758 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uba74\uc11c \uc2dc\uac04\uc744 \ub2e8\ucd95\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c \ub3d9\uc77c \uc791\uc5c5\uc744 14\ucd08\uc5d0\uc11c 6\ucd08 \uc815\ub3c4\ub85c \uc904\uc77c \uc218 \uc788\uc5c8\uc2b5\ub2c8\ub2e4!"},{"id":"14","metadata":{"permalink":"/14","source":"@site/blog/2023-07-14-server-architecture.mdx","title":"\uce74\ud398\uc778\ud300 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98\ub97c \uc124\uba85\ud574\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4","description":"\uc548\ub155\ud558\uc138\uc694 \uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4 \uce74\ud398\uc778\ud300 \ub204\ub204\uc785\ub2c8\ub2e4","date":"2023-07-14T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 14\uc77c","tags":[{"label":"ec2","permalink":"/tags/ec-2"},{"label":"aws","permalink":"/tags/aws"},{"label":"\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4","permalink":"/tags/\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4"},{"label":"\ubc30\ud3ec","permalink":"/tags/\ubc30\ud3ec"},{"label":"\uc11c\ubc84","permalink":"/tags/\uc11c\ubc84"},{"label":"\uc544\ud0a4\ud14d\ucc98","permalink":"/tags/\uc544\ud0a4\ud14d\ucc98"}],"readingTime":10.495,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"14","title":"\uce74\ud398\uc778\ud300 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98\ub97c \uc124\uba85\ud574\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4","authors":["nunu"],"tags":["ec2","aws","\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4","\ubc30\ud3ec","\uc11c\ubc84","\uc544\ud0a4\ud14d\ucc98"]},"prevItem":{"title":"\uc8fc\uae30\uc801\uc778 \ub370\uc774\ud130 \uc694\uccad\uc73c\ub85c \ubc1b\uc740 \ub370\uc774\ud130\ub97c \ud6a8\uc728\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8 \ubc0f \uc0bd\uc785\ud558\uae30 (with. \ubc15\uc2a4\ud130)","permalink":"/15"},"nextItem":{"title":"\ucda9\uc804\uc18c \ub9ac\uc2a4\ud2b8 \ud074\ub9ad\uc2dc \ub9c8\ucee4\uc5d0 \uac04\ub2e8\uc815\ubcf4 \ubaa8\ub2ec\uc744 \ub744\uc6b0\ub294 \uae30\ub2a5 \ucd94\uac00\uc5d0\uc11c \uacaa\uc5c8\ub358 \ud2b8\ub7ec\ube14 \uc288\ud305","permalink":"/13"}},"content":"\uc548\ub155\ud558\uc138\uc694 \uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4 \uce74\ud398\uc778\ud300 \ub204\ub204\uc785\ub2c8\ub2e4\\n\\n\uc774\ubc88\uc5d0 \uce74\ud398\uc778 \ud300\uc5d0\uc11c \ubc30\ud3ec \uc544\ud0a4\ud14d\ucc98\ub97c \uacb0\uc815\ud558\uac8c \ub418\uc5c8\ub358 \uacfc\uc815\uc5d0 \ub300\ud574\uc11c \uc815\ub9ac\ub97c \ud574\ubcf4\uace0 \uc2f6\uc5b4\uc11c \uae00\uc744 \uc4f0\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\ud0a4\ud14d\ucc98\uc640 \uc11c\ubc84\uac00 \ubc30\ud3ec\ub418\ub294 \uacfc\uc815\uc744 \ubcf4\uc5ec\ub4dc\ub9ac\uba74\uc11c \uc2dc\uc791\ud558\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\n![\ubc30\ud3ec \uc544\ud0a4\ud14d\ucc98](https://blog.kakaocdn.net/dn/dKVRTG/btsnFE7Nb82/GRONsIJPqd8WFVzjzqsgqk/img.png)\\n\\n\uc11c\ubc84\uac00 \ubc30\ud3ec\ub418\ub294 \uacfc\uc815\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n![server image](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdto7By%2FbtsnD31hYHy%2F7rWKwxulxXzfhRigE60Sd0%2Fimg.png)\\n\\n## \uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4 \uc778\uc2a4\ud134\uc2a4\uc5d0 \ub300\ud55c \uc18c\uac1c\\n\\n\uc6b0\ud14c\ucf54\uc5d0\uc11c \uc120\ud0dd\ud560 \uc218 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub294 \ucd1d 2\uac00\uc9c0 \uc885\ub958\uc785\ub2c8\ub2e4.\\n\\n1. \ud37c\ube14\ub9ad \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\\n - \ucea0\ud37c\uc2a4\uc5d0\uc11c\ub9cc SSH \uc811\uadfc\uc774 \uac00\ub2a5\ud55c \uc778\uc2a4\ud134\uc2a4\uc785\ub2c8\ub2e4.\\n - \ubbf8\ub9ac \uc5f4\ub824\uc788\ub294 \ud3ec\ud2b8\ub4e4\ub9cc \ud5c8\uc6a9\uc774 \ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\\n - \uac19\uc740 \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub07c\ub9ac\ub294 \ubaa8\ub4e0 \ud3ec\ud2b8\uac00 \ud5c8\uc6a9\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4\\n2. \ud504\ub77c\uc774\ube57 \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\\n - \ud37c\ube14\ub9ad \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub97c \ud1b5\ud574\uc11c\ub9cc \uc811\uadfc\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n - \uac19\uc740 \uc11c\ube0c\ub137\uc5d0 \uc788\ub294 \uc778\uc2a4\ud134\uc2a4\ub07c\ub9ac\ub294 \ubaa8\ub4e0 \ud3ec\ud2b8\uac00 \ud5c8\uc6a9\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1\ubc88 \uc778\uc2a4\ud134\uc2a4\ub97c 2\uac1c \uc0ac\uc6a9 \uac00\ub2a5\ud558\uace0, 2\ubc88 \uc778\uc2a4\ud134\uc2a4\ub97c 1\uac1c \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n\\n\uad8c\uc7a5\ub418\ub294 \ud658\uacbd\uc5d0\uc11c 1\uac1c\ub294 db \uc11c\ubc84\ub85c \uc0ac\uc6a9\ud558\uace0, \ub098\uba38\uc9c0 2\uac1c\ub294 \uc790\uc720\ub86d\uac8c \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud588\uc2b5\ub2c8\ub2e4.\\n\\n## \uadf8\uc804\uc5d0 \uc54c\uba74 \uc88b\uc544\uc694\\n\\n\uc5ec\uae30\uc11c\ub294 Self Hosted Runner\ub97c \uc0ac\uc6a9\ud588\ub294\ub370\uc694.\\n\\nSelf Hosted Runner\uc5d0 \ub300\ud55c \ub0b4\uc6a9\uc740 [\uc5ec\uae30](https://be-student.tistory.com/75#%EC%99%9C%20Self%20Hosted%20Runner%EC%95%BC%3F-1) \uc5d0 \uc798 \ub098\uc640\uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc678\ubd80 IP\ub85c\ubd80\ud130 SSH \uc811\uadfc\uc774 \ubd88\uac00\ub2a5\ud558\uae30\uc5d0, Self Hosted Runner \ub098, Jenkins \uac19\uc740 \ubc29\ubc95\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5c8\ub294\ub370, \ub7ec\ub2dd \ucee4\ube0c\ub97c \uace0\ub824\ud574\uc11c Self Hosted Runner\ub97c \uc120\ud0dd\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \ubc30\ud3ec \uc544\ud0a4\ud14d\ucc98\uc5d0 \ub300\ud55c \uace0\ubbfc\\n\\n\uc800\ud76c \ud300\uc774 \uc774\ubc88 \uc544\ud0a4\ud14d\ucc98\ub97c \ub9cc\ub4e4\uae30 \uc704\ud574\uc11c \uace0\ubbfc\ud588\ub358 \uc810\ub4e4\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc5b4\ub5bb\uac8c \ud558\uba74 \uc7a5\uc560\uc758 \uc601\ud5a5\uc744 \ucd5c\uc18c\ud654\ud560 \uc218 \uc788\uc744\uae4c?\\n2. \uc6b4\uc601 \uc11c\ubc84\ub97c \ub098\uc911\uc5d0 \ucd94\uac00\ud558\uac8c \ub418\uc5c8\uc744 \ub54c, \uc5b4\ub5bb\uac8c \uc911\ubcf5\uc73c\ub85c \uad00\ub9ac\ub418\ub294 \ubd80\ubd84\uc744 \ucd5c\uc18c\ud654\ud560 \uc218 \uc788\uc744\uae4c?\\n3. 2\ucc28 \ub370\ubaa8\ub370\uc774\uae4c\uc9c0\uc758 \uacfc\uc81c\uc778 \uac1c\ubc1c \uc11c\ubc84\ub97c \uc5b4\ub5bb\uac8c \uad6c\uc131\ud560 \uc218 \uc788\uc744\uae4c?\\n\\n\uc5ec\uae30\uc11c 1\ubc88\uc744 \uac00\uc7a5 \uba3c\uc800 \uc0dd\uac01\ud55c \uc544\ud0a4\ud14d\ucc98\ub97c \uad6c\uc131\ud558\uac8c \ub418\uc5c8\ub294\ub370, \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc120\ud0dd\uc758 \uae30\uc900\uc774 \ub418\uc5c8\ub358 \uac83\uc740 \ucd1d 3\uac00\uc9c0\uc600\uc2b5\ub2c8\ub2e4.\\n\\n1. DB\ub294 \ud504\ub77c\uc774\ube57 \uc11c\ube0c\ub137\uc5d0 \uc704\uce58\uc2dc\ud0a4\uace0, \uc6b0\ub9ac \uc778\uc2a4\ud134\uc2a4\ub97c \uac70\uccd0\uc11c\ub9cc \uc811\uadfc\uc774 \uac00\ub2a5\ud558\uac8c \ud55c\ub2e4.\\n - \uc774 \ubd80\ubd84\uc740 \ubcf4\uc548\uc744 \uc704\ud574\uc11c \uc5b4\uca54 \uc218 \uc5c6\uc774 \uc120\ud0dd\ud558\uac8c \ub41c \ubd80\ubd84\uc785\ub2c8\ub2e4.\uc774 \ubd80\ubd84\uc744 \uace0\ub824\ud558\ub2e4 \ubcf4\ub2c8, \ucd5c\uc18c\ud55c\uc73c\ub85c \uad6c\uc131\ud560 \uc218 \uc788\ub294 \uad6c\uc870\uac00 db \uc6a9 private \uc778\uc2a4\ud134\uc2a4 1\uac1c, \uadf8\ub9ac\uace0 \uc6b0\ub9ac\uac00 \uc0ac\uc6a9\ud560 public \uc778\uc2a4\ud134\uc2a4 1\uac1c\uac00 \ub429\ub2c8\ub2e4\\n2. \uc6b4\uc601 \uc11c\ubc84\ub97c \ub098\uc911\uc5d0 \ucd94\uac00\ud558\uac8c \ub418\uc5c8\uc744 \ub54c, \uc5b4\ub5bb\uac8c \uc911\ubcf5\uc73c\ub85c \uad00\ub9ac\ub418\ub294 \ubd80\ubd84\uc744 \ucd5c\uc18c\ud654\ud560 \uc218 \uc788\uc744\uae4c?\\n - \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc5d0 CD \ud234\uc774\ub098, \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uc124\uce58\ud558\uac8c \ub418\uba74, \uc6b4\uc601 \uc11c\ubc84\uc5d0\ub3c4 \ub3d9\uc77c\ud558\uac8c \uc791\uc5c5\uc744 \ud574\uc57c \ud569\ub2c8\ub2e4.\\n - \uc774 \ubd80\ubd84\uc744 \ucd5c\uc18c\ud654\ud558\uae30 \uc704\ud574\uc11c, \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc640, CD \ud234, \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uc124\uce58\ud55c \uc778\uc2a4\ud134\uc2a4\ub97c \ubd84\ub9ac\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n3. \uc5b4\ub5bb\uac8c \ud558\uba74 \uc7a5\uc560\uc758 \uc601\ud5a5\uc744 \ucd5c\uc18c\ud654\ud560 \uc218 \uc788\uc744\uae4c?\\n - \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc640, CD \ud234, \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uc124\uce58\ud55c \uc778\uc2a4\ud134\uc2a4\ub97c \ubd84\ub9ac\ud558\uac8c \uc54a\ub294\ub2e4\uba74 \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc5d0\uc11c \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c, CD \ud234\uacfc \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc5d0\ub3c4 \uc601\ud5a5\uc744 \ubbf8\uce58\uac8c \ub429\ub2c8\ub2e4. \uc774 \ubd80\ubd84\uc744 \uc0dd\uac01\ud588\uc744 \ub54c\ub3c4, \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc640, CD \ud234, \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uc124\uce58\ud55c \uc778\uc2a4\ud134\uc2a4\ub97c \ubd84\ub9ac\ud574\uc57c \ud55c\ub2e4\uace0 \uacb0\uc815\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4\\n - \ud55c \ubd80\ubd84\uc758 \uc7a5\uc560\uac00 \ub2e4\ub978 \ud234\uae4c\uc9c0 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uac8c \ub9cc\ub4e4\uac8c \ub418\uc5b4\uc11c, \ub864\ubc31\uc774\ub098, \uc0c1\ud669 \ud30c\uc545\uc744 \ud558\uae30 \ud798\ub4e4\uac8c \ub9cc\ub4e4\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uacfc\uc815\ub4e4\uc744 \uc0dd\uac01\ud588\uc744 \ub54c, \uc778\uc2a4\ud134\uc2a4 1\uac1c\ub97c \uac1c\ubc1c \uc11c\ubc84\uc6a9\uc73c\ub85c, \uc778\uc2a4\ud134\uc2a4 1\uac1c\ub97c CD \ud234\uacfc \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uc124\uce58\ud55c \uc778\uc2a4\ud134\uc2a4\ub85c \uc0ac\uc6a9\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n## \uc2e4\uc81c \ub0b4\ubd80 \uad6c\uc131\uc740 \uc5b4\ub5bb\uac8c \ub420\uae4c\uc694?\\n\\n### \uac1c\ubc1c \uc11c\ubc84\\n\\n\uc774 \uc778\uc2a4\ud134\uc2a4\uc5d0\ub294 \ucd1d 2\uac00\uc9c0 \uae30\ub2a5\uc774 \ub4e4\uc5b4\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1. \ud504\ub860\ud2b8 \uc11c\ubc84\\n - react\ub85c \ub418\uc5b4\uc788\ub294 \ud504\ub860\ud2b8\uc5d4\ub4dc \ucf54\ub4dc\ub97c \uc0ac\uc6a9\uc790\uc5d0\uac8c \uc804\ub2ec\ud574 \uc8fc\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n2. \ubc31\uc5d4\ub4dc \uc11c\ubc84\\n - spring\uc73c\ub85c \ub418\uc5b4\uc788\ub294 api \uc11c\ubc84\uc785\ub2c8\ub2e4.\\n\\n\ubb3c\ub860, \uc774\ub807\uac8c \ud558\uba74 \ub450 \uacf3 \uc911 \ud55c \uacf3\uc5d0 \uc7a5\uc560\uac00 \ubc1c\uc0dd\ud588\uc744 \ub54c, \ud504\ub860\ud2b8 \uc11c\ubc84\uc640 \ubc31\uc5d4\ub4dc \uc11c\ubc84\uac00 \ubaa8\ub450 \uc601\ud5a5\uc744 \ubc1b\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uac19\uc774 \uad00\ub9ac\ud558\uac8c \ub41c \uccab \ubc88\uc9f8 \uc774\uc720\ub85c \ube44\uc6a9\uc774 \ub4e4\uae30 \ub54c\ubb38\uc5d0 \ube44\uc6a9\uc758 \ubb38\uc81c\ub97c \uace0\ub824\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uac1c\ubc1c \uc11c\ubc84\uc5d0\uc11c \ud504\ub860\ud2b8 \uc11c\ubc84\uc640 \ubc31\uc5d4\ub4dc \uc11c\ubc84\ub97c \uad00\ub9ac\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub450 \ubc88\uc9f8 \uc774\uc720\ub85c\ub294, \uc544\uc9c1 \ud504\ub85c\uc81d\ud2b8 \ucd08\ucc3d\uae30 \uc774\uae30 \ub54c\ubb38\uc5d0, \ubc31\uc5d4\ub4dc\uc5d0\uc11c \uc7a5\uc560\uac00 \ub0ac\uc744 \ub54c, \ud504\ub860\ud2b8\uc5d0\uc11c \uc77c\uc815 \uc774\uc0c1\uc758 \uc5d0\ub7ec \ucc98\ub9ac\uac00 \ubd88\uac00\ub2a5\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud504\ub85c\uc81d\ud2b8\uac00 \ub9ce\uc774 \uc9c4\ud589\ub418\uc5c8\ub2e4\uba74, \ud504\ub860\ud2b8\uc5d4\ub4dc\ub9cc\uc73c\ub85c \ud639\uc740 \uc7a5\uc560\uac00 \ub098\uc9c0 \uc54a\uc740 \uc11c\ubc84\ub97c \ud65c\uc6a9\ud574 \uc5d0\ub7ec \ucc98\ub9ac\ub97c \ud560 \uc218 \uc788\uc9c0\ub9cc, \uc544\uc9c1\uc740 \uadf8\ub7f0 \uae30\ub2a5\uc744 \uad6c\ud604\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc640\ub294 \ubcc4\uac1c\ub85c \uc2e4\ud589 \uc2dc \ud3b8\uc758\ub97c \uc704\ud574\uc11c \ub3c4\ucee4\ub97c \uc0ac\uc6a9\ud574 \uac1c\ubc1c \uc11c\ubc84\ub97c \uad00\ub9ac\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### CD \ud234\uacfc \ubaa8\ub2c8\ud130\ub9c1 \ud234\\n\\n\uc774 \uc778\uc2a4\ud134\uc2a4\uc5d0\ub294 \ucd1d 3\uac00\uc9c0 \uae30\ub2a5\uc774 \ub4e4\uc5b4\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1. CD \ud234\\n - \uc704\uc5d0\uc11c \uc124\uba85\ub4dc\ub9b0 \uac83\ucc98\ub7fc, self hosted runner \uac00 \ub3d9\uc791\ud558\uac8c \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4\\n2. \ubcf4\uc548\uc744 \uc704\ud55c \ub9ac\ubc84\uc2a4 \ud504\ub85d\uc2dc\\n - \uc800\ud76c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c \uad6c\uae00 \uc9c0\ub3c4\ub97c \uc0ac\uc6a9\ud558\uac8c \ub418\ub294\ub370, \uc774\ub54c API \ud0a4\ub97c \uc0ac\uc6a9\ud558\uac8c \ub429\ub2c8\ub2e4. \uc774\ub807\uac8c \ud558\uba74, API \ud0a4\ub97c \ub178\ucd9c\uc2dc\ud0a4\uc9c0 \uc54a\uace0, \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n - \uc774 API \ud0a4\ub97c \ub178\ucd9c\uc2dc\ud0a4\uc9c0 \uc54a\uae30 \uc704\ud574\uc11c, \ub9ac\ubc84\uc2a4 \ud504\ub85d\uc2dc\ub97c \ud558\ub098 \ub450\uace0, \uc5ec\uae30\uc11c API \ud0a4\ub97c \ucd94\uac00\ud574 \uc694\uccad\uc744 \ubcf4\ub0b4\ub294 \ubc29\uc2dd\uc73c\ub85c \uad6c\uc131\ud558\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n3. \ubaa8\ub2c8\ud130\ub9c1 \ud234\\n - \uc800\ud76c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c \uc544\uc9c1 \ub3c4\uc785\ud558\uc9c0 \uc54a\uc558\uc9c0\ub9cc, \ud604\uc7ac \uc774\uc288\ub85c\ub294 \uc62c\ub77c\uac00 \uc788\ub294 \uc0c1\ud0dc\uc785\ub2c8\ub2e4.\\n - Actuator, \ud504\ub85c\uba54\ud14c\uc6b0\uc2a4, \uadf8\ub77c\ud30c\ub098 \uc774 3\uac00\uc9c0\ub97c \ud65c\uc6a9\ud574\uc11c \ubaa8\ub2c8\ud130\ub9c1 \ud234\uc744 \uad6c\uc131\ud558\uac8c \ub420 \uc608\uc815\uc785\ub2c8\ub2e4\\n\\n\uc704 \uae30\ub2a5\ub4e4\uc774 \ud55c \uc778\uc2a4\ud134\uc2a4\uc5d0 \ubaa8\uc5ec\uc788\uae30\uc5d0, \uc704\uc758 \uae30\ub2a5\ub4e4\uc740 \ucd94\ud6c4\uc5d0 \uc6b4\uc601 \uc11c\ubc84\uac00 \ucd94\uac00\ub418\uc5c8\uc744 \ub54c, \uc911\ubcf5\uc73c\ub85c \uad00\ub9ac\ud558\uc9c0 \uc54a\uc544\ub3c4 \ub429\ub2c8\ub2e4.\\n\\n## \ubc30\ud3ec \uacfc\uc815 \ub354 \uc790\uc138\ud788 \uc54c\uc544\ubcf4\uae30\\n\\n\uc544\ub798\uc5d0 \uc0ac\uc9c4\uc5d0\uc11c \ubcf4\uc774\ub294 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c \ubc30\ud3ec\ub97c \uc9c4\ud589\ud558\uace0 \uc788\ub294\ub370\uc694\\n\\n![server image](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdto7By%2FbtsnD31hYHy%2F7rWKwxulxXzfhRigE60Sd0%2Fimg.png)\\n\\n1. \uc0ac\uc6a9\uc790\uac00 push\ub97c \ud558\uba74, github actions\uc5d0\uc11c \ub3c4\ucee4 \ube4c\ub4dc\ub97c \uc9c4\ud589\ud558\uace0, \ub3c4\ucee4 \ud5c8\ube0c\uc5d0 \uc774\ubbf8\uc9c0\ub97c \uc62c\ub9bd\ub2c8\ub2e4.\\n2. \ub3c4\ucee4 \ud5c8\ube0c\uc5d0 \uc774\ubbf8\uc9c0\uac00 \uc62c\ub77c\uac04 \uc774\ud6c4\uc5d0, self hosted runner \uac00 \uc791\ub3d9\uc744 \uc2dc\uc791\ud569\ub2c8\ub2e4.\\n3. \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud574\uc11c, \uc774\ubbf8\uc9c0\ub97c \ubc1b\uace0, \ucee8\ud14c\uc774\ub108\ub97c \uc2e4\ud589\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c, \uac1c\ubc1c\uc6a9 \uc778\uc2a4\ud134\uc2a4\uc5d0 \ubc30\ud3ec\ub97c \uc9c4\ud589\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \ub290\ub080 \uc810\\n\\n\uc88b\uc740 \uc544\ud0a4\ud14d\ucc98\ub97c \uc124\uacc4\ud558\uae30 \uc704\ud574\uc11c\ub294 \uace0\ub824\ud574\uc57c \ud560 \uc810\ub4e4\uc774 \uc815\ub9d0 \ub9ce\ub2e4\ub294 \uac83\uc744 \ub2e4\uc2dc \ud55c\ubc88 \ub290\uaf08\uc2b5\ub2c8\ub2e4.\\n\\n\uc6b4\uc601 \uc11c\ubc84\uac00 \ucd94\uac00\ub41c\ub2e4\ub358\uac00, \uc778\uc2a4\ud134\uc2a4\uac00 \ub298\uc5b4\ub098\uace0, \uc904\uc5b4\ub4dc\ub294 \uc0c1\ud669\uc5d0 \uc720\uc5f0\ud558\uac8c \ub300\ucc98\ud560 \uc218 \uc788\ub3c4\ub85d \uc124\uacc4\ub97c \ud574\uc57c \ud55c\ub2e4\ub294 \uac83\uc744 \ub2e4\uc2dc \ud55c\ubc88 \ub290\uaf08\uc2b5\ub2c8\ub2e4.\\n\\n\uc911\ubcf5\uc73c\ub85c \uad00\ub9ac\ub420 \ud3ec\uc778\ud2b8\ub97c \uc904\uc5ec\uc57c \ud55c\ub2e4\ub294 \uac83\ub3c4 \ub2e4\uc2dc \ud55c\ubc88 \ub290\ub084 \uc218 \uc788\uc5c8\uace0\uc694\\n\\n\uae34 \uae00\uc744 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4"},{"id":"13","metadata":{"permalink":"/13","source":"@site/blog/2023-07-14-trouble-shooting-with-info-window.mdx","title":"\ucda9\uc804\uc18c \ub9ac\uc2a4\ud2b8 \ud074\ub9ad\uc2dc \ub9c8\ucee4\uc5d0 \uac04\ub2e8\uc815\ubcf4 \ubaa8\ub2ec\uc744 \ub744\uc6b0\ub294 \uae30\ub2a5 \ucd94\uac00\uc5d0\uc11c \uacaa\uc5c8\ub358 \ud2b8\ub7ec\ube14 \uc288\ud305","description":"Untitled","date":"2023-07-14T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 14\uc77c","tags":[{"label":"react","permalink":"/tags/react"},{"label":"google maps api","permalink":"/tags/google-maps-api"},{"label":"useSyncExternalStore","permalink":"/tags/use-sync-external-store"}],"readingTime":17.7,"hasTruncateMarker":false,"authors":[{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"}],"frontMatter":{"slug":"13","title":"\ucda9\uc804\uc18c \ub9ac\uc2a4\ud2b8 \ud074\ub9ad\uc2dc \ub9c8\ucee4\uc5d0 \uac04\ub2e8\uc815\ubcf4 \ubaa8\ub2ec\uc744 \ub744\uc6b0\ub294 \uae30\ub2a5 \ucd94\uac00\uc5d0\uc11c \uacaa\uc5c8\ub358 \ud2b8\ub7ec\ube14 \uc288\ud305","authors":["scent"],"tags":["react","google maps api","useSyncExternalStore"]},"prevItem":{"title":"\uce74\ud398\uc778\ud300 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98\ub97c \uc124\uba85\ud574\ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4","permalink":"/14"},"nextItem":{"title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc18c\uac1c\ud569\ub2c8\ub2e4.","permalink":"/11"}},"content":"![Untitled](https://file.notion.so/f/s/16a32751-2088-4261-8bf6-3d556c0bf2e8/Untitled.png?id=020fb0e2-81d8-4dca-bb76-cf4536ca7b29&table=block&spaceId=e725e94b-8029-47f5-aecb-8eb1ef7c939f&expirationTimestamp=1689364800000&signature=3KH3gvfzTgKmmFsrNBluQ3evQ6jwe2C-tj8LqB6gQyw&downloadName=Untitled.png)\\n\\n\uc704 \uc774\ubbf8\uc9c0\ub294 \ud604\uc7ac\uae4c\uc9c0 \uad6c\ud604\ud55c \uc9c0\ub3c4\uc758 \ubaa8\uc2b5\uc774\ub2e4. \uad6c\ud604\ub41c \uae30\ub2a5\uc740 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uc11c\ubc84\uc5d0 \uc694\uccad\ud574 \ubc1b\uc544\uc628 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \ubc14\ud0d5\uc73c\ub85c \ud654\uba74\uc5d0 \ub9c8\ucee4\ub97c \ud45c\uc2dc\ud558\ub294 \uae30\ub2a5\\n- \ud654\uba74\uc774 \uc774\ub3d9\ud558\uac70\ub098 \uc90c\uc778, \uc90c \uc544\uc6c3\uc744 \ud560 \uc2dc \ud654\uba74\uc758 \ub9c8\ucee4 \uc815\ubcf4\uac00 \ucd5c\uc2e0\ud654 \ub418\ub294 \uae30\ub2a5\\n- \ub9c8\ucee4 \uc815\ubcf4\ub97c \ucd5c\uc2e0\ud654 \ud560 \ub54c \ud654\uba74\uc5d0\uc11c \uc0ac\ub77c\uc9c4 \ub9c8\ucee4\ub97c dom\uc5d0\uc11c \uc81c\uac70\ud558\ub294 \uae30\ub2a5\\n- \ub9c8\ucee4 \uc815\ubcf4\ub97c \ucd5c\uc2e0\ud654 \ud560 \ub54c \uc774\uc804 \ud654\uba74\uc5d0\uc11c\ub3c4 \uc788\uc5c8\ub358 \ub9c8\ucee4\ub97c \uc7ac\uc0dd\uc131 \ud558\uc9c0 \uc54a\ub294 \uae30\ub2a5\\n- \ub9c8\ucee4\ub97c \ud074\ub9ad\ud588\uc744 \uc2dc \ud574\ub2f9 \ub9c8\ucee4\uc5d0 \ub300\ud55c \uac04\ub2e8 \uc815\ubcf4\ub97c \ubaa8\ub2ec\ub85c \ub744\uc6cc\uc8fc\ub294 \uae30\ub2a5\\n- \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c \ub9c8\ucee4\ub4e4\uc5d0 \ub300\ud55c \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \ub9ac\uc2a4\ud2b8\ub85c \ubcf4\uc5ec\uc8fc\ub294 \uae30\ub2a5\\n\\n\uc774\ubc88\uc5d0 \uc0c8\ub85c \ucd94\uac00\ud558\uace0\uc790 \ud55c \uae30\ub2a5\uc740 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- \ucda9\uc804\uc18c \ub9ac\uc2a4\ud2b8\uc5d0\uc11c \ucda9\uc804\uc18c\ub97c \uc120\ud0dd\ud558\uba74 \ud654\uba74\uc758 \uc911\uc2ec\uc774 \uc120\ud0dd\ud55c \ucda9\uc804\uc18c \ub9c8\ucee4\ub85c \uc774\ub3d9\ud558\uace0, \ucda9\uc804\uc18c\uc758 \uac04\ub2e8 \uc815\ubcf4\ub97c \ubaa8\ub2ec\ub85c \ub744\uc6cc\uc8fc\ub294 \uae30\ub2a5\\n\\n\uc704 \uae30\ub2a5\uc744 \uad6c\ud604\ud558\uae30 \uc704\ud574\uc120 google maps api\uc758 InfoWindow\uac1d\uccb4\ub97c \uc774\uc6a9\ud574\uc57c \ud55c\ub2e4. \uc0ac\uc6a9 \ubc29\uc2dd\uc740 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n```jsx\\nconst infowindow = new google.maps.InfoWindow({\\n content: contentString,\\n ariaLabel: \'Uluru\',\\n});\\n\\nconst marker = new google.maps.Marker({\\n position: uluru,\\n map,\\n title: \'Uluru (Ayers Rock)\',\\n});\\n\\ninfowindow.open({\\n anchor: marker,\\n map,\\n});\\n```\\n\\n\uac04\ub2e8\ud558\uac8c \uc694\uc57d\ud558\uc790\uba74 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- `InfoWindow` \uc0dd\uc131\uc790 \ud568\uc218\ub97c \ud1b5\ud574 `infoWindow` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4.\\n - \uc0dd\uc131\uc2dc dom \uc694\uc18c \ud639\uc740 string\uc744 \uc804\ub2ec\ud574 `infoWindow`\uac00 \uc0dd\uc131\ub420 dom\uc704\uce58\ub97c \uc9c0\uc815\ud574\uc900\ub2e4.\\n- `marker` \uc778\uc2a4\ud134\uc2a4\ub97c `infoWindow` \uc778\uc2a4\ud134\uc2a4\uc758 `open` \uba54\uc11c\ub4dc\uc5d0 \uc778\uc790\ub85c \uc804\ub2ec\ud55c\ub2e4.\\n- `infoWindow` \uc0dd\uc131 \uc2dc \uc804\ub2ec\ud588\ub358 dom\uc694\uc18c\uc758 \uc704\uce58\uac00 `marker`\uc758 \uc704\uce58\ub85c \uace0\uc815\ub418\uba74\uc11c \ud654\uba74\uc5d0 \uadf8\ub824\uc9c4\ub2e4.\\n\\n---\\n\\n![Untitled](https://file.notion.so/f/s/3079d7b9-8226-46b1-9482-054d1ea78016/Untitled.png?id=bce7685b-8a95-429c-bb75-98a4402cfc17&table=block&spaceId=e725e94b-8029-47f5-aecb-8eb1ef7c939f&expirationTimestamp=1689364800000&signature=jKnY-AhoxwqTiWrMi66uUtIamSOZDj8GGBTzgKeu_qY&downloadName=Untitled.png)\\n\\n\ucda9\uc804\uc18c \uc815\ubcf4\ub97c \ubcf4\uc5ec\uc8fc\ub294 \uc704 `StationList` \ucef4\ud3ec\ub10c\ud2b8\ub294 \ucda9\uc804\uc18c \uc815\ubcf4\uc5d0 \uc811\uadfc\ud560 \ub54c react-query\ub97c \ud1b5\ud574 \uc11c\ubc84 \uc0c1\ud0dc\ub97c \uc9c1\uc811 \ub0b4\ub824 \ubc1b\uc544 \ucef4\ud3ec\ub10c\ud2b8 \ub0b4\ubd80 \ub9ac\uc2a4\ud2b8\ub97c \ub80c\ub354\ub9c1 \ud55c\ub2e4.\\n\\n\ub610\ud55c, `StationMarkersContainer`\uc5d0\uc11c\ub3c4 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c react-query\uc758 \uc11c\ubc84 \uc0c1\ud0dc\uc5d0\uc11c \ucc38\uc870\ud574 \ub9c8\ucee4\ub97c \ub80c\ub354\ub9c1 \ud558\uace0 \uc788\ub2e4.\\n\\n\ub530\ub77c\uc11c `StationList` \ucef4\ud3ec\ub10c\ud2b8\uc640 `StationMarkersContainer`\ub294 \uac01\uac01 \ub530\ub85c \uc11c\ubc84 \uc0c1\ud0dc\uc5d0 \uc811\uadfc\ud574 \ub80c\ub354\ub9c1\uc744 \uc218\ud589\ud558\uace0 \uc788\uc73c\ubbc0\ub85c \ub458 \uc0ac\uc774\uc5d0\ub294 \uc5b4\ub5a0\ud55c \uc5f0\uacb0 \uace0\ub9ac\uac00 \uc5c6\ub2e4.\\n\\n\uc5ec\uae30\uc11c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uac8c \ub418\uc5c8\ub2e4.\\n\\n---\\n\\n\ud604\uc7ac\uae4c\uc9c0\uc758 \ucf54\ub4dc\uc5d0\uc11c\ub294 `infoWindow`\uc778\uc2a4\ud134\uc2a4\ub97c `StationMarkersContainer`\ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \uc0dd\uc131\ud55c\ub2e4. \uc774\ub97c \ud558\uc704 \ucef4\ud3ec\ub10c\ud2b8\uc778 `StationMarker`\uc5d0 \ub0b4\ub824\uc8fc\uace0, \uc774 \ucef4\ud3ec\ub10c\ud2b8 \ub0b4\ubd80\uc5d0\uc11c `marker`\uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4.\\n\\n\uc774\ubc88\uc5d0 \uad6c\ud604\ud558\uae30\ub85c \ud55c \uae30\ub2a5\uc740 `StationList`\uc758 \ud56d\ubaa9 \uc911 \ud558\ub098\ub97c \uc120\ud0dd\ud588\uc744 \uc2dc \uc120\ud0dd\ub41c \ucda9\uc804\uc18c\uc5d0 \ud574\ub2f9\ud558\ub294 \ub9c8\ucee4\uc5d0 \uac04\ub2e8 \uc815\ubcf4 \ubaa8\ub2ec\uc774 \ub728\uba70 \ud654\uba74\uc744 \ud574\ub2f9 \ub9c8\ucee4\uac00 \uc911\uc2ec\uc73c\ub85c \uc624\ub3c4\ub85d \uc774\ub3d9 \uc2dc\ud0a4\ub294 \uac83\uc774\uc5c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc9c0\uae08\uc758 \ucf54\ub4dc \uad6c\uc870\uc0c1 `StationList`\uc640 `StationMarkersContainer`\uc0ac\uc774\uc5d0\ub294 \uc5b4\ub5a0\ud55c \uc5f0\uacb0 \uace0\ub9ac\ub3c4 \uc5c6\uc73c\ubbc0\ub85c `infoWindow`\uc640 `marker`\uc5d0 `StationList`\ub294 \uc811\uadfc\ud560 \uc218 \uc5c6\ub294 \uc0c1\ud0dc\uac00 \ub41c\ub2e4.\\n\\n\uc774\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574\uc11c \ub2e4\uc74c\uacfc \uac19\uc740 \ubc29\ubc95\uc744 \uc0ac\uc6a9\ud558\uae30\ub85c \ud588\ub2e4.\\n\\n- `infoWindow`\uc778\uc2a4\ud134\uc2a4\ub97c root \ub2e8\uc5d0\uc11c \uc0dd\uc131\ud574 \uc804\uc5ed\uc801\uc73c\ub85c \uad00\ub9ac\ud55c\ub2e4.\\n- \uc0dd\uc131\ub420 `marker` \uc778\uc2a4\ud134\uc2a4\ub4e4\uc744 \ubc30\uc5f4 \ud615\ud0dc\uc758 \uc804\uc5ed \uc0c1\ud0dc\ub85c \uad00\ub9ac\ud55c\ub2e4.\\n\\n\uc704 \ub0b4\uc6a9\uc744 \ub9d0\ub85c\ub9cc \ubcf8\ub2e4\uba74 \ubcc4\ub85c \uc5b4\ub824\uc6b8 \uac83 \uc5c6\uc5b4 \ubcf4\uc774\uc9c0\ub9cc \uc2e4\uc81c \uad6c\ud604\uc744 \uc9c4\ud589\ud574\ubcf4\ub2c8 \ub0b4\ubd80\uc801\uc73c\ub85c \ud070 \ubb38\uc81c\uac00 \ub450 \uac00\uc9c0 \uc874\uc7ac\ud588\ub2e4.\\n\\n1. \ub530\ub85c \ubaa8\ub4c8\uc744 \ubd84\ub9ac\ud574 `infoWindow`\ub97c \uc0dd\uc131\ud560 \uc218 \uc5c6\ub2e4.\\n2. `marker`\uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uc8fc\uccb4\uac00 `StationMarkersContainer`\uac00 \ub418\uc5b4\uc11c\ub294 \uc548\ub41c\ub2e4.\\n\\n\uac01\uac01\uc758 \ubb38\uc81c\uc810\uc744 \uc0b4\ud3b4\ubcf4\uc790.\\n\\n---\\n\\n### 1. \ub530\ub85c \ubaa8\ub4c8\uc744 \ubd84\ub9ac\ud574 `infoWindow`\ub97c \uc0dd\uc131\ud560 \uc218 \uc5c6\ub2e4.\\n\\n`infoWinodw`\ub97c \uc804\uc5ed \uc0c1\ud0dc\ub85c \ub9cc\ub4e4\uc5b4 \uc0ac\uc6a9\ud558\uae30 \uc704\ud574 \ucc98\uc74c\uc73c\ub85c \ud588\ub358 \uc0dd\uac01\uc740 `infoWindowStore.ts`\ub85c \ubaa8\ub4c8\uc744 \ubd84\ub9ac\ud558\uc5ec `infoWindow`\ub97c \uc0dd\uc131\ud574 store\uc758 \ucd08\uae30\uac12\uc73c\ub85c \uc9c0\uc815\ud558\ub294 \uac83\uc774\uc5c8\ub2e4.\\n\\n\uc704 \uc0dd\uac01\uc744 \uac00\uc9c0\uace0 \uadf8\ub300\ub85c \uad6c\ud604\ud574\ubcf4\uc558\ub354\ub2c8 `google`\uc744 \ucc38\uc870\ud560 \uc218 \uc5c6\ub2e4\ub294 \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\ub2e4. `InfoWindow`\uc0dd\uc131\uc790 \ud568\uc218\ub294 `google.maps.InfoWindow`\ub97c \ud1b5\ud574 \uc811\uadfc\ud560 \uc218 \uc788\uae30 \ub54c\ubb38\uc5d0 \ud574\ub2f9 \uc5d0\ub7ec\ub294 `infoWindow`\uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud560 \uc218 \uc5c6\ub2e4\ub294 \uac83\uc744 \uc758\ubbf8\ud588\ub2e4.\\n\\n\uc65c `google`\uc744 \ucc38\uc870\ud560 \uc218 \uc5c6\ub294\uc9c0 \uc774\uc720\ub97c \ubd84\uc11d\ud574\ubcf4\ub2c8 \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\uc558\ub2e4.\\n\\n\uc6b0\ub9ac \ud300\uc774 \uad6c\uae00 \uc9c0\ub3c4 \ub85c\ub4dc\ub97c \uc704\ud574 \uc120\ud0dd\ud55c \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 `@googlemaps/react-wrapper`\uc774\ub2e4. \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc758 \ub3d9\uc791\uc744 \uc0b4\ud3b4\ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- `Wrapper`\ucef4\ud3ec\ub10c\ud2b8\uac00 `@googlemaps/js-loader`\ub77c\uc774\ube0c\ub7ec\ub9ac\uc758 `Loader`\uc0dd\uc131\uc790 \ud568\uc218\ub97c \ud638\ucd9c\ud55c\ub2e4.\\n- \uc0dd\uc131\ub41c `loader`\uc778\uc2a4\ud134\uc2a4\uc758 `load`\uba54\uc11c\ub4dc\ub97c \uc2e4\ud589\uc2dc\ucf1c \uc9c0\ub3c4\uc758 \ub85c\ub529 \uc791\uc5c5\uc744 \uc2dc\uc791\ud55c\ub2e4.\\n - `load` \uba54\uc11c\ub4dc\ub294 \ucd5c\uc885\uc801\uc73c\ub85c `Promise`\uc744 \ubc18\ud658\ud558\ub294\ub370, \uc9c0\ub3c4 \ub85c\ub4dc\uc5d0 \uc131\uacf5\ud558\uba74 `resolve(window.google)` \uc744 \uc2e4\ud589\uc2dc\ucf1c `google`\uc744 \uc804\uc5ed\uc801\uc73c\ub85c \uc0ac\uc6a9 \uac00\ub2a5\ud558\ub3c4\ub85d \ub9cc\ub4e4\uc5b4\uc900\ub2e4.\\n- \uc9c0\ub3c4\uc758 \ub85c\ub529\uc774 \uc644\ub8cc\ub418\uba74 `Wrapper`\uc758 `render` props\ub97c \ud1b5\ud574 \ubc1b\uc740 \ucf5c\ubc31 \ud568\uc218\ub97c \uc2e4\ud589\uc2dc\ud0a8\ub2e4.\\n - `render`\ucf5c\ubc31 \ud568\uc218\ub294 \ub85c\ub529 \uc0c1\ud0dc\ub97c \ub098\ud0c0\ub0b4\ub294 Status\ub97c \ud30c\ub77c\ubbf8\ud130\ub85c \ub118\uaca8 \ubc1b\uc544 \ud638\ucd9c\ub41c\ub2e4.\\n\\n\ucd5c\uc885\uc801\uc73c\ub85c `render`\ub97c \uc2e4\ud589 \uc2dc\ucf30\uc744 \ub54c \ubc18\ud658 \ub418\ub294 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c\ub294 `google` \ub85c\ub529 \ub418\uc5b4 \uc804\uc5ed\uc801\uc73c\ub85c \uc811\uadfc\uc774 \uac00\ub2a5\ud568\uc744 \ubcf4\uc7a5\ud560 \uc218 \uc788\uc73c\ubbc0\ub85c \uc774\ub54c\ubd80\ud130 `google`\uc5d0 \uc811\uadfc\uc774 \uac00\ub2a5\ud574\uc9c4\ub2e4. \u2192 \ub530\ub77c\uc11c `Wrapper`\ub97c \ud1b5\ud574 \ubc18\ud658\ub418\ub294 \ucef4\ud3ec\ub10c\ud2b8\uc758 \ud558\uc704 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c `google.maps.Map`\uc0dd\uc131\uc790 \ud568\uc218\ub97c \uc0ac\uc6a9\ud574 \uc9c0\ub3c4\ub97c \uc0dd\uc131\ud560 \uc218 \uc788\uac8c \ub41c\ub2e4.\\n\\n`infoWindow`\ub97c \uc0dd\uc131\ud558\uae30 \uc704\ud574 \ub9cc\ub4e0 \uc0c8\ub85c\uc6b4 \ubaa8\ub4c8\uc740 \uccab `import`\uc2dc\uae30\uc5d0 \ud3c9\uac00\ub420 \uac83\uc774\uae30 \ub54c\ubb38\uc5d0 `Wrapper`\uc758 \ud558\uc704 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c `import`\ub97c \uc218\ud589\ud55c\ub2e4\uba74 \ub85c\ub4dc\uac00 \uc644\ub8cc\ub41c \uc774\ud6c4 \uc2dc\uc810\uc77c \uac83\uc774\ubbc0\ub85c `window.google`\uc774 \ub4f1\ub85d\ub418\uc5b4 `google`\uc5d0 \uc811\uadfc\uc774 \uac00\ub2a5\ud560 \uac83\uc73c\ub85c \uc608\uc0c1\ud588\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc6f9\ud329\uc744 \ud1b5\ud55c \ubc88\ub4e4\ub9c1 \uacfc\uc815\uc5d0\uc11c \ubaa8\ub4c8\uc774 \ub4a4\uc11e\uc5ec \ud30c\uc77c\uc758 \ud3c9\uac00 \uc2dc\uae30\ub97c \ubcf4\uc7a5\ud560 \uc218 \uc5c6\uc5b4\uc838 \uc0c8\ub85c \ub9cc\ub4e0 \ubaa8\ub4c8\uc5d0\uc11c\ub294 `google`\uc5d0 \ub300\ud55c \uc811\uadfc\uc774 \ubd88\uac00\ub2a5\ud574\uc9c0\uac8c \ub418\uc5c8\ub2e4. \uc6f9\ud329\uc744 \uc880 \ub354 \uacf5\ubd80\ud574\ubcf8\ub2e4\uba74 \uc774 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \uc218 \uc788\uc744 \uac83 \uac19\uc558\uc9c0\ub9cc, \ub108\ubb34 \uc9c0\uc5fd\uc801\uc778 \ubd80\ubd84\uc5d0\uc11c \ub9ce\uc740 \uc2dc\uac04\uc744 \ub4e4\uc774\uae30 \ubcf4\ub2e8 \uae30\uc874\uc5d0 \uac1c\ubc1c\ud558\ub358 \ubc29\uc2dd\uc744 \ud1b5\ud574 \ubb38\uc81c\ub97c \ud574\uacb0\ud574\ubcf4\uae30\ub85c \uacb0\uc815\ud588\ub2e4.\\n\\n\ucd5c\uc885\uc801\uc73c\ub85c \ubb38\uc81c\ub97c \ud574\uacb0\ud55c \ubc29\uc2dd\uc740 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- `InfoWindow`\uc0dd\uc131\uc790 \ud568\uc218\ub97c \ud638\ucd9c\ud560 `CarFfeineInfoWindowInitializer`\ucef4\ud3ec\ub10c\ud2b8\ub97c \ub9cc\ub4e0\ub2e4.\\n- `Wrapper`\ub85c \uac10\uc2f8\uc9c4 \ucef4\ud3ec\ub10c\ud2b8 \ud558\uc704\uc5d0 `CarFfeineInfoWindowInitializer` \ucef4\ud3ec\ub10c\ud2b8\ub97c \ucd94\uac00\ud55c\ub2e4.\\n- `google`\uc5d0 \uc811\uadfc\uc774 \uac00\ub2a5\ud55c \uc0c1\ud0dc\ub97c \ubcf4\uc7a5\ubc1b\uc740 `CarFfeineInfoWindowInitializer`\ub0b4\ubd80\uc5d0\uc11c `infoWindow`\uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4.\\n- `store`\uc5d0 `infoWindow`\uc778\uc2a4\ud134\uc2a4\ub97c `set`\ud574\uc8fc\uc5b4 \uc804\uc5ed\uc801\uc73c\ub85c `infoWindow`\ub97c \uc0ac\uc6a9 \uac00\ub2a5\ud558\ub3c4\ub85d \ud55c\ub2e4.\\n\\n---\\n\\n### 2. `marker`\uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uc8fc\uccb4\uac00 `StationMarkersContainer`\uac00 \ub418\uc5b4\uc11c\ub294 \uc548\ub41c\ub2e4.\\n\\n\uc774\ubc88 \ud300 \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c \uc9c0\ub3c4\ub97c \uad6c\ud604\ud558\uae30 \uc704\ud574 google maps api\ub97c \uc0ac\uc6a9\ud558\uac8c \ub418\uc5c8\ub2e4. \ub72c\uae08\uc5c6\uc774 \uc774 \uc774\uc57c\uae30\ub97c \ud55c \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- google maps api\ub294 \ubc14\ub2d0\ub77c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8\ub97c \uae30\ubc18\uc73c\ub85c \ub3d9\uc791\ud55c\ub2e4.\\n- \uc774\ubc88 \ud300 \ud504\ub85c\uc81d\ud2b8\ub294 \ub9ac\uc561\ud2b8\ub97c \uae30\ubc18\uc73c\ub85c \uac1c\ubc1c\uc744 \uc9c4\ud589\ud560 \uac83\uc774\ub2e4.\\n- \uc9c0\ub3c4\ub97c \uadf8\ub9ac\uae30 \uc704\ud574\uc11c \ubc14\ub2d0\ub77c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8\uc640 \ub9ac\uc561\ud2b8\uc758 \uc801\uc808\ud55c \uc870\ud654\uac00 \ud544\uc694\ud558\ub2e4.\\n- \ub2e4\uc18c \ud63c\ub780\uc2a4\ub7ec\uc6b8 \uc218 \uc788\ub294 \uc9c0\ub3c4\uc758 \uc870\uc791 \ubc29\uc2dd\uc744 \ub9ac\uc561\ud2b8\uc640 \uc870\ud654\ub86d\uac8c \uc0ac\uc6a9\ud558\uae30 \uc704\ud574\uc11c \ucef4\ud3ec\ub10c\ud2b8 \uc124\uacc4\uc2dc \ucef4\ud3ec\ub10c\ud2b8\uc758 \ucc45\uc784\uc744 \ud655\uc2e4\ud558\uac8c \uad6c\ubd84\ud574\uc57c\uaca0\ub2e4\ub294 \uc0dd\uac01\uc744 \ud558\uac8c \ub418\uc5c8\ub2e4.\\n\\n\uc774 \ucef4\ud3ec\ub10c\ud2b8\uc758 \ucc45\uc784\uc5d0 \ub300\ud55c \ubb38\uc81c\ub85c \uc778\ud574 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uc8fc\uccb4\uc5d0 \ub300\ud574 \ub9ce\uc740 \uace0\ubbfc\uc744 \ud558\uac8c \ub418\uc5c8\ub2e4.\\n\\n\uc77c\ub2e8 \uc6d0\ub798 \ucf54\ub4dc \uad6c\uc870\uc5d0\uc11c \ub9c8\ucee4\ub97c \uadf8\ub9ac\uae30 \uc704\ud574 \ucef4\ud3ec\ub10c\ud2b8\ub97c \ub2e4\uc74c\uacfc \uac19\uc774 \ucd94\uc0c1\ud654 \ud588\ub2e4.\\n\\n- `StationMarkersContainer` \ucef4\ud3ec\ub10c\ud2b8\\n - \ub9ac\uc561\ud2b8 \ucffc\ub9ac\ub97c \ud1b5\ud574 \ubc1b\uc544\uc628 \uc11c\ubc84 \uc0c1\ud0dc(\ucda9\uc804\uc18c \uc815\ubcf4 \ubc30\uc5f4)\ub85c `StationMarker`\ub97c \ud638\ucd9c\ud55c\ub2e4.\\n- `StationMarker` \ucef4\ud3ec\ub10c\ud2b8\\n - \uc0c1\uc704\uc5d0\uc11c \ub0b4\ub824\ubc1b\uc740 \ucda9\uc804\uc18c \uc815\ubcf4 props\ub97c \ud1b5\ud574 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4. (google maps api\uc5d0\uc11c\ub294 \uc778\uc2a4\ud134\uc2a4 \uc0dd\uc131\uc774 \uace7 \ub80c\ub354\ub9c1\uc744 \uc758\ubbf8\ud55c\ub2e4)\\n - \uc0dd\uc131\ud55c `marker` \uc778\uc2a4\ud134\uc2a4\uc5d0 `infoWindow` \uc778\uc2a4\ud134\uc2a4\uc758 `open` \uba54\uc11c\ub4dc\ub97c \ud2b8\ub9ac\uac70 \ud558\ub294 \ud074\ub9ad \uc774\ubca4\ud2b8 \ub9ac\uc2a4\ub108\ub97c \ucd94\uac00\ud574\uc900\ub2e4.\\n - `useEffect`\uc758 \ud074\ub9b0\uc5c5 \ud568\uc218\ub97c \uc774\uc6a9\ud574 \ucda9\uc804\uc18c \uc815\ubcf4\uac00 \ucd5c\uc2e0\ud654 \ub418\uc5c8\uc744 \ub54c \ub9c8\ucee4\uac00 \ub354\uc774\uc0c1 \ud654\uba74\uc5d0 \ubcf4\uc774\uc9c0 \uc54a\ub294\ub2e4\uba74 `marker` \uc778\uc2a4\ud134\uc2a4\uc758 `setMap(null)` \uba54\uc11c\ub4dc\ub97c \ud638\ucd9c\ud574 google maps api\uc5d0\uc11c \ub9c8\ucee4\ub97c \uc9c0\uc6b0\ub3c4\ub85d \ud55c\ub2e4. (\ub9c8\ucee4 \ub80c\ub354\ub9c1 \ucd5c\uc801\ud654)\\n\\n\uac04\ub7b5\ud788 \uc124\uba85\ud558\uc790\uba74 `StationMarkersContainer` \ucef4\ud3ec\ub10c\ud2b8\ub294 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uc11c\ubc84\uc5d0\uc11c \ubc1b\uc544 `StationMarker`\ub97c \ud638\ucd9c\ud558\ub294 \uc5ed\ud560\ub9cc\uc744 \uc218\ud589\ud558\uace0, \ub9c8\ucee4\uc5d0 \ub300\ud55c \ubaa8\ub4e0 \uc138\ubd80 \ub85c\uc9c1\uc740 `StationMarker`\uac00 \uc218\ud589\ud558\ub3c4\ub85d \ucef4\ud3ec\ub10c\ud2b8\ub97c \ucd94\uc0c1\ud654 \ud574\ubcf4\uc558\ub2e4.\\n\\n\uc774\ub984\uc5d0\uc11c\ub3c4 \ub4dc\ub7ec\ub098\ub4ef `StationMarker` \ucef4\ud3ec\ub10c\ud2b8\uac00 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uc8fc\uccb4\uac00 \ub418\uc5b4\uc57c \ubc14\ub2d0\ub77c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8\uc640 \ub9ac\uc561\ud2b8\uc758 \ud63c\uc885\uc778 \uc774 \ud504\ub85c\uc81d\ud2b8\uc758 \ucf54\ub4dc\ub97c \ucd94\ud6c4 \uc720\uc9c0\ubcf4\uc218 \ud560 \ub54c \ubb38\uc81c\uac00 \uc5c6\uc73c\ub9ac\ub77c \ud310\ub2e8\ud588\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774\ub807\uac8c \ucd94\uc0c1\ud654 \ub41c \ucef4\ud3ec\ub10c\ud2b8\ub4e4\uc740 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \ubc30\uc5f4 \ud615\uc2dd\uc758 \uc804\uc5ed \uc0c1\ud0dc\uc5d0 \ub2f4\uc544 \uad00\ub9ac\ud558\uace0\uc790 \ud560 \ub54c \ubb38\uc81c\uac00 \ub418\uc5c8\ub2e4.\\n\\n---\\n\\n\uc77c\ub2e8 \uba3c\uc800 \uc11c\ubc84\uc5d0\uc11c \ub0b4\ub824 \ubc1b\uc740 \ucda9\uc804\uc18c \uc815\ubcf4\ub97c `station`\uc774\ub77c\uace0 \ud558\uc790, \uc6b0\ub9ac\ub294 \uc774 `station`\uc744 \ud1b5\ud574 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\uace0\uc790 \ud55c\ub2e4.\\n\\n\uc774\ub54c \uc0dd\uac01 \ud560 \uc218 \uc788\ub294 \uac00\uc7a5 \uac04\ub2e8\ud55c \ubc29\ubc95\uc740 `station`\uc5d0\uc11c `map` \uba54\uc11c\ub4dc\ub97c \ud1b5\ud574 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\uc5ec \uc774 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \ud558\uc704 \ucef4\ud3ec\ub10c\ud2b8\uc778 `StationMarker`\uc5d0 \ub118\uaca8\uc8fc\ub294 \ubc29\uc2dd\uc77c \uac83\uc774\ub2e4.\\n\\n\ud558\uc9c0\ub9cc \uc774 \ubc29\uc2dd\uc740 \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\ub294 \uac83\uc774 \uace7 \ud654\uba74\uc5d0 \ub80c\ub354\ub9c1\uc744 \ubc1c\uc0dd\uc2dc\ud0a4\ub294 \uac83\uc744 \uc758\ubbf8\ud558\ub294 google maps api\uc758 \ud2b9\uc131\uc0c1 \uc6b0\ub9ac\uac00 \ucc98\uc74c \uc124\uacc4\ud55c \ucef4\ud3ec\ub10c\ud2b8\uc758 \ucc45\uc784\uc744 \ubc18\ud558\ub294 \uad6c\uc870\ub97c \ub9cc\ub4e4\uc5b4\ub0b4\uac8c \ub41c\ub2e4.\\n\\n\uc790\uc138\ud788 \uc124\uba85\ud574\ubcf4\uc790\uba74 \ub9c8\ucee4\uc758 \ub80c\ub354\ub9c1\uc740 `StationMarkersContainer`\uac00 \uc218\ud589\ud558\uace0 \uc788\ub294\ub370 \ud654\uba74\uc5d0 \ubcf4\uc774\uc9c0 \uc54a\ub294 \ub9c8\ucee4\ub97c \uc9c0\uc6b0\ub294 \uc5ed\ud560\uc740 `StationMarker`\ucef4\ud3ec\ub10c\ud2b8\uac00 \uc218\ud589\ud558\uace0 \uc788\uace0, \uc774\ubca4\ud2b8 \ud578\ub4e4\ub7ec\uc758 \ucd94\uac00 \uc5ed\uc2dc \ub9c8\ucee4\uac00 \uc0dd\uc131\ub41c \uc774\ud6c4\uc5d0 \ud558\uc704 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c \uc774\ub97c \uc218\ud589\ud558\ub294 \uad34\uc0c1\ud55c \ucf54\ub4dc\uac00 \ub9cc\ub4e4\uc5b4\uc9c0\uac8c \ub41c\ub2e4.\\n\\n\ucd94\ud6c4 \ucf54\ub4dc\uc758 \uc720\uc9c0\ubcf4\uc218\uc131\uc744 \uc704\ud574\uc120 \ud53c\ud574\uc57c \ud560 \ubc29\uc2dd\uc784\uc774 \uba85\ud655\ud588\ub2e4.\\n\\n\ud574\uacb0 \ubc29\uc2dd\uc744 \uace0\ubbfc\ud574\ubcf4\ub2e4\uac00 \ub2e4\uc74c\uacfc \uac19\uc740 \ud574\uacb0 \ubc29\uc548\uc744 \uc0dd\uac01\ud558\uac8c \ub418\uc5c8\ub2e4.\\n\\n`StationMarker` \ucef4\ud3ec\ub10c\ud2b8\uc758 \uc5ed\ud560\\n\\n- `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud55c\ub2e4.\\n- `marker` \uc778\uc2a4\ud134\uc2a4\uc758 \uc774\ubca4\ud2b8 \ud578\ub4e4\ub7ec\ub97c \ucd94\uac00\ud55c\ub2e4.\\n- \uc0dd\uc131\ub41c `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \ubc30\uc5f4 \ud615\uc2dd\uc758 \uc804\uc5ed \uc0c1\ud0dc\uc5d0 \ucd94\uac00\ud55c\ub2e4.\\n- \ucda9\uc804\uc18c \uc815\ubcf4\uac00 \ucd5c\uc2e0\ud654 \ub418\uc5c8\uc744 \ub54c \ub9c8\ucee4\uac00 \ud654\uba74\uc5d0 \ubcf4\uc774\uc9c0 \uc54a\ub294 \uc0c1\ud0dc\uac00 \ub418\uc5c8\ub2e4\uba74 `marker` \uc778\uc2a4\ud134\uc2a4\ub97c \uc804\uc5ed \uc0c1\ud0dc\uc5d0\uc11c \uc0ad\uc81c\ud55c\ub2e4.\\n\\n\uc704\uc640 \uac19\uc774 `StationMarker` \uc758 \uc5ed\ud560\uc744 \uc7a1\uac8c \ub418\uba74 \uae30\uc874\uc758 \ucef4\ud3ec\ub10c\ud2b8 \uc124\uacc4 \uad6c\uc870\ub97c \ud574\uce58\uc9c0 \uc54a\uc73c\uba74\uc11c \uc804\uc5ed \uc0c1\ud0dc\uc5d0 `marker`\uc778\uc2a4\ud134\uc2a4\ub97c \uc798 \ucd94\uac00\ud560 \uc218 \uc788\uac8c \ub41c\ub2e4. \ud558\uc9c0\ub9cc \uc774\ub807\uac8c \ub418\uba74 `StationMarker` \ucef4\ud3ec\ub10c\ud2b8\ub294 \ub2e4\uc74c\uc758 \ud070 \ubb38\uc81c\ub4e4\uc744 \uac00\uc9c0\uac8c \ub41c\ub2e4.\\n\\n1. `marker`\ub4e4\uc744 \uac00\uc9c0\ub294 \uc804\uc5ed \uc0c1\ud0dc\ub97c \uad6c\ub3c5\ud558\uace0 \uc788\ub294 \ucef4\ud3ec\ub10c\ud2b8\uac00 \uc0c8\ub85c \uc0dd\uc131\ub418\ub294 \ub9c8\ucee4\uc758 \uac1c\uc218\ub9cc\ud07c \ub9ac\ub80c\ub354\ub9c1 \ub41c\ub2e4.\\n2. \ud604\uc7ac \uc0ac\uc6a9\ud558\uace0 \uc788\ub294 \uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac \ub3c4\uad6c\uc758 \ud2b9\uc131\uc0c1 \uc774\uc804 \uc0c1\ud0dc\ub97c \ucc38\uc870\ud574\uc640\uc57c `marker`\ub97c \ucd94\uac00\ud560 \uc218 \uc788\uac8c \ub418\ub294\ub370, \uc774 \ub54c \uc774\uc804 \uc0c1\ud0dc\uac00 \ucd5c\uc2e0\uc758 \uc0c1\ud0dc\uc784\uc744 \ubcf4\uc7a5\ud558\uc9c0 \ubabb\ud560 \uc218 \uc788\ub2e4.\\n\\n\uc774 \ub450 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \ubc29\uc2dd\uc744 \uace0\ubbfc\ud574\ubcf4\uc558\uc744 \ub54c \ub2e4\uc74c\uacfc \uac19\uc740 \uacb0\ub860\uc5d0 \ub3c4\ub2ec\ud558\uac8c \ub418\uc5c8\ub2e4.\\n\\n- \ud604\uc7ac \uc0ac\uc6a9\ud558\uace0 \uc788\ub294 \uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac \ub3c4\uad6c\ub294 React 18\uc5d0 \uc0c8\ub85c \ucd94\uac00\ub41c `useSyncExternalState` \ud6c5\uc744 \uae30\ubc18\uc73c\ub85c `recoil`\uacfc \ube44\uc2b7\ud558\uac8c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub3c4\ub85d \uacc4\uce35\uc744 \ubd84\ub9ac\ud558\uc5ec \ub9cc\ub4e0 \ub3c4\uad6c\uc774\ub2e4.\\n- \uae30\uc874\uc5d0 \uc0ac\uc6a9\ud558\ub358 \uc804\uc5ed \uc0c1\ud0dc \uad00\ub9ac \ub3c4\uad6c\uc758 \uba54\uc11c\ub4dc `useExternalState`, `useExternalValue`, `useSetExternalState` \uc774\uc678\uc5d0 `store` \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc9c1\uc811 \uc811\uadfc\ud558\uc5ec \ucd5c\uc2e0\uc758 \uc0c1\ud0dc\ub97c \ucc38\uc870\ud558\ub294 `getStoreSnapShot` \uba54\uc11c\ub4dc\ub97c \ucd94\uac00\ud55c\ub2e4.\\n- `store`\uc5d0 \uc9c1\uc811 \uc811\uadfc\ud574 \ubc1b\uc544\uc628 \ucd5c\uc2e0\uc758 \uc0c1\ud0dc\ub294 \ubc14\ub2d0\ub77c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8 \uac1d\uccb4 \uc774\ubbc0\ub85c \ub9ac\uc561\ud2b8\uc758 \ub9ac\ub80c\ub354\ub9c1\uc744 \ubc1c\uc0dd \uc2dc\ud0a4\uc9c0 \uc54a\ub294\ub2e4.\\n- \ub9ac\ub80c\ub354\ub9c1\uc73c\ub85c \uc778\ud55c \ubb38\uc81c\uc810\ub4e4\uc744 `getStoreSnapShot` \uba54\uc11c\ub4dc\ub97c \ucd94\uac00\ud568\uc73c\ub85c\uc368 \ud574\uacb0\ud560 \uc218 \uc788\ub2e4.\\n\\n---\\n\\n\uc0c8\ub85c\uc6b4 \uae30\ub2a5 \ucd94\uac00\ub97c \uc704\ud574 \ub9c8\uc8fc\ud588\ub358 \uc55e\uc120 \ub450 \uac00\uc9c0\uc758 \ubb38\uc81c\uc640 \ud574\uacb0 \ubc29\uc2dd\uc744 \uc0b4\ud3b4 \ubcf4\uc558\ub2e4. \uadf8\ub798\uc11c \ucd5c\uc885\uc801\uc73c\ub85c \uc774\uc804\uae4c\uc9c0 \uacc4\uc18d\ud574\uc11c \uace0\ubbfc\ud574\uc654\ub358 \ubb38\uc81c\ub97c \ud574\uacb0\ud55c \uacfc\uc815\uc744 \uac04\ucd94\ub824\ubcf4\uc790\uba74 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n- \ucda9\uc804\uc18c \uc815\ubcf4\ub97c \uc11c\ubc84\uc5d0\uc11c \ubc1b\uc544\uc640 \ub80c\ub354\ub9c1 \ud558\ub294 `StationList` \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c `marker` \uc778\uc2a4\ud134\uc2a4 \ubc30\uc5f4\uc744 \uc800\uc7a5\ud558\uace0 \uc788\ub294 `store`\uc778\uc2a4\ud134\uc2a4\uc5d0 \uc9c1\uc811 \uc811\uadfc\ud574 \ucd5c\uc2e0\uc758 `marker`\uc778\uc2a4\ud134\uc2a4\ub4e4\uc744 \uac00\uc838\uc628\ub2e4.\\n- \ucda9\uc804\uc18c \ubaa9\ub85d\uc5d0\uc11c \uc0ac\uc6a9\uc790\uac00 \ucda9\uc804\uc18c\ub97c \ud074\ub9ad\ud588\uc744 \ub54c \uc804\uc5ed\uc73c\ub85c \uad00\ub9ac\ub418\ub294 `infoWindow` \uc778\uc2a4\ud134\uc2a4\uc758 `open`\uba54\uc11c\ub4dc\uc5d0 `marker` \uc778\uc2a4\ud134\uc2a4\ub4e4 \uc911 \uc120\ud0dd\ub41c `marker`\ub97c \uc804\ub2ec\ud574 \uac04\ub2e8 \uc815\ubcf4 \ubaa8\ub2ec\uc744 \ub744\uc6cc\uc900\ub2e4."},{"id":"11","metadata":{"permalink":"/11","source":"@site/blog/2023-07-10-google-maps-api-with-car-ffeine.mdx","title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc18c\uac1c\ud569\ub2c8\ub2e4.","description":"\uc9c0\ub3c4 api \ubca4\ub354 \uc120\ud0dd \uc774\uc720","date":"2023-07-10T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 10\uc77c","tags":[{"label":"react","permalink":"/tags/react"},{"label":"google maps","permalink":"/tags/google-maps"},{"label":"google maps api","permalink":"/tags/google-maps-api"},{"label":"react-wrapper","permalink":"/tags/react-wrapper"},{"label":"@googlemaps/react-wrapper","permalink":"/tags/googlemaps-react-wrapper"}],"readingTime":8.165,"hasTruncateMarker":false,"authors":[{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"11","title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc18c\uac1c\ud569\ub2c8\ub2e4.","authors":["gabriel"],"tags":["react","google maps","google maps api","react-wrapper","@googlemaps/react-wrapper"]},"prevItem":{"title":"\ucda9\uc804\uc18c \ub9ac\uc2a4\ud2b8 \ud074\ub9ad\uc2dc \ub9c8\ucee4\uc5d0 \uac04\ub2e8\uc815\ubcf4 \ubaa8\ub2ec\uc744 \ub744\uc6b0\ub294 \uae30\ub2a5 \ucd94\uac00\uc5d0\uc11c \uacaa\uc5c8\ub358 \ud2b8\ub7ec\ube14 \uc288\ud305","permalink":"/13"},"nextItem":{"title":"jasypt\ub97c \ud65c\uc6a9\ud558\uc5ec \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\uc790","permalink":"/12"}},"content":"## \uc9c0\ub3c4 api \ubca4\ub354 \uc120\ud0dd \uc774\uc720\\n\\n\uad6d\ub0b4 \uc11c\ube44\uc2a4 \uc911\uc778 \uc9c0\ub3c4 \uc11c\ube44\uc2a4\ub85c\ub294 google, naver, kakao\uac00 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uc911\uc5d0\uc11c\ub3c4 google maps api\ub294 css\ub85c `\uc9c0\ub3c4\uc758 \ud14c\ub9c8\ub97c \uc9c1\uc811 \uc2a4\ud0c0\uc77c\ub9c1\ud560 \uc218 \uc788\ub294 \uae30\ub2a5\uc774 \uc788\uc5b4\uc11c \uc120\ud0dd`\ud558\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\ngoogle maps api\ub97c \uc0ac\uc6a9\ud558\uae30 \uc704\ud574\uc11c \ubcc4\ub3c4\uc758 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc0ac\uc6a9\uc774 \ud544\uc218\ub294 \uc544\ub2c8\uc9c0\ub9cc\\n\\n\uc800\ud76c \ud300\uc5d0\uc11c \ub300\uc911\uc801\uc778 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uacfc \uae30\ubcf8 \ud658\uacbd \uc124\uc815\ubc95\uc744 \ubaa8\ub450 \ud14c\uc2a4\ud2b8 \ud588\uc744 \ub54c, \ubc18\ub4dc\uc2dc \uc0ac\uc6a9\ud558\uace0 \uc2f6\uc740 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \uc874\uc7ac\ud558\uc5ec \ube44\uad50\ub97c \uae30\ub85d\uc73c\ub85c \ub0a8\uae30\uac8c \ub410\uc2b5\ub2c8\ub2e4.\\n\\n# google maps api \uad00\ub828 \ub77c\uc774\ube0c\ub7ec\ub9ac\\n\\n(\uc120\ud0dd\ud55c \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc740 \u2705\uc73c\ub85c \ud45c\uc2dc\ud588\uc2b5\ub2c8\ub2e4.)\\n\\n### google maps API\\n\\nhttps://github.com/tomchentw/react-google-maps\\n\\n\uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 \uad6c\uae00\uc5d0\uc11c \uacf5\uc2dd\uc73c\ub85c \uc81c\uacf5\ud558\ub294 \uc9c0\ub3c4 api\ub85c, HTML DOM\uc5d0 \uad6c\uae00 \uc9c0\ub3c4\ub97c \ubd80\ucc29\ud558\uace0, \uc0ac\uc6a9(\uc870\uc791)\ud560 \uc218 \uc788\ub3c4\ub85d \ub3c4\uc640\uc90d\ub2c8\ub2e4. \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 `vanilla Javascript \uae30\ubc18\uc73c\ub85c \ub3d9\uc791`\ud569\ub2c8\ub2e4.\\n\\n### **@types/google.maps** \u2705\\n\\nhttps://www.npmjs.com/package/@types/google.maps\\n\\nTypeScript\uc5d0\uc11c \uad6c\uae00 \uc9c0\ub3c4\ub97c \uc0ac\uc6a9\ud560 \ub54c `\ud0c0\uc785\uc744 \uc81c\uacf5`\ud574\uc8fc\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\n### **@googlemaps/js-api-loader**\\n\\nhttps://www.npmjs.com/package/@googlemaps/js-api-loader\\n\\n\uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 \uad6c\uae00\uc5d0\uc11c \uacf5\uc2dd\uc73c\ub85c \uc81c\uacf5\ud558\ub294 \uc9c0\ub3c4 \ud638\ucd9c api\ub85c, api key\ub9cc \ub118\uaca8\uc8fc\ub354\ub77c\ub3c4 \uad6c\uae00 \uc9c0\ub3c4\ub97c \uc2a4\ud06c\ub9bd\ud2b8 \ud615\ud0dc\ub85c \ubd88\ub7ec\uc640\uc8fc\ub294 \uc5ed\ud560\uc744 \ud558\ub294 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4. \ubcc4\ub3c4\ub85c html \uc870\uc791 \uc5c6\uc774 \ubd88\ub7ec\uc628 `\ub77c\uc774\ube0c\ub7ec\ub9ac\uc5d0\uc11c \uad6c\uae00 \uc9c0\ub3c4\ub97c \uaebc\ub0b4\uc11c \ub3d9\uc801\uc73c\ub85c \uc0ac\uc6a9`\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. vanilla Javascript \uae30\ubc18\uc73c\ub85c \ub3d9\uc791\ud558\uc5ec \uc5b4\ub514\uc5d0\uc11c\ub098 \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n\\n### \ub300\uc911\uc801\uc778 \ub77c\uc774\ube0c\ub7ec\ub9ac \ube44\uad50\\n\\n| | react-google-maps | @react-google-maps/api | @googlemaps/react-wrapper |\\n| --- | --- | --- | --- |\\n| \ub9c1\ud06c | https://www.npmjs.com/package/react-google-maps | https://www.npmjs.com/package/@react-google-maps/api | https://www.npmjs.com/package/@googlemaps/react-wrapper |\\n| \uc124\uba85 | \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 \uac1c\uc778\uc774 \ub9cc\ub4e0 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c, google maps API\ub97c react DOM \uc704\uc5d0 \uc62c\ub824\uc11c \uc0ac\uc6a9\ud558\uac8c \ub3d5\uc2b5\ub2c8\ub2e4.
    \uad6c\uae00 \uc9c0\ub3c4\uc640 \ub9c8\ucee4\ub97c react component \ucc98\ub7fc \uc0ac\uc6a9\ud558\uc5ec react\uc2a4\ub7fd\uac8c \ub80c\ub354\ub9c1 \ud558\ub294 \uac83\uc744 \uc9c0\uc6d0\ud569\ub2c8\ub2e4.
    react \uc9c4\uc601\uc5d0\uc11c \uac00\uc7a5 \ub300\uc911\uc801\uc73c\ub85c \uc0ac\uc6a9\ub418\ub294 \uad6c\uae00 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc600\uc9c0\ub9cc 2018\ub144 \uc774\ud6c4\ub85c \uc5c5\ub370\uc774\ud2b8\uac00 \ub04a\uacbc\uc2b5\ub2c8\ub2e4. | \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub3c4 \uac1c\uc778\uc774 \ub9cc\ub4e0 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c \uc55e\uc11c \uc18c\uac1c\ud55c react-google-maps\ub97c \uac1c\ub7c9\ud558\uc5ec \ub9cc\ub4e0 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.
    \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc5ed\uc2dc react\uc5d0 \uc9c0\ub3c4\ub098 \ub9c8\ucee4 \ucef4\ud3ec\ub10c\ud2b8\ub97c \ud638\ucd9c\ud574\uc11c \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4.
    \ud604\uc7ac react \uc9c4\uc601\uc5d0\uc11c \uac00\uc7a5 \ub300\uc911\uc801\uc73c\ub85c \uc0ac\uc6a9\ub418\ub294 \uad6c\uae00 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc785\ub2c8\ub2e4. | \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 \uad6c\uae00\uc5d0\uc11c \uacf5\uc2dd\uc73c\ub85c \uc81c\uacf5\ud558\ub294 react\uc6a9 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.
    \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub294 \uc55e\uc11c \uc18c\uac1c\ud55c js-api-loader\ub97c \ud65c\uc6a9\ud558\uc5ec \ub9cc\ub4e0 Wrapper \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc81c\uacf5\ud558\ub294\ub370, \uad6c\uae00 \uc9c0\ub3c4\ub97c \ud638\ucd9c\ud558\ub294 \uacfc\uc815\uc5d0\uc11c \uc218\uc2e0\uc911, \uc2e4\ud328, \uc131\uacf5\uc5d0 \ub530\ub77c \uc9c0\ub3c4\ub97c \ubcf4\uc5ec\uc904 \uc9c0, \ub85c\ub529\uc911 \ucef4\ud3ec\ub10c\ud2b8\ub97c \ubcf4\uc5ec\uc904 \uc9c0, \uc5d0\ub7ec \ucef4\ud3ec\ub10c\ud2b8\ub97c \ubcf4\uc5ec\uc904 \uc9c0 \uacb0\uc815\ud558\ub294 \uae30\ub2a5\uc774 \uc788\uc2b5\ub2c8\ub2e4.
    \uc774\uc678\uc5d0\ub294 \uae30\uc874\uc758 js-api-loader\uc758 \uae30\ub2a5\uacfc \uc644\ubcbd\ud558\uac8c \ub3d9\uc77c\ud569\ub2c8\ub2e4. (\ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc5f4\uc5b4\uc11c \uc9c1\uc811 \ud655\uc778\ud574\ubd24\uc2b5\ub2c8\ub2e4.) |\\n| \uc120\ud0dd\uc5ec\ubd80 | | | \u2705 |\\n\\n# \ub77c\uc774\ube0c\ub7ec\ub9ac \uc120\ud0dd \uc774\uc720\\n\\n\uc800\ud76c \ud504\ub85c\uc81d\ud2b8\ub294 `\uc2e4\uc2dc\uac04 \uc804\uae30\uc790\ub3d9\ucc28 \ucda9\uc804\uc18c \uc9c0\ub3c4 \ubc0f \uc0ac\uc6a9 \ud1b5\uacc4 \uc870\ud68c \uc11c\ube44\uc2a4` \ub2e4\ubcf4\ub2c8 \uc9c0\ub3c4 \uc704\uc5d0 \ub744\uc6cc\uc918\uc57c \ud560 \ub9c8\ucee4\ub97c \ucd5c\uc801\ud654 \ud558\ub294 \uacfc\uc815\uc774 \uad49\uc7a5\ud788 \uc911\uc694\ud569\ub2c8\ub2e4.\\n\\n1. \uc804\uad6d 6\ub9cc\uc5ec \uac1c\uc758 \ub9c8\ucee4\ub97c \uc804\ubd80 \ubcf4\uc5ec\uc904 \uc218 \uc5c6\ub2e4.\\n2. \ud604\uc7ac \ub514\uc2a4\ud50c\ub808\uc774 \uc601\uc5ed\uc758 \ub9c8\ucee4\ub9cc\uc744 \ud638\ucd9c\ud574\uc57c\ud55c\ub2e4.\\n3. \uadf8 \ub9c8\ucee4\ub4e4\uc758 \ub80c\ub354\ub9c1 \uacfc\uc815\uc744 \uc800\uc218\uc900\uc5d0\uc11c \ub2e4\ub8f0 \uc218 \uc788\uc5b4\uc57c \ud55c\ub2e4.\\n\\n\uc774\ub7f0 \uc6d0\uce59\uc744 \uac00\uc9c0\uace0 \uc788\uae30\uc5d0 \ub300\uc911\uc801\uc778 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4(react-google-maps, @react-google-maps/api)\uc740 \uc800\ud76c\uc758 \uc120\ud0dd\uc9c0\uc5d0 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uad6c\uae00 \uc9c0\ub3c4\ub294 \uc624\ub85c\uc9c0 vanilla\ub85c \uc81c\uacf5\ub418\ub294 \uc0c1\ud0dc\uc5d0\uc11c \uc9c1\uc811 \uc81c\uc5b4\ud558\uae30\ub85c \uacb0\uc815\ud558\uc600\uace0, \ub9c8\ucee4\ub97c \uad00\ub9ac\ud558\ub294 \uc8fc\uccb4 \ub610\ud55c \uad6c\uae00 \uc9c0\ub3c4\uc5d0\uc11c \uc9c1\uc811 \ucee8\ud2b8\ub864\uc744 \ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c \uad6c\uae00 \uc9c0\ub3c4\ub97c \ud638\ucd9c\ud558\ub294 \uc791\uc5c5\uc740 @googlemaps/react-wrapper\uc5d0 \ub9e1\uae30\uace0, \ubd88\ub7ec\uc628 \uad6c\uae00 \uc9c0\ub3c4\ub294 vanilla\ub85c \ud1b5\uc81c\ud558\uae30\ub85c \ud588\uc2b5\ub2c8\ub2e4.\\n\\n\uc9c0\ub3c4\uc758 \uc870\uc791, \uc9c0\ub3c4\uc5d0 \ub9c8\ucee4\ub97c \ucc0d\ub294 \uacfc\uc815\uc744 \ubaa8\ub450 `\uacf5\uc2dd \ubb38\uc11c\uc5d0 \ub098\uc640\uc788\ub294 \ubc29\ubc95\ub300\ub85c \ud1b5\uc81c`\ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\uae30\uc874\uc758 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc740 \ub9c8\ucee4\ub098 \uc9c0\ub3c4\ub97c \ucef4\ud3ec\ub10c\ud2b8\ud654 \ud55c \uc0c1\ud0dc\uc774\uae30\uc5d0 \ucd5c\uc801\ud654 \uacfc\uc815\uc5d0\uc11c \uc800\ud76c\uac00 \uc81c\uc5b4\ud560 \uc218 \uc5c6\ub294 \ubd80\ubd84\ub4e4\uc774 \uc788\ub2e4\uace0 \uc0dd\uac01\ud569\ub2c8\ub2e4. \ub530\ub77c\uc11c \ud2b8\ub7ec\ube14\uc288\ud305 \uacfc\uc815\uc5d0\uc11c \ub9c8\ucee4\uc758 \ud638\ucd9c \uc2dc\uc810, \uba54\ubaa8\ub9ac\uc5d0\uc11c \ud574\uc81c\ud558\ub294 \uc2dc\uc810, \ub80c\ub354\ub9c1\ud558\ub294 \uc2dc\uc810 \ub4f1\uc758 \uc791\uc5c5\ub4e4\uc744 \ud6e8\uc52c \ub354 \uc138\ubc00\ud558\uac8c \ud558\ub824\uba74 google maps api\uc744 \uc788\ub294 \uadf8\ub300\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ub530\ub77c\uc11c \uc9c0\ub3c4\uc5d0 \uad00\ub828\ub41c \uae30\ub2a5\uc740 react DOM \uc704\uc5d0\uc11c\uac00 \uc544\ub2cc vanilla \ud658\uacbd\uc5d0\uc11c \uc791\uc5c5\uc744 \ud560 \uac83\uc785\ub2c8\ub2e4.\\n\\n# \uad6c\uae00 \uc9c0\ub3c4 \uc81c\uc5b4 \uc804\ub7b5\\n\\n1. \uad6c\uae00 \uc9c0\ub3c4\uc640 \ub9c8\ucee4\ub294 \ud56d\uc0c1 \ubc14\ub2d0\ub77c \ud658\uacbd(react DOM \ubc14\uae65)\uc5d0\uc11c \ub3d9\uc791\ud558\uac8c \ud55c\ub2e4.\\n2. \ubc14\ub2d0\ub77c \ud658\uacbd\uc5d0\uc11c\ub9cc \ub3d9\uc791\ud558\uac8c \ud558\uc5ec \ub9ac\uc561\ud2b8 \ucef4\ud3ec\ub10c\ud2b8\uc5d0\uc11c\uc758 \uc7ac \ub80c\ub354\ub9c1\uc744 \uc77c\uc808 \ubc29\uc9c0\ud55c\ub2e4.\\n3. \ub9c8\ucee4\ub098 \uc9c0\ub3c4\uc758 \ub3d9\uc791 \uc774\ubca4\ud2b8\uc5d0 \uc758\ud574 UI\ub97c \uc870\uc791\ud574\uc57c\ud558\ub294 \uacbd\uc6b0\uc5d0\ub294 react DOM \uc870\uc791\uc744 \ud558\ub3c4\ub85d \ud55c\ub2e4.\\n4. \ubc14\ub2d0\ub77c \ud658\uacbd\uc778 google maps api\uc640 react DOM \uc0ac\uc774\uc758 \uc81c\uc5b4 \uacfc\uc815\uc5d0\ub294 useSyncExternalStore \ud6c5\uc744 \uc774\uc6a9\ud558\uc5ec \ub9ac\uc561\ud2b8 UI\ub97c \uac15\uc81c\ub85c \ub3d9\uae30\ud654 \uc2dc\ud0ac \uc218 \uc788\ub3c4\ub85d \ud55c\ub2e4.\\n\\n\uad6c\uae00 \uc9c0\ub3c4\ub294 \ubc14\ub2d0\ub77c \ud658\uacbd\uc5d0\uc11c, \uac01\uc885 UI \ud1b5\uc81c\ub294 \ub9ac\uc561\ud2b8\uc5d0\uc11c \ud1b5\ud569\ud558\uc5ec \uc0ac\uc6a9\ud558\ub294 \ud658\uacbd\uc744 \uad6c\uc0c1\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc2dc\uc911\uc5d0 \ub098\uc640\uc788\ub294 \ub300\ubd80\ubd84\uc758 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uc744 \ud65c\uc6a9\ud558\uc5ec \ube44\uad50\ud558\uace0 \ud14c\uc2a4\ud2b8\ud55c \uacb0\uacfc @googlemaps/react-wrapper\ub97c \uc120\ud0dd\ud558\ub294 \uac83\uc774 \ucd5c\uc801\ud654\uc640 \uc0dd\uc0b0\uc131, \uc571 \uc548\uc815\uc131\uc744 \ubaa8\ub450 \ud655\ubcf4\ud560 \uc218 \uc788\ub294 \uc120\ud0dd\uc774\ub77c\uace0 \uc0dd\uac01\ud588\uc2b5\ub2c8\ub2e4.\\n\\n\ud604\uc7ac \uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\uc911\uc778 \uc9c0\ub3c4 \uc81c\uc5b4\uc5d0 \uad00\ud55c \ubc29\ubc95\uc740 \uc774\ud6c4\uc5d0 \uc791\uc131 \ub420 \uae00\uc5d0\uc11c \uc0c1\uc138\ud558\uac8c \uc124\uba85\ud558\uaca0\uc2b5\ub2c8\ub2e4."},{"id":"12","metadata":{"permalink":"/12","source":"@site/blog/2023-07-10-kiara-jasypt.mdx","title":"jasypt\ub97c \ud65c\uc6a9\ud558\uc5ec \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\uc790","description":"\uc11c\ub860","date":"2023-07-10T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 10\uc77c","tags":[{"label":"jasypt","permalink":"/tags/jasypt"},{"label":"Spring","permalink":"/tags/spring"}],"readingTime":5.59,"hasTruncateMarker":false,"authors":[{"name":"\ud0a4\uc544\ub77c","title":"Backend","url":"https://github.com/kiarakim","imageURL":"https://github.com/kiarakim.png","key":"kiara"}],"frontMatter":{"slug":"12","title":"jasypt\ub97c \ud65c\uc6a9\ud558\uc5ec \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\uc790","authors":["kiara"],"tags":["jasypt","Spring"]},"prevItem":{"title":"\uce74\ud398\uc778 \ud300\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc9c0\ub3c4 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc18c\uac1c\ud569\ub2c8\ub2e4.","permalink":"/11"},"nextItem":{"title":"Pull Request \uc2dc \uc790\ub3d9\uc73c\ub85c test \uc2e4\ud589\ud558\uae30","permalink":"/9"}},"content":"## \uc11c\ub860\\n\\n\uc548\ub155\ud558\uc138\uc694 \uce74\ud398\uc778\ud300 `\ud0a4\uc544\ub77c`\uc785\ub2c8\ub2e4.\\n\\n\uc774\ubc88 \ud504\ub85c\uc81d\ud2b8\ub97c \uc2dc\uc791\ud558\uba74\uc11c \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\ub294 \ubc29\ubc95\uc73c\ub85c jasypt\ub97c \uc54c\uac8c\ub418\uc5b4\\n\\n\uc0ac\uc6a9\ud558\ub294 \ubc29\ubc95\uc744 \uc775\ud600 \uc800\ud76c \ud504\ub85c\uc81d\ud2b8\uc5d0 \uc801\uc6a9\ud574\ubcfc \uacc4\ud68d\uc785\ub2c8\ub2e4.\\n\\n## \ud504\ub85c\ud37c\ud2f0 \uc554\ud638\ud654\ub294 \uc65c \ud544\uc694\ud560\uae4c?\\n\\n```java\\nspring:\\n datasource:\\n url: \ub370\uc774\ud130\ubca0\uc774\uc2a4 url\\n username: \uacc4\uc815\\n password: \ube44\ubc00\ubc88\ud638\\n```\\n\\n\ud504\ub85c\uc81d\ud2b8\ub97c \uc9c4\ud589\ud558\uba74\uc11c yml \ud30c\uc77c\uc5d0 DB \uc5f0\uacb0 URL\uc774\ub098 \uacc4\uc815, \ube44\ubc00\ubc88\ud638 \uac19\uc774 \ub178\ucd9c\ub418\uc5b4\uc120 \uc548 \ub418\ub294 \ubbfc\uac10\ud55c \uc815\ubcf4\ub4e4\uc774 \ub9ce\uc2b5\ub2c8\ub2e4.\\n\\ngit\uc758 public repository\uc640 CI/CD\ub97c \uc5f0\ub3d9\ud574 \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \ubc30\ud3ec\ud55c\ub2e4\uba74 \uc911\uc694\ud55c \uc815\ubcf4\uac00 \ud0c8\ucde8\ub420 \uac00\ub2a5\uc131\uc774 \uc788\uc8e0.\\n\\nJasypt \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\uba74 \ud3c9\ubb38\uc73c\ub85c \ub41c \ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc811\uc18d \uc815\ubcf4\ub97c \uc554\ud638\ud654 \ud558\uc5ec \ubc29\uc5b4\ub9c9\uc744 \ud55c \uacb9 \uc313\uc744 \uc218 \uc788\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uac04\ub7b5\ud558\uac8c \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc18c\uac1c\ud558\uace0 \uc0ac\uc6a9 \ubc29\ubc95\uc744 \uc54c\uc544\ubcfc\uae4c\uc694?\\n\\n## jasypt\ub294 \ubb50\uc9c0?\\n\\nJasypt\uc774\ub780 \uc27d\uac8c \uc554\ud638\ud654 \uae30\ub2a5\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\ub3c4\ub85d \uc81c\uacf5\ud558\ub294 Java \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.\\n\\n\ubbfc\uac10\ud55c \ud3c9\ubb38 \uc815\ubcf4\ub97c \uc554\ud638\ud654\ud558\uace0, \uc544\ub798\ucc98\ub7fc \uc124\uc815 \uac12\uc744 \uc9c0\uc815\ud558\uba74 \uc5b4\ud50c\ub9ac\ucf00\uc774\uc158\uc774 \uc2e4\ud589\ub420 \ub54c \uc790\ub3d9\uc73c\ub85c \uc774\ub97c \ubcf5\ud638\ud654\ud558\uc5ec \uc0ac\uc6a9\ud569\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790\uac00 \ud3b8\ud558\uac8c \uc554\ud638\ud654 \uae30\ub2a5\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\ub3c4\ub85d \uc81c\uacf5\ud558\ub294 Java \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c\\n\\n\uacf5\uc2dd \ud648\ud398\uc774\uc9c0\ub294 http://www.jasypt.org/ \uc5d0 \uac00\uba74 \ub354 \uc790\uc138\ud55c \uc815\ubcf4\ub97c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uc0ac\uc6a9 \ubc29\ubc95\\n\\n\uc815\ub9d0 \uac04\ub2e8\ud558\uac8c \ub77c\uc774\ube0c\ub7ec\ub9ac \ucd94\uac00, key\uac12 \ub118\uaca8\uc8fc\uae30, \uc554\ud638\ud654 \uc138 \uac00\uc9c0 \ub2e8\uacc4\ub85c \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\uc5ec \uad00\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### 1. \ub77c\uc774\ube0c\ub7ec\ub9ac \ucd94\uac00 (= \uc758\uc874\uc131 \ucd94\uac00)\\n\\n```java\\nimplementation \\"com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.3\\"\\n```\\n\\n### 2. Jasypt \uc124\uc815 \ubc0f Bean \ub4f1\ub85d\\n\\nkey\ub97c \uc0ac\uc6a9\ud574\uc11c Bean\uc744 \ub4f1\ub85d\ud558\ub294 \uae30\ubcf8 \uc124\uc815\uc785\ub2c8\ub2e4. \uc5ec\uae30\uc11c Bean\uc758 \uc774\ub984\uc744 jasyptEncryptor\ub77c\uace0 \uc124\uc815\ud588\ub2e4\uba74 \ud504\ub85c\ud37c\ud2f0 \ub4f1\ub85d\ud574\uc57c \ud569\ub2c8\ub2e4.\\n\\n```java\\n@Configuration\\npublic class JasyptConfig {\\n\\n private String ENCRYPT_KEY = \\"hello\\";\\n\\n @Bean(name = \\"jasyptEncryptor\\")\\n public StringEncryptor stringEncryptor() {\\n PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();\\n\\n SimpleStringPBEConfig config = new SimpleStringPBEConfig();\\n\\n config.setPassword(ENCRYPT_KEY);\\n config.setAlgorithm(\\"PBEWithMD5AndDES\\");\\n config.setKeyObtentionIterations(1000);\\n config.setPoolSize(1);\\n config.setSaltGeneratorClassName(\\"org.jasypt.salt.RandomSaltGenerator\\");\\n config.setStringOutputType(\\"base64\\");\\n encryptor.setConfig(config);\\n return encryptor;\\n }\\n}\\n```\\n\\n```java\\njasypt:\\n encryptor:\\n bean: jasyptEncryptor\\n```\\n\\n### 3. \uc554\ud638\ud654\\n\\n\ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud560 \uc900\ube44\ub294 \uac70\uc758 \ub2e4 \ub05d\ub0ac\uc2b5\ub2c8\ub2e4. \uc774\uc81c \uc554\ud638\ud654\ud558\uc5ec \ud504\ub85c\ud37c\ud2f0\uc5d0 \uc791\uc131\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub54c \uc554\ud638\ud654 \ud558\ub294 \ubc29\ubc95\uc740, \uc544\ub798 \uc0ac\uc774\ud2b8\uc5d0 \uc811\uc18d\ud574 \ud3c9\ubb38\uacfc \ud0a4\ub97c \uc785\ub825\ud55c \ud6c4 \ub098\uc628 \uc554\ud638\ubb38\uc744 \ud504\ub85c\ud37c\ud2f0 \ud30c\uc77c\uc5d0 \'ENC(\uc554\ud638\ubb38)\' \ub85c \uc791\uc131\ud569\ub2c8\ub2e4.\\n\\n[\uc554\ubcf5\ud638\ud654 \uc0ac\uc774\ud2b8](https://www.devglan.com/online-tools/jasypt-online-encryption-decryption)\\n\\n![\ud3c9\ubb38](https://github.com/kiarakim/algorithm/assets/101039161/b0293dfc-e0d8-45a0-91af-becf790a1002)\\n\\n```java\\n datasource:\\n url: \ub370\uc774\ud130\ubca0\uc774\uc2a4 url\\n username: \uacc4\uc815\\n password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)\\n```\\n\\n\ub098\uba38\uc9c0\ub3c4 \ub9c8\uc800 \uc554\ud638\ud654\ud574\uc90d\uc2dc\ub2e4.\\n\\n```java\\n datasource:\\n url: ENC(j94r94hQbd1SfFHGCUeweg+GGDosfnxP8dL0FQxfXtE=)\\n username: ENC(vp3Gw8kLpwDZhmMMqf88/Q==)\\n password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)\\n```\\n\\n## \uc2e4\ud589\\n\\n\uc62c\ubc14\ub978 \uc554\ud638\ubb38\uc744 \uc785\ub825\ud588\ub2e4\uba74 \uc815\uc0c1\uc801\uc73c\ub85c \uc2e4\ud589\uc774 \ub429\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\ub098 \uc774\ub54c \uc784\uc758\ub85c \uc554\ud638\ubb38\uc744 \uc218\uc815\ud55c\ub2e4\uba74 \ub2e4\uc74c\uacfc \uac19\uc774 \ube4c\ub4dc\ub97c \uc2e4\ud328\ud569\ub2c8\ub2e4.\\n\\n![\uc2e4\ud589 \uc2e4\ud328](https://github.com/kiarakim/algorithm/assets/101039161/d003df00-bf4f-4ed2-a1ee-293cd7da6fc1)\\n\\n\uadf8\ub7f0\ub370 \ubb54\uac00 \uc774\uc0c1\ud558\uc9c0 \uc54a\ub098\uc694?\\n\\n\ud504\ub85c\ud37c\ud2f0\ub294 \ubd84\uba85 \uc554\ud638\ud654 \ud588\ub294\ub370 \ud0a4\uac00 \ucf54\ub4dc\uc5d0 \uadf8\ub300\ub85c \ub178\ucd9c\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\nGit\uc758 public Repository\uc5d0 \ubc30\ud3ec\ud558\uba74 \ub2e4\ub978 \uc0ac\ub78c\ub4e4\ub3c4 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc \uc774 \ud0a4\ub97c \uc5b4\ub514\uc5d0 \uc228\uae38 \uc218 \uc788\uc744\uae4c\uc694?\\n\\n\uc800\ub294 \ucc98\uc74c\uc5d0 \uc77c\ubc18 file\uc5d0 \ud0a4\ub97c \ub123\uc5b4\ub193\uace0 \ud30c\uc77c\uc744 \uc77d\uc5b4\uc624\ub294 \uc2dd\uc73c\ub85c \ud0a4\ub97c \uad00\ub9ac\ud558\ub824\uace0 \ud588\uc2b5\ub2c8\ub2e4. \ub2f9\uc5f0\ud788 \ud574\ub2f9 \ud30c\uc77c\uc740 .gitignore\ub85c \ucee4\ubc0b \ub300\uc0c1\uc5d0\uc11c \uc81c\uc678\ud574\uc57c\uaca0\uc8e0.\\n\\n\uadf8\ub7f0\ub370 \uc774\uac83\ubcf4\ub2e4 \ub354 \uc27d\uace0 \ube60\ub978 \ubc29\ubc95\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ubc14\ub85c \ud658\uacbd\ubcc0\uc218\ub97c \uc124\uc815\ud558\ub294 \uac83\uc774\uc8e0.\\n\\n### + \ud658\uacbd\ubcc0\uc218 \uc124\uc815\\n\\n```java\\nprivate String ENCRYPT_KEY = \\"hello\\";\\n```\\n\uae30\uc874\uc758 \ud0a4\ub97c \uad00\ub9ac\ud558\ub294 \ubc29\uc2dd\uc774\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc6b0\uc120 \uc774 \ud0a4\ub97c \ud504\ub85c\ud37c\ud2f0\uc5d0\uc11c \uad00\ub9ac\ud558\ub3c4\ub85d \uc124\uc815\ud574\ubcfc\uae4c\uc694?\\n\\n```java\\n// JasyptConfig.class\\n@Value(\\"${jasypt.encryptor.password}\\")\\n private String ENCRYPT_KEY;\\n```\\n```java\\n// application.yml\\njasypt:\\n encryptor:\\n password: hello\\n```\\n\\n\uc774\uc81c \ud658\uacbd\ubcc0\uc218\ub97c \uc124\uc815\ud574\ubd05\uc2dc\ub2e4.\\n\\nRun > Edit Configurations... \uacbd\ub85c\ub85c \ub4e4\uc5b4\uac00\uba74\\n\\nRun/Debug Configurations \ucc3d\uc774 \ub098\uc624\ub294\ub370\\n\\nEnvironment variables: \ubd80\ubd84\uc5d0 ENCRYPT_KEY=hello\\n\\n\ub77c\uace0 \uc801\uc5b4\uc8fc\uc138\uc694.\\n\\n\uadf8 \ud6c4 \ub2e4\uc2dc yml \ud30c\uc77c\ub85c \ub3cc\uc544\uc640 \uae30\uc874 hello\ub85c \ub418\uc5b4\uc788\ub294 \ubd80\ubd84\uc744 ${ENCRYPT_KEY}\ub85c \ubcc0\uacbd\ud558\uace0 \uc2e4\ud589\ud55c\ub2e4\uba74 \uc815\uc0c1\uc801\uc73c\ub85c \uc791\ub3d9\ub429\ub2c8\ub2e4.\\n\\n```java\\njasypt:\\n encryptor:\\n password: ${ENCRYPT_KEY}\\n```\\n\\n\uae34 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4."},{"id":"9","metadata":{"permalink":"/9","source":"@site/blog/2023-07-09-github_actions_pull_request_test.mdx","title":"Pull Request \uc2dc \uc790\ub3d9\uc73c\ub85c test \uc2e4\ud589\ud558\uae30","description":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4.","date":"2023-07-09T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 9\uc77c","tags":[{"label":"github","permalink":"/tags/github"},{"label":"action","permalink":"/tags/action"},{"label":"pr","permalink":"/tags/pr"},{"label":"test","permalink":"/tags/test"}],"readingTime":8.985,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"9","title":"Pull Request \uc2dc \uc790\ub3d9\uc73c\ub85c test \uc2e4\ud589\ud558\uae30","authors":["boxster"],"tags":["github","action","pr","test"]},"prevItem":{"title":"jasypt\ub97c \ud65c\uc6a9\ud558\uc5ec \ud504\ub85c\ud37c\ud2f0\ub97c \uc554\ud638\ud654\ud558\uc790","permalink":"/12"},"nextItem":{"title":"webpack\uc73c\ub85c msw \uc124\uc815\ud558\uae30","permalink":"/10"}},"content":"\uc548\ub155\ud558\uc138\uc694 \ubc15\uc2a4\ud130\uc785\ub2c8\ub2e4.\\n## Pull Request\uc2dc \uc790\ub3d9\uc73c\ub85c test\ub97c \uc2e4\ud589\ud558\uba74 \uc88b\uc740 \uc810\\npull request \uc0dd\uc131 \uc2dc \uc790\ub3d9\uc73c\ub85c \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub824\uc900\ub2e4\uba74 \ub2e4\ub978 \ud300\uc6d0\uc758 pr\uc744 \uad73\uc774 \uc81c \ub85c\uceec\uc5d0 clone\ud558\uc5ec \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub824\ubcf4\uc9c0 \uc54a\uc544\ub3c4 \ub429\ub2c8\ub2e4.\\n\ub9ce\uc740 \uc2dc\uac04\uc744 \ub2e8\ucd95\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 test\uac00 \uc2e4\ud328\ud55c\ub2e4\uba74 \uac15\uc81c\ub85c Merge\uac00 \ub418\uc9c0 \uc54a\ub3c4\ub85d \ud55c\ub2e4\uba74 \uc2e4\uc218\ub85c \ud14c\uc2a4\ud2b8\uac00 \ub418\uc9c0 \uc54a\ub294 \ucee4\ubc0b\uc744 \uc62c\ub9ac\ub294 \uac83\uc744 \ubc29\uc9c0\ud560 \uc218 \uc788\uaca0\uc8e0.\\n\\n\uc774 \ub450\uac00\uc9c0\ub9cc\uc73c\ub85c\ub3c4 \uc0dd\uc0b0\uc131\uc774 \ub9ce\uc774 \uc62c\ub77c\uac08 \uac83\uc744 \uae30\ub300\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uc5b4\ub5bb\uac8c \ud560 \uc218 \uc788\ub098\uc694\\n\\nGithub Action\uc744 \uc774\uc6a9\ud558\uc5ec \uc124\uc815\ud55c \uc870\uac74\uc5d0 \ub9de\ub294 \uc0c1\ud669\uc5d0\uc11c \uba85\ub839\uc5b4\ub97c \uc2e4\ud589\ud558\uc5ec test\ub97c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### Github Action \ud30c\uc77c \uc0dd\uc131\\n\\n1. \uba3c\uc800 \ucd5c\uc0c1\uc704 \ud3f4\ub354\uc5d0 `.github/workflows` \ud3f4\ub354\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.\\n2. \ud574\ub2f9 \ud3f4\ub354 \ub0b4\uc5d0 `example.yml`\uc744 \uc0dd\uc131\ud569\ub2c8\ub2e4.\\n3. \uc544\ub798\uc640 \uac19\uc774 yml \ud30c\uc77c\uc744 \uc791\uc131\ud569\ub2c8\ub2e4.\\n\\n```yml\\nname: pr test\\n\\non:\\n pull_request:\\n branches:\\n - main\\n - develop\\n\\npermissions:\\n contents: read\\n\\njobs:\\n test:\\n name: merge-test\\n runs-on: ubuntu-latest\\n environment: test\\n defaults:\\n run:\\n working-directory: ./backend\\n steps:\\n - uses: actions/checkout@v3\\n - name: Set up JDK 17\\n uses: actions/setup-java@v3\\n with:\\n java-version: \'17\'\\n distribution: \'adopt\'\\n - name: Grant execute permission for gradlew\\n run: chmod +x gradlew\\n - name: Test with Gradle\\n run: ./gradlew build\\n```\\n\\n### Job \uc774\ub984 \uc124\uc815\\n\ubcf5\uc7a1\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uba3c\uc800 **name** \uc18d\uc131\uc740 github action\uc5d0\uc11c \ubcf4\uc5ec\uc9c8 Job\uc758 \uc774\ub984\uc744 \uc815\ud558\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4.\\n\\n\uc9c0\uae08\uc740 `pr test`\ub85c \ud574\ub450\uc5c8\uc2b5\ub2c8\ub2e4. \uadf8\ub7fc \uc544\ub798 \uc0ac\uc9c4\uacfc \uac19\uc774 \ubc18\uc601\ub429\ub2c8\ub2e4.\\n\\n![workflows name](https://github.com/car-ffeine/car-ffeine.github.io/assets/106640954/28494d8e-66b5-4eec-a98a-414968b03306)\\n\\n### workflow \ud2b8\ub9ac\uac70 \uc124\uc815\\n\ub2e4\uc74c\uc73c\ub860 `on` \uc18d\uc131\uc785\ub2c8\ub2e4. \uc774 \uc18d\uc131\uc740 workflow\ub97c \uc2e4\ud589\ud560 \uc774\ubca4\ud2b8\ub97c \uc9c0\uc815\ud558\ub294\ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \ud2b9\uc815 \uc774\ubca4\ud2b8 \uc720\ud615\uacfc \uc870\uac74\uc744 \uae30\ubc18\uc73c\ub85c workflow\ub97c \ud2b8\ub9ac\uac70\ud558\ub3c4\ub85d \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc608\ub97c \ub4e4\uc5b4 \uc544\ub798\uc640 \uac19\uc774 \uc815\uc758\ud588\uc2b5\ub2c8\ub2e4.\\n```yml\\non:\\n push:\\n branches:\\n - main\\n pull_request:\\n branches:\\n - develop\\n```\\n\uadf8\ub807\ub2e4\uba74 \uc774 workflow\uac00 \uc791\ub3d9\ub418\ub294 \uc2dc\uc810\uc740 `main` \ube0c\ub79c\uce58\uc5d0 **push**\uac00 \ub418\uac70\ub098 `develop` \ube0c\ub79c\uce58\uc5d0 **pull request**\ub97c \ubcf4\ub0bc \ub54c \uc791\ub3d9\ud569\ub2c8\ub2e4.\\n\\n### \uad8c\ud55c \ubd80\uc5ec\\n```yml\\npermissions:\\n contents: read\\n```\\n\uc774\ub7f0 \uad8c\ud55c\uc744 \uc8fc\uac8c \ub41c\ub2e4\uba74 \uc774 job\uc740 \uc77d\uae30 \uad8c\ud55c\ubc16\uc5d0 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \uc2e4\uc218\ub85c \ub2e4\ub978 \uac83\uc744 \ucd94\uac00\ud558\uc9c0 \ubabb\ud558\uac8c \ub9c9\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n### \ub3d9\uc791\ud560 \uba85\ub839\uc5b4 \uc785\ub825\\n```yml\\njobs:\\n test:\\n name: merge-test\\n runs-on: ubuntu-latest\\n environment: test\\n defaults:\\n run:\\n working-directory: ./backend\\n steps:\\n - uses: actions/checkout@v3\\n - name: Set up JDK 17\\n uses: actions/setup-java@v3\\n with:\\n java-version: \'17\'\\n distribution: \'adopt\'\\n - name: Grant execute permission for gradlew\\n run: chmod +x gradlew\\n - name: Test with Gradle\\n run: ./gradlew build\\n```\\n\\n#### name\\n\uc81c\uc77c \uac04\ub2e8\ud788 \ubcfc \uc218 \uc788\ub294 **name** \uc124\uc815\uc740 \uc544\ub798 \uc0ac\uc9c4\ucc98\ub7fc \uc5b4\ub5a4\uc2dd\uc73c\ub85c \ubcf4\uc5ec\uc904\uc9c0 \uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n![job image](https://github.com/car-ffeine/car-ffeine.github.io/assets/106640954/43ebf3c1-4632-447f-89c3-0e74ed01dc3c)\\n\\n#### runs-on\\n**runs-on** \uc18d\uc131\uc785\ub2c8\ub2e4. \ud574\ub2f9 \uc6b4\uc601\uccb4\uc81c\ub97c \uc0ac\uc6a9\ud55c\ub2e4\uace0 \uc815\uc758\ud558\ub294 \ubd80\ubd84\uc785\ub2c8\ub2e4. \uc9c0\uae08\uc740 \uc800\ud76c\uac00 \uc0ac\uc6a9\ud560 ec2\uc640 \uac19\uc740 \ud658\uacbd\uc778 `ubuntu`\uc5d0\uc11c \uc791\ub3d9\ud558\ub3c4\ub85d \uc124\uc815\ud588\uc9c0\ub9cc,\\n`windows-latest`, `macos-latest`\ub85c \ubcc0\uacbd\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### environment\\n**environment** \uc18d\uc131\uc785\ub2c8\ub2e4. \ud574\ub2f9 \uc18d\uc131\uc740 \uaf2d \ud544\uc694\ud55c \ubd80\ubd84\uc774 \uc544\ub2c8\uc9c0\ub9cc branch\uc758 rule \uc124\uc815\uc5d0 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \ud658\uacbd\uc744 \ud55c\uaebc\ubc88\uc5d0 \uad00\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \ubd80\ubd84\uc740 \uc544\ub798\uc5d0 branch rule\uc744 \uc815\ud558\ub294 \ubd80\ubd84\uc744 \ubcf4\uc2dc\uba74 \uc544\ub9c8 \uc774\ud574\uac00 \ub420 \uac83 \uc785\ub2c8\ub2e4.\\n\\n#### defaults\\n\ud574\ub2f9 \uc18d\uc131\uc740 \uc5b4\ub5a4 \ud3f4\ub354\uc5d0\uc11c \uba85\ub839\uc5b4\ub97c \uc2e4\ud589\ud560 \uc9c0 \uc9c0\uc815\ud569\ub2c8\ub2e4. \uc9c0\uae08\uc758 \uc800\ud76c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c\ub294 \ud55c repository\uc5d0 backend, frontend \ud3f4\ub354\ub97c \ub098\ub204\uc5c8\uae30 \ub54c\ubb38\uc5d0 backend \ud3f4\ub354\ub85c \uc774\ub3d9\ud558\uc5ec \uba85\ub839\uc5b4\ub97c \uc2e4\ud589\ud574\uc57c \ud569\ub2c8\ub2e4.\\n\\n\uadf8\ub798\uc11c **working-directory**\ub97c `./backend`\ub77c\uace0 \uc9c0\uc815\ud588\uc2b5\ub2c8\ub2e4.\\n\\n#### steps\\n\\n\uc81c\uc77c \uc911\uc694\ud55c **steps**\uc785\ub2c8\ub2e4. \ud574\ub2f9 \uc18d\uc131\uc740 \uc5b4\ub5a4 \uba85\ub839\uc5b4\ub97c \uc5b4\ub5a4 \uc21c\uc11c\ub85c \uc2e4\ud589\uc2dc\ud0ac\uc9c0 \uc815\uc758\ud569\ub2c8\ub2e4. \uc9c0\uae08\uc758 workflow\uc5d0\uc120\\n\\n1. Java 17 \uc124\uce58\\n2. gradlew \ud30c\uc77c\uc5d0 \uc2e4\ud589 \uad8c\ud55c \ubd80\uc5ec\\n3. gradle build \uc2e4\ud589\\n\\n\uc21c\uc73c\ub85c \ub3d9\uc791\ud569\ub2c8\ub2e4.\\n\\n### \ub2e4\ub978 \uc870\uac74\uacfc \uc774\ubca4\ud2b8\ub3c4 \ucd94\uac00\ud558\uace0 \uc2f6\uc5b4\uc694\\n\\n\uc800\ud76c \ud504\ub85c\uc81d\ud2b8\ub294 \ud558\ub098\uc758 repository\uc5d0\uc11c frontend, backend \ucf54\ub4dc\ub97c \uac19\uc774 \uad00\ub9ac\ud558\ub294 \uc0c1\ud669\uc785\ub2c8\ub2e4. \ud558\uc9c0\ub9cc frontend \ucf54\ub4dc\ub97c \uc218\uc815\ud588\ub2e4\uace0 java \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub9ac\ub294 \uac83\uc740 \uc624\ud788\ub824 \uc0dd\uc0b0\uc131\uc774 \uc904\uc5b4\ub4e4\uaca0\uc8e0.\\n\\n\uadf8\ub9ac\uace0 frontend\ub3c4 \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub9ac\uace0 \uc2f6\uc9c0\ub9cc gradle\uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7f4 \ub54c \uac04\ub2e8\ud55c \uc18d\uc131\uc744 \ucd94\uac00\ud558\uba74 **\ud30c\uc77c\uc758 \ubcc0\uacbd\uc5d0 \ub530\ub77c \ud574\ub2f9 job\uc744 \uc2e4\ud589\ud560 \uc870\uac74**\uc744 \uc815\uc758\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```yml\\non:\\n pull_request:\\n branches:\\n - main\\n - develop\\n paths:\\n - backend/**\\n - .github/**\\n```\\n\uc704\uc640 \ub2ec\ub9ac \uc9c0\uae08 **pull request**\uc5d0\ub294 \uc18d\uc131\uc774 \ud558\ub098\uac00 \ub354 \uc788\ub294\ub370\uc694. **paths**\ub97c \uc801\uc6a9\ud558\uba74 `backend` \ud3f4\ub354 \ud558\uc704\uc758 \ubb34\uc5b8\uac00 \ubcc0\uacbd\uc774 \uc788\ub294 **pull request**\uc5d0\ub9cc \uc791\ub3d9\uc744 \ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\uadf8\ub7fc backend\uc758 workflow \ud30c\uc77c\uc5d0 **paths** \uc18d\uc131\uc744 \ud558\ub098 \ucd94\uac00\ud558\uace0, \ube44\uc2b7\ud55c frontend workflow\ub97c \ub9cc\ub4e4\uc5b4\uc8fc\uba74 \ub418\uaca0\uc8e0.\\n```yml\\nname: frontend test\\n\\non:\\n pull_request:\\n branches:\\n - main\\n - develop\\n paths:\\n - frontend/**\\n\\npermissions:\\n contents: read\\n\\njobs:\\n test:\\n name: jest\\n runs-on: ubuntu-latest\\n environment: test\\n defaults:\\n run:\\n working-directory: ./frontend\\n steps:\\n - uses: actions/checkout@v3\\n - name: NPM Install\\n run: npm i\\n - name: Jest run\\n run: npm run test\\n```\\n\uc774\ub7f0 \uc2dd\uc73c\ub85c yml \ud30c\uc77c\uc744 \ud558\ub098 \ucd94\uac00\ud558\uba74 **frontend\uc758 \uc218\uc815\uc774 \uc77c\uc5b4\ub0a0 \ub54c\ub294 jest**\ub97c \uc2e4\ud589\ud558\uace0, **backend \ud3f4\ub354\uc758 \uc218\uc815\uc774 \uc77c\uc5b4\ub098\uba74 gradlew**\ub97c \uc2e4\ud589\ud558\uac8c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## Test\uac00 \uc2e4\ud328\ud558\ub294 PR\uc740 Merge \ub9c9\uae30\\n\\nTest\uac00 \uc2e4\ud328\ud558\ub294 Pull Request\uac00 Merge \ub418\ub294 \uc77c\uc740 \uc808\ub300\ub85c \uc5c6\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uadf8\ub7f0 \uc2e4\uc218\ub97c \ubc29\uc9c0\ud558\ub824\uba74 \ud300\uc6d0 \uc804\ubd80\uac00 \ub9ac\ubdf0\ud560 \ub54c \ud14c\uc2a4\ud2b8\ub97c \ub3cc\ub824\ubd10\uc57c\ud558\ub294 \uadc0\ucc2e\uc74c\uc774 \uc0dd\uae38 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc0ac\ub78c\uc740 \uc2e4\uc218\ud574\ub3c4 \uae30\uacc4\ub294 \uac70\uc9d3\ub9d0\uc744 \ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc790\ub3d9\uc73c\ub85c \ub9c9\ub3c4\ub85d \ub3d9\uc791\ud558\uac8c \ub9cc\ub4e4\uc5b4\ub193\uc73c\uba74 \uadf8\ub7f4 \uc77c\uc744 \ubbf8\uc5f0\uc5d0 \ubc29\uc9c0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### Environments \ud655\uc778\ud558\uae30\\n\uba3c\uc800 \ud574\ub2f9 Repository\uc758 Settings -> Environments \ud0ed\uc73c\ub85c \ub4e4\uc5b4\uac11\ub2c8\ub2e4.\\n![environments](https://github.com/car-ffeine/car-ffeine.github.io/assets/106640954/4e3e867a-1037-46bc-865a-6d7d52527518)\\n\uc544\uae4c **environment** \uc18d\uc131\uc744 \ubcf4\uba74 `test`\ub77c\uace0 \uc124\uc815\ud574\ub193\uc740 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ud658\uacbd\uc774 \uc5ec\uae30\uc5d0 \uc801\uc6a9\ub429\ub2c8\ub2e4.\\n\\n### Branch rule \uc815\uc758\ud558\uae30\\n\uc774\ubc88\uc5d0\ub294 \ud574\ub2f9 Repository\uc758 Settings -> Branches \ud0ed\uc73c\ub85c \ub4e4\uc5b4\uac11\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc6d0\ud558\ub294 branch\uc5d0 \ub4e4\uc5b4\uac00 `edit` \ubc84\ud2bc\uc744 \ub204\ub985\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc0ac\uc9c4\uacfc \uac19\uc774 **Require deployments to succeed before merging** \uc18d\uc131\uc744 \ud074\ub9ad\ud569\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc544\ub798\uc640 \uac19\uc774 \uc5b4\ub5a4 \ud658\uacbd\uc744 \uc801\uc6a9\ud560 \uac83\uc778\uc9c0 \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774 \uc18d\uc131\uc740 \ud574\ub2f9 \ubc30\ud3ec\uac00 \uc131\uacf5\ud574\uc57c merge \ud560 \uc218 \uc788\ub3c4\ub85d \ube0c\ub79c\uce58\ub97c \ubcf4\ud638\ud558\ub294 \uae30\ub2a5\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0 \uc800\ud76c\ub294 frontend\uc640 backend Job\uc758 \ud658\uacbd\uc744 \ub458 \ub2e4 test\ub77c\ub294 \uc774\ub984\uc73c\ub85c \uc815\uc758\ud588\uae30 \ub54c\ubb38\uc5d0 \ud558\ub098\uc758 environment\ub9cc \uc120\ud0dd\ud574\ub3c4 \ub458 \ub2e4 \uc801\uc6a9\ub418\ub294 \ud6a8\uacfc\ub97c \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n![branch rule](https://github.com/car-ffeine/car-ffeine.github.io/assets/106640954/02be4679-56a2-4e47-ae01-b7025f8778a4)\\n\\n#### \uc801\uc6a9 \ud6c4\\n\\n\uc544\ub798\uc640 \uac19\uc774 merge\uac00 \uc548\ub41c\ub2e4\ub294 \uae00\uacfc \ube68\uac04\uc0c9\uc73c\ub85c \uacbd\uace0 \ud45c\uc2dc\ub97c \ud574\uc8fc\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n![blocked](https://github.com/car-ffeine/car-ffeine.github.io/assets/106640954/7dfba566-c8c8-4a24-a0e3-42081f3af31c)\\n\\n\\n## \uacb0\ub860\\n\\n\uac04\ub2e8\ud55c github action\uc744 \ud1b5\ud574\uc11c \uc0dd\uc0b0\uc131\uc744 \ub9ce\uc774 \uc62c\ub9b4 \uc218 \uc788\ub294 \uc88b\uc740 \uae30\ub2a5\uc778 \uac83 \uac19\uc2b5\ub2c8\ub2e4. \ub2e4\ub978 \ud300\ub4e4\ub3c4 \uc774 \uae30\ub2a5\uc744 \ub3c4\uc785\ud558\uc5ec \uc0ac\uc6a9\ud558\ub294 \uac83\uc744 \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4."},{"id":"10","metadata":{"permalink":"/10","source":"@site/blog/2023-07-09-msw-setup-with-webpack.mdx","title":"webpack\uc73c\ub85c msw \uc124\uc815\ud558\uae30","description":"\uc6f9\ud329\uc5d0\uc11c msw \uc124\uc815","date":"2023-07-09T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 9\uc77c","tags":[{"label":"msw","permalink":"/tags/msw"},{"label":"webpack","permalink":"/tags/webpack"}],"readingTime":4.35,"hasTruncateMarker":false,"authors":[{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"}],"frontMatter":{"slug":"10","title":"webpack\uc73c\ub85c msw \uc124\uc815\ud558\uae30","authors":["scent"],"tags":["msw","webpack"]},"prevItem":{"title":"Pull Request \uc2dc \uc790\ub3d9\uc73c\ub85c test \uc2e4\ud589\ud558\uae30","permalink":"/9"},"nextItem":{"title":"\uc2a4\ud504\ub9c1\uc5d0\uc11c \ubc1c\uc0dd\ud55c \uc5d0\ub7ec \ub85c\uadf8\ub97c \uc2ac\ub799\uc73c\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95","permalink":"/8"}},"content":"## \uc6f9\ud329\uc5d0\uc11c msw \uc124\uc815\\n\\n\uc774\ubc88 \ud300 \ud504\ub85c\uc81d\ud2b8\ub294 CRA\uc640 \uac19\uc740 \ubcf4\uc77c\ub7ec \ud50c\ub808\uc774\ud2b8 \ucf54\ub4dc\ub97c \uc0ac\uc6a9\ud558\uc9c0 \ubabb\ud558\uac8c \uc81c\ud55c\uc774 \uc788\ub2e4. \ub610\ud55c \uc694\uc998 \ub9ce\uc774 \uc0ac\uc6a9\ub41c\ub2e4\ub294 Vite\uc758 \uc0ac\uc6a9\ub3c4 \uc81c\ud55c\uc774 \uc788\uace0, \uc6f9\ud329\uc73c\ub85c \ud504\ub85c\uc81d\ud2b8\ub97c \uc2dc\uc791\ud558\ub3c4\ub85d \uac15\uc81c\ud558\uace0 \uc788\ub2e4.\\n\\n\ud300\uc6d0 \ubaa8\ub450 \ud55c \ubc88\ub3c4 \uc6f9\ud329\uc744 \ud1b5\ud574 \ud504\ub85c\uc81d\ud2b8\ub97c \uc2dc\uc791\ud574\ubcf8 \uacbd\ud5d8\uc774 \uc5c6\uc5b4 \ud504\ub860\ud2b8\uc5d4\ub4dc \ud300\uc6d0 \uac01\uc790 \uac1c\uc778 \ub808\ud3ec\uc5d0\uc11c \uc6f9\ud329 \uacf5\ubd80\ub97c \uc9c4\ud589\ud55c \ud6c4 \uc5b4\ub290\uc815\ub3c4 \uc9c4\ucc99\uc774 \uc788\uc744 \ub54c \ud300 \ub808\ud3ec\uc5d0 \ud504\ub85c\uc81d\ud2b8\ub97c \uc2dc\uc791\ud558\uae30\ub85c \ud588\ub2e4.\\n\\n\ub2e4\ud589\ud788 \uc6f9\ud329\uc73c\ub85c \uc2dc\uc791\ud558\ub294 \ud504\ub85c\uc81d\ud2b8\uc5d0 \ub300\ud55c \ub9ce\uc740 \ucc38\uace0 \uc790\ub8cc\ub4e4\uc774 \uc788\uc5b4 \uccab \ub9ac\uc561\ud2b8 \ud504\ub85c\uc81d\ud2b8 \ud654\uba74\uc744 \ub744\uc6b0\ub294\ub370 \uae4c\uc9c0\ub294 \uadf8\ub9ac \uc624\ub79c \uc2dc\uac04\uc774 \uac78\ub9ac\uc9c0 \uc54a\uc558\ub2e4. \uadf8\ub807\uac8c \ubaa8\ub4e0 \ud300\uc6d0\uc774 \uccab \uc6f9\ud329 \ud504\ub85c\uc81d\ud2b8\ub97c \uc131\uacf5\uc2dc\ud0a8 \ud6c4 \ubaa8\uc5ec \ud300 \ud504\ub85c\uc81d\ud2b8 \ucd08\uae30 \uc124\uc815\uc744 \uc2dc\uc791\ud574\ubcf4\uc558\ub2e4.\\n\\neslint, prettier, \uc6f9\ud329 \ub4f1\ub4f1 \uc5ec\ub7ec \uc124\uc815\ub4e4\uc744 \ud558\uace0 \ud544\uc694\ud55c \ud328\ud0a4\uc9c0\ub97c \uc124\uce58\ud558\ub294\ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\ub2e4. \ud070 \ub370\uc774\ud130\ub97c \ub2e4\ub8e8\ub294 \ubc31\uc5d4\ub4dc\uc758 \uac1c\ubc1c \uc18d\ub3c4\ub97c \uace0\ub824\ud574 \ud504\ub860\ud2b8\uc5d4\ub4dc \uac1c\ubc1c\uc744 \uc9c4\ud589\ud558\uae30 \uc704\ud574\uc11c \ubbf8\uc158\uc911\uc5d0 \ubc30\uc6e0\ub358 MSW \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\uae30\ub85c \uacb0\uc815\ud588\ub294\ub370, \uc774 \ub77c\uc774\ube0c\ub7ec\ub9ac\uac00 \uc6b0\ub9ac \ud300\uc758 \uac1c\ubc1c \ud658\uacbd\uc5d0\uc11c \ub3d9\uc791\ud558\uc9c0 \uc54a\uc558\ub2e4.\\n\\n\uc65c \ub3d9\uc791\ud558\uc9c0 \uc54a\ub294\uc9c0 \uc6d0\uc778\uc744 \ucc3e\uc544\ubcf4\ub2c8 MSW service worker \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\ub2e4\ub294 \uc624\ub958 \uba54\uc138\uc9c0\uac00 \ub098\uc624\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc5c8\ub2e4. \uc6d0\uc778\uc744 \ub354 \uc790\uc138\ud788 \uc54c\uc544\ubcf4\ub2c8 public \ud3f4\ub354\uc5d0 \uc788\ub294 \ud30c\uc77c\ub4e4\uc740 \uc6f9\ud329\uc774 \ubc88\ub4e4\ub9c1\uc744 \uc9c4\ud589\ud560 \ub54c \ud3ec\ud568\uc774 \ub418\uc9c0 \uc54a\ub294\ub2e4\ub294 \uac83\uc744 \uc54c \uc218 \uc788\uc5c8\uace0, \uc774\ub97c \uc5b4\ub5bb\uac8c \ud574\uacb0\ud560 \uc9c0 \ud300\uc6d0\ub4e4\uacfc \ubc29\ubc95\uc744 \ucc3e\uc544\ubcf4\uc558\ub2e4.\\n\\n\uc57d \ud55c\uc2dc\uac04\ucbe4 \uc9c0\ub0ac\uc744 \ubb34\ub835 copy-webpack-plugin \ud328\ud0a4\uc9c0\ub97c \ud1b5\ud574 public \uacbd\ub85c\uc5d0 \uc788\ub294 \ud30c\uc77c\ub4e4\ub3c4 \ube4c\ub4dc \ud3f4\ub354\uc5d0 \ud3ec\ud568\uc2dc\ud0ac \uc218 \uc788\ub2e4\ub294 \uac83\uc744 \uc54c\uac8c \ub418\uc5c8\ub2e4. \ud558\uc9c0\ub9cc \uc774 copy-webpack-plugin\uc5d0 \ub300\ud55c \uc0ac\uc6a9\ubc95\uc774 \ubbf8\uc219\ud574 public \ud3f4\ub354\uc5d0 \uc788\ub294 mockServiceWorker.js \ud30c\uc77c\ub9cc \ube4c\ub4dc \ud3f4\ub354\ub85c \uc62e\uacbc\uc5b4\uc57c \ud588\ub294\ub370 index.html\uacfc \uac19\uc740 \ub2e4\ub978 \ud30c\uc77c\ub4e4 \uae4c\uc9c0 \ud55c\uaebc\ubc88\uc5d0 \ube4c\ub4dc \ud3f4\ub354\ub85c \uc62e\uaca8\uc9c0\uac8c \ub418\uc5c8\ub2e4.\\n\\n\uc774\ub7f0 \uc800\ub7f0 \ubc29\ubc95\ub4e4\uc744 \uc2dc\ub3c4\ud574\ubcf4\ub2e4 webpack.config.js \ud30c\uc77c\uc758 plugins\uc5d0 \uc544\ub798\uc640 \uac19\uc740 \uc124\uc815\uc744 \ucd94\uac00 \ud574\uc8fc\uc5b4 MSW\ub97c \ud504\ub85c\uc81d\ud2b8\uc5d0 \uc801\uc6a9\ud560 \uc218 \uc788\uac8c \ub418\uc5c8\ub2e4.\\n\\n```jsx\\nnew CopyWebpackPlugin({\\n patterns: [\\n { from: \'public/mockServiceWorker.js\', to: \'.\' }, // msw service worker\\n ],\\n}),\\n```\\n\\n\uc124\uc815\uc744 \uac04\ub2e8\ud788 \ubcf4\uba74 public \uacbd\ub85c\uc5d0 \uc788\ub294 mockServiceWorker.js \ud30c\uc77c\uc744 \ube4c\ub4dc \ud6c4 \ud3f4\ub354\uc758 \ub8e8\ud2b8 \ub514\ub809\ud1a0\ub9ac\uc5d0 \ucd94\uac00\ud574\uc900\ub2e4\ub294 \uc124\uc815\uc774\ub2e4.\\n\\n\ubb38\uc81c \uc0c1\ud669\uacfc \ud574\uacb0 \ubc29\ubc95\uc744 \uac04\ub2e8\ud558\uac8c \ub2e4\uc2dc \uc815\ub9ac\ud574\ubcf4\uba74 \ub2e4\uc74c\uacfc \uac19\ub2e4.\\n\\n1. MSW\ub97c \uc801\uc6a9\ud574\ubcf4\ub824\uace0 \ud568.\\n2. \uc6f9\ud329\uc5d0\uc11c \uac1c\ubc1c \uc11c\ubc84\ub97c \uc5f4\uc5c8\uc744 \ub54c MSW \uc2e4\ud589\uc744 \uc704\ud574 \ud544\uc694\ud55c mockServiceWorker.js \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\ub2e4\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud568.\\n3. \ubb38\uc81c\uc758 \uc6d0\uc778\uc740 \uc6f9\ud329\uc5d0\uc11c \ubc88\ub4e4\ub9c1\uc744 \uc9c4\ud589\ud560 \ub54c public \ud3f4\ub354 \ud558\uc704 \uacbd\ub85c\uc5d0 \uc788\ub294 \ud30c\uc77c\ub4e4\uc744 \ubb34\uc2dc\ud558\uae30 \ub54c\ubb38\uc774\uc5c8\uc74c.\\n4. \ubb38\uc81c\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574 public \uacbd\ub85c\uc5d0 \uc788\ub294 mockServiceWorker.js \ud30c\uc77c\uc744 \ubc88\ub4e4\ub9c1 \ud6c4 \ud3f4\ub354\uc758 \ub8e8\ud2b8 \ub514\ub809\ud1a0\ub9ac\uc5d0 \uc800\uc7a5\ud558\ub3c4\ub85d \ud558\ub294 \uc124\uc815\uc744 \ucd94\uac00\ud574\uc90c."},{"id":"8","metadata":{"permalink":"/8","source":"@site/blog/2023-07-07-error-slack-notification.mdx","title":"\uc2a4\ud504\ub9c1\uc5d0\uc11c \ubc1c\uc0dd\ud55c \uc5d0\ub7ec \ub85c\uadf8\ub97c \uc2ac\ub799\uc73c\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95","description":"\uc548\ub155\ud558\uc138\uc694 \uce74\ud398\uc778\ud300 nunu\uc785\ub2c8\ub2e4.","date":"2023-07-07T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 7\uc77c","tags":[{"label":"spring","permalink":"/tags/spring"},{"label":"slack","permalink":"/tags/slack"},{"label":"error","permalink":"/tags/error"}],"readingTime":11.83,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"8","title":"\uc2a4\ud504\ub9c1\uc5d0\uc11c \ubc1c\uc0dd\ud55c \uc5d0\ub7ec \ub85c\uadf8\ub97c \uc2ac\ub799\uc73c\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95","authors":["nunu"],"tags":["spring","slack","error"]},"prevItem":{"title":"webpack\uc73c\ub85c msw \uc124\uc815\ud558\uae30","permalink":"/10"},"nextItem":{"title":"\uae43 \ucee4\ubc0b \uba54\uc2dc\uc9c0\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud560 \uc21c \uc5c6\uc744\uae4c?","permalink":"/7"}},"content":"\uc548\ub155\ud558\uc138\uc694 \uce74\ud398\uc778\ud300 nunu\uc785\ub2c8\ub2e4.\\n\\n\uc624\ub298\uc740 \uc2a4\ud504\ub9c1\uc5d0\uc11c \ubc1c\uc0dd\ud55c \uc5d0\ub7ec \ub85c\uadf8\ub97c \uc2ac\ub799\uc73c\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95\uc5d0 \ub300\ud574\uc11c \uc54c\uc544\ubcf4\ub824\uace0 \ud569\ub2c8\ub2e4.\\n\\n\ubaa9\ucc28\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n1. \uc2a4\ud504\ub9c1\uc5d0\uc11c \ub85c\uadf8\ub97c \ub0a8\uae30\ub294 \ubc29\ubc95\\n2. Slf4 j\uc758 \ub3d9\uc791\uc6d0\ub9ac\\n3. Logback\uc758 \ub3d9\uc791\uc6d0\ub9ac\\n4. Logback\uc744 \uc0ac\uc6a9\ud574\uc11c \uc2ac\ub799\uc73c\ub85c \uc5d0\ub7ec \ub85c\uadf8\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95\\n\\n## \uc2a4\ud504\ub9c1\uc5d0\uc11c \ub85c\uadf8\ub294 \uc5b4\ub5bb\uac8c \ucc0d\uc744\uae4c?\\n\\n\uc2a4\ud504\ub9c1\uc5d0\uc11c \ub85c\uadf8\ub97c \ucc0d\ub294 \ubc29\ubc95\uc740 \uc5ec\ub7ec \uac00\uc9c0\uac00 \uc788\uc9c0\ub9cc, \uac00\uc7a5 \uac04\ub2e8\ud55c \ubc29\ubc95\uc740 `System.out.println()`\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc785\ub2c8\ub2e4.\\n\\n```java\\n@RestController\\npublic class TestController {\\n\\n @GetMapping(\\"/test\\")\\n public String test() {\\n System.out.println(\\"test\\");\\n return \\"test\\";\\n }\\n}\\n```\\n\\n\ub2f9\uc5f0\ud558\uc9c0\ub9cc, \uc131\ub2a5\uc774 \uc548 \uc88b\uc544\uc11c \uc2e4\uc81c \uc11c\ube44\uc2a4\uc5d0\uc11c\ub294 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n\uc2a4\ud504\ub9c1\uc5d0\uc11c\ub294 Slf4 j\ub97c \ud1b5\ud574\uc11c \ub85c\uadf8\ub97c \ub0a8\uae38 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n```java\\n@Slf4j // private final Logger log = LoggerFactory.getLogger(this.getClass()); \uc640 \uac19\ub2e4.\\n@RestController\\npublic class TestController {\\n\\n @GetMapping(\\"/test\\")\\n public String test() {\\n log.info(\\"test\\");\\n return \\"test\\";\\n }\\n}\\n```\\n\\n\uc774 \ucf54\ub4dc\ub97c \ud1b5\ud574\uc11c \ub85c\uadf8\ub97c \ub0a8\uae38 \uc218 \uc788\ub294\ub370, \uc790\ub3d9\uc73c\ub85c \ucf58\uc194\uc5d0 \ucd9c\ub825\uc774 \ub429\ub2c8\ub2e4.\\n\\n## \uc2a4\ud504\ub9c1\uc5d0\uc11c \ub85c\uae45\uc740 \uc5b4\ub5bb\uac8c \uc791\ub3d9\ud558\ub294 \uac70\uc9c0?\\n\\n\uc2a4\ud504\ub9c1 4\uae4c\uc9c0\ub294 `Commons Logging`\uc744 \uc0ac\uc6a9\ud588\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n`Commons Logging`\uc740 `JCL`\uc774\ub77c\uace0\ub3c4 \ubd88\ub9ac\uba70, `JDK Logging`, `Log4 j,` `Logback` \ub4f1 \ub2e4\uc591\ud55c \ub85c\uae45 \ud504\ub808\uc784\uc6cc\ud06c\ub97c \uc9c0\uc6d0\ud569\ub2c8\ub2e4.\\n\\nJCL \uc740 \ub7f0\ud0c0\uc784\uc5d0 \uc5b4\ub5a4 \ub85c\uae45 \ud504\ub808\uc784\uc6cc\ud06c\ub97c \uc0ac\uc6a9\ud560\uc9c0 \uacb0\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub7f0\ud0c0\uc784\uc5d0 \uc5b4\ub5a4 \ub85c\uae45 \ud504\ub808\uc784\uc6cc\ud06c\ub97c \uc0ac\uc6a9\ud560\uc9c0 \uacb0\uc815\ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \ud074\ub798\uc2a4 \ub85c\ub354\uc5d0\uac8c \uc9c8\uc758\ub97c \ud558\ub294 \ubc29\uc2dd\uc73c\ub85c \uc791\ub3d9\ud558\uac8c \ub418\ub294\ub370\\n\\n\ud074\ub798\uc2a4 \ub85c\ub354\uc5d0\uac8c \uc9c8\uc758\ub97c \ud588\uc744 \uacbd\uc6b0\uc5d0 \uba87 \uac00\uc9c0 \ubb38\uc81c\uc810\uc774 \uc0dd\uae41\ub2c8\ub2e4\\n\\n1. \ud074\ub798\uc2a4 \ub85c\ub354\uc5d0 \uba85\ud655\ud55c \ud45c\uc900\uc774 \uc5c6\uace0, \ubd80\ubaa8 \uc790\uc2dd \ubaa8\ub378\uc774 \uc788\uc5b4\uc11c, \ud074\ub798\uc2a4 \ub85c\ub354\uc5d0 \ub530\ub77c\uc11c \ub2e4\ub978 \uacb0\uacfc\uac00 \ub098\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4. [\ucc38\uace0](http://articles.qos.ch/classloader.html)\\n2. \ud074\ub798\uc2a4\ub85c\ub354\ub294 gc\uc758 \ub3d9\uc791\uc5d0 \ubc29\ud574\ub97c \uc77c\uc73c\ucf1c\uc11c \uba54\ubaa8\ub9ac \ub204\uc218\ub97c \ubc1c\uc0dd\uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4. [\ucc38\uace0](https://cwiki.apache.org/confluence/display/COMMONS/Logging+UndeployMemoryLeak)\\n\\n`@Slf4j` \uc5b4\ub178\ud14c\uc774\uc158\uc744 \ubd99\uc774\uba74, \ucef4\ud30c\uc77c \uc2dc\uc810\uc5d0 `private final Logger log = LoggerFactory.getLogger(this.getClass());` \uc640 \uac19\uc740 \ucf54\ub4dc\ub85c \ubcc0\ud658\ub429\ub2c8\ub2e4.\\n\\n\uc2a4\ud504\ub9c1 5\uc5d0\uc11c\ub294 Slf4j \uac00 \uc0ac\uc6a9\ud558\ub294 \uac83\ucc98\ub7fc, \ucef4\ud30c\uc77c \ud0c0\uc784\uc5d0 \uc5b4\ub5a4 \ub85c\uae45 \ud504\ub808\uc784\uc6cc\ud06c\ub97c \uc0ac\uc6a9\ud560\uc9c0 \uacb0\uc815\ud558\ub294 \uae30\ub2a5\uc744 \uc791\uc131\ud588\uace0, `Commons Logging`\uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n[spring 5\uc5d0\uc11c \ubcc0\uacbd\ub418\uc5c8\ub2e4\ub294 \ub9c1\ud06c](https://docs.spring.io/spring-framework/docs/5.0.0.RC3/spring-framework-reference/overview.html#overview-logging)\\n\\n## Slf4 j\uc5d0 \ub300\ud574\uc11c \uc54c\uc544\ubcf4\uc790\\n\\nSlf4 j\ub294 \ub85c\uae45\uc744 \uc704\ud55c \uc778\ud130\ud398\uc774\uc2a4\ub97c \uc81c\uacf5\ud558\ub294 \ud504\ub808\uc784\uc6cc\ud06c\uc785\ub2c8\ub2e4.(Simple Logging Facade for Java)\\n\\n\ucef4\ud30c\uc77c \ud0c0\uc784\uc5d0, \uc5b4\ub5a4 \ub85c\uadf8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud560\uc9c0 \uacb0\uc815\ud558\ub294 \uae30\ub2a5\uc744 \uc81c\uacf5\ud569\ub2c8\ub2e4.\\n\\n\ub85c\uadf8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \ubc14\uafb8\ub824\uace0 \ud588\uc744 \ub54c, \uae30\uc874 \ucf54\ub4dc\ub294 \ud558\ub098\ub3c4 \uac74\ub4dc\ub9ac\uc9c0 \uc54a\uace0, \ub85c\uadf8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub9cc \ubc14\uafd4\uc8fc\uba74 \ub418\ub3c4\ub85d \ud574\uc90d\ub2c8\ub2e4.\\n\\n### \uc870\uae08 \ub354 \uc790\uc138\ud55c \ub3d9\uc791 \uc6d0\ub9ac\ub97c \uc54c\uc544\ubcf4\uc790\\n\\n![only slf4j](https://blog.kakaocdn.net/dn/lCcTc/btsmBw3OEJz/1njLV283KdUWc9qyppEdak/img.png)\\n\\nSlf4 j \ub9cc\uc744 \uc0ac\uc6a9\ud588\uc744 \uacbd\uc6b0 \uc704 \uc0ac\uc9c4 \uac19\uc740 \ud615\ud0dc\ub85c \uc694\uccad\uc774 \ucc98\ub9ac\uac00 \ub429\ub2c8\ub2e4.\\n\\nSlf4 j \ub77c\ub294 \uc778\ud130\ud398\uc774\uc2a4\ub97c \ud1b5\ud574\uc11c \ub85c\uadf8\ub97c \ub0a8\uae30\uace0, \uc5b4\ub5a4 \ub85c\uadf8 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud560\uc9c0\ub294 `Slf4j binding`\uc774\ub77c\ub294 \uac83\uc744 \ud1b5\ud574\uc11c \uacb0\uc815\ud569\ub2c8\ub2e4.\\n\\n`Slf4j binding` \uc740 `Slf4j`\uc758 \uc778\ud130\ud398\uc774\uc2a4\ub97c \uad6c\ud604\ud558\uace0 \uc788\uc9c0 \uc54a\uc740 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc758 \uad6c\ud604\uccb4\ub97c \uc5f0\uacb0\ud574 \uc8fc\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\n\uadf8 \uad6c\ud604\uccb4\ub85c `Slf4j-log4 j12-{version}. jar` \uac19\uc740 \uac83\uc774 \uc788\ub2e4.\\n\\n\uc774\uc640\ub294 \ub2e4\ub974\uac8c Logback \uc740 Slf4 j \ub97c \uad6c\ud604\ud558\uace0 \uc788\uae30\uc5d0, `Slf4j binding` \uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc544\ub3c4 \ub429\ub2c8\ub2e4.\\n\\n![logback example](https://blog.kakaocdn.net/dn/IYC3k/btsmy0einLF/F0aiMnteJeGB00fkGdBjRK/img.png)\\n\\n\uc704 \uc0ac\uc9c4\ucc98\ub7fc `Slf4j binding` \uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0, `Logback` \ubc14\ub85c \uc0ac\uc6a9\ud558\ub294 \uac83\ub3c4 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n\\n\uadf8\ub807\ub2e4\uba74 Slf4 j\ub97c \ubc14\ub85c \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc740 \ucf54\ub4dc\uc5d0\uc11c `Slf4j` \ub97c \uc0ac\uc6a9\ud558\ub824\uba74 \uc5b4\ub5bb\uac8c \ud574\uc57c \ud560\uae4c\uc694?\\n\\n![slf4j working principle](https://blog.kakaocdn.net/dn/msTPw/btsmziy04VE/sXSOKYvi9yXSoiRmg6mIGk/img.png)\\n\\n\uc704 \uc0ac\uc9c4\ucc98\ub7fc `Slf4j bridge` \ub97c \ud1b5\ud574\uc11c \uc678\ubd80 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\ucc98\ub7fc \uac08\uc544 \ub07c\uc6b8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n`Log4j2` \ub97c \uc0ac\uc6a9\ud558\ub294 \ucf54\ub4dc\ub97c \uc804\ud600 \ubc14\uafb8\uc9c0 \uc54a\uc544\ub3c4, `Bridge` \uac00 `Slf4j` \ub97c \ud1b5\ud574 `Logback`\uc73c\ub85c \uc790\uc5f0\uc2a4\ub7fd\uac8c \ub85c\uadf8\ub97c \ub0a8\uae38 \uc218 \uc788\ub3c4\ub85d \ud574\uc90d\ub2c8\ub2e4.\\n\\n## Logback\uc5d0 \ub300\ud574\uc11c \uc54c\uc544\ubcf4\uc790\\n\\nLogback \uc740 \uc2a4\ud504\ub9c1\uc5d0\uc11c \uae30\ubcf8\uc73c\ub85c \uc0ac\uc6a9\ub420 \ub9cc\ud07c \uc778\uae30 \uc788\ub294 \ub85c\uadf8 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc785\ub2c8\ub2e4.\\n\\n![logback \ub3d9\uc791 \uacfc\uc815](https://logback.qos.ch/manual/images/chapters/architecture/underTheHoodSequence2_small.gif)\\n\\n\uacf5\uc2dd\ubb38\uc11c\uc5d0\uc11c \uc544\uc8fc \ud575\uc2ec\uc801\uc778 \ub3d9\uc791\uc6d0\ub9ac\ub97c \uc124\uba85\ud574\uc8fc\uace0 \uc788\ub294 \uc0ac\uc9c4\uc774\ub77c\uc11c \uac00\uc838\uc654\uc2b5\ub2c8\ub2e4.\\n\\n\ub108\ubb34 \uc5b4\ub824\uc6cc \ubcf4\uc5ec\uc11c, \uc870\uae08 \uc790\uc138\ud558\uac8c \uac01\uac01\uc758 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud574\uc11c \uc54c\uc544\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\n\uc774\uc5d0 \ub300\ud574 \uc54c\uc544\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\n## \ub85c\uadf8\ubc31\uc758 \uad6c\uc131\uc694\uc18c\\n\\n### Appender\\n\\nAppender\ub294 \ub85c\uadf8\ub97c \uc5b4\ub514\uc5d0 \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\n\uc678\ubd80\ub85c\ubd80\ud130 \uc5b4\ub5a4 \ub370\uc774\ud130\ub97c \ubc1b\uc544\uc11c, \uc5b4\ub5a4 \ubc29\uc2dd\uc73c\ub85c \ucc98\ub9ac\ud560\uc9c0\uc5d0 \ub300\ud574\uc11c \uc804\uccb4\uc801\uc73c\ub85c \uc124\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uae30\ubcf8\uc801\uc73c\ub85c \uc218\ub9ce\uc740 Appender \uac00 \uc81c\uacf5\ub418\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n- ConsoleAppender\\n- FileAppender\\n- RollingFileAppender\\n- AsyncAppender\\n- DBAppender\\n- SMTPAppender\\n- SocketAppender\\n- SyslogAppender\\n\\n\uc800\ud76c\ub294 Slack\uc5d0 \uc54c\ub9bc\uc744 \uc8fc\ub294 \uac83\uc774 \ubaa9\uc801\uc774\uae30 \ub54c\ubb38\uc5d0, SlackAppender\ub97c \uc0ac\uc6a9\ud558\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ud558\uc9c0\ub9cc SlackAppender\ub294 \uc81c\uacf5\ub418\uace0 \uc788\uc9c0 \uc54a\uae30\uc5d0 \uc9c1\uc811 \uad6c\ud604\uc744 \ud574\uc57c \ud558\ub294\ub370\uc694\\n\\n\uc774\ub97c \uad6c\ud604\ud588\uc744 \ub54c, Slack API \uac00 \ub05d\ub0a0 \ub54c\uae4c\uc9c0, \uacc4\uc18d \uae30\ub2e4\ub9ac\uace0 \uc788\uc744 \ud544\uc694\uac00 \uc5c6\uae30\uc5d0, AsyncAppender\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9 \ubc29\ubc95\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4. xml \uae30\ubc18\uc73c\ub85c \uac00\ub2a5\ud55c\ub370\uc694\\n\\n```xml\\n\\n \\n myapp.log\\n \\n %logger{35} -%kvp -%msg%n\\n \\n \\n\\n \\n \\n \\n\\n \\n \\n \\n\\n```\\n\\n\ub9cc\uc57d \uc5ec\uae30\uc5d0 \uc788\ub294 \uae30\ub2a5\ub4e4\ub85c \ubd80\uc871\ud558\ub2e4\uba74, \uc9c1\uc811 Appender \ub97c \uad6c\ud604\ud574\uc11c \uc0ac\uc6a9\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc9c1\uc811 \uad6c\ud604\ud558\ub824\uba74 AppenderBase\ub97c \uc0c1\uc18d\ubc1b\uc544\uc11c \uad6c\ud604\ud558\uba74 \ub429\ub2c8\ub2e4.\\n\\n\uc774 \ud074\ub798\uc2a4\ub294 \ud544\uc694\ud55c \ubd80\ubd84\uc774 \ub300\ubd80\ubd84 \uad6c\ud604\ub418\uc5b4 \uc788\uace0, appender \ub9cc \uad6c\ud604\ud558\uba74 \ubc14\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub2f9\uc5f0\ud558\uc9c0\ub9cc \ud544\uc694\ud558\ub2e4\uba74 override \ub3c4 \uac00\ub2a5\ud558\uc8e0\\n\\n### Layout\\n\\nLayout \uc740 \ub85c\uadf8\ub97c \uc5b4\ub5a4 \ud615\uc2dd\uc73c\ub85c \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\nAppender\ub294 \ub85c\uadf8\ub97c \uc5b4\ub514\uc5d0 \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud558\uace0, Layout \uc740 \ub85c\uadf8\ub97c \uc5b4\ub5a4 \ud615\uc2dd\uc73c\ub85c \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud558\ub3c4\ub85d \ud558\ub294 \uac83\uc774 \uc774\uc0c1\uc801\uc774\uc9c0\ub9cc\\n\\nLogback \uc740 Appender\uc5d0\uc11c Layout \uc744 \uc9c1\uc811 \uc9c0\uc815\ud560 \uc218 \uc788\ub3c4\ub85d \ud574\uc8fc\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c, \uc9c1\uc811 Layout \uc744 \ub9cc\ub4e4\uc9c0 \uc54a\uace0, Appender \uc5d0\uc11c \uae30\uc874\uc5d0 \uc774\ubbf8 \uc788\ub294 \ud328\ud134\ub9cc \uc0ac\uc6a9\ud558\ub824\uace0 \ud569\ub2c8\ub2e4\\n\\n### Encoder\\n\\nEncoder\ub294 Layout \uacfc \ube44\uc2b7\ud55c \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\nLayout \uc740 \ub85c\uadf8\ub97c \uc5b4\ub5a4 \ud615\uc2dd\uc73c\ub85c \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud558\uace0, Encoder \ub294 \uc2e4\uc81c byte \ud615\ud0dc\ub85c \ubcc0\ud658\ud558\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\nSlack\uc758 webhook\uc744 \uc0ac\uc6a9\ud560 \uac83\uc774\uc9c0\ub9cc, AppenderBase\ub97c \uc0ac\uc6a9\ud558\uae30\uc5d0, \uc774\ubc88\uc5d0\ub294 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n### Filter\\n\\nFilter\ub294 \ub85c\uadf8\ub97c \uc5b4\ub5a4 \uc870\uac74\uc5d0 \ub530\ub77c\uc11c \ucd9c\ub825\ud560\uc9c0\ub97c \uacb0\uc815\ud558\ub294 \uc5ed\ud560\uc744 \ud569\ub2c8\ub2e4.\\n\\nFilter \ub294 Appender\ub97c \ub4f1\ub85d\ud558\uba70 \uac19\uc774 \ub4f1\ub85d\ud560 \uc218 \uc788\ub294\ub370\uc694\\n\\n\uc774\ubc88 \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c\ub294 Level \uc774 ERROR \uc774\uc0c1\uc778 \uac83\ub9cc \ucd9c\ub825\ud558\ub3c4\ub85d \ud558\uace0 \uc2f6\uae30\uc5d0, LevelFilter\ub97c \uc0ac\uc6a9\ud558\uba74 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n```xml\\n\\n \\n \\n INFO\\n ACCEPT\\n DENY\\n \\n \\n \\n %-4relative [%thread] %-5level %logger{30} -%kvp -%msg%n\\n \\n \\n \\n \\n \\n \\n\\n```\\n\\n\uc640 \ube44\uc2b7\ud558\uac8c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5b4 \ubcf4\uc785\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 \uc2e4\uc81c\ub85c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c error \ubc1c\uc0dd \uc2dc slack\uc73c\ub85c \uc54c\ub9bc\uc744 \uc8fc\ub294 \uac83\uc744 \uad6c\ud604\ud574 \ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## \uc2ac\ub799\uc5d0 \ucd94\uac00\ud558\ub294 \ubc29\ubc95\\n\\n[\uc774 \ube14\ub85c\uadf8](https://velog.io/@king/slack-incoming-webhook)\ub97c \ubcf4\uace0\uc11c \uc791\uc131\ud588\uc2b5\ub2c8\ub2e4\\n\\n## \uc2e4\uc81c \uad6c\ud604\\n\\n\uad6c\ud604\ub41c \uacb0\uacfc\ubb3c\uc740 \uc544\ub798\uc640 \uac19\uc2b5\ub2c8\ub2e4\\n\\n![slack appender](https://blog.kakaocdn.net/dn/d3z7QG/btsmQCCV69f/NwiyNhQGZOBnKBP2hT8kf0/img.png)\\n\\n### SlackAppender \uad6c\ud604\ud558\uae30\\n\\n```java\\npublic class SlackAppender extends AppenderBase {\\n\\n @Override\\n protected void append(final ILoggingEvent eventObject) {\\n final var restTemplate = new RestTemplate();\\n final var url = \\"https://hooks.slack.com/services/\\";\\n final Map body = createSlackErrorBody(eventObject);\\n restTemplate.postForEntity(url, body, String.class);\\n }\\n\\n private Map createSlackErrorBody(final ILoggingEvent eventObject) {\\n final String message = createMessage(eventObject);\\n return Map.of(\\n \\"attachments\\", List.of(\\n Map.of(\\n \\"fallback\\", \\"\uc694\uccad\uc744 \uc2e4\ud328\ud588\uc5b4\uc694 :cry:\\",\\n \\"color\\", \\"#2eb886\\",\\n \\"pretext\\", \\"\uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\uc5b4\uc694 \ud655\uc778\ud574\uc8fc\uc138\uc694 :cry:\\",\\n \\"author_name\\", \\"car-ffeine\\",\\n \\"text\\", message,\\n \\"fields\\", List.of(\\n Map.of(\\n \\"title\\", \\"\uc6b0\uc120\uc21c\uc704\\",\\n \\"value\\", \\"High\\",\\n \\"short\\", false\\n ),\\n Map.of(\\n \\"title\\", \\"\uc11c\ubc84 \ud658\uacbd\\",\\n \\"value\\", \\"local\\",\\n \\"short\\", false\\n )\\n ),\\n \\"ts\\", eventObject.getTimeStamp()\\n )\\n )\\n );\\n }\\n\\n private String createMessage(final ILoggingEvent eventObject) {\\n final String baseMessage = \\"\uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\\\\n\\";\\n final String pattern = baseMessage + \\"```%s %s %s [%s] - %s```\\";\\n final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(\\"yyyy-MM-dd HH:mm:ss.SSS\\");\\n return String.format(pattern,\\n simpleDateFormat.format(eventObject.getTimeStamp()),\\n eventObject.getLevel(),\\n eventObject.getThreadName(),\\n eventObject.getLoggerName(),\\n eventObject.getFormattedMessage());\\n }\\n}\\n```\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c url\uc744 \uc9c1\uc811 \uc785\ub825\ud558\uc2dc\uba74 \ub429\ub2c8\ub2e4.\\n\\n\uadf8\ub9ac\uace0, \uc774\ub807\uac8c \ub9cc\ub4e0 SlackAppender\ub97c logback-spring.xml \uc5d0 \ub4f1\ub85d\ud558\uba74 \ub429\ub2c8\ub2e4.\\n\\n```xml\\n\\n\\n\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n\\n \\n\\n\\n```\\n\\n\uc774\ub807\uac8c \ud558\uba74, racingcar \ud328\ud0a4\uc9c0\uc5d0\uc11c \uc5d0\ub7ec\uac00 \ubc1c\uc0dd\ud560 \ub54c\ub9cc slack\uc73c\ub85c \uc54c\ub9bc\uc744 \ubc1b\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## \uacb0\ub860\\n\\n![slack appender](https://blog.kakaocdn.net/dn/d3z7QG/btsmQCCV69f/NwiyNhQGZOBnKBP2hT8kf0/img.png)\\n\\n\uc774\ubc88 \uae00\uc5d0\uc11c\ub294 log \ub808\ubca8\uc5d0 \ub530\ub77c slack \uc73c\ub85c \uc54c\ub9bc\uc744 \ubc1b\ub294 \ubc29\ubc95\uc744 \uc54c\uc544\ubcf4\uc558\uc2b5\ub2c8\ub2e4.\\n\\n\uae34 \uae00\uc744 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4"},{"id":"7","metadata":{"permalink":"/7","source":"@site/blog/2023-07-06-auto-issue-number-commit-msg.mdx","title":"\uae43 \ucee4\ubc0b \uba54\uc2dc\uc9c0\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud560 \uc21c \uc5c6\uc744\uae4c?","description":"\ud504\ub85c\uc81d\ud2b8 \ube0c\ub79c\uce58\uba85 \ucee8\ubca4\uc158\uc774 feat/\uc774\uc288\ubc88\ud638\uc5ec\uc11c, \ube0c\ub79c\uce58\uba85\uc5d0\uc11c \uc774\uc288\ubc88\ud638\ub9cc \uac00\uc838\uc628 \ub2e4\uc74c \ucee4\ubc0b\ud560 \ub54c\ub9c8\ub2e4 \ucee4\ubc0b \uba54\uc2dc\uc9c0 \uc544\ub798\ub2e8(footer)\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud574\uc8fc\uace0 \uc2f6\uc5c8\ub2e4. \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ub41c\ub2e4\uba74 \uae5c\ube61\ud558\uace0 \uc774\uc288 \ubc88\ud638\ub97c \uc548 \uc801\ub294 \uc77c\ub3c4 \uc5c6\uace0, \uc2dc\uac04\ub3c4 \ub2e8\ucd95\ud560 \uc218 \uc788\uae30 \ub54c\ubb38\uc774\ub2e4.","date":"2023-07-06T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 6\uc77c","tags":[{"label":"git","permalink":"/tags/git"},{"label":"commit","permalink":"/tags/commit"},{"label":"message","permalink":"/tags/message"},{"label":"issue","permalink":"/tags/issue"},{"label":"auto","permalink":"/tags/auto"}],"readingTime":2.89,"hasTruncateMarker":false,"authors":[{"name":"\uc57c\ubbf8","title":"Frontend","url":"https://github.com/feb-dain","imageURL":"https://github.com/feb-dain.png","key":"yummy"}],"frontMatter":{"slug":"7","title":"\uae43 \ucee4\ubc0b \uba54\uc2dc\uc9c0\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud560 \uc21c \uc5c6\uc744\uae4c?","authors":["yummy"],"tags":["git","commit","message","issue","auto"]},"prevItem":{"title":"\uc2a4\ud504\ub9c1\uc5d0\uc11c \ubc1c\uc0dd\ud55c \uc5d0\ub7ec \ub85c\uadf8\ub97c \uc2ac\ub799\uc73c\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub294 \ubc29\ubc95","permalink":"/8"},"nextItem":{"title":"[DB] \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud574\ubcf4\uc790","permalink":"/6"}},"content":"\ud504\ub85c\uc81d\ud2b8 \ube0c\ub79c\uce58\uba85 \ucee8\ubca4\uc158\uc774 feat/\uc774\uc288\ubc88\ud638\uc5ec\uc11c, \ube0c\ub79c\uce58\uba85\uc5d0\uc11c \uc774\uc288\ubc88\ud638\ub9cc \uac00\uc838\uc628 \ub2e4\uc74c \ucee4\ubc0b\ud560 \ub54c\ub9c8\ub2e4 \ucee4\ubc0b \uba54\uc2dc\uc9c0 \uc544\ub798\ub2e8(footer)\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud574\uc8fc\uace0 \uc2f6\uc5c8\ub2e4. \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ub41c\ub2e4\uba74 \uae5c\ube61\ud558\uace0 \uc774\uc288 \ubc88\ud638\ub97c \uc548 \uc801\ub294 \uc77c\ub3c4 \uc5c6\uace0, \uc2dc\uac04\ub3c4 \ub2e8\ucd95\ud560 \uc218 \uc788\uae30 \ub54c\ubb38\uc774\ub2e4.\\n\\n\uc544\ub798 \uc21c\uc11c\ub300\ub85c \uc9c4\ud589\ud55c\ub2e4\uba74 \uc774\uc288 \ubc88\ud638 POSTFIX \uc790\ub3d9\ud654\ub97c \ud560 \uc218 \uc788\ub2e4.\\n\\n### 1) \ud504\ub85c\uc81d\ud2b8 \ud3f4\ub354\uc5d0 .githooks \ud3f4\ub354 \uc0dd\uc131\\n\\n### 2) .githooks \ud3f4\ub354\uc5d0 commit-msg \ud30c\uc77c \uc0dd\uc131\\n\\n```shell\\n#!/bin/bash\\n\\nCOMMIT_MESSAGE_FILE_PATH=$1\\nMESSAGE=$(cat \\"$COMMIT_MESSAGE_FILE_PATH\\")\\n\\n# \ucee4\ubc0b \uba54\uc2dc\uc9c0\uac00 \uc5c6\uc744 \ub54c, \ucee4\ubc0b \ubc29\uc9c0\\nif [[ $(head -1 \\"$COMMIT_MESSAGE_FILE_PATH\\") == \'\' ]]; then\\n exit 0\\nfi\\n\\n# \ube0c\ub79c\uce58\uba85\uc5d0\uc11c \uc774\uc288 \ubc88\ud638\ub9cc \ucd94\ucd9c (\'/\' \ub4a4\uc5d0 \uc788\ub294 \ubb38\uc790\ub9cc \ucd94\ucd9c)\\nPOSTFIX=$(git branch | grep \'\\\\*\' | sed \'s/* //\' | sed \'s/^.*\\\\///\' | sed \'s/^\\\\([^-]*-[^-]*\\\\).*/\\\\1/\')\\n\\nCOMMIT_SOURCE=$2\\nCURRENT_BRANCH=$(git branch --show-current)\\n\\n# [[ \\"$CURRENT_BRANCH\\" != \\"$POSTFIX\\" ]] \ud83d\udc49\ud83c\udffb \ud604\uc7ac \ube0c\ub79c\uce58\uba85\uacfc POSTFIX\uac00 \ub611\uac19\uc73c\uba74 POSTFIX \uc785\ub825 \ubc29\uc9c0\\n# [ \\"$COMMIT_SOURCE\\" != \\"merge\\" ] \ud83d\udc49\ud83c\udffb merge\ud560 \ub54c, POSTFIX \uc785\ub825 \ubc29\uc9c0\\n# [[ \\"$MESSAGE\\" != *\\"[#$POSTFIX]\\"* ]] \ud83d\udc49\ud83c\udffb \uc774\ubbf8 POSTFIX\uac00 \uc874\uc7ac\ud560 \ub54c, POSTFIX \uc911\ubcf5 \uc785\ub825 \ubc29\uc9c0\\nif [[ \\"$CURRENT_BRANCH\\" != \\"$POSTFIX\\" ]] && [ \\"$COMMIT_SOURCE\\" != \\"merge\\" ] && [[ \\"$MESSAGE\\" != *\\"[#$POSTFIX]\\"* ]]; then\\n printf \\"%s\\\\n\\\\n[#%s]\\" \\"$MESSAGE\\" \\"$POSTFIX\\" > \\"$COMMIT_MESSAGE_FILE_PATH\\"\\nfi\\n```\\n\\n\ud83e\uddd0 \uc774\uc288 \ubc88\ud638 \ucd94\ucd9c\uc5d0 \uc0ac\uc6a9\ub41c \uba85\ub839\uc5b4 \uc124\uba85\\n\\n- grep \'\\\\*\' \ud83d\udc49 `*` \ud45c\uc2dc\ub41c \ube0c\ub79c\uce58(\ud604\uc7ac \uc704\uce58\uc758 \ube0c\ub79c\uce58)\ub97c \uac00\uc838\uc628\ub2e4.\\n- sed \'s/_ //\' \ud83d\udc49 `*` \uc81c\uac70\\n- sed \'s/\\\\([^/]_\\\\)._/\\\\1/\' \ud83d\udc49 `/` \uc774\ud6c4\uc758 \ubb38\uc790\ub9cc \ucd94\ucd9c\\n- sed \'s/^\\\\([^-]_-[^-]_\\\\).\\\\_/\\\\1/\' \ud83d\udc49 \ud558\ub098\uc758 \uc774\uc288\uc5d0 \uc5ec\ub7ec \ube0c\ub79c\uce58\ub97c \ub9cc\ub4e4\uba74\uc11c feat/10-1 \uc774\ub7f0 \ud615\ud0dc\ub85c \ube0c\ub79c\uce58\ub97c \ub9cc\ub4e4 \uacbd\uc6b0, \uccab \ubc88\uc9f8 \'-\' \uc55e \ub4a4\ub9cc \ucd94\ucd9c (ex. 10-1)\\n\\n### 3) \ud504\ub85c\uc81d\ud2b8 \ud3f4\ub354\uc5d0 Makefile \ud30c\uc77c \uc0dd\uc131\\n\\n```shell\\ninit:\\n git config core.hooksPath .githooks\\n chmod +x .githooks/commit-msg\\n git update-index --chmod=+x .githooks/commit-msg\\n\\n # chmod +x .githooks/commit-msg \ud83d\udc49\ud83c\udffb macOS, \ub9ac\ub205\uc2a4\uc5d0\uc11c \uc2a4\ud06c\ub9bd\ud2b8 \uad8c\ud55c \ubd80\uc5ec\\n # git update-index --chmod=+x .githooks/commit-msg\\n # \ud83d\udc49 macOS, \ub9ac\ub205\uc2a4\uc5d0\uc11c \ube0c\ub79c\uce58\uac00 \ubc14\ub014 \ub54c\ub9c8\ub2e4 \uc2a4\ud06c\ub9bd\ud2b8 \uc2e4\ud589\uc2dc\ucf1c\uc918\uc57c \ud558\ub294 \ubb38\uc81c \ud574\uacb0\\n```\\n\\n### 4) \uc544\ub798 \ucf54\ub4dc \uc2e4\ud589\\n\\n\uc0c8\ub85c git clone\uc744 \ud560 \ub54c\ub9c8\ub2e4 \uc544\ub798 \ucf54\ub4dc\ub97c \uc2e4\ud589\uc2dc\ucf1c\uc918\uc57c \ud55c\ub2e4. \ud55c \ubc88\ub9cc \uc2e4\ud589\uc2dc\ud0a4\uba74 \uacc4\uc18d \uc801\uc6a9\ub41c\ub2e4. (window \uae30\uc900)\\n\\n```shell\\ngit config core.hooksPath .githooks\\n```\\n\\n\u2757macOS\ub294 git clone \ud560 \ub54c\ub9c8\ub2e4 \uc544\ub798 \ucf54\ub4dc\ub97c \uc2e4\ud589\uc2dc\ucf1c\uc918\uc57c \ud55c\ub2e4.\\n\\n```shell\\nmake\\n```\\n\\n---\\n\\n\ucc38\uace0 \ube14\ub85c\uadf8\\nhttps://blog.deering.co/commit-convention/"},{"id":"6","metadata":{"permalink":"/6","source":"@site/blog/2023-07-05-nunu-db-optimization.mdx","title":"[DB] \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud574\ubcf4\uc790","description":"\uc548\ub155\ud558\uc138\uc694 \uce74\ud398\uc778\ud300 \ub204\ub204\uc785\ub2c8\ub2e4","date":"2023-07-05T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 5\uc77c","tags":[{"label":"DB","permalink":"/tags/db"},{"label":"JPA","permalink":"/tags/jpa"},{"label":"Hibernate","permalink":"/tags/hibernate"},{"label":"Spring","permalink":"/tags/spring"}],"readingTime":8.16,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"},{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"}],"frontMatter":{"slug":"6","title":"[DB] \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud574\ubcf4\uc790","authors":["nunu","boxster"],"tags":["DB","JPA","Hibernate","Spring"]},"prevItem":{"title":"\uae43 \ucee4\ubc0b \uba54\uc2dc\uc9c0\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uc785\ub825\ud560 \uc21c \uc5c6\uc744\uae4c?","permalink":"/7"},"nextItem":{"title":"pr \ubcf8\ubb38\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \ub2ec\uc544\uc8fc\ub294 \uae30\ub2a5\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4","permalink":"/5"}},"content":"\uc548\ub155\ud558\uc138\uc694 \uce74\ud398\uc778\ud300 `\ub204\ub204`\uc785\ub2c8\ub2e4\\n\\n\uc774\ubc88\uc5d0\ub294 \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud558\ub294 \uacfc\uc815\uc5d0\uc11c \uc54c\uac8c \ub41c \ub0b4\uc6a9\uc744 \uacf5\uc720\ud558\ub824\uace0 \ud569\ub2c8\ub2e4\\n\\n## \uc774\ubc88 \ucd5c\uc801\ud654\uc758 \ubaa9\ud45c\\n\\n\uc804\uae30\ucc28 \ucda9\uc804\uc18c\uc5d0 \ub300\ud55c \uacf5\uacf5 \ub370\uc774\ud130\ub97c \uac00\uc838\uc624\uace0, \uadf8 \ub370\uc774\ud130\ub97c DB \uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud574\ubcf4\uc790\\n\\n## \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c \uc0bd\uc785\ud558\ub294 \uacfc\uc815\\n\\n\uc800\ud76c \ud300\uc758 \uc694\uad6c\uc0ac\ud56d\uc744 \uac04\ub2e8\ud558\uac8c \uc815\ub9ac\ud558\uba74 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4\\n\\n1. \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c \uacf5\uacf5 \ub370\uc774\ud130\uc5d0\uc11c \uc804\uae30\ucc28 \ucda9\uc804\uc18c\uc640 \uc804\uae30\ucc28 \ucda9\uc804\uae30\uc5d0 \ub300\ud55c \ub370\uc774\ud130\ub97c \uac00\uc838\uc628\ub2e4\\n - \ucda9\uc804\uc18c\ub294 6\ub9cc \uac1c, \ucda9\uc804\uae30\ub294 23\ub9cc \uac1c\uc758 \ub370\uc774\ud130\uac00 \uc874\uc7ac\ud55c\ub2e4.\\n - \ud55c \ubc88\uc5d0 \uac00\uc838\uc62c \uc218 \uc788\ub294 \uc591\uc740 9999\uac1c \uae4c\uc9c0\ub2e4.\\n2. \uc774 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294\ub2e4\\n - \ucda9\uc804\uc18c\uc640 \ucda9\uc804\uae30\ub294 1:N \uad00\uacc4\uc774\ub2e4\\n\\n## \ucd5c\uc801\ud654 \uc804\uc740 \uc5b4\ub5a4 \uc0c1\ud669\uc774\uc5c8\ub294\ub370?\\n\\n![before_optimize](https://veiled-starfish-4c7.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Ffb934c88-4589-4096-90bc-36b4bc88f6a2%2FUntitled.png?id=f7f7c2af-7b95-42e8-8d95-ddd952e53005&table=block&spaceId=9db11c89-12d2-4910-8822-5ffecbdb8ccd&width=2000&userId=&cache=v2)\\n\\n\uc704 \uc0ac\uc9c4\uc744 \uc798 \ubcf4\uc2dc\uba74 \uc544\uc2e4 \uc218 \uc788\uc73c\uc2dc\uaca0\uc9c0\ub9cc, 2000\uac1c\ub97c \uc800\uc7a5\ud558\ub294\ub370, 231.762 \ucd08\uac00 \uc0ac\uc6a9\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ubb3c\ub860 \ucd9c\ub825\uc744 \uc704\ud55c \uc2dc\uac04\ub3c4 \ud3ec\ud568\ub418\uc5c8\uae30\uc5d0, 230\ucd08 \uc815\ub3c4\ub77c\uace0 \uc0dd\uac01\ud558\uc154\ub3c4 \uc88b\uc2b5\ub2c8\ub2e4\\n\\n1\ub9cc \uac1c\ub77c\uba74? 231.762\ucd08 \\\\* 5 = 1,158.81\ucd08\\n\\n23\ub9cc \uac1c\ub77c\uba74? 1158.81 \\\\* 23 = 26,652.63\ucd08\\n\\n\uc2dc\uac04\uc73c\ub85c \ubc14\uafd4\ubcf4\uba74 7.4 \uc2dc\uac04\uc774 \uac78\ub9b0\ub2e4\ub294 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\n1. \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud560 \ub54c\ub9c8\ub2e4, \uc0c8\ub85c\uc6b4 Transaction \uc774 \uc0dd\uc131\ub41c\ub2e4.\\n\\n### \uc5b4\ub5bb\uac8c \uac1c\uc120\ud560 \uc218 \uc788\uc744\uae4c?\\n\\n\ub370\uc774\ud130\ub97c \uc800\uc7a5\ud560 \ub54c\ub9c8\ub2e4, \uc0c8\ub85c\uc6b4 Transaction \uc774 \uc0dd\uc131\ub418\ub294 \uac83\uc744 \ubc29\uc9c0\ud558\uae30 \uc704\ud574, \uc804\uccb4\ub97c \ud558\ub098\uc758 \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\ub294\ub2e4\\n\\n## \uc804\uccb4\ub97c \ud55c \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\uc740 \ubc84\uc804\\n\\n![all_in_transaction](https://veiled-starfish-4c7.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F9ff34622-4a26-4acd-980c-ae175c83143d%2FUntitled.png?id=979aa2c5-e972-4c52-a44a-1669c497c84e&table=block&spaceId=9db11c89-12d2-4910-8822-5ffecbdb8ccd&width=2000&userId=&cache=v2)\\n\\n\uc774 \uacfc\uc815\uc5d0\uc11c 2000\uac1c\ub97c \uc800\uc7a5\ud558\ub294\ub370 65\ucd08 \uac00 \uc0ac\uc6a9\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n1\ub9cc \uac1c\ub77c\uba74? 65\ucd08 \\\\* 5 = 325\ucd08\\n\\n23\ub9cc \uac1c\ub77c\uba74? 325\ucd08 \\\\* 23 = 7,475\ucd08\\n\\n\uc2dc\uac04\uc73c\ub85c \ubc14\uafd4\ubcf4\uba74 2\uc2dc\uac04\uc774 \uac78\ub9b0\ub2e4\ub294 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n\uc804\uccb4\uc801\uc73c\ub85c 3\ubc30 \uc815\ub3c4 \ube68\ub77c\uc84c\uc2b5\ub2c8\ub2e4\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\n1. 23\ub9cc \uac1c\uc758 \uc800\uc7a5\uc774 \ubaa8\ub450 \ud55c \ud2b8\ub79c\uc7ad\uc158\uc774 \ub418\uc5b4\uc11c, \ud558\ub098\uac00 \uc2e4\ud328\ud558\uba74 23\ub9cc\uac1c\ub97c \uc0c8\ub85c \uc800\uc7a5\ud574\uc57c \ud558\ub294 \uc0c1\ud669\uc5d0 \ucc98\ud55c\ub2e4\\n\\n### \uc5b4\ub5bb\uac8c \uac1c\uc120\ud560 \uc218 \uc788\uc744\uae4c?\\n\\n23\ub9cc\uac1c\uc758 \uc800\uc7a5\uc774 \ubaa8\ub450 \ud55c \ud2b8\ub79c\uc7ad\uc158\uc774 \ub418\ub294 \uac83\uc744 \ubc29\uc9c0\ud558\uae30 \uc704\ud574, 1\ub9cc \uac1c\uc529 \uc601\uc18d\ud654\uc2dc\ud0a8\ub2e4\\n\\n## 1\ub9cc \uac1c\uac00 \ud55c \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\uc778 \ubc84\uc804\\n\\n![separateTransaction](https://blog.kakaocdn.net/dn/c2mgfd/btsmrWCfnKy/9Y6Dv8vYzcftsket61tub1/img.png)\\n\\n\uc131\ub2a5\uc0c1\uc73c\ub85c \uac1c\uc120\ud55c \ubd80\ubd84\uc740 \uadf8\ub807\uac8c \ud06c\uc9c0 \uc54a\uc9c0\ub9cc, \uc2e4\ud328\ud588\uc744 \ub54c, 1\ub9cc \uac1c\ub9cc \ub2e4\uc2dc \uc800\uc7a5\ud558\uba74 \ub418\uae30\uc5d0, \ud6e8\uc52c \ube60\ub974\uac8c \ubcf5\uad6c\uac00 \uac00\ub2a5\ud569\ub2c8\ub2e4.\\n\\n\uc5ec\uae30\uc11c PageNo\ub77c\ub294 \ud074\ub798\uc2a4\ub294, i\ub97c \ubc14\ub85c \ucc38\uc870\ud588\uc744 \uacbd\uc6b0, effectively final\uc744 \ubcf4\uc7a5\ud560 \uc218 \uc5c6\uc5b4\uc11c \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc131\ub2a5\uc740 \uc804\uccb4\ub97c \ud55c \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\uc740 \ubc84\uc804\uacfc \ud070 \ucc28\uc774\uac00 \ub098\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\n1. id \uc0dd\uc131 \uc804\ub7b5\uc774 `GenerationType.IDENTITY` \uc774\uae30\uc5d0, \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud560 \ub54c\ub9c8\ub2e4, DB\uc5d0\uc11c id\ub97c \uc0dd\uc131\ud574\uc57c \ud55c\ub2e4.\\n\\nJPA\uc5d0 \uc788\ub294 \uc4f0\uae30 \uc9c0\uc5f0\uc744 \uc804\ud600 \ud65c\uc6a9\ud560 \uc218 \uc5c6\uace0, DB\uc5d0\uc11c id\ub97c \uc0dd\uc131\ud558\uae30 \uc704\ud574, DB\uc640 \ub9e4\ubc88 \ud1b5\uc2e0\uc744 \ud574\uc57c \ud55c\ub2e4.\\n\\n### \uc5b4\ub5bb\uac8c \uac1c\uc120\ud560 \uc218 \uc788\uc744\uae4c?\\n\\nid\ub97c \ubbf8\ub9ac \uc0dd\uc131\ud574\uc11c, DB \uc5d0\uc11c id \ub97c \uc0dd\uc131\ud558\ub294 \uacfc\uc815\uc744 \uc0dd\ub7b5\ud55c\ub2e4\\n\\nID \uc0dd\uc131 \uc804\ub7b5\uc744 `GenerationType.Table\uc758` \ud615\ud0dc\ub85c \ubc14\uafd4\uc11c, DB\uc5d0\uc11c id\ub97c \uc0dd\uc131\ud558\ub294 \uacfc\uc815\uc744 \uc904\uc5ec\uc11c, \uc131\ub2a5\uc744 \uac1c\uc120\ud55c\ub2e4\\n\\n## 1\ub9cc \uac1c\uac00 \ud55c \ud2b8\ub79c\uc7ad\uc158\uc73c\ub85c \ubb36\uc774\uace0, id\ub97c \ubbf8\ub9ac \uc0dd\uc131\ud55c \ubc84\uc804\\n\\n\uc774\ub54c batch size\ub97c 1000 \ub2e8\uc704\ub85c \uc124\uc815\ud574\uc11c 1000\uac1c\uc529 id \uac00 \ub298\uc5b4\ub098\ub3c4\ub85d \uc124\uc815\ud588\ub2e4\\n\\n![charger_generator](https://blog.kakaocdn.net/dn/bFjNWb/btsmuoLmzVh/GddHebu2V43fpk2t3IUmz0/img.png)![station_generator](https://blog.kakaocdn.net/dn/pae8w/btsmrANjAGi/gjUhD6sMvBLpmsPl9c1tAk/img.png)\\n\\n```\\nspring.jdbc.template.fetch-size=10000\\n```\\n\\n![10000batch_size](https://blog.kakaocdn.net/dn/mtBFp/btsmtEt48jp/3mFOfrIBWbjJhHHuyP4zPk/img.png)\\n\\n1\uc790\ub9ac \uc22b\uc790\ub294 \uc55e\uc5d0\uc11c\ubd80\ud130 n(\ub9cc\uac1c)\ub97c \uc758\ubbf8\ud558\uace0, 2\ubc88\uc9f8 \uc22b\uc790\ub294 1\ub9cc \uac1c\ub97c \uc800\uc7a5\ud558\ub294 \ub370 \uac78\ub9b0 \uc2dc\uac04(ms)\uc744 \uc758\ubbf8\ud569\ub2c8\ub2e4.\\n\\n\ucc98\uc74c 1\ub9cc \uac1c\ub294 142\ucd08\uac00 \uac78\ub9ac\uace0, 2\ub9cc \uac1c\ub294 285\ucd08\uac00 \uac78\ub838\uc2b5\ub2c8\ub2e4.\\n\\n23\ub9cc \uac1c\ub77c\uba74? 142 \\\\* 26 = 3,266\ucd08\\n\\n\ucc98\uc74c\uacfc \ube44\uad50\ud558\uc790\uba74 7.4\uc2dc\uac04\uc774 \uac78\ub9ac\ub294 \uac83\uc5d0\uc11c 54\ubd84 \uc815\ub3c4 \uac78\ub9ac\ub294 \uac83\uc73c\ub85c \uac1c\uc120\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\n\ud558\ub098\uc758 \uc2a4\ub808\ub4dc\uc5d0\uc11c\ub9cc \ub3d9\uc791\ud558\uae30\uc5d0, \uc131\ub2a5\uc774 \uac1c\uc120\ub418\uc5c8\uc9c0\ub9cc, \uc5ec\uc804\ud788 \ub290\ub9bd\ub2c8\ub2e4.\\n\\n\ud558\ub098\uc758 \uc2a4\ub808\ub4dc\uc5d0\uc11c\ub9cc \ub3d9\uc791\ud558\uae30\uc5d0, \ud558\ub098\uc758 \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n### \uc5b4\ub5bb\uac8c \uac1c\uc120\ud560 \uc218 \uc788\uc744\uae4c?\\n\\n\uc5ec\ub7ec \uc2a4\ub808\ub4dc\uc5d0\uc11c \ub3d9\uc791\ud558\uac8c \ud558\uace0, \uc5ec\ub7ec \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\uac8c \ud569\ub2c8\ub2e4.\\n\\n## \uc5ec\ub7ec \uc2a4\ub808\ub4dc\uc5d0\uc11c \ub3d9\uc791\ud558\uace0, \uc5ec\ub7ec \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\ub294 \ubc84\uc804\\n\\n![multi_thread](https://blog.kakaocdn.net/dn/bPV2aa/btsmrSfU2D4/phDwk77XiKvwiXa5geX0PK/img.png)\\n\\n\uc774 \ubc84\uc804\uc5d0\uc11c 89991 \uac1c\ub97c \uc800\uc7a5\ud558\ub294\ub370 \ucd1d 157\ucd08\uac00 \uac78\ub838\uc2b5\ub2c8\ub2e4.\\n\\n23\ub9cc \uac1c\ub77c\uba74? 157 \\\\* 3 = 471\ucd08\\n\\n\uc2dc\uac04\uc73c\ub85c \ubc14\uafd4\ubcf4\uba74 5\ubd84\ub3c4 \ucc44 \uac78\ub9ac\uc9c0 \uc54a\ub294 \uc2dc\uac04\uc774\uc8e0\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\nhikari connection pool \uc0ac\uc774\uc988\ub97c 10\uc73c\ub85c \uc124\uc815\ud588\ub294\ub370, 10\uac1c\uc758 \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\uba74\uc11c \uc800\uc7a5\uc744 \ud558\ub2e4 \ubcf4\ub2c8, 10\uac1c\uc758 \ucee4\ub125\uc158\uc744 \ubaa8\ub450 \uc0ac\uc6a9\ud558\uace0 \ub098\uc11c, 11\ubc88\uc9f8\ubd80\ud130\ub294 \ucee4\ub125\uc158\uc744 \uac00\uc838\uc624\uae30 \uc704\ud574, \uae30\ub2e4\ub824\uc57c \ud558\ub294 \uc0c1\ud669\uc774 \ubc1c\uc0dd\ud569\ub2c8\ub2e4.\\n\\n### \uc5b4\ub5bb\uac8c \uac1c\uc120\ud560 \uc218 \uc788\uc744\uae4c?\\n\\nhikari connection pool \uc0ac\uc774\uc988\ub97c 25\ub85c \uc124\uc815\ud574\uc11c, 25\uac1c\uc758 \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\ub3c4\ub85d \ud569\ub2c8\ub2e4.\\n\\n```\\nspring.datasource.hikari.maximum-pool-size=25\\n```\\n\\n## \uc5ec\ub7ec \uc2a4\ub808\ub4dc\uc5d0\uc11c \ub3d9\uc791\ud558\uace0, \uc5ec\ub7ec \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\ub294 \ubc84\uc804 2\\n\\n![multi_thread2](https://blog.kakaocdn.net/dn/vJEoD/btsmsfau8Mv/j0CT8fVrAp3LKGRMmyMVeK/img.png)\\n\\n\ucd1d 13\ub9cc \uac1c\uc758 \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud558\ub294\ub370, 147\ucd08\uac00 \uac78\ub9ac\uace0, db \uc778\uc2a4\ud134\uc2a4\uc758 cpu \uc0ac\uc6a9\ub960\uc774 100%\uc5d0 \uac00\uae4c\uc6cc\uc838\uc11c ec2 \uac00 \ub2e4\uc6b4\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n### \uc774 \uacfc\uc815\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \ubb38\uc81c\uc810\\n\\ndb\uc758 cpu \uc0ac\uc6a9\ub7c9\uc744 \uace0\ub824\ud558\uc9c0 \uc54a\uace0, 23\ub9cc \uac1c\uac00 \uc870\uae08 \ub118\ub294 \ub370\uc774\ud130\ub97c 25\uac1c\uc758 \ucee4\ub125\uc158\uc744 \ud65c\uc6a9\ud574 \uc800\uc7a5\ud558\ub824\uace0 \ud588\uc2b5\ub2c8\ub2e4\\n\\n# \uacb0\ub860\\n\\n1. \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud560 \ub54c\ub9c8\ub2e4, transaction\uc744 \uc0ac\uc6a9\ud558\uc9c0 \ub9d0\uc790\\n2. \ub370\uc774\ud130\ub97c \uc800\uc7a5\ud560 \ub54c\ub9c8\ub2e4, id\ub97c \uc0dd\uc131\ud558\uc9c0 \ub9d0\uc790\\n3. \uc5ec\ub7ec \uc2a4\ub808\ub4dc\uc5d0\uc11c \ub3d9\uc791\ud558\uace0, \uc5ec\ub7ec \ucee4\ub125\uc158\uc744 \uc0ac\uc6a9\ud558\uc790\\n4. db\uc758 cpu \uc0ac\uc6a9\ub7c9\uc744 \uace0\ub824\ud558\uc790\\n\\n\uae34 \uae00 \uc77d\uc5b4\uc8fc\uc154\uc11c \uac10\uc0ac\ud569\ub2c8\ub2e4"},{"id":"5","metadata":{"permalink":"/5","source":"@site/blog/2023-07-04-github_actions_pullrequest_issue.mdx","title":"pr \ubcf8\ubb38\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \ub2ec\uc544\uc8fc\ub294 \uae30\ub2a5\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4","description":"\uc548\ub155\ud558\uc138\uc694 \uc6b0\ud14c\ucf54 \uce74\ud398\uc778\ud300 \ub204\ub204\uc785\ub2c8\ub2e4","date":"2023-07-04T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 4\uc77c","tags":[{"label":"github","permalink":"/tags/github"},{"label":"action","permalink":"/tags/action"},{"label":"pr","permalink":"/tags/pr"},{"label":"issue","permalink":"/tags/issue"}],"readingTime":3.19,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"5","title":"pr \ubcf8\ubb38\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \ub2ec\uc544\uc8fc\ub294 \uae30\ub2a5\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4","authors":["nunu"],"tags":["github","action","pr","issue"]},"prevItem":{"title":"[DB] \ub300\ub7c9\uc758 \ub370\uc774\ud130\ub97c DB\uc5d0 \ub123\ub294 \uacfc\uc815\uc744 \ucd5c\uc801\ud654\ud574\ubcf4\uc790","permalink":"/6"},"nextItem":{"title":"\ud070 \ud2c0\uc5d0\uc11c \ubc14\ub77c\ubcf4\ub294 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98 \uacc4\ud68d","permalink":"/4"}},"content":"\uc548\ub155\ud558\uc138\uc694 \uc6b0\ud14c\ucf54 \uce74\ud398\uc778\ud300 \ub204\ub204\uc785\ub2c8\ub2e4\\n\\n\ube60\ub974\uac8c \uacb0\uacfc\ubd80\ud130 \ubcf4\uace0 \uac00\uc2dc\uc8e0.\\n\\n## \uc5b4\ub5a4 \uacb0\uacfc\uac00 \ub098\uc654\ub098\uc694?\\n\\npr\uc758 \ubcf8\ubb38 \ub05d\uc5d0, \uc5f0\uad00\ub41c \uc774\uc288 \ubc88\ud638\ub97c \ub2ec\uc544\uc8fc\ub294 \uae30\ub2a5\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\ubc11\uc5d0 \uc0ac\uc9c4\uc744 \ubcf4\uc2dc\uba74 \uc27d\uac8c \uc774\ud574\ud558\uc2e4 \uc218 \uc788\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://user-images.githubusercontent.com/80899085/250614527-e2672cf2-786a-434c-a8b6-8b374de4d689.png)![img](https://user-images.githubusercontent.com/80899085/250614882-d99aa570-51e2-4565-ab4c-ccdbd4d36e57.png)\\n\\ngithub\uc5d0\uc11c issue \ubc88\ud638\uac00 pr\uc5d0 \ub2f4\uaca8\uc788\ub2e4\uba74 2\uac00\uc9c0 \uc7a5\uc810\uc774 \uc0dd\uae30\ub294\ub370\uc694.\\n\\n1. issue\ub97c \ud074\ub9ad\ud588\uc744 \ub54c, \uc790\ub3d9\uc73c\ub85c \uadf8 issue\ub85c \ub118\uc5b4\uac08 \uc218 \uc788\uc2b5\ub2c8\ub2e4. (\ud638\ubc84\ub9cc\uc73c\ub85c \uc774\uc288\uc5d0 \ub300\ud55c \uac04\ub2e8\ud55c \uc815\ubcf4\ub97c \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4)\\n2. pr \uc774 merge \ub418\uc5c8\uc744 \ub54c, \uc790\ub3d9\uc73c\ub85c issue \uac00 close \ub429\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc744 \uc190\uc73c\ub85c \uc9c4\ud589\ud558\ub294 \uac83\ubcf4\ub2e4, \uc790\ub3d9\uc73c\ub85c \uc9c4\ud589\ud558\uac8c \ub418\uba74 \uc2e4\uc218\ub3c4 \uc904\uc5b4\ub4e4\uace0, \uac1c\ubc1c \uacfc\uc815\uc774 \ud3b8\ud574\uc9c8 \uac83 \uac19\uc544\uc11c \uc774 \uae30\ub2a5\uc744 \uc81c\uc791\ud558\uac8c \ub418\uc5c8\ub294\ub370\uc694\\n\\n## \uc911\uc694\ud55c \uc810\\n\\n**\uc774 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub824\uba74 \ubc11\uc5d0\uc11c \uc18c\uac1c\ud574\ub4dc\ub9b4 \ube0c\ub79c\uce58 \ub124\uc774\ubc0d \uaddc\uce59\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.**\\n\\n## \ube0c\ub79c\uce58 \uc774\ub984 \uaddc\uce59\\n\\n- \ube0c\ub79c\uce58 \uc774\ub984\uc740 `\ud0c0\uc785/\uc774\uc288\ubc88\ud638` \uc73c\ub85c \uad6c\uc131\ud569\ub2c8\ub2e4. ex) `feat/1`\\n- \ud0c0\uc785\uc740 `feat`, `fix`, `docs`, `refactor`, `test` \ub4f1 \uc5ec\ub7ec \uac00\uc9c0\uac00 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub807\uac8c \ud588\uc744 \ub54c, \uc774\uc288 \ubc88\ud638\ub97c \ube0c\ub79c\uce58 \uba85\uc5d0\uc11c\ubd80\ud130 \uac00\uc838\uc62c \uc218 \uc788\uae30\uc5d0, \uc790\ub3d9\ud654\ub97c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uaddc\uce59\uc774 \uc544\ub2cc, feat/action \uac19\uc740 \ud615\ud0dc\uac00 \ub41c\ub2e4\uba74 issue \ubc88\ud638\ub97c \ucc3e\uae30 \uc5b4\ub835\uaca0\uc8e0?\\n\\n## \uc0ac\uc6a9 \ubc29\ubc95\\n\\n\uc791\uc131\ub41c \ucf54\ub4dc\ubd80\ud130 \ubcf4\uc2dc\uace0, \uc124\uba85\uc744 \ub4dc\ub9ac\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n\uc544\ub798\uc5d0 \uc791\uc131\ub41c \ucf54\ub4dc\ub97c. github/workflows/assign\\\\_issue\\\\_number\\\\_to\\\\_pr\\\\_body.yml\ub85c \uc800\uc7a5\ud558\uc2dc\uba74 \ub05d\uc785\ub2c8\ub2e4.\\n\\n```yml\\nname: assign_issue_number_to_pr_body\\n\\non:\\n pull_request:\\n types: [ opened ]\\n branches-ignore:\\n - develop\\n\\njobs:\\n append_issue_number_to_pr_body:\\n runs-on: ubuntu-latest\\n steps:\\n - name: append feature number to pr body pr branch = feat/(issueNumber)\\n uses: actions/github-script@v4\\n with:\\n github-token: ${{ secrets.GITHUB_TOKEN }}\\n script: |\\n const pr = await github.pulls.get({\\n owner: context.repo.owner,\\n repo: context.repo.repo,\\n pull_number: context.issue.number\\n });\\n const body = pr.data.body;\\n const issueNumber= pr.data.head.ref.split(\'/\')[1];\\n const newBody = body + \\"\\\\n\\\\n\\" + \\"close #\\" + issueNumber;\\n await github.pulls.update({\\n owner: context.repo.owner,\\n repo: context.repo.repo,\\n pull_number: context.issue.number,\\n body: newBody\\n });\\n```\\n\\n## \uc9c4\ud589 \uacfc\uc815\\n\\n1. pr \uc774 \uc0dd\uc131\ub418\uba74, pr\uc5d0 \ub300\ud55c \uc815\ubcf4\ub97c \uac00\uc838\uc635\ub2c8\ub2e4.\\n2. pr\uc758 \ubcf8\ubb38\uc744 \uac00\uc838\uc635\ub2c8\ub2e4.\\n3. pr\uc758 \ube0c\ub79c\uce58 \uc774\ub984\uc5d0\uc11c \uc774\uc288 \ubc88\ud638\ub97c \uac00\uc838\uc635\ub2c8\ub2e4.\\n4. pr\uc758 \ubcf8\ubb38\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.\\n5. pr\uc758 \ubcf8\ubb38\uc744 \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4.\\n\\n\uc774 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c, \uc9c1\uc811 pr\uc758 \ubcf8\ubb38\uc744 \uc218\uc815\ud558\uc9c0 \uc54a\uc544\ub3c4, \uc790\ub3d9\uc73c\ub85c \uc774\uc288 \ubc88\ud638\uac00 \ucd94\uac00\ub418\uae30\uc5d0, \uc2e4\uc218\ub97c \uc904\uc77c \uc218 \uc788\uc73c\ub2c8, \ud55c \ubc88 \uc2dc\ub3c4\ud574 \ubcf4\uc138\uc694"},{"id":"4","metadata":{"permalink":"/4","source":"@site/blog/2023-07-03-jay-infra.mdx","title":"\ud070 \ud2c0\uc5d0\uc11c \ubc14\ub77c\ubcf4\ub294 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98 \uacc4\ud68d","description":"\uc11c\ub860","date":"2023-07-03T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 3\uc77c","tags":[{"label":"java17","permalink":"/tags/java-17"},{"label":"infra","permalink":"/tags/infra"},{"label":"ec2","permalink":"/tags/ec-2"},{"label":"ci","permalink":"/tags/ci"},{"label":"cd","permalink":"/tags/cd"},{"label":"aws","permalink":"/tags/aws"}],"readingTime":7.19,"hasTruncateMarker":false,"authors":[{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"}],"frontMatter":{"slug":"4","title":"\ud070 \ud2c0\uc5d0\uc11c \ubc14\ub77c\ubcf4\ub294 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98 \uacc4\ud68d","authors":["jay"],"tags":["java17","infra","ec2","ci","cd","aws"]},"prevItem":{"title":"pr \ubcf8\ubb38\uc5d0 \uc774\uc288 \ubc88\ud638\ub97c \ub2ec\uc544\uc8fc\ub294 \uae30\ub2a5\uc744 \ub9cc\ub4e4\uc5c8\uc2b5\ub2c8\ub2e4","permalink":"/5"},"nextItem":{"title":"Java 17 \uc744 \ub3c4\uc785\ud55c \uc774\uc720","permalink":"/3"}},"content":"## \uc11c\ub860\\n\\n\uc548\ub155\ud558\uc138\uc694\ud83d\udc4b\ud83d\udc4b `\uce74\ud398\uc778` \ud300\uc758 \uc81c\uc774\uc785\ub2c8\ub2e4.\\n\\n\ud68c\uc758\ub97c \ud558\uba74\uc11c \uc774\ubc88 \uc8fc \uc81c\uac00 \ub9e1\uc740 \ud30c\ud2b8\ub294 \uc11c\ubc84 \uc778\ud504\ub77c\uc785\ub2c8\ub2e4.\\n\\n\uc544\uc9c1\uc740 EC2 \uc2a4\ud399\uacfc \ub370\uc774\ud130\ub4e4\uc774 \uc815\ud655\ud788 \ub098\uc624\uc9c4 \uc54a\uc558\uc9c0\ub9cc,\\n\uc6b0\ud14c\ucf54\uc5d0\uc11c \uc801\uc740 EC2 \uc2a4\ud399\uc744 \uc81c\uacf5\ud55c\ub2e4\ub294 \uae30\uc900\uc73c\ub85c \uacc4\ud68d\ub3c4\ub97c \uc801\uc5b4\ubcfc \uc0dd\uac01\uc785\ub2c8\ub2e4.\\n\\n\\n## \uc0c1\ud669 \uc778\uc2dd\\n\\n\uc608\uc0c1\ud558\ub294 \uc0c1\ud669\uc740 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\\n- API\uc758 \ub370\uc774\ud130\ub97c \ub2e4\ub8e8\ub294 \uc0c1\ud669\uc5d0\uc11c \ucd5c\uc18c \uc57d 150\ub9cc \uac74\uc5d0\uc11c \ucd5c\uc545 \uc57d 3700\ub9cc \uac74\uc758 \ub370\uc774\ud130\ub97c \ub2e4\ub8f9\ub2c8\ub2e4.\\n- \uc774\uc804 \uae30\uc218\ub97c \ubd24\uc744 \ub54c EC2\uc758 \uac1c\uc218\ub294 \ub9ce\uc774 \ub098\ub220\uc8fc\ub294 \uac83\uc73c\ub85c \ud30c\uc545 \ub410\uc2b5\ub2c8\ub2e4. (\uc774 \ubd80\ubd84\uc740 \ub2ec\ub77c\uc9c8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.)\\n- \uc0c1\ud669\uc5d0 \ub530\ub77c\uc11c \uacf5\uacf5 API\ub97c \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\ub294 \uc11c\ubc84\uc640, \uc81c\uacf5 \uc11c\ubc84\ub97c \ub098\ub20c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n- Conflict\uac00 \ub098\uc9c0 \uc54a\uae30 \uc704\ud574\uc11c \uc548\uc815\uc801\uc778 \uac80\uc99d\uc744 \uac70\uce5c \ud6c4 Merge\ub97c \ud574\uc57c\ud569\ub2c8\ub2e4.\\n- \ud504\ub85c\uc81d\ud2b8\uc758 \ubc84\uc804\uc774 \uac31\uc2e0\ub41c\ub2e4\uba74 EC2 \uc11c\ubc84\uc5d0\uc11c \uc790\ub3d9\uc73c\ub85c \uc2a4\ud06c\ub9bd\ud2b8\ub97c \uc791\ub3d9\uc2dc\ucf1c Pull \ubc0f \uc11c\ubc84 \uc7ac\ubc30\ud3ec\ub97c \ud574\uc57c\ud569\ub2c8\ub2e4.\\n- \uc11c\ubc84\uc758 \ubc84\uc804\uc774 \ubc14\ub00c\ub294 \uacbd\uc6b0 \uae30\uc874 \uc11c\ubc84\ub97c \ub044\uace0 \uc0c8\ub85c\uc6b4 \uc11c\ubc84\ub97c \ud0a4\uba74 \uc0ac\uc6a9\uc790\uac00 \uc774\uc6a9\ud560 \uc218 \uc5c6\ub294 \ud140\uc774 \uc0dd\uae30\uae30 \ub54c\ubb38\uc5d0 \ubb34\uc911\ub2e8 \ubc30\ud3ec\ub97c \ud574\uc57c\ud569\ub2c8\ub2e4.\\n\\n## \ubb38\uc81c\uc810\\n\\n\uc704\uc5d0 \uc0c1\ud669\uc5d0\uc11c \ud30c\uc545\ub418\ub294 \ubb38\uc81c\uc810\ub4e4\uc740 \uba3c\uc800 \uc801\uc740 \uc131\ub2a5\uc758 EC2 \uc11c\ubc84\ub85c \uc778\ud574 \ub370\uc774\ud130\ub97c \ubc1b\uc544\uc624\ub294 \uacfc\uc815 \ud639\uc740 \uc5c5\ub370\uc774\ud2b8 \uacfc\uc815\uc5d0\uc11c \uc11c\ubc84\uac00 \ud130\uc9c8 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\\n\uc131\ub2a5\uc774 \uc88b\ub2e4\uba74 \ud558\ub098\ub85c \ubaa8\ub4e0 \uac83\uc744 \ud560 \uc218 \uc788\uc9c0\ub9cc, \uadf8\ub807\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \ud604\uc7ac \uc5ec\ub7ec \uac1c\uc758 EC2\ub97c \uae30\uc900\uc73c\ub85c \uc544\ud0a4\ud14d\ucc98\ub97c \uad6c\uc131\ud560 \uc608\uc815\uc785\ub2c8\ub2e4.\\n\\n## \ubb38\uc81c \ud574\uacb0\uc744 \uc704\ud55c \ud604\uc7ac \uc0dd\uac01\\n\\n### \uc11c\ubc84\uc758 \uae30\ub2a5 \ubd84\uc0b0\\n\uc704\uc5d0\uc11c \uc5b8\uae09\ud55c \uac83\ucc98\ub7fc \uc11c\ubc84\uc758 \uc131\ub2a5\uc774 \ubc1b\uccd0\uc8fc\uc9c0 \ubabb\ud560 \uac00\ub2a5\uc131\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uc131\ub2a5\uc744 \uc0dd\uac01\ud574\uc11c \uc774\ub97c \ub098\ub204\uae30 \uc704\ud574\uc11c\ub294 \uba3c\uc800 \ub2e4\uc74c\uacfc \uac19\uc774 \uc11c\ubc84\ub97c \ubd84\uc0b0\ud560 \ud544\uc694\uac00 \uc788\ub2e4\uace0 \uc0dd\uac01\ud569\ub2c8\ub2e4.\\n(\ubb3c\ub860 \uc11c\ubc84\uac00 \ubabb \ubc84\ud2f8 \uacbd\uc6b0\uc774\uace0, \uc5b4\ub5bb\uac8c \ub098\ub258\ub294 \uc9c0\ub294 \ud68c\uc758 \ud6c4 \uacb0\uc815\ud558\uaca0\uc9c0\ub9cc!)\\n- `\uacf5\uacf5 API \ub370\uc774\ud130 \uc801\uc7ac \ubc0f \uc8fc\uae30\uc801\uc778 \uc5c5\ub370\uc774\ud2b8`\\n- `\uc2e4\uc2dc\uac04 \ud63c\uc7a1\ub3c4\ub97c \uc704\ud55c \uc2e4\uc2dc\uac04 \ub370\uc774\ud130 \uc5c5\ub370\uc774\ud2b8`\\n- `\uc694\uccad \ucc98\ub9ac`\\n\\n\uc801\uc740 \uc131\ub2a5\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\uc640 \uc694\uccad \ucc98\ub9ac\ub97c \ub3d9\uc2dc\uc5d0 \ud55c\ub2e4\uba74, \uc11c\ubc84\uac00 \uadf8 \ubd80\ud558\ub97c \uacac\ub514\uc9c0 \ubabb\ud560 \uc218\ub3c4 \uc788\uaca0\uc8e0?\\n\ub530\ub77c\uc11c \uc11c\ubc84\uc758 \uc5ed\ud560\uc744 \ubd84\ub2f4\ud558\uace0, \uac01 \uc5ed\ud560\uc5d0 \ucda9\uc2e4\ud558\ub3c4\ub85d \uad6c\ud604\ud55c\ub2e4\uba74 \ubcf4\ub2e4 \ud6a8\uc728\uc801\uc778 \ucc98\ub9ac\ub97c \ud560 \uc218 \uc788\uc744 \uac83\uc774\ub77c\uace0 \uc608\uc0c1\ub429\ub2c8\ub2e4.\\n\\n\\n### \uc548\uc815\uc801\uc778 Merge\\n\\n\uc798\ubabb\ub41c PR\uc744 Merge \uc2dc\ucf1c\ubc84\ub9ac\uba74 \uc5b4\ub5a8\uae4c\uc694? Conflict\ub3c4 \ub0a0 \uc218 \uc788\uace0.. \uc0dd\uac01\ub9cc\ud574\ub3c4 \ub054\ucc0d\ud569\ub2c8\ub2e4.\\n\\n\ucf54\ub4dc\ub9ac\ubdf0\ub97c \ud1b5\ud574\uc11c \uc774\ub97c \uc5b4\ub290\uc815\ub3c4 \ud574\uc18c\ud55c\ub2e4\uace0 \ud574\ub3c4, \uc0ac\ub78c\uc774\ub2e4\ubcf4\ub2c8 \uc2e4\uc218\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\uc774\ub97c \ud574\uacb0\ud558\uae30 \uc704\ud574\uc11c `Github Actions`\ub97c \uc774\uc6a9\ud558\uc5ec \ubbf8\ub9ac \uc9c0\uc815\ud574\ub454 Task\ub97c \uc2dc\ud0a4\uace0, \uc774\uac8c \ud1b5\uacfc\ud55c\ub2e4\uba74 Merge\ud560 \uc218 \uc788\ub3c4\ub85d \ud560 \uc608\uc815\uc785\ub2c8\ub2e4.\\n\\n\uc774\ub807\uac8c \ud55c\ub2e4\uba74 \ud611\uc5c5\ud560 \ub54c\uc5d0\ub3c4 \uc548\uc804\ud55c Merge\uac00 \uac00\ub2a5\ud558\ub2e4\uace0 \uc0dd\uac01\ud569\ub2c8\ub2e4.\\n\\n### CI/CD\\n\\n\uc9c0\uae08\uae4c\uc9c0 \uc6b0\ud14c\ucf54 \ubbf8\uc158\uc5d0\uc11c\ub294 \ubc30\ud3ec\ub97c \ub2e4\uc74c\uacfc \uac19\uc740 \uacfc\uc815\uc73c\ub85c \uc9c4\ud589\ud588\uc2b5\ub2c8\ub2e4.\\n\\n1. \ubc30\ud3ec\\n2. \ub9ac\ud329\ud1a0\ub9c1 \ubc0f \ucee4\ubc0b\\n3. EC2 \uc11c\ubc84\uc5d0\uc11c \uc2a4\ud06c\ub9bd\ud2b8 \uc2e4\ud589\ud558\uc5ec \uc7ac\ubc30\ud3ec\\n\\n\uc774\ub807\uac8c \ubc30\ud3ec\ub97c \ud574\ub3c4 \uc0c1\uad00\uc5c6\uc9c0\ub9cc, \ub9e4\ubc88 \ub9ac\ud329\ud1a0\ub9c1\uacfc \uae30\ub2a5 \ucd94\uac00\ub97c \ud560 \ub54c\ub9c8\ub2e4 EC2 \uc11c\ubc84\ub85c \ub4e4\uc5b4\uac00\uc11c \ube4c\ub4dc \uc2a4\ud06c\ub9bd\ud2b8\ub97c \uc0ac\uc6a9\ud574\uc11c \uc11c\ubc84\ub97c \uc7ac\uc2dc\uc791 \ud574\uc57c\ud560\uae4c\uc694?\\n\uc774\ub807\uac8c \ub41c\ub2e4\uba74 \ubd88\ud544\uc694\ud55c \uc2dc\uac04\uc774 \uc18c\ubaa8\ub418\uace0, \ubd88\ud3b8\ud55c \uc810\uc774 \ub9ce\uc744 \uac83\uc774\ub77c\uace0 \uc0dd\uac01\ub429\ub2c8\ub2e4.\\n\\n\ub530\ub77c\uc11c CI/CD \uac1c\ub150\uc744 \uc801\uc6a9\ud574\uc11c \uc774 \uacfc\uc815\uc744 \uc790\ub3d9\uc73c\ub85c \uc9c4\ud589\ud558\uace0\uc790 \ud569\ub2c8\ub2e4.\\n\\n\uc774 \ubd80\ubd84\uc740 \ub354 \uc54c\uc544\ubd10\uc57c\uaca0\uc9c0\ub9cc, Github Actions\ub97c \uc774\uc6a9\ud574\uc11c \uc774\ub97c \uc801\uc6a9\ud558\uba74, \uc678\ubd80\uc5d0\uc11c SSH \uc811\uadfc\uc774 \ubd88\uac00\ub2a5\ud558\uae30 \ub54c\ubb38\uc5d0 Jenkins\ub97c \uc774\uc6a9\ud560 \uc608\uc815\uc785\ub2c8\ub2e4.\\n\uae43\ud5c8\ube0c\uc758 \ubcc0\ub3d9 \uc0ac\ud56d\uc744 Webhook\uc744 \uc774\uc6a9\ud574\uc11c Jenkins\ub85c \ub118\uae30\uace0, \uc774\ub97c \ud1b5\ud574 CI\ub97c \uc801\uc6a9\ud558\uba74 \ub420 \uac83 \uac19\ub2e4\uace0 \ud310\ub2e8\ud588\uc2b5\ub2c8\ub2e4.\\n\ubb3c\ub860 \uc774\ub294 \uacc4\ud68d\uc774\uace0 \uacf5\ubd80\ud558\uc9c0 \uc54a\uc740 \ub2e4\ub978 \ub0b4\uc6a9\uc774 \uc788\uc744 \uc218 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc5b8\uc81c\ub4e0 \ubc14\ub014 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n### \ubb34\uc911\ub2e8 \ubc30\ud3ec \uc544\ud0a4\ud14d\ucc98 \uc801\uc6a9\\n\uc774 \ub610\ud55c \uc544\uc9c1\uc740 \uba3c \uc774\uc57c\uae30\uc9c0\ub9cc, \uace0\ub824\ud574 \ubcfc \uc0c1\ud669\uc774\ub77c\uc11c \uc801\uc5b4\ubd24\uc2b5\ub2c8\ub2e4.\\n\\n\uc0ac\uc6a9\uc790\uac00 \uc774\uc6a9\ud558\uace0 \uc788\ub294 \uc11c\ube44\uc2a4\uac00 \uac11\uc790\uae30 \uc911\ub2e8\ub41c\ub2e4\uba74 \uc5b4\ub5a8\uae4c\uc694?\\n\uc800\ub294 \ud654\uac00 \ub9ce\uc774 \ub0a0 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ud53c\uce58 \ubabb\ud560 \uc0ac\uc815\uc73c\ub85c \uc11c\ubc84\uac00 \ud130\uc838\ub3c4, \uc0ac\uc6a9\uc790\uac00 \uc11c\ube44\uc2a4\ub97c \uacc4\uc18d \uc774\uc6a9\ud560 \ubc29\ubc95\uc774 \uc5c6\uc744\uae4c\uc694?\\n\\n\uc774\ub7f0 \uace0\ubbfc\uc744 \ud574\uacb0\ud558\uae30 \uc704\ud574\uc11c \ub098\uc628 \uac1c\ub150\uc774 \ubb34\uc911\ub2e8 \ubc30\ud3ec\uc785\ub2c8\ub2e4.\\n\\n`\uce74\ub098\ub9ac\uc544 \ubc30\ud3ec`, `Blue/Green \ubc30\ud3ec`, `\ub864\ub9c1`\ub4f1 \ubb34\uc911\ub2e8 \ubc30\ud3ec\ub97c \uc704\ud55c \uc5ec\ub7ec\uac00\uc9c0 \uc804\ub7b5\uc740 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4.\\n\uc774 \ubd80\ubd84\uc740 \uc544\uc9c1\uc740 \uc11c\ubc84\uc758 \uba85\uc138\uac00 \uc815\ud655\ud558\uc9c0 \uc54a\uc544\uc11c \uc5b4\ub5a4 \ubc29\uc2dd\uc73c\ub85c \uc5b4\ub5bb\uac8c \ucc98\ub9ac\ud560 \uac83\uc778\uc9c0\uc5d0 \ub300\ud574\uc11c\ub294 \uc544\uc9c1 \uc815\ud560 \uc218\ub294 \uc5c6\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub294 \uba85\uc138\uac00 \ud655\uc2e4\ud558\uac8c \uc815\ud574\uc9c4 \ud6c4 \ud300\uc6d0\uacfc \uc7a5\ub2e8\uc810\uc744 \uc0c1\uc758\ud558\uba70 \uacb0\uc815\ud560 \uc77c\uc774\uae30 \ub54c\ubb38\uc5d0 \ud604\uc7ac\uae4c\uc9c0\ub294 \\"\uc774 \uc815\ub3c4\ub97c \uace0\ub824\ud558\uace0 \uc788\ub2e4.\\" \uc815\ub3c4\ub9cc \uc54c\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4."},{"id":"3","metadata":{"permalink":"/3","source":"@site/blog/2023-07-02-nunu-java-version.mdx","title":"Java 17 \uc744 \ub3c4\uc785\ud55c \uc774\uc720","description":"\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4\uc5d0\uc11c \uc790\ubc14 11\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \ub108\ubb34 \uc775\uc219\ud574\uc9c4 \uc0c1\ud669\uc774\uc5b4\uc11c, java 11 \ub300\uc2e0 java 17\uc744 \uc4f0\ub824\uba74 \uc4f0\ub294 \ub300\uc2e0, \uc65c java 17\uc744 \uc4f0\uba74 \uc88b\uc740\uc9c0\uc5d0 \ub300\ud574\uc11c \uc124\ub4dd\uc744 \ud558\ub294 \uc2dc\uac04\uc774 \uc788\uc5b4\uc57c \ud558\ub294\ub370\uc694","date":"2023-07-02T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 2\uc77c","tags":[{"label":"java17","permalink":"/tags/java-17"},{"label":"java11","permalink":"/tags/java-11"},{"label":"record","permalink":"/tags/record"},{"label":"toList","permalink":"/tags/to-list"},{"label":"gc","permalink":"/tags/gc"}],"readingTime":5.88,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"3","title":"Java 17 \uc744 \ub3c4\uc785\ud55c \uc774\uc720","authors":["nunu"],"tags":["java17","java11","record","toList","gc"]},"prevItem":{"title":"\ud070 \ud2c0\uc5d0\uc11c \ubc14\ub77c\ubcf4\ub294 \uc11c\ubc84 \uc544\ud0a4\ud14d\ucc98 \uacc4\ud68d","permalink":"/4"},"nextItem":{"title":"git branch \uc804\ub7b5 \uc791\uc131\ud574\ubcf4\uae30","permalink":"/2"}},"content":"\uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4\uc5d0\uc11c \uc790\ubc14 11\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \ub108\ubb34 \uc775\uc219\ud574\uc9c4 \uc0c1\ud669\uc774\uc5b4\uc11c, java 11 \ub300\uc2e0 java 17\uc744 \uc4f0\ub824\uba74 \uc4f0\ub294 \ub300\uc2e0, \uc65c java 17\uc744 \uc4f0\uba74 \uc88b\uc740\uc9c0\uc5d0 \ub300\ud574\uc11c \uc124\ub4dd\uc744 \ud558\ub294 \uc2dc\uac04\uc774 \uc788\uc5b4\uc57c \ud558\ub294\ub370\uc694\\n\\n\ucc98\uc74c\uc5d0\ub294 \ub2e8\uc21c\ud788 record \ud074\ub798\uc2a4\uac00 \uc88b\uc544\uc694, collect(Collectors.toList()); \ub300\uc2e0 toList() \ub9cc\uc73c\ub85c \ud574\uacb0\ud560 \uc218 \uc788\uc5b4\uc11c \uc88b\uc544\uc694\\n\\n\uae4c\uc9c0\ubc16\uc5d0 \uc124\uba85\ud560 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uac83\ub9cc\uc73c\ub85c \ub3d9\uc758\ub97c \ud574\uc918\uc11c \uc77c\ub2e8 java 17 \uc744 \uc0ac\uc6a9\ud558\uae30\ub85c \ud588\uc9c0\ub9cc, \uc774\ubc88 \uae30\ud68c\uc5d0 \uc870\uae08 \ub354 \uc790\uc138\ud558\uac8c \uc54c\uc544\ubcf4\ub824\uace0 \ud569\ub2c8\ub2e4\\n\\n## Java 17 \uacfc Java 11\uc758 \uc911\uc694\ud55c \ucc28\uc774\ub4e4\\n\\n\uae30\ub2a5\uc801\uc778 \ubd80\ubd84\uacfc, \uc228\uaca8\uc9c4 \ubd80\ubd84\uc744 \ub098\ub204\uc5b4\ubcfc \uc218 \uc788\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n## \uae30\ub2a5\uc801\uc778 \ucc28\uc774\uc810\\n\\n\uc5b8\uc81c\ub098 \uc9c1\uc811 \ucc28\uc774\ub97c \ubcf4\uba74 \ub354 \uc9c1\uad00\uc801\uc774\uae30 \ub54c\ubb38\uc5d0, \uc9c1\uc811 \ucf54\ub4dc\ub97c \ubcf4\uba74\uc11c \uc124\uba85\uc744 \ud574\ubcf4\ub824\uace0 \ud569\ub2c8\ub2e4\\n\\n### record \ud074\ub798\uc2a4\\n\\n\uac04\ub2e8\ud55c dto \ud074\ub798\uc2a4\ub97c \ub9cc\ub4e4\uc5c8\uc744 \ub54c \ucf54\ub4dc\uac00 \uc815\ub9d0 \uac04\ub2e8\ud574\uc9c0\ub294 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n#### Java 11\\n\\n```\\npublic class Dto {\\n private final int data;\\n\\n public Dto(int data) {\\n this.data = data;\\n }\\n\\n public int getData() {\\n return data;\\n }\\n}\\n```\\n\\nlombok \uc744 \uc0ac\uc6a9\ud588\uc744 \ub54c\\n\\n```\\n\\n@Getter\\n@AllArgsConstructor\\npublic class Dto {\\n private final int data;\\n}\\n```\\n\\n#### Java17\\n\\n```\\npublic record Record(int data) {\\n}\\n```\\n\\n\uc774\ub807\uac8c \ubcf4\uba74 \ud6e8\uc52c \uac04\ub2e8\ud574\uc9c4 \uac83\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n#### \uc608\uc0c1\ub418\ub294 \ubb38\uc81c\uc810\\n\\nobjectMapper\ub97c \uc0ac\uc6a9\ud558\uba74 \uc5b4\ub5bb\uac8c \ub418\ub098\uc694? noArgsConstructor \uac00 \ud544\uc694\ud558\uc9c0 \uc54a\ub098\uc694?\\n\\n```java\\nclass RecordTest {\\n\\n @Test\\n void objectMapper_\ub85c_\ubcc0\ud658() throws JsonProcessingException {\\n // given\\n ObjectMapper objectMapper = new ObjectMapper();\\n Record record = new Record(1);\\n\\n // when\\n String json = objectMapper.writeValueAsString(record);\\n\\n // then\\n assertEquals(\\"{\\\\\\"data\\\\\\":1}\\", json);\\n }\\n\\n @Test\\n void string_\uc5d0\uc11c_\uac1d\uccb4\ub85c_\ubcc0\ud658() throws JsonProcessingException {\\n // given\\n String json = \\"{\\\\\\"data\\\\\\":1}\\";\\n ObjectMapper objectMapper = new ObjectMapper();\\n\\n // when\\n Record record = objectMapper.readValue(json, Record.class);\\n\\n // then\\n assertEquals(1, record.data());\\n }\\n}\\n```\\n\\n\uc774 \ud14c\uc2a4\ud2b8\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \uac83\ucc98\ub7fc \uc131\uacf5\uc801\uc73c\ub85c deserialize, serialize \uac00 \uac00\ub2a5\ud569\ub2c8\ub2e4\\n\\n### toList() method\\n\\n#### Java 11\\n\\n\uc774 \ubd80\ubd84\ub3c4 \uc815\ub9d0 \ud3b8\uc758\uc131\uc774 \ub192\ub2e4\uace0 \uc0dd\uac01\ud558\ub294 \ubd80\ubd84 \uc911 \ud558\ub098\uc778\ub370\uc694\\n\\nCollectors.toList() \ub300\uc2e0 toList() \ub9cc\uc73c\ub85c\ub3c4 \uc0ac\uc6a9\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4\\n\\n```java\\npublic class ToListWith11 {\\n\\n public static void main(String[] args) {\\n List list = List.of(1, 2, 3, 4, 5);\\n List result = list.stream()\\n .filter(i -> i > 3)\\n .collect(Collectors.toList());\\n System.out.println(result);\\n }\\n}\\n```\\n\\n#### Java 17\\n\\n```java\\npublic class ToListWith17 {\\n\\n public static void main(String[] args) {\\n List list = List.of(1, 2, 3, 4, 5);\\n List result = list.stream()\\n .filter(i -> i > 3)\\n .toList();\\n System.out.println(result);\\n }\\n}\\n```\\n\\n### switch expression\\n\\n#### Java 11\\n\\n\uc6b0\ud14c\ucf54\uc5d0\uc11c\ub294 switch, case \ub97c \uc2eb\uc5b4\ud558\uae30\uc5d0 \ubcfc \uc218\ub294 \uc5c6\uaca0\uc9c0\ub9cc\\n\\nswitch \ubb38\uc5d0\ub3c4 \uc815\ub9d0 \ud3b8\ud558\uac8c \ubc14\ub00c\uc5c8\ub294\ub370\uc694\\n\\n```java\\npublic class SwitchWith11 {\\n\\n public static void main(String[] args) {\\n String day = \\"Sunday\\";\\n int result = 0;\\n switch (day) {\\n case \\"Monday\\":\\n result = 1;\\n break;\\n case \\"Tuesday\\":\\n result = 2;\\n break;\\n case \\"Wednesday\\":\\n result = 3;\\n break;\\n case \\"Thursday\\":\\n result = 4;\\n break;\\n case \\"Friday\\":\\n result = 5;\\n break;\\n case \\"Saturday\\":\\n result = 6;\\n break;\\n case \\"Sunday\\":\\n result = 7;\\n break;\\n }\\n System.out.println(result);\\n }\\n}\\n```\\n\\n#### Java 17\\n\\n```java\\npublic class SwitchWith17 {\\n\\n public static void main(String[] args) {\\n String day = \\"Sunday\\";\\n int result = switch (day) {\\n case \\"Monday\\" -> 1;\\n case \\"Tuesday\\" -> 2;\\n case \\"Wednesday\\" -> 3;\\n case \\"Thursday\\" -> 4;\\n case \\"Friday\\" -> 5;\\n case \\"Saturday\\" -> 6;\\n case \\"Sunday\\" -> 7;\\n default -> 0;\\n };\\n System.out.println(result);\\n }\\n}\\n```\\n\\n\ucf54\ub4dc \ub7c9\uc774 \uc5c4\uccad \uc904\uc5b4\ub4e0 \uac83\uc744 \ud655\uc778\ud558\uc2e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n### instanceof pattern matching\\n\\n\ubb3c\ub860 instanceof \ub97c \uc0ac\uc6a9\ud560 \uacbd\uc6b0\uac00 \ub9ce\uc740\uac00? \ud558\uba74 \ub9ce\uc9c0\ub294 \uc54a\uaca0\uc9c0\ub9cc\\n\\n\uc544\ub798\uc640 \uac19\uc774 \ubcc0\uacbd\ub418\uc5c8\uc2b5\ub2c8\ub2e4\\n\\n#### Java 11\\n\\n```java\\npublic class InstanceOfWith11 {\\n\\n public static void main(String[] args) {\\n Object obj = \\"Hello\\";\\n if (obj instanceof String) {\\n String str = (String) obj;\\n System.out.println(str.toUpperCase());\\n }\\n }\\n}\\n```\\n\\n#### Java 17\\n\\n```java\\npublic class InstanceOfWith17 {\\n\\n public static void main(String[] args) {\\n Object obj = \\"Hello\\";\\n if (obj instanceof String str) {\\n System.out.println(str.toUpperCase());\\n }\\n }\\n}\\n```\\n\\n### number format\\n\\n\uc774 \uae30\ub2a5\uc740 12\uc5d0 \ub098\uc654\ub294\ub370\uc694\\n\\n\uc5b8\uc5b4\ubcc4\ub85c \uc22b\uc790\ub97c \ud45c\ud604\ud558\ub294 \ubc29\uc2dd\uc774 \ub2e4\ub974\uc9c0\ub9cc, \uc27d\uac8c \ud45c\ud604\ud560 \uc218 \uc788\ub3c4\ub85d \ub3c4\uc640\uc8fc\ub294 \uae30\ub2a5\uc785\ub2c8\ub2e4\\n\\n#### Java 17\\n\\n```java\\npublic class NumberFormatterWith11 {\\n public static void main(String[] args) {\\n int number = 1_000_000;\\n\\n String result = NumberFormat.getCompactNumberInstance(Locale.KOREA, NumberFormat.Style.LONG).format(number);\\n\\n System.out.println(result.equals(\\"100\ub9cc\\"));\\n }\\n}\\n```\\n\\n\ub098\uba38\uc9c0 \ubd80\ubd84\uc740 \uc0ac\uc2e4 \uadf8\ub807\uac8c \ud070 \uc5ed\ud560\uc744 \ud560 \uac83 \uac19\uc9c0\ub294 \uc54a\uc544\uc11c \uc0dd\ub7b5\ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\n## \uc228\uaca8\uc9c4 \ubd80\ubd84\ub4e4\\n\\n![gc throughput](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXhFJg%2Fbtsl9uZOa5R%2FrzrlotCERUqAWM2pknDwq0%2Fimg.png)\\n\\n\uc704\uc758 \uc0ac\uc9c4\uc740 gc \uc758 \ubc84\uc804\ubcc4 \ucc98\ub9ac\ub7c9\uc785\ub2c8\ub2e4.\\n\\nG1 GC \ub97c \uae30\uc900\uc73c\ub85c \ubcf8\ub2e4\uba74 Java8 \uacfc\uc758 \ucc28\uc774\ub294 15% \uc815\ub3c4 \ud5a5\uc0c1\ub418\uc5c8\uace0, java 11\uacfc\ub294 10% \uc815\ub3c4 \ud5a5\uc0c1\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n![gc latency](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZusmb%2Fbtsl5jYN68u%2FWCKRCFnYjQK4AjkcHRNAt0%2Fimg.png)\\n\\n\uc704\uc758 \uc0ac\uc9c4\uc740 gc\uc758 \ubc84\uc804\ubcc4 \uc9c0\uc5f0\uc2dc\uac04\uc785\ub2c8\ub2e4.\\n\\nG1 GC \ub97c \uae30\uc900\uc73c\ub85c \ubcf8\ub2e4\uba74 Java8 \uacfc\uc758 \ucc28\uc774\ub294 30% \uc815\ub3c4 \ud5a5\uc0c1\ub418\uc5c8\uace0, java 11\uacfc\ub294 25% \uc815\ub3c4 \ud5a5\uc0c1\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\uc640 \uac19\uc774, \ub2e8\uc21c\ud558\uac8c \uc0c8\ub85c\uc6b4 \uae30\ub2a5\ub9cc \ucd94\uac00\ub418\ub294 \uac83\uc774 \uc544\ub2c8\ub77c \uafb8\uc900\ud788 \uc131\ub2a5\ub3c4 \ud5a5\uc0c1\ub418\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \ubd80\ubd84\uc744 \uace0\ub824\ud588\uc744 \ub54c, Java 17\uc744 \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4.\\n\\n\ucc38\uace0\\n\\n- [https://kstefanj.github.io/2021/11/24/gc-progress-8-17.html](https://kstefanj.github.io/2021/11/24/gc-progress-8-17.html)"},{"id":"2","metadata":{"permalink":"/2","source":"@site/blog/2023-07-01-nunu-gitbranch.mdx","title":"git branch \uc804\ub7b5 \uc791\uc131\ud574\ubcf4\uae30","description":"\ud604\uc7ac \uc0c1\ud669\uc740 \uc5b4\ub5a4\ub370?","date":"2023-07-01T00:00:00.000Z","formattedDate":"2023\ub144 7\uc6d4 1\uc77c","tags":[{"label":"git","permalink":"/tags/git"},{"label":"branch","permalink":"/tags/branch"},{"label":"git branch","permalink":"/tags/git-branch"},{"label":"github flow","permalink":"/tags/github-flow"},{"label":"gitlab flow","permalink":"/tags/gitlab-flow"},{"label":"git flow","permalink":"/tags/git-flow"}],"readingTime":10.735,"hasTruncateMarker":false,"authors":[{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"}],"frontMatter":{"slug":"2","title":"git branch \uc804\ub7b5 \uc791\uc131\ud574\ubcf4\uae30","authors":["nunu"],"tags":["git","branch","git branch","github flow","gitlab flow","git flow"]},"prevItem":{"title":"Java 17 \uc744 \ub3c4\uc785\ud55c \uc774\uc720","permalink":"/3"},"nextItem":{"title":"Hello World","permalink":"/1"}},"content":"## \ud604\uc7ac \uc0c1\ud669\uc740 \uc5b4\ub5a4\ub370?\\n\\n\ud604\uc7ac \uc6b0\uc544\ud55c\ud14c\ud06c\ucf54\uc2a4\uc5d0\uc11c\ub294 \ud504\ub860\ud2b8 \ucf54\ub4dc\uc640 \ubc31\uc5d4\ub4dc \ucf54\ub4dc\uac00 \uac19\uc740 \ub808\ud3ec\uc9c0\ud1a0\ub9ac\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\ud504\ub860\ud2b8\uc640 \ubc31\uc5d4\ub4dc\uac00 \uac19\uc774 \uc791\uc5c5\ud558\uae30\uc5d0, \uc758\ub3c4\uce58 \uc54a\uc740 \ucda9\ub3cc\uc774 \uc790\uc8fc \uc0dd\uae38 \uc218 \uc788\ub294 \uad6c\uc870\uc774\uae30\uc5d0, \uc774\ub97c git branch \uc804\ub7b5\uc73c\ub85c \ucda9\ub3cc\uc744 \uc904\uc774\uace0\uc790 \ud569\ub2c8\ub2e4\\n\\n## Git Branch \uc804\ub7b5\uc774\ub780?\\n\\ngit\uc744 \uc0ac\uc6a9\ud574\uc11c \uc18c\ud504\ud2b8\uc6e8\uc5b4 \uac1c\ubc1c\uc744 \uad00\ub9ac\ud558\ub294 \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\\n\uc5ec\ub7ec \uac1c\ubc1c\uc790\uac00 \ub3d9\uc2dc\uc5d0 \uc791\uc5c5\ud558\uace0 \ucf54\ub4dc\ub97c \ud1b5\ud569\ud560 \ub54c \uc0dd\uae30\ub294 \ucda9\ub3cc\uc744 \ud6a8\uc728\uc801\uc73c\ub85c \uc870\uc815\ud558\uae30 \uc704\ud55c \ubc29\ubc95\uc785\ub2c8\ub2e4.\\n\\n## \uc65c git branch \uc804\ub7b5\uc774 \uc911\uc694\ud55c\ub370?\\n\\n\uc544\ub798\uc5d0 \uc788\ub294 4\uac00\uc9c0\ub97c \uc81c\uc678\ud558\uace0\ub3c4 \ud6e8\uc52c \ub9ce\uc740 \uc7a5\uc810\uc774 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### 1\\\\. \ub3d9\uc2dc \uc791\uc5c5\uc774 \ud3b8\ud558\ub2e4\\n\\n\uc5ec\ub7ec \uc0ac\ub78c\uc774 \ub3c5\ub9bd\uc801\uc73c\ub85c \uc791\uc5c5\ud558\uace0, \ucee4\ubc0b\uc744 \ud560 \ub54c, \uc790\uc2e0\uc758 \ube0c\ub79c\uce58\uc5d0\uc11c \ubcc0\uacbd \uc0ac\ud56d\uc744 \ucee4\ubc0b\ud558\uac8c \ub429\ub2c8\ub2e4.\\n\\n\ube0c\ub79c\uce58\uac00 \ubcd1\ud569\ub420 \ub54c\ub9cc \ucda9\ub3cc\uc744 \ud574\uacb0\ud558\uba74 \ub418\ub2c8, \uc544\ubb34 \uaddc\uce59\uc774 \uc5c6\ub294 \uac83\ubcf4\ub2e4 \ucda9\ub3cc \uc2dc\uc810\uc774 \uba85\ud655\ud574\uc9c0\uae30\uc5d0 \uc0dd\uc0b0\uc131\uc744 \ub192\uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### 2\\\\. \ubaa9\uc801\uc774 \uba85\ud655\ud55c \ube0c\ub79c\uce58\\n\\n\uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc758 \uc0c1\ud0dc\uc5d0 \uba87 \uac00\uc9c0\uac00 \uc788\ub294\ub370, \uc548\uc815\ub41c \ud504\ub85c\ub355\uc158, \ud14c\uc2a4\ud2b8 \ud658\uacbd, \uae30\ub2a5 \ucd94\uac00 \ud658\uacbd... \ub4f1\uc774 \uc788\uc2b5\ub2c8\ub2e4\\n\\n\uc5ec\ub7ec \uae30\ub2a5\ubcc4 \ube0c\ub79c\uce58(\uc548\uc815\ub41c \ubc84\uc804\uc758 \ucf54\ub4dc\ub9cc\uc774 \uad00\ub9ac\ub418\ub294 \ube0c\ub79c\uce58, \ud14c\uc2a4\ud2b8 \ud658\uacbd\uc744 \uc704\ud55c \ube0c\ub79c\uce58, \uae30\ub2a5 \ucd94\uac00\ub97c \uc704\ud55c \ube0c\ub79c\uce58)\ub97c\\n\\n\ub124\uc774\ubc0d\uc744 \ud1b5\ud574 \uad6c\ubd84\ud558\uba74 \uac01\uac01\uc758 \ube0c\ub79c\uce58\uc5d0 \ub300\ud574\uc11c \ucd94\uac00\uc801\uc778 \uc124\uba85\uc744 \ud560 \ud544\uc694 \uc5c6\uc774 \uba85\ud655\ud558\uac8c \uad00\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### 3\\\\. \ubc30\ud3ec \ud30c\uc774\ud504\ub77c\uc778 \uad00\ub9ac\uac00 \ud3b8\ud568\\n\\n\ube0c\ub79c\uce58\uac00 \ub124\uc774\ubc0d\uc73c\ub85c \uba85\ud655\ud558\uac8c \uad6c\ubd84\uc774 \ub418\uc5b4\uc788\ub2e4\uba74, \uc870\uac74\uc744 \uc124\uc815\ud558\uae30 \uc27d\uc2b5\ub2c8\ub2e4.\\n\\n\ud2b9\uc815 \ud0c0\uc785\uc758 \ube0c\ub79c\uce58\uc5d0 push \ub418\uc5c8\uc744 \ub54c, pull request\ub97c \ub9cc\ub4e4\uc5c8\uc744 \ub54c \uac19\uc740 \uc870\uac74\uc5d0 \ub530\ub978 \uc2a4\ud06c\ub9bd\ud2b8\ub97c \ub9cc\ub4e4\uc5b4\ub454\ub2e4\uba74 CI/CD\ub97c \uad6c\ucd95\ud558\uae30 \uc27d\uc2b5\ub2c8\ub2e4.\\n\\n#### 4\\\\. \ubc84\uc804 \uad00\ub9ac\uac00 \ud3b8\ub9ac\ud558\ub2e4\\n\\n\uc11c\ubc84\uc5d0 \ubb38\uc81c\uac00 \uc0dd\uacbc\uc744 \ub54c, \uc5b4\ub5a4 \ube0c\ub79c\uce58\ub85c \ub3cc\uc544\uac00\uc11c \ub864\ubc31\uc744 \ud574\uc57c \ud558\ub294\uc9c0\uc5d0 \ub300\ud55c \uac83\ub4e4\uc774 \uba85\ud655\ud569\ub2c8\ub2e4.\\n\\n\uc548\uc815\ub41c \ube0c\ub79c\uce58\uac00 \uc5b4\ub5a4 \uac83\uc778\uc9c0 \uba85\ud655\ud558\uae30\uc5d0, \ub864\ubc31 \uacfc\uc815\uc5d0 \ub300\ud55c \uc758\uc0ac\uacb0\uc815\uc744 \uc904\uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uadf8\ub7ec\uba74 \uc5b4\ub5a4 \uc885\ub958\uac00 \uc788\ub294\uc9c0 \ub354 \uc790\uc138\ud558\uac8c \uc54c\uc544\ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## Git Branch \uc804\ub7b5\uc758 \uc885\ub958\ub294?\\n\\n\ucd1d 3\uac00\uc9c0\uc758 \uc804\ub7b5\uc774 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1\\\\. Github Flow\\n\\n2\\\\. Gitlab Flow\\n\\n3\\\\. Git Flow\\n\\ngit\uc744 \uc0ac\uc6a9\ud558\uae30\uc5d0, Git Flow\ub77c\ub294 \ub124\uc774\ubc0d\uc774 \uac00\uc7a5 \uc9c1\uad00\uc801\uc774\uace0 \uc720\uba85\ud55c\ub370\uc694.\xa0\\n\\n3\uac00\uc9c0 \uc804\ub7b5 \uc911\uc5d0\uc11c \uac00\uc7a5 \ubcf5\uc7a1\ud558\uae30\uc5d0, \uc26c\uc6b4 \uc21c\uc11c\ub300\ub85c \uc9c4\ud589\ud574 \ubcf4\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n## 1\\\\. Github Flow\\n\\n\uadf8\ub9bc\uc73c\ub85c flow \uac04\ub2e8\ud558\uac8c \ubcf4\uace0 \uac00\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![img](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblgfI6%2FbtslEWRFdaJ%2F3KmwR2yqlfgKk0msnufYNk%2Fimg.png)\\n\\n![img2](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtUzxm%2FbtslJ1xWHzy%2FMP0s11FoCTKpqwQnUJUm30%2Fimg.png)\\n\\n\ube0c\ub79c\uce58\ub294 \ucd1d 2\uac00\uc9c0 \uc885\ub958\uac00 \uc874\uc7ac\ud569\ub2c8\ub2e4\\n\\n#### 1\\\\. master \ube0c\ub79c\uce58\\n\\n\uc5ec\uae30\uc5d0 \uba38\uc9c0\uac00 \ub418\uba74 \ubc30\ud3ec\uac00 \ub418\ub3c4\ub85d CD\ub97c \uc5f0\uacb0\ud574 \ub193\uc740 \uacbd\uc6b0\uac00 \ub9ce\uc2b5\ub2c8\ub2e4.\\n\\n\uc548\uc815\ub41c \ubc84\uc804\uc758 \ucf54\ub4dc\uac00 \uad00\ub9ac\ub418\ub294 \ube0c\ub79c\uce58\uc785\ub2c8\ub2e4.\\n\\n#### 2\\\\. feature \ube0c\ub79c\uce58\\n\\n\uae30\ub2a5 \ucd94\uac00, \ubc84\uadf8 \uc218\uc815 \ub4f1 \ubaa8\ub4e0 \uc791\uc5c5\uc740 feature \ube0c\ub79c\uce58\uc5d0\uc11c \uc77c\uc5b4\ub0a9\ub2c8\ub2e4.\\n\\nmaster \ube0c\ub79c\uce58\uc5d0\uc11c \uc0c8\ub85c\uc6b4 \ube0c\ub79c\uce58\ub97c \ub9cc\ub4e4\uc5b4\uc11c, \ub9c8\uc2a4\ud130\ub85c \uba38\uc9c0\ub418\ub294 \ub2e8\uc21c\ud55c \uc0ac\uc774\ud074\uc744 \uac00\uc9c0\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### \uc7a5\uc810\\n\\n\uc704\uc5d0\uc11c \ubcfc \uc218 \uc788\ub294 \uac83\ucc98\ub7fc 2\uc885\ub958\uc758 \ube0c\ub79c\uce58\ub9cc \uc788\uae30\uc5d0, \uc815\ub9d0 \uac04\ub2e8\ud569\ub2c8\ub2e4.\\n\\n\ud559\uc2b5 \uacfc\uc815\uae4c\uc9c0\uc758 \ub7ec\ub2dd \ucee4\ube0c\uac00 \uac70\uc758 \uc5c6\ub2e4\uc2dc\ud53c \ud558\uae30\uc5d0, \uac04\ub2e8\ud55c \ud504\ub85c\uc81d\ud2b8\uc5d0 \uc801\uc6a9\ud558\uae30 \uc815\ub9d0 \uc88b\uc2b5\ub2c8\ub2e4.\\n\\n\ub9b4\ub9ac\uc988 \ub418\uc9c0 \uc54a\uc740 \ucf54\ub4dc\uac00 \ucd5c\uc18c\ud654\ub429\ub2c8\ub2e4. \ucd5c\uc2e0 \ubc84\uc804\uc758 \ucf54\ub4dc\uc640 \ucd5c\ub300\ud55c \ube60\ub974\uac8c \ub3d9\uae30\ud654\ub97c \uacc4\uc18d\ud574\uc11c \uc2dc\ud0ac \uc218 \uc788\uc2b5\ub2c8\ub2e4\\n\\n#### \ub2e8\uc810\\n\\n\ubaa8\ub4e0 \ucf54\ub4dc\ub294 \ub2e4 master \ube0c\ub79c\uce58\uc5d0 \uba38\uc9c0\uac00 \ub418\uc5b4\uc57c \ud55c\ub2e4\ub294 \uc810\uc774 \uac1c\ubc1c \uc11c\ubc84\uc640, \uc6b4\uc601\uc11c\ubc84\ub97c \ub098\ub204\uae30 \uc560\ub9e4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uac1c\ubc1c \uc11c\ubc84\uc5d0 \ubc30\ud3ec\ub97c \ud558\uace0 \uc2f6\uc740 \uc0c1\ud669\uc774\ub77c\uba74, master\uc5d0 \uba38\uc9c0\uac00 \ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4.\\n\\n\uba38\uc9c0\uac00 \ub41c \uc774\ud6c4\uc5d0 cd \ud30c\uc774\ud504\ub77c\uc778\uc744 \ud1b5\ud574\uc11c \uac1c\ubc1c \uc11c\ubc84\uc640 \uc6b4\uc601 \uc11c\ubc84 \ubaa8\ub450\uc5d0 \ubc30\ud3ec\uac00 \ub429\ub2c8\ub2e4.\\n\\n\uc5ec\ub7ec \ud658\uacbd\uc744 \ub098\ub204\uace0 \uad00\ub9ac\ub97c \ud558\uace0 \uc2f6\uc73c\uc2dc\ub2e4\uba74 \ub2e4\uc74c\uc5d0 \uc18c\uac1c\ud574\ub4dc\ub9b4 \uc804\ub7b5\uc744 \uc0ac\uc6a9\ud574 \ubcf4\uc154\ub3c4 \uc88b\uc744 \uac83 \uac19\uc2b5\ub2c8\ub2e4\\n\\n## 2\\\\. Gitlab Flow\\n\\n\uadf8\ub9bc\uc73c\ub85c flow \uac04\ub2e8\ud558\uac8c \ubcf4\uace0 \uac00\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4.\\n\\n![img2](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdlarwn%2FbtslKkYqqTR%2FXi8NnZIEXahoVFusk0xV31%2Fimg.png)\\n\\n\ubc11\uc5d0 \ud658\uacbd\uc740 \ucd1d 2\uac1c\uc758 \uc11c\ubc84\uac00 \uc874\uc7ac\ud560 \ub54c\ub97c \uac00\uc815\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1\\\\. pre-production \uc11c\ubc84\\n\\n2\\\\. production \uc11c\ubc84\\n\\n\ud3b8\uc758\ub97c \uc704\ud574 main\uc5d0 \uba38\uc9c0\ub418\ub294 \uacfc\uc815\uc740 \uac04\ub2e8\ud558\uac8c \ud45c\ud604\ud588\uc2b5\ub2c8\ub2e4.\\n\\n![img3](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbkNc9%2FbtslJ0MBrWb%2F0CT7DVQoCDFOpbqyAko9mk%2Fimg.png)\\n\\n#### \ube0c\ub79c\uce58 \uc885\ub958\\n\\n\ucd1d 3\uac00\uc9c0 \ube0c\ub79c\uce58\uac00 \ud544\uc694\ud558\uace0, \ucd94\uac00\uc5d0 \ub530\ub77c\uc11c \ub354 \ucd94\uac00\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n1\\\\. main(or develop) \ube0c\ub79c\uce58\\n\\n\uae30\ub2a5\uc5d0 \ub300\ud55c \uac1c\ubc1c\uc774 \uc644\ub8cc\ub418\uc5c8\uc9c0\ub9cc, \uc5ec\uae30\uc5d0 \uba38\uc9c0\ub418\uc5b4\ub3c4 \ubc14\ub85c \ubc30\ud3ec\ub418\uc9c0\ub294 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\n2\\\\. feature\ube0c\ub79c\uce58\\n\\n\uae30\ub2a5\uc744 \uac1c\ubc1c\ud558\ub294 \ube0c\ub79c\uce58\uc785\ub2c8\ub2e4. Github Flow \uc640\ub3c4 \uc720\uc0ac\ud569\ub2c8\ub2e4.\\n\\n3\\\\. production \ube0c\ub79c\uce58\\n\\n\uc2e4\uc81c \ubc30\ud3ec\uac00 \uc77c\uc5b4\ub098\ub294 \ube0c\ub79c\uce58\uc785\ub2c8\ub2e4.\xa0\\n\\n\uc5ec\uae30\uc5d0 \uba38\uc9c0\uac00 \ub418\ub294 \uc21c\uac04 \ubc30\ud3ec\uac00 \uc77c\uc5b4\ub0a9\ub2c8\ub2e4.\\n\\n\uc704 \uc0ac\uc9c4\uc5d0 \uc788\ub294 \uac83\ucc98\ub7fc, \ud544\uc694\uc5d0 \ub530\ub77c\uc11c pre-production\uc774\ub098, staging \uac19\uc740 \ud658\uacbd\uc5d0 \ub530\ub978 \ube0c\ub79c\uce58\ub97c \ucd94\uac00\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n#### \ud2b9\uc9d5\\n\\n1\\\\. \ubb34\uc870\uac74 \ub2e8\ubc29\ud5a5\uc73c\ub85c \uba38\uc9c0\uac00 \uc77c\uc5b4\ub0a9\ub2c8\ub2e4.\\n\\n\uae34\uae09\ud558\uac8c \ub77c\uc774\ube0c \uc11c\ubc84\uc5d0 \uc218\uc815\uc744 \ud574\uc57c \ud560 \ub54c, production \ubd80\ud130 \uc2dc\uc791\ud558\ub294 \uac83\uc774 \uc544\ub2cc, main \ubd80\ud130 \ucc28\uadfc\ucc28\uadfc \uc62c\ub77c\uac00\uc57c \ud569\ub2c8\ub2e4\\n\\n2\\\\. \ud658\uacbd\uc5d0 \ub530\ub77c \ube0c\ub79c\uce58 \uc885\ub958\uac00 \ub298\uc5b4\ub0a0 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n\uc704 \uc0ac\uc9c4\uc5d0\uc11c\ub294 pre-production \uc774 \uadf8 \uc608\uc2dc\uac00 \ub418\uaca0\ub124\uc694.\\n\\n#### \uc7a5\uc810\\n\\n1\\\\. Github Flow\uc5d0\uc11c \ud658\uacbd\ubcc4 \ube0c\ub79c\uce58\ub97c \ud1b5\ud574\uc11c \uac1c\ubc1c \uc11c\ubc84\ub098 pre-production \uc11c\ubc84\uc5d0 \ubc84\uc804\uc744 \uae54\ub054\ud558\uac8c \uad00\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\\n\\n## 3\\\\. Git Flow\\n\\n\ube0c\ub79c\uce58 \uc804\ub7b5 \uc911 \uac00\uc7a5 \ucc98\uc74c\uc73c\ub85c \uc720\uba85\ud574\uc9c4 \ube0c\ub79c\uce58 \uc804\ub7b5\uc785\ub2c8\ub2e4.\\n\\n\ubc30\ud3ec\uac00 \ud2b9\uc815 \uc8fc\uae30\ub97c \uac00\uc9c0\uace0 \uc788\ub294 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc77c \ub54c, \uac00\uc7a5 \uc801\ud569\ud569\ub2c8\ub2e4.\\n\\n\uac00\uc7a5 \ubcf5\uc7a1\ud55c \uc804\ub7b5\uc744 \uac00\uc9c0\uace0 \uc788\uc5b4\uc11c, \ubaa8\ub450\uac00 \ube0c\ub79c\uce58 \uc804\ub7b5\uc5d0 \ub300\ud574\uc11c \uc774\ud574\ud558\uace0 \uc788\ub2e4\uba74 \uc5ed\ud560\uc5d0 \ub530\ub978 \uae54\ub054\ud55c \ubd84\ub9ac\uac00 \uac00\ub2a5\ud569\ub2c8\ub2e4\\n\\n\uadf8\ub9bc\uc73c\ub85c \ubcf4\uace0 \uac00\ub3c4\ub85d \ud558\uaca0\uc2b5\ub2c8\ub2e4\\n\\n![img4](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd9WzKn%2FbtslKdkAHNP%2F2fCAqKSVxtPVWqYnBS8juk%2Fimg.png)\\n\\n\uac00\uc7a5 \uc720\uba85\ud55c \ube0c\ub79c\uce58 \uc804\ub7b5\uc774\uc9c0\ub9cc, \uac00\uc7a5 \uc5b4\ub824\uc6b4 \uc804\ub7b5\uc774\uae30\ub3c4 \ud569\ub2c8\ub2e4.\\n\\n#### \ud2b9\uc9d5\\n\\n1\\\\. \ube0c\ub79c\uce58\uc5d0 \ub300\ud574\uc11c \uc591\ubc29\ud5a5\uc73c\ub85c \uba38\uc9c0\uac00 \uc77c\uc5b4\ub0a9\ub2c8\ub2e4\\n\\nrelease \ube0c\ub79c\uce58\uc5d0\uc11c \ubc84\uadf8 \uc218\uc815\uc774 \uc77c\uc5b4\ub098\uba74, develop \ube0c\ub79c\uce58\uc5d0\ub3c4 \uba38\uc9c0\ud574\uc918\uc57c \ud569\ub2c8\ub2e4.\\n\\nhotfix \ube0c\ub79c\uce58\ub97c main \ube0c\ub79c\uce58\ubfd0\ub9cc \uc544\ub2c8\ub77c, develop \ube0c\ub79c\uce58\uc5d0\ub3c4 \uba38\uc9c0\ud574\uc918\uc57c \ud569\ub2c8\ub2e4\\n\\n\ube0c\ub79c\uce58\uc758 \uc885\ub958\uac00 5\uac00\uc9c0\ub098 \ub429\ub2c8\ub2e4\\n\\n1\\\\. main\\n\\nproduction \uc774 \ubc30\ud3ec\ub418\uc5c8\uc744 \ub54c, \uc774 \ube0c\ub79c\uce58\uc5d0 \uba38\uc9c0\ub418\ub294 \uac83\uc774 \uae30\uc900\uc774 \ub429\ub2c8\ub2e4.\\n\\n2\\\\. develop\xa0\\n\\n\uc704\uc5d0\uc11c \uc124\uba85\ub4dc\ub838\ub358 \ube0c\ub79c\uce58\ub4e4\uacfc \ud070 \ucc28\uc774\uac00 \uc5c6\uc774 \ubc30\ud3ec \uc804 \ube0c\ub79c\uce58\uc785\ub2c8\ub2e4.\\n\\n3\\\\. feature\\n\\n\uae30\ub2a5\uc744 \uac1c\ubc1c\ud560 \ub54c \uc0ac\uc6a9\ud558\ub294 \ube0c\ub79c\uce58\uc785\ub2c8\ub2e4. \uc774\uac83\ub3c4 \uc704\uc640 \ud070 \ucc28\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4\\n\\n4\\\\. release\\n\\nGitlab Flow\uc5d0\uc11c pre-production\uc5d0 \ud574\ub2f9\ud55c\ub2e4\uace0 \ubd10\ub3c4 \ubb34\ubc29\ud569\ub2c8\ub2e4.\\n\\n\uc5ec\uae30\uc11c \ubc84\uadf8 \uc218\uc815\uc774 \uc77c\uc5b4\ub0ac\uc744 \uacbd\uc6b0\uc5d0,\xa0 develop\uc5d0 \uba38\uc9c0\ud558\ub294 \uac83\uc744 \uae4c\uba39\uc73c\uba74 \uc548 \ub429\ub2c8\ub2e4.\\n\\n5\\\\. hotfix\\n\\nmain \ube0c\ub79c\uce58\uc5d0\uc11c \uc0dd\uc131\ub41c \ube0c\ub79c\uce58\ub85c, \uae34\uae09\ud55c \ubcc0\uacbd\uc0ac\ud56d\uc744 \ucc98\ub9ac\ud569\ub2c8\ub2e4.\\n\\n\uc774\ub54c, develop\uc5d0 \uba38\uc9c0\ud558\ub294 \uac83\uc744 \uae5c\ube61\ud558\uba74 \uc548 \ub429\ub2c8\ub2e4.\\n\\n\ub354 \uc790\uc138\ud558\uac8c \uc54c\uc544\ubcf4\uc2e4 \ubd84\uc740 \uc544\ub798 \ub9c1\ud06c\ub4e4\uc744 \ud655\uc778\ud574 \ubcf4\uc138\uc694\\n\\n## \uc6b0\ub9ac \ud504\ub85c\uc81d\ud2b8\uc5d0\ub294 \uc5b4\ub5a4 \uac83\uc774 \uc801\uc808\ud560\uae4c?\\n\\n\ub098\uc911\uc5d0 \uac1c\ubc1c \uc11c\ubc84 \ud639\uc740 \uc2a4\ud14c\uc774\uc9d5 \uc11c\ubc84\ub97c \ub450\uace0 \uc2f6\uae30\uc5d0, \uc774 \ubd80\ubd84\uc5d0 \ub300\ud55c \ucc98\ub9ac\uac00 \ubd80\uc871\ud55c Github Flow\ub294 \uc801\uc808\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\\n\\nGit Flow\ub294 \uae54\ub054\ud558\uac8c \ucc98\ub9ac\ud560 \uc218 \uc788\uc9c0\ub9cc, \ub7ec\ub2dd \ucee4\ube0c\uac00 Gitlab Flow \ubcf4\ub2e4 \uc57d\uac04 \ub354 \uc788\uc5b4\uc11c, \ube60\ub974\uac8c \uac1c\ubc1c\ud558\ub294 \ucde8\uc9c0\uc5d0 \ub9de\uc9c0 \uc54a\uc544 \ubcf4\uc600\uc2b5\ub2c8\ub2e4.\\n\\n\uc774\ub7f0 \uacfc\uc815\uc744 \ud1b5\ud574\uc11c Gitlab Flow\ub97c \uc0ac\uc6a9\ud558\ub824\uace0 \ud569\ub2c8\ub2e4\xa0\\n\\n\ucc38\uace0\\n\\n[https://techblog.woowahan.com/2553/](https://techblog.woowahan.com/2553/)\\n\\n[https://docs.gitlab.com/ee/topics/gitlab\\\\_flow.html](https://docs.gitlab.com/ee/topics/gitlab_flow.html)"},{"id":"1","metadata":{"permalink":"/1","source":"@site/blog/2023-06-29-hello-car-ffeine.mdx","title":"Hello World","description":"\uc548\ub155\ud558\uc138\uc694","date":"2023-06-29T00:00:00.000Z","formattedDate":"2023\ub144 6\uc6d4 29\uc77c","tags":[{"label":"hello","permalink":"/tags/hello"},{"label":"world","permalink":"/tags/world"}],"readingTime":0.025,"hasTruncateMarker":false,"authors":[{"name":"\ubc15\uc2a4\ud130","title":"Backend","url":"https://github.com/drunkenhw","imageURL":"https://github.com/drunkenhw.png","key":"boxster"},{"name":"\ub204\ub204","title":"Backend","url":"https://github.com/be-student","imageURL":"https://github.com/be-student.png","key":"nunu"},{"name":"\uc81c\uc774","title":"Backend","url":"https://github.com/sosow0212","imageURL":"https://github.com/sosow0212.png","key":"jay"},{"name":"\ud0a4\uc544\ub77c","title":"Backend","url":"https://github.com/kiarakim","imageURL":"https://github.com/kiarakim.png","key":"kiara"},{"name":"\uc57c\ubbf8","title":"Frontend","url":"https://github.com/feb-dain","imageURL":"https://github.com/feb-dain.png","key":"yummy"},{"name":"\uc13c\ud2b8","title":"Frontend","url":"https://github.com/kyw0716","imageURL":"https://github.com/kyw0716.png","key":"scent"},{"name":"\uac00\ube0c\ub9ac\uc5d8","title":"Frontend","url":"https://github.com/gabrielyoon7","imageURL":"https://github.com/gabrielyoon7.png","key":"gabriel"}],"frontMatter":{"slug":"1","title":"Hello World","authors":["boxster","nunu","jay","kiara","yummy","scent","gabriel"],"tags":["hello","world"]},"prevItem":{"title":"git branch \uc804\ub7b5 \uc791\uc131\ud574\ubcf4\uae30","permalink":"/2"}},"content":"\uc548\ub155\ud558\uc138\uc694"}]}')}}]); \ No newline at end of file diff --git a/assets/js/c17ec8e8.4a256193.js b/assets/js/c17ec8e8.4a256193.js new file mode 100644 index 0000000..4e1d276 --- /dev/null +++ b/assets/js/c17ec8e8.4a256193.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcar_ffeine=self.webpackChunkcar_ffeine||[]).push([[7373],{3905:(e,t,r)=>{r.d(t,{Zo:()=>p,kt:()=>m});var n=r(67294);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function o(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function i(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var c=n.createContext({}),l=function(e){var t=n.useContext(c),r=t;return e&&(r="function"==typeof e?e(t):i(i({},t),e)),r},p=function(e){var t=l(e.components);return n.createElement(c.Provider,{value:t},e.children)},f="mdxType",g={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},u=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,o=e.originalType,c=e.parentName,p=s(e,["components","mdxType","originalType","parentName"]),f=l(r),u=a,m=f["".concat(c,".").concat(u)]||f[u]||g[u]||o;return r?n.createElement(m,i(i({ref:t},p),{},{components:r})):n.createElement(m,i({ref:t},p))}));function m(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=r.length,i=new Array(o);i[0]=u;var s={};for(var c in t)hasOwnProperty.call(t,c)&&(s[c]=t[c]);s.originalType=e,s[f]="string"==typeof e?e:a,i[1]=s;for(var l=2;l{r.r(t),r.d(t,{assets:()=>c,contentTitle:()=>i,default:()=>g,frontMatter:()=>o,metadata:()=>s,toc:()=>l});var n=r(87462),a=(r(67294),r(3905));const o={slug:43,title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",authors:["gabriel"],tags:["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},i=void 0,s={permalink:"/43",source:"@site/blog/2023-10-19-visitors/index.mdx",title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",description:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",date:"2023-10-19T00:00:00.000Z",formattedDate:"2023\ub144 10\uc6d4 19\uc77c",tags:[{label:"\uce74\ud398\uc778",permalink:"/tags/\uce74\ud398\uc778"},{label:"\uc11c\ube44\uc2a4 \uacbd\ud5d8",permalink:"/tags/\uc11c\ube44\uc2a4-\uacbd\ud5d8"},{label:"\ud53c\ub4dc\ubc31",permalink:"/tags/\ud53c\ub4dc\ubc31"},{label:"\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30",permalink:"/tags/\uc804\uae30\ucc28-\uc0ac\uc6a9\uae30"},{label:"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571",permalink:"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"}],readingTime:.3,hasTruncateMarker:!1,authors:[{name:"\uac00\ube0c\ub9ac\uc5d8",title:"Frontend",url:"https://github.com/gabrielyoon7",imageURL:"https://github.com/gabrielyoon7.png",key:"gabriel"}],frontMatter:{slug:"43",title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",authors:["gabriel"],tags:["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},prevItem:{title:"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5",permalink:"/42"},nextItem:{title:"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec",permalink:"/41"}},c={authorsImageUrls:[void 0]},l=[],p={toc:l},f="wrapper";function g(e){let{components:t,...o}=e;return(0,a.kt)(f,(0,n.Z)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"no offset",src:r(8190).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(38707).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(7699).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(59493).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(50753).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(69573).Z,width:"1444",height:"1114"})))}g.isMDXComponent=!0},8190:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-1-24a19304076da351d45ffd4fad22dd3c.jpg"},38707:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-2-0e4dcf52281a9327ab66d76e8889ee5a.jpg"},7699:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-3-4d57784043b803d60241e3d4417c3582.jpg"},59493:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-4-73cea9b32c1a5f8557a64a28c7867923.jpg"},50753:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-5-914a0377b591f9d9dc58ea570c6fc8d1.jpg"},69573:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-6-038d09fa5b07bf8b8814e5e2463c4ccd.jpg"}}]); \ No newline at end of file diff --git a/assets/js/c17ec8e8.b6a2f2da.js b/assets/js/c17ec8e8.b6a2f2da.js deleted file mode 100644 index a8778e0..0000000 --- a/assets/js/c17ec8e8.b6a2f2da.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcar_ffeine=self.webpackChunkcar_ffeine||[]).push([[7373],{3905:(e,t,r)=>{r.d(t,{Zo:()=>p,kt:()=>m});var n=r(67294);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function o(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function i(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var s=n.createContext({}),c=function(e){var t=n.useContext(s),r=t;return e&&(r="function"==typeof e?e(t):i(i({},t),e)),r},p=function(e){var t=c(e.components);return n.createElement(s.Provider,{value:t},e.children)},f="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},g=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),f=c(r),g=a,m=f["".concat(s,".").concat(g)]||f[g]||u[g]||o;return r?n.createElement(m,i(i({ref:t},p),{},{components:r})):n.createElement(m,i({ref:t},p))}));function m(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=r.length,i=new Array(o);i[0]=g;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[f]="string"==typeof e?e:a,i[1]=l;for(var c=2;c{r.r(t),r.d(t,{assets:()=>s,contentTitle:()=>i,default:()=>u,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var n=r(87462),a=(r(67294),r(3905));const o={slug:43,title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",authors:["gabriel"],tags:["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},i=void 0,l={permalink:"/43",source:"@site/blog/2023-10-19-visitors/index.mdx",title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",description:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",date:"2023-10-19T00:00:00.000Z",formattedDate:"2023\ub144 10\uc6d4 19\uc77c",tags:[{label:"\uce74\ud398\uc778",permalink:"/tags/\uce74\ud398\uc778"},{label:"\uc11c\ube44\uc2a4 \uacbd\ud5d8",permalink:"/tags/\uc11c\ube44\uc2a4-\uacbd\ud5d8"},{label:"\ud53c\ub4dc\ubc31",permalink:"/tags/\ud53c\ub4dc\ubc31"},{label:"\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30",permalink:"/tags/\uc804\uae30\ucc28-\uc0ac\uc6a9\uae30"},{label:"\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571",permalink:"/tags/\uc804\uae30\ucc28-\ucda9\uc804\uc18c-\uc571"}],readingTime:.29,hasTruncateMarker:!1,authors:[{name:"\uac00\ube0c\ub9ac\uc5d8",title:"Frontend",url:"https://github.com/gabrielyoon7",imageURL:"https://github.com/gabrielyoon7.png",key:"gabriel"}],frontMatter:{slug:"43",title:"\uce74\ud398\uc778 \uc11c\ube44\uc2a4 \ubc29\ubb38\uc790 \ubd84\uc11d - 2",authors:["gabriel"],tags:["\uce74\ud398\uc778","\uc11c\ube44\uc2a4 \uacbd\ud5d8","\ud53c\ub4dc\ubc31","\uc804\uae30\ucc28 \uc0ac\uc6a9\uae30","\uc804\uae30\ucc28 \ucda9\uc804\uc18c \uc571"]},prevItem:{title:"\uce74\ud398\uc778 \ud300\uc758 \uc0ac\uc6a9\uc790 \ud3b8\uc758\ub97c \uc704\ud55c \ud611\uc5c5",permalink:"/42"},nextItem:{title:"\uce74\ud398\uc778 \ud300\uc758 \ubb34\uc911\ub2e8 \ubc30\ud3ec",permalink:"/41"}},s={authorsImageUrls:[void 0]},c=[],p={toc:c},f="wrapper";function u(e){let{components:t,...o}=e;return(0,a.kt)(f,(0,n.Z)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,"\uce74\ud398\uc778 \uc11c\ube44\uc2a4\uc758 \ubc29\ubb38\uc790 \ud1b5\uacc4\ub97c \uacf5\uac1c\ud569\ub2c8\ub2e4. \uc0ac\uc9c4 - \uc6b0\ud074\ub9ad - \uc0c8 \ud0ed\uc5d0\uc11c \ubcf4\uae30\ub97c \ub204\ub974\uba74 \uc880 \ub354 \ud070 \ud654\uba74\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"no offset",src:r(8190).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(38707).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(7699).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(59493).Z,width:"1444",height:"1114"}),"\n",(0,a.kt)("img",{alt:"no offset",src:r(50753).Z,width:"1444",height:"1114"})))}u.isMDXComponent=!0},8190:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-1-24a19304076da351d45ffd4fad22dd3c.jpg"},38707:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-2-0e4dcf52281a9327ab66d76e8889ee5a.jpg"},7699:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-3-4d57784043b803d60241e3d4417c3582.jpg"},59493:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-4-73cea9b32c1a5f8557a64a28c7867923.jpg"},50753:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/report-5-914a0377b591f9d9dc58ea570c6fc8d1.jpg"}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.a8937271.js b/assets/js/runtime~main.3ca96668.js similarity index 99% rename from assets/js/runtime~main.a8937271.js rename to assets/js/runtime~main.3ca96668.js index f71ce5f..fd032a3 100644 --- a/assets/js/runtime~main.a8937271.js +++ b/assets/js/runtime~main.3ca96668.js @@ -1 +1 @@ -(()=>{"use strict";var e,f,a,c,b,d={},t={};function r(e){var f=t[e];if(void 0!==f)return f.exports;var a=t[e]={exports:{}};return d[e].call(a.exports,a,a.exports,r),a.exports}r.m=d,e=[],r.O=(f,a,c,b)=>{if(!a){var d=1/0;for(i=0;i=b)&&Object.keys(r.O).every((e=>r.O[e](a[o])))?a.splice(o--,1):(t=!1,b0&&e[i-1][2]>b;i--)e[i]=e[i-1];e[i]=[a,c,b]},r.n=e=>{var f=e&&e.__esModule?()=>e.default:()=>e;return r.d(f,{a:f}),f},a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,c){if(1&c&&(e=this(e)),8&c)return e;if("object"==typeof e&&e){if(4&c&&e.__esModule)return e;if(16&c&&"function"==typeof e.then)return e}var b=Object.create(null);r.r(b);var d={};f=f||[null,a({}),a([]),a(a)];for(var t=2&c&&e;"object"==typeof t&&!~f.indexOf(t);t=a(t))Object.getOwnPropertyNames(t).forEach((f=>d[f]=()=>e[f]));return d.default=()=>e,r.d(b,d),b},r.d=(e,f)=>{for(var a in f)r.o(f,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:f[a]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((f,a)=>(r.f[a](e,f),f)),[])),r.u=e=>"assets/js/"+({9:"e4e8611b",53:"935f2afb",56:"f9ed38fd",100:"d50fd269",161:"b0e32a59",172:"96adae60",199:"67d8056c",211:"55347c97",224:"c5c65120",229:"c6a127bd",240:"4959fc42",286:"1893cb59",288:"7a9bf44c",292:"8bcbaad2",299:"029258fc",321:"0c071de2",324:"280572f1",367:"75945c6d",424:"fae4bc46",454:"1a665c6f",461:"7853a999",471:"38d8699e",498:"6ba49b42",582:"c4c26f23",682:"28177e2f",719:"b88b1bad",721:"2cec5164",808:"2487d3de",843:"e4ebfe18",949:"f4738242",964:"c573638f",970:"32b2299c",988:"754fb852",994:"6d7fbd92",998:"f4af9f40",1002:"9c25eec0",1028:"9a953f96",1065:"69c28c32",1109:"d4122def",1119:"71016178",1120:"2620e7b9",1144:"aa137ad6",1185:"cc909c85",1236:"d2611248",1318:"600a7791",1363:"c6b4d86c",1386:"b3597569",1389:"7baa2934",1416:"207a8efc",1417:"fc04ac13",1436:"8e498bb6",1467:"601fd4f0",1483:"8f4b370c",1501:"64868a43",1511:"8e21b108",1566:"1252d667",1567:"49f50ebd",1587:"9dbc8adf",1627:"b3706a4c",1651:"163d37ca",1655:"ab705a1f",1664:"803fa4b0",1704:"0faaa152",1722:"22c62d68",1772:"5cfd338e",1803:"4e8d87e8",1826:"5924da1d",1851:"a30cd2a8",1866:"f1a9275a",1877:"8b74b8e0",1894:"2e044ce1",1955:"4d920b72",1971:"3617515a",2001:"7db1f2ec",2040:"37be277e",2041:"0260fa5b",2064:"0f313731",2098:"d350f5d9",2155:"c4d52cca",2160:"1457c284",2203:"807f61b6",2256:"1b7b671a",2262:"ad932f90",2288:"87506be9",2289:"d3ff88aa",2334:"7af1d52f",2340:"fcad85e8",2346:"2514f2ba",2420:"77b86cd7",2460:"f27c2916",2463:"b1bcf66a",2468:"81a656f3",2476:"2832e534",2504:"6c756788",2516:"b5230011",2535:"814f3328",2582:"7aafac26",2595:"d0e4eea8",2616:"3766ff11",2619:"7b1db77a",2670:"4ed0d1cc",2717:"f332d221",2730:"b583d190",2752:"0abff1f0",2753:"7762a24e",2758:"a0ce7679",2772:"153869a1",2821:"87847907",2859:"18c41134",2889:"ae61c7bf",2897:"007cdc83",2922:"cc9b0e25",2950:"1809876f",2955:"03f1f4ea",2997:"a8ccd094",3025:"f7ae9295",3063:"b18281db",3079:"074793ea",3085:"1f391b9e",3089:"a6aa9e1f",3091:"626b0166",3146:"b7972c94",3165:"8e044d98",3193:"d4497f2f",3237:"1df93b7f",3252:"27097cf2",3293:"37b9d916",3341:"4303a4a6",3354:"97136fd7",3367:"a50e10e9",3426:"547d36c4",3433:"81c2fdf5",3467:"e357b521",3495:"ae6362ae",3553:"210bacf6",3560:"8a0a9511",3608:"9e4087bc",3650:"23922d93",3654:"9ae81301",3667:"bcddcc8f",3688:"bce37a09",3694:"0b70ffa9",3724:"410af8a2",3739:"a0a681be",3759:"8d7fa36c",3760:"8660c6f2",3792:"dff1c289",3796:"cefbed25",3823:"97788a79",3828:"f09c0f77",3842:"1cd7fa68",3851:"b840888d",3861:"06db3492",3888:"d455ae7d",3907:"e63633a5",3936:"c613688f",3959:"a555d30b",3997:"660c0a9e",3999:"8bd490e1",4013:"01a85c17",4014:"0d81c928",4089:"a09e4d68",4149:"8d05b77c",4170:"d52058f1",4171:"b6c52e21",4191:"469dc3ee",4193:"f55d3e7a",4194:"db0d15fb",4200:"1d6afaf2",4201:"90a7e6ea",4202:"38f82c99",4234:"43ea9b4e",4273:"61f14ad3",4335:"a37176f0",4337:"5c31d10c",4344:"f3322a15",4349:"5e5ae3cc",4358:"cace0f84",4361:"fc69ad3c",4388:"c8cc1d07",4395:"9ffe4f1a",4402:"20fcf238",4446:"e7972e79",4468:"49859754",4471:"494882d1",4496:"02cc5bda",4568:"e613e09c",4573:"a7b32a40",4607:"533a09ca",4689:"eef36cfd",4695:"4fbdb8ff",4708:"034316ba",4759:"6120dc83",4768:"bae98e44",4800:"bbf87d95",4820:"bacd660c",4837:"2d05811a",4877:"a1c4ff9a",4889:"5f81b25c",4940:"857c637e",4953:"eec33099",4957:"483fd5d0",4999:"0be517de",5006:"ca4b344f",5026:"ff4d8b69",5045:"cf597922",5061:"43bed105",5088:"54150be7",5102:"d26080ac",5159:"5133b0c6",5160:"f4948b2c",5161:"9348a89e",5187:"3ce54efd",5205:"fdf3f179",5208:"16d0e52e",5256:"cb4b66ee",5306:"eb7def01",5308:"aec899bc",5331:"26896e6a",5391:"c9630fa2",5392:"6692f06b",5407:"fd250280",5439:"1cc4c623",5465:"d0e4cdf1",5478:"48a67c51",5501:"3ee6368b",5570:"b3b4a184",5573:"18e1dbe5",5583:"fa940ad3",5589:"5c868d36",5593:"d03a6f97",5610:"9899d8a5",5612:"b58cb1a9",5632:"decd4a10",5634:"fc3046f6",5650:"f5eecd74",5666:"337c555a",5669:"00931cc3",5670:"b98794bd",5721:"8ac474ef",5722:"d613ee27",5772:"e9652e75",5783:"f1568e05",5797:"7fbacf84",5802:"d01b1d1b",5853:"ebbab0c1",5877:"0023d7b0",5883:"ae2f64a1",5922:"e6caa061",5963:"93f098ab",5964:"09fbb6bd",5966:"871c1e5a",5983:"d5dc80ab",5987:"be4773d4",5991:"a5557bb9",6017:"6093f82b",6035:"226700de",6048:"1ec645a9",6103:"ccc49370",6123:"f3e308ad",6148:"f7743d3b",6173:"ee7d88ad",6184:"b7a74434",6205:"8e751dff",6261:"df01e1d1",6293:"e947f001",6325:"9bc95288",6332:"ea23132c",6337:"9097e19c",6404:"e726a561",6412:"37b2180d",6430:"181cad37",6432:"af971a0b",6445:"acbf6f0e",6447:"d9c03e5c",6491:"7220f6f9",6504:"822bd8ab",6508:"12cbeba7",6518:"bb25b787",6524:"bfe1055c",6525:"ea88f2a1",6526:"36c3ed9f",6530:"cc21b615",6572:"06f0a746",6573:"23e2728d",6735:"668a56ad",6755:"e44a2883",6789:"6ab5bcea",6794:"89e28a64",6815:"9031057c",6823:"743a3b39",6831:"77310d5d",6837:"fbd57548",6871:"e8622e75",6885:"c185dccb",6887:"f4f49e13",6897:"66e6cd6b",6919:"820d7f62",6984:"274c9143",7009:"54cb095e",7056:"bf03d367",7059:"e8c68abf",7064:"6dd1c948",7088:"9a8c4dbd",7116:"517acb4e",7145:"2728eda2",7157:"3ed04b60",7216:"d7c95adf",7218:"1aff6e78",7310:"852b2c90",7339:"bc0a62f1",7342:"e1d88fa0",7346:"899b6f7f",7373:"c17ec8e8",7412:"70275fcd",7414:"393be207",7511:"75f50328",7516:"f152f207",7534:"9391e08d",7555:"06a46f69",7581:"2e10a69c",7599:"80960b4b",7605:"09d822bf",7640:"76760c9d",7654:"37d538cb",7697:"35293ec4",7725:"9cfe8fd1",7766:"e8e37e6f",7769:"e70a8c2b",7779:"dce2839d",7806:"992b7323",7891:"635a92d5",7901:"2a8faff0",7918:"17896441",7949:"7085ca87",7975:"270346fa",8035:"389b50e0",8053:"eed983e8",8060:"4cbec242",8071:"e5531274",8113:"258958af",8117:"d0a8fb3e",8207:"9a9cf8cc",8209:"0b036d6e",8276:"dc154858",8283:"2576d4bf",8311:"d097edc6",8351:"e806107b",8355:"fe273484",8385:"5128a070",8419:"41ce545f",8428:"6efb579b",8450:"14759c52",8461:"a064989a",8502:"950dc7df",8534:"0cd70852",8561:"52b9c8f3",8566:"ab37b3d8",8586:"4a412608",8610:"6875c492",8659:"f51c3f5f",8667:"f5bc9f59",8684:"02ee3536",8692:"29bf81f3",8718:"79422113",8721:"88c8cf4c",8740:"cef71b63",8744:"18de7563",8758:"d5e55b08",8786:"6f6ec9cb",8787:"a05878b0",8788:"ddfb44b9",8818:"1e4232ab",8875:"cd9dc9ea",8882:"f75a8651",8921:"fb4958d9",8979:"d50b0fbf",9008:"28773698",9020:"73eafdfe",9039:"3ff85ced",9056:"002c05d5",9059:"198f8d8a",9062:"f37f9c20",9107:"e2f1a170",9118:"c8862eea",9142:"4b1569d6",9242:"c29bedb9",9310:"d1cef389",9323:"f1e649df",9326:"c844b82d",9354:"943a7d8e",9357:"2dcd9e41",9377:"dbe0b734",9417:"a3d6bdf1",9438:"dc36452a",9450:"2e801cce",9462:"f3277db2",9467:"ffe1d649",9481:"b5698685",9497:"0d852ea0",9514:"1be78505",9519:"7676fa9c",9529:"70d3c5f0",9563:"9d4c58e5",9582:"ea10567b",9583:"1f182e80",9606:"ef5b2427",9627:"1c01e504",9671:"0e384e19",9713:"1bb997fc",9727:"537b82b2",9734:"fd355a6b",9745:"7188ba4d",9792:"895a9c33",9817:"14eb3368",9840:"7995b933",9858:"53abb968",9926:"de6bae66",9953:"67f9c5aa",9984:"cc24784a",9987:"c41fbd98",9993:"417cb48d",9996:"203a6f4f"}[e]||e)+"."+{9:"c344abd0",53:"7dbb118c",56:"353ee7fa",100:"494f5c87",161:"491fbc3d",172:"0aad6d97",199:"13f5fcdf",211:"ae755df8",224:"2a5ad376",229:"2e399a0c",240:"7eb98aef",286:"e139eb8d",288:"12220033",292:"f013d68e",299:"a17fe3ab",321:"c2deb5f1",324:"889d6f8b",367:"6de7e70f",424:"8a0c8746",454:"d26cb1a5",461:"86c70532",471:"c0f7ea6c",498:"1b851b18",582:"857d1705",682:"e1c1df6d",719:"345bbed1",721:"460dedba",808:"13062fb1",843:"ad07df2a",949:"edf3b966",964:"b848eb9e",970:"5b3a80df",988:"f7438749",994:"1ecdd543",998:"c6604207",1002:"aed4b325",1028:"7632d9a3",1065:"b07159c0",1098:"9db7de8a",1109:"bb75ed99",1119:"36ae86d5",1120:"ed67e245",1144:"7e67fa18",1185:"35ae04c8",1236:"1f022807",1318:"b6766e56",1363:"292351fb",1386:"cfc964ad",1389:"93321c57",1416:"80e6f303",1417:"e06fce9c",1436:"d567b968",1467:"927e215d",1483:"9ca8fb8e",1501:"a9773c97",1511:"55cf768a",1566:"d3416106",1567:"ea56c93f",1587:"4a5cba2d",1627:"98cda7a2",1651:"fe384266",1655:"b7fcfeca",1664:"7dbfa2b3",1704:"79a83585",1722:"b74d841a",1772:"3415acc8",1803:"299e216a",1826:"dd91651e",1851:"37fe0fb0",1866:"53b23d7b",1877:"04caccc1",1894:"b2e9b340",1955:"69083ff1",1971:"fc91bad7",2001:"e00bcdc9",2040:"390f4f69",2041:"6a9cdb18",2064:"a1eafc78",2098:"921b57b6",2155:"f1f7821e",2160:"809bc7ea",2203:"31944c97",2256:"f88cf52e",2262:"4cfcdfe5",2288:"c2831bb5",2289:"00a0ee80",2334:"7c0894bb",2340:"07ceff48",2346:"b23df587",2420:"60e4bf52",2460:"b2cd9183",2463:"8e8f9e8d",2468:"3260781d",2476:"42858c30",2504:"0f6926f5",2516:"872eb4b7",2529:"42822631",2535:"5ce84827",2582:"0f920857",2595:"edb09eec",2616:"3e3e8afc",2619:"36967c23",2670:"265dc885",2717:"82eb2a90",2730:"51de0861",2752:"475a3c89",2753:"9c61f48a",2758:"283da8bb",2772:"07997860",2821:"18a39f9e",2859:"c682064a",2889:"f5f69db9",2897:"62692369",2922:"98e68a56",2950:"cdecbd73",2955:"1432bcbd",2997:"65c50095",3025:"6beec333",3063:"67a5613e",3079:"68c8352c",3085:"de0a24e4",3089:"b158668a",3091:"abdf9319",3146:"aa2ba35d",3165:"3881a3fd",3193:"8670a78d",3237:"23eb0085",3252:"a11557e8",3293:"a17a260b",3341:"1074ce83",3354:"97985843",3367:"faad8fb9",3426:"437c55c5",3433:"58ad179a",3467:"f659d38e",3495:"303dac6e",3553:"83d5d90b",3560:"a464831f",3608:"3ab11b8d",3650:"dfe90dc6",3654:"bc99872d",3667:"7e03a255",3688:"7b2ea426",3694:"36fc3dde",3724:"3930965e",3739:"9a8511f0",3759:"04b2e9dd",3760:"07ae25cc",3792:"7fe49081",3796:"b4dbff93",3823:"c2c20875",3828:"3ddcb8f0",3842:"fb12a445",3851:"5df36ea7",3861:"853f24a9",3888:"45455a1e",3907:"37a4b741",3936:"4fe1f474",3959:"2a05729b",3997:"9279e5b1",3999:"4badc016",4013:"ad6273d4",4014:"c61467cc",4089:"506658d5",4149:"96a02a9a",4170:"42cb4952",4171:"68f47c00",4191:"f50b356e",4193:"d1d63e80",4194:"8c0ffbc9",4200:"2b0d7cc2",4201:"87799e17",4202:"d997fd03",4234:"1fffd3d8",4273:"e6ca178c",4335:"a0426a6c",4337:"836c5106",4344:"f410688b",4349:"d74e62ac",4358:"1f12693e",4361:"1ea42860",4388:"66c29fe9",4395:"1e4da950",4402:"19e6cdd0",4446:"ec3e9cfd",4468:"f54519c5",4471:"22121436",4496:"d1cc53b0",4568:"58e2d470",4573:"649fd3fc",4607:"ad181021",4689:"94b2ae10",4695:"c0d9d74b",4708:"8970d15e",4759:"46c7d80d",4768:"096d27d2",4800:"368d3f6f",4820:"6cba59c0",4837:"dcb4a461",4877:"169ab758",4889:"2094fc80",4940:"b4fe7414",4953:"08612623",4957:"e5fdad62",4972:"179070ba",4999:"c448436f",5006:"1eac4f3c",5026:"41619737",5045:"3891161f",5061:"6c16d091",5088:"f928e18c",5102:"ad5853a5",5159:"5d8371fc",5160:"6b664d6a",5161:"6c1b9ca1",5187:"0c106841",5205:"75b957f2",5208:"765610c8",5256:"03638e79",5306:"6ffe37fd",5308:"161c1957",5331:"97b230b2",5391:"85b95023",5392:"9042f409",5407:"e3d3d866",5439:"33570cae",5465:"ef423a18",5478:"64ff7a1d",5501:"ee4c0de6",5570:"da146d27",5573:"654e4339",5583:"5be1c2bf",5589:"3534a716",5593:"3451aa38",5610:"16e43df5",5612:"6444c96b",5632:"0ab7320a",5634:"230bf113",5650:"111a4733",5666:"08f8e38c",5669:"0ca62ddf",5670:"76621e15",5721:"747d2367",5722:"99343f2d",5772:"f304b8c8",5783:"da95d4d1",5797:"41c5044d",5802:"3f21705f",5853:"c83fd92b",5877:"c40aceaa",5883:"6f824bd4",5922:"fe770004",5963:"f321d6bd",5964:"66280048",5966:"57aea329",5983:"ed5d7a76",5987:"482a11b7",5991:"9104697a",6017:"a6d42b70",6035:"8410045a",6048:"e116bec8",6103:"4fe095e6",6123:"780c2f1e",6148:"f064477d",6173:"a590b436",6184:"4b38f8cb",6205:"2faf2ff8",6261:"367b755d",6293:"07c2abd4",6316:"48ea0a83",6325:"3957b2a9",6332:"f4eac952",6337:"116dc8c1",6404:"939952df",6412:"e574a499",6430:"7261fea7",6432:"d75d83d3",6445:"bb92a9de",6447:"7f2fdf1a",6491:"cd1ff234",6504:"a9e36de2",6508:"a35ff3fb",6518:"ebce0f6d",6524:"207ee58b",6525:"d12af83b",6526:"04ba7cfe",6530:"82fab025",6572:"5e95d99f",6573:"41ee2873",6735:"1e41d25f",6755:"9d08a472",6789:"9fe1dcd9",6794:"fdcd1137",6815:"6400937c",6823:"e6987e37",6831:"1bc52a0b",6837:"fd91e3bf",6871:"a2446d95",6885:"22e4d37e",6887:"25ecdfe3",6897:"d1c21210",6919:"a6b8a8ef",6984:"fa583df2",7009:"9ac8cf97",7056:"548b5f97",7059:"cd1c81c7",7064:"63507c28",7088:"eff1fb05",7116:"a245bdd4",7145:"f5731efc",7157:"7a532699",7216:"0ce04934",7218:"7027fde5",7310:"89888e46",7339:"848ab7e4",7342:"71867401",7346:"d6dd1e26",7373:"b6a2f2da",7412:"095de08e",7414:"e9867ead",7511:"9b023b28",7516:"7868ea90",7534:"b3d66b42",7555:"790d6c60",7581:"2548bc16",7599:"69afbaf8",7605:"77295117",7640:"671b7643",7654:"ce775806",7697:"aedd8fc2",7724:"fa70f2a1",7725:"257e62ad",7766:"4d223b22",7769:"21d5a984",7779:"b4a23f17",7806:"0aaba4e8",7891:"eac25364",7901:"00873d96",7918:"4e53a1a6",7949:"b7ef60fb",7975:"6bf42a8a",8035:"4a142f8a",8053:"caeaf9bc",8060:"5b07ad37",8071:"30ee727c",8113:"a1f7acef",8117:"ca99c0ee",8207:"272ea7f4",8209:"66e787b3",8276:"43ef3b08",8283:"a34bf3a1",8311:"a8809d07",8351:"40a39f50",8355:"2aaaafdd",8385:"3dc43011",8419:"347e7610",8428:"511eb768",8450:"651a86b2",8461:"f2b48995",8502:"25dbe475",8534:"b7f87f8d",8561:"4ac46c63",8566:"9673399f",8586:"5c520248",8610:"3c6dcc8a",8659:"94988e89",8667:"71b3cda2",8684:"230c9f8e",8692:"6456ad41",8718:"f7ad87ef",8721:"2d5af889",8740:"6ae22027",8744:"9a149bfa",8758:"a6c9de59",8786:"d05357bd",8787:"9c0d8e56",8788:"668838e4",8818:"5519c4e7",8875:"84f55692",8882:"61b78995",8921:"47358e39",8979:"728f01ca",9008:"dec253ff",9020:"55394400",9039:"7212b3c5",9056:"6540dc1a",9059:"3deffc09",9062:"eb22593f",9107:"2964b719",9118:"56e42199",9142:"8cdc6c87",9242:"03588e81",9310:"e02ec7cc",9323:"c0341581",9326:"82a1c072",9354:"d08557f0",9357:"34c9bdbe",9377:"adf4007a",9417:"daf7025d",9438:"211e5168",9450:"c1156dde",9462:"c183a035",9467:"c19b9af6",9481:"e40b0cf3",9487:"d37a310c",9497:"ea1724d7",9514:"8cac7ad7",9519:"e7571b84",9529:"866292f6",9563:"70094647",9582:"60ff361f",9583:"f5e02180",9606:"4d34b891",9627:"2ff85ed2",9671:"15396710",9713:"7e414064",9727:"ee6848bc",9734:"7a86b357",9745:"dbde2d95",9792:"89e12578",9817:"3edb9f9c",9840:"68c09fa4",9858:"639666c7",9926:"1d9e59b5",9953:"4633dedc",9984:"7c499f3f",9987:"7ff08f24",9993:"5da28777",9996:"f06de3d9"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,f)=>Object.prototype.hasOwnProperty.call(e,f),c={},b="car-ffeine:",r.l=(e,f,a,d)=>{if(c[e])c[e].push(f);else{var t,o;if(void 0!==a)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var b=c[e];if(delete c[e],t.parentNode&&t.parentNode.removeChild(t),b&&b.forEach((e=>e(a))),f)return f(a)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=l.bind(null,t.onerror),t.onload=l.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"7918",28773698:"9008",49859754:"4468",71016178:"1119",79422113:"8718",87847907:"2821",e4e8611b:"9","935f2afb":"53",f9ed38fd:"56",d50fd269:"100",b0e32a59:"161","96adae60":"172","67d8056c":"199","55347c97":"211",c5c65120:"224",c6a127bd:"229","4959fc42":"240","1893cb59":"286","7a9bf44c":"288","8bcbaad2":"292","029258fc":"299","0c071de2":"321","280572f1":"324","75945c6d":"367",fae4bc46:"424","1a665c6f":"454","7853a999":"461","38d8699e":"471","6ba49b42":"498",c4c26f23:"582","28177e2f":"682",b88b1bad:"719","2cec5164":"721","2487d3de":"808",e4ebfe18:"843",f4738242:"949",c573638f:"964","32b2299c":"970","754fb852":"988","6d7fbd92":"994",f4af9f40:"998","9c25eec0":"1002","9a953f96":"1028","69c28c32":"1065",d4122def:"1109","2620e7b9":"1120",aa137ad6:"1144",cc909c85:"1185",d2611248:"1236","600a7791":"1318",c6b4d86c:"1363",b3597569:"1386","7baa2934":"1389","207a8efc":"1416",fc04ac13:"1417","8e498bb6":"1436","601fd4f0":"1467","8f4b370c":"1483","64868a43":"1501","8e21b108":"1511","1252d667":"1566","49f50ebd":"1567","9dbc8adf":"1587",b3706a4c:"1627","163d37ca":"1651",ab705a1f:"1655","803fa4b0":"1664","0faaa152":"1704","22c62d68":"1722","5cfd338e":"1772","4e8d87e8":"1803","5924da1d":"1826",a30cd2a8:"1851",f1a9275a:"1866","8b74b8e0":"1877","2e044ce1":"1894","4d920b72":"1955","3617515a":"1971","7db1f2ec":"2001","37be277e":"2040","0260fa5b":"2041","0f313731":"2064",d350f5d9:"2098",c4d52cca:"2155","1457c284":"2160","807f61b6":"2203","1b7b671a":"2256",ad932f90:"2262","87506be9":"2288",d3ff88aa:"2289","7af1d52f":"2334",fcad85e8:"2340","2514f2ba":"2346","77b86cd7":"2420",f27c2916:"2460",b1bcf66a:"2463","81a656f3":"2468","2832e534":"2476","6c756788":"2504",b5230011:"2516","814f3328":"2535","7aafac26":"2582",d0e4eea8:"2595","3766ff11":"2616","7b1db77a":"2619","4ed0d1cc":"2670",f332d221:"2717",b583d190:"2730","0abff1f0":"2752","7762a24e":"2753",a0ce7679:"2758","153869a1":"2772","18c41134":"2859",ae61c7bf:"2889","007cdc83":"2897",cc9b0e25:"2922","1809876f":"2950","03f1f4ea":"2955",a8ccd094:"2997",f7ae9295:"3025",b18281db:"3063","074793ea":"3079","1f391b9e":"3085",a6aa9e1f:"3089","626b0166":"3091",b7972c94:"3146","8e044d98":"3165",d4497f2f:"3193","1df93b7f":"3237","27097cf2":"3252","37b9d916":"3293","4303a4a6":"3341","97136fd7":"3354",a50e10e9:"3367","547d36c4":"3426","81c2fdf5":"3433",e357b521:"3467",ae6362ae:"3495","210bacf6":"3553","8a0a9511":"3560","9e4087bc":"3608","23922d93":"3650","9ae81301":"3654",bcddcc8f:"3667",bce37a09:"3688","0b70ffa9":"3694","410af8a2":"3724",a0a681be:"3739","8d7fa36c":"3759","8660c6f2":"3760",dff1c289:"3792",cefbed25:"3796","97788a79":"3823",f09c0f77:"3828","1cd7fa68":"3842",b840888d:"3851","06db3492":"3861",d455ae7d:"3888",e63633a5:"3907",c613688f:"3936",a555d30b:"3959","660c0a9e":"3997","8bd490e1":"3999","01a85c17":"4013","0d81c928":"4014",a09e4d68:"4089","8d05b77c":"4149",d52058f1:"4170",b6c52e21:"4171","469dc3ee":"4191",f55d3e7a:"4193",db0d15fb:"4194","1d6afaf2":"4200","90a7e6ea":"4201","38f82c99":"4202","43ea9b4e":"4234","61f14ad3":"4273",a37176f0:"4335","5c31d10c":"4337",f3322a15:"4344","5e5ae3cc":"4349",cace0f84:"4358",fc69ad3c:"4361",c8cc1d07:"4388","9ffe4f1a":"4395","20fcf238":"4402",e7972e79:"4446","494882d1":"4471","02cc5bda":"4496",e613e09c:"4568",a7b32a40:"4573","533a09ca":"4607",eef36cfd:"4689","4fbdb8ff":"4695","034316ba":"4708","6120dc83":"4759",bae98e44:"4768",bbf87d95:"4800",bacd660c:"4820","2d05811a":"4837",a1c4ff9a:"4877","5f81b25c":"4889","857c637e":"4940",eec33099:"4953","483fd5d0":"4957","0be517de":"4999",ca4b344f:"5006",ff4d8b69:"5026",cf597922:"5045","43bed105":"5061","54150be7":"5088",d26080ac:"5102","5133b0c6":"5159",f4948b2c:"5160","9348a89e":"5161","3ce54efd":"5187",fdf3f179:"5205","16d0e52e":"5208",cb4b66ee:"5256",eb7def01:"5306",aec899bc:"5308","26896e6a":"5331",c9630fa2:"5391","6692f06b":"5392",fd250280:"5407","1cc4c623":"5439",d0e4cdf1:"5465","48a67c51":"5478","3ee6368b":"5501",b3b4a184:"5570","18e1dbe5":"5573",fa940ad3:"5583","5c868d36":"5589",d03a6f97:"5593","9899d8a5":"5610",b58cb1a9:"5612",decd4a10:"5632",fc3046f6:"5634",f5eecd74:"5650","337c555a":"5666","00931cc3":"5669",b98794bd:"5670","8ac474ef":"5721",d613ee27:"5722",e9652e75:"5772",f1568e05:"5783","7fbacf84":"5797",d01b1d1b:"5802",ebbab0c1:"5853","0023d7b0":"5877",ae2f64a1:"5883",e6caa061:"5922","93f098ab":"5963","09fbb6bd":"5964","871c1e5a":"5966",d5dc80ab:"5983",be4773d4:"5987",a5557bb9:"5991","6093f82b":"6017","226700de":"6035","1ec645a9":"6048",ccc49370:"6103",f3e308ad:"6123",f7743d3b:"6148",ee7d88ad:"6173",b7a74434:"6184","8e751dff":"6205",df01e1d1:"6261",e947f001:"6293","9bc95288":"6325",ea23132c:"6332","9097e19c":"6337",e726a561:"6404","37b2180d":"6412","181cad37":"6430",af971a0b:"6432",acbf6f0e:"6445",d9c03e5c:"6447","7220f6f9":"6491","822bd8ab":"6504","12cbeba7":"6508",bb25b787:"6518",bfe1055c:"6524",ea88f2a1:"6525","36c3ed9f":"6526",cc21b615:"6530","06f0a746":"6572","23e2728d":"6573","668a56ad":"6735",e44a2883:"6755","6ab5bcea":"6789","89e28a64":"6794","9031057c":"6815","743a3b39":"6823","77310d5d":"6831",fbd57548:"6837",e8622e75:"6871",c185dccb:"6885",f4f49e13:"6887","66e6cd6b":"6897","820d7f62":"6919","274c9143":"6984","54cb095e":"7009",bf03d367:"7056",e8c68abf:"7059","6dd1c948":"7064","9a8c4dbd":"7088","517acb4e":"7116","2728eda2":"7145","3ed04b60":"7157",d7c95adf:"7216","1aff6e78":"7218","852b2c90":"7310",bc0a62f1:"7339",e1d88fa0:"7342","899b6f7f":"7346",c17ec8e8:"7373","70275fcd":"7412","393be207":"7414","75f50328":"7511",f152f207:"7516","9391e08d":"7534","06a46f69":"7555","2e10a69c":"7581","80960b4b":"7599","09d822bf":"7605","76760c9d":"7640","37d538cb":"7654","35293ec4":"7697","9cfe8fd1":"7725",e8e37e6f:"7766",e70a8c2b:"7769",dce2839d:"7779","992b7323":"7806","635a92d5":"7891","2a8faff0":"7901","7085ca87":"7949","270346fa":"7975","389b50e0":"8035",eed983e8:"8053","4cbec242":"8060",e5531274:"8071","258958af":"8113",d0a8fb3e:"8117","9a9cf8cc":"8207","0b036d6e":"8209",dc154858:"8276","2576d4bf":"8283",d097edc6:"8311",e806107b:"8351",fe273484:"8355","5128a070":"8385","41ce545f":"8419","6efb579b":"8428","14759c52":"8450",a064989a:"8461","950dc7df":"8502","0cd70852":"8534","52b9c8f3":"8561",ab37b3d8:"8566","4a412608":"8586","6875c492":"8610",f51c3f5f:"8659",f5bc9f59:"8667","02ee3536":"8684","29bf81f3":"8692","88c8cf4c":"8721",cef71b63:"8740","18de7563":"8744",d5e55b08:"8758","6f6ec9cb":"8786",a05878b0:"8787",ddfb44b9:"8788","1e4232ab":"8818",cd9dc9ea:"8875",f75a8651:"8882",fb4958d9:"8921",d50b0fbf:"8979","73eafdfe":"9020","3ff85ced":"9039","002c05d5":"9056","198f8d8a":"9059",f37f9c20:"9062",e2f1a170:"9107",c8862eea:"9118","4b1569d6":"9142",c29bedb9:"9242",d1cef389:"9310",f1e649df:"9323",c844b82d:"9326","943a7d8e":"9354","2dcd9e41":"9357",dbe0b734:"9377",a3d6bdf1:"9417",dc36452a:"9438","2e801cce":"9450",f3277db2:"9462",ffe1d649:"9467",b5698685:"9481","0d852ea0":"9497","1be78505":"9514","7676fa9c":"9519","70d3c5f0":"9529","9d4c58e5":"9563",ea10567b:"9582","1f182e80":"9583",ef5b2427:"9606","1c01e504":"9627","0e384e19":"9671","1bb997fc":"9713","537b82b2":"9727",fd355a6b:"9734","7188ba4d":"9745","895a9c33":"9792","14eb3368":"9817","7995b933":"9840","53abb968":"9858",de6bae66:"9926","67f9c5aa":"9953",cc24784a:"9984",c41fbd98:"9987","417cb48d":"9993","203a6f4f":"9996"}[e]||e,r.p+r.u(e)},(()=>{var e={1303:0,532:0};r.f.j=(f,a)=>{var c=r.o(e,f)?e[f]:void 0;if(0!==c)if(c)a.push(c[2]);else if(/^(1303|532)$/.test(f))e[f]=0;else{var b=new Promise(((a,b)=>c=e[f]=[a,b]));a.push(c[2]=b);var d=r.p+r.u(f),t=new Error;r.l(d,(a=>{if(r.o(e,f)&&(0!==(c=e[f])&&(e[f]=void 0),c)){var b=a&&("load"===a.type?"missing":a.type),d=a&&a.target&&a.target.src;t.message="Loading chunk "+f+" failed.\n("+b+": "+d+")",t.name="ChunkLoadError",t.type=b,t.request=d,c[1](t)}}),"chunk-"+f,f)}},r.O.j=f=>0===e[f];var f=(f,a)=>{var c,b,d=a[0],t=a[1],o=a[2],n=0;if(d.some((f=>0!==e[f]))){for(c in t)r.o(t,c)&&(r.m[c]=t[c]);if(o)var i=o(r)}for(f&&f(a);n{"use strict";var e,f,a,c,b,d={},t={};function r(e){var f=t[e];if(void 0!==f)return f.exports;var a=t[e]={exports:{}};return d[e].call(a.exports,a,a.exports,r),a.exports}r.m=d,e=[],r.O=(f,a,c,b)=>{if(!a){var d=1/0;for(i=0;i=b)&&Object.keys(r.O).every((e=>r.O[e](a[o])))?a.splice(o--,1):(t=!1,b0&&e[i-1][2]>b;i--)e[i]=e[i-1];e[i]=[a,c,b]},r.n=e=>{var f=e&&e.__esModule?()=>e.default:()=>e;return r.d(f,{a:f}),f},a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,c){if(1&c&&(e=this(e)),8&c)return e;if("object"==typeof e&&e){if(4&c&&e.__esModule)return e;if(16&c&&"function"==typeof e.then)return e}var b=Object.create(null);r.r(b);var d={};f=f||[null,a({}),a([]),a(a)];for(var t=2&c&&e;"object"==typeof t&&!~f.indexOf(t);t=a(t))Object.getOwnPropertyNames(t).forEach((f=>d[f]=()=>e[f]));return d.default=()=>e,r.d(b,d),b},r.d=(e,f)=>{for(var a in f)r.o(f,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:f[a]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((f,a)=>(r.f[a](e,f),f)),[])),r.u=e=>"assets/js/"+({9:"e4e8611b",53:"935f2afb",56:"f9ed38fd",100:"d50fd269",161:"b0e32a59",172:"96adae60",199:"67d8056c",211:"55347c97",224:"c5c65120",229:"c6a127bd",240:"4959fc42",286:"1893cb59",288:"7a9bf44c",292:"8bcbaad2",299:"029258fc",321:"0c071de2",324:"280572f1",367:"75945c6d",424:"fae4bc46",454:"1a665c6f",461:"7853a999",471:"38d8699e",498:"6ba49b42",582:"c4c26f23",682:"28177e2f",719:"b88b1bad",721:"2cec5164",808:"2487d3de",843:"e4ebfe18",949:"f4738242",964:"c573638f",970:"32b2299c",988:"754fb852",994:"6d7fbd92",998:"f4af9f40",1002:"9c25eec0",1028:"9a953f96",1065:"69c28c32",1109:"d4122def",1119:"71016178",1120:"2620e7b9",1144:"aa137ad6",1185:"cc909c85",1236:"d2611248",1318:"600a7791",1363:"c6b4d86c",1386:"b3597569",1389:"7baa2934",1416:"207a8efc",1417:"fc04ac13",1436:"8e498bb6",1467:"601fd4f0",1483:"8f4b370c",1501:"64868a43",1511:"8e21b108",1566:"1252d667",1567:"49f50ebd",1587:"9dbc8adf",1627:"b3706a4c",1651:"163d37ca",1655:"ab705a1f",1664:"803fa4b0",1704:"0faaa152",1722:"22c62d68",1772:"5cfd338e",1803:"4e8d87e8",1826:"5924da1d",1851:"a30cd2a8",1866:"f1a9275a",1877:"8b74b8e0",1894:"2e044ce1",1955:"4d920b72",1971:"3617515a",2001:"7db1f2ec",2040:"37be277e",2041:"0260fa5b",2064:"0f313731",2098:"d350f5d9",2155:"c4d52cca",2160:"1457c284",2203:"807f61b6",2256:"1b7b671a",2262:"ad932f90",2288:"87506be9",2289:"d3ff88aa",2334:"7af1d52f",2340:"fcad85e8",2346:"2514f2ba",2420:"77b86cd7",2460:"f27c2916",2463:"b1bcf66a",2468:"81a656f3",2476:"2832e534",2504:"6c756788",2516:"b5230011",2535:"814f3328",2582:"7aafac26",2595:"d0e4eea8",2616:"3766ff11",2619:"7b1db77a",2670:"4ed0d1cc",2717:"f332d221",2730:"b583d190",2752:"0abff1f0",2753:"7762a24e",2758:"a0ce7679",2772:"153869a1",2821:"87847907",2859:"18c41134",2889:"ae61c7bf",2897:"007cdc83",2922:"cc9b0e25",2950:"1809876f",2955:"03f1f4ea",2997:"a8ccd094",3025:"f7ae9295",3063:"b18281db",3079:"074793ea",3085:"1f391b9e",3089:"a6aa9e1f",3091:"626b0166",3146:"b7972c94",3165:"8e044d98",3193:"d4497f2f",3237:"1df93b7f",3252:"27097cf2",3293:"37b9d916",3341:"4303a4a6",3354:"97136fd7",3367:"a50e10e9",3426:"547d36c4",3433:"81c2fdf5",3467:"e357b521",3495:"ae6362ae",3553:"210bacf6",3560:"8a0a9511",3608:"9e4087bc",3650:"23922d93",3654:"9ae81301",3667:"bcddcc8f",3688:"bce37a09",3694:"0b70ffa9",3724:"410af8a2",3739:"a0a681be",3759:"8d7fa36c",3760:"8660c6f2",3792:"dff1c289",3796:"cefbed25",3823:"97788a79",3828:"f09c0f77",3842:"1cd7fa68",3851:"b840888d",3861:"06db3492",3888:"d455ae7d",3907:"e63633a5",3936:"c613688f",3959:"a555d30b",3997:"660c0a9e",3999:"8bd490e1",4013:"01a85c17",4014:"0d81c928",4089:"a09e4d68",4149:"8d05b77c",4170:"d52058f1",4171:"b6c52e21",4191:"469dc3ee",4193:"f55d3e7a",4194:"db0d15fb",4200:"1d6afaf2",4201:"90a7e6ea",4202:"38f82c99",4234:"43ea9b4e",4273:"61f14ad3",4335:"a37176f0",4337:"5c31d10c",4344:"f3322a15",4349:"5e5ae3cc",4358:"cace0f84",4361:"fc69ad3c",4388:"c8cc1d07",4395:"9ffe4f1a",4402:"20fcf238",4446:"e7972e79",4468:"49859754",4471:"494882d1",4496:"02cc5bda",4568:"e613e09c",4573:"a7b32a40",4607:"533a09ca",4689:"eef36cfd",4695:"4fbdb8ff",4708:"034316ba",4759:"6120dc83",4768:"bae98e44",4800:"bbf87d95",4820:"bacd660c",4837:"2d05811a",4877:"a1c4ff9a",4889:"5f81b25c",4940:"857c637e",4953:"eec33099",4957:"483fd5d0",4999:"0be517de",5006:"ca4b344f",5026:"ff4d8b69",5045:"cf597922",5061:"43bed105",5088:"54150be7",5102:"d26080ac",5159:"5133b0c6",5160:"f4948b2c",5161:"9348a89e",5187:"3ce54efd",5205:"fdf3f179",5208:"16d0e52e",5256:"cb4b66ee",5306:"eb7def01",5308:"aec899bc",5331:"26896e6a",5391:"c9630fa2",5392:"6692f06b",5407:"fd250280",5439:"1cc4c623",5465:"d0e4cdf1",5478:"48a67c51",5501:"3ee6368b",5570:"b3b4a184",5573:"18e1dbe5",5583:"fa940ad3",5589:"5c868d36",5593:"d03a6f97",5610:"9899d8a5",5612:"b58cb1a9",5632:"decd4a10",5634:"fc3046f6",5650:"f5eecd74",5666:"337c555a",5669:"00931cc3",5670:"b98794bd",5721:"8ac474ef",5722:"d613ee27",5772:"e9652e75",5783:"f1568e05",5797:"7fbacf84",5802:"d01b1d1b",5853:"ebbab0c1",5877:"0023d7b0",5883:"ae2f64a1",5922:"e6caa061",5963:"93f098ab",5964:"09fbb6bd",5966:"871c1e5a",5983:"d5dc80ab",5987:"be4773d4",5991:"a5557bb9",6017:"6093f82b",6035:"226700de",6048:"1ec645a9",6103:"ccc49370",6123:"f3e308ad",6148:"f7743d3b",6173:"ee7d88ad",6184:"b7a74434",6205:"8e751dff",6261:"df01e1d1",6293:"e947f001",6325:"9bc95288",6332:"ea23132c",6337:"9097e19c",6404:"e726a561",6412:"37b2180d",6430:"181cad37",6432:"af971a0b",6445:"acbf6f0e",6447:"d9c03e5c",6491:"7220f6f9",6504:"822bd8ab",6508:"12cbeba7",6518:"bb25b787",6524:"bfe1055c",6525:"ea88f2a1",6526:"36c3ed9f",6530:"cc21b615",6572:"06f0a746",6573:"23e2728d",6735:"668a56ad",6755:"e44a2883",6789:"6ab5bcea",6794:"89e28a64",6815:"9031057c",6823:"743a3b39",6831:"77310d5d",6837:"fbd57548",6871:"e8622e75",6885:"c185dccb",6887:"f4f49e13",6897:"66e6cd6b",6919:"820d7f62",6984:"274c9143",7009:"54cb095e",7056:"bf03d367",7059:"e8c68abf",7064:"6dd1c948",7088:"9a8c4dbd",7116:"517acb4e",7145:"2728eda2",7157:"3ed04b60",7216:"d7c95adf",7218:"1aff6e78",7310:"852b2c90",7339:"bc0a62f1",7342:"e1d88fa0",7346:"899b6f7f",7373:"c17ec8e8",7412:"70275fcd",7414:"393be207",7511:"75f50328",7516:"f152f207",7534:"9391e08d",7555:"06a46f69",7581:"2e10a69c",7599:"80960b4b",7605:"09d822bf",7640:"76760c9d",7654:"37d538cb",7697:"35293ec4",7725:"9cfe8fd1",7766:"e8e37e6f",7769:"e70a8c2b",7779:"dce2839d",7806:"992b7323",7891:"635a92d5",7901:"2a8faff0",7918:"17896441",7949:"7085ca87",7975:"270346fa",8035:"389b50e0",8053:"eed983e8",8060:"4cbec242",8071:"e5531274",8113:"258958af",8117:"d0a8fb3e",8207:"9a9cf8cc",8209:"0b036d6e",8276:"dc154858",8283:"2576d4bf",8311:"d097edc6",8351:"e806107b",8355:"fe273484",8385:"5128a070",8419:"41ce545f",8428:"6efb579b",8450:"14759c52",8461:"a064989a",8502:"950dc7df",8534:"0cd70852",8561:"52b9c8f3",8566:"ab37b3d8",8586:"4a412608",8610:"6875c492",8659:"f51c3f5f",8667:"f5bc9f59",8684:"02ee3536",8692:"29bf81f3",8718:"79422113",8721:"88c8cf4c",8740:"cef71b63",8744:"18de7563",8758:"d5e55b08",8786:"6f6ec9cb",8787:"a05878b0",8788:"ddfb44b9",8818:"1e4232ab",8875:"cd9dc9ea",8882:"f75a8651",8921:"fb4958d9",8979:"d50b0fbf",9008:"28773698",9020:"73eafdfe",9039:"3ff85ced",9056:"002c05d5",9059:"198f8d8a",9062:"f37f9c20",9107:"e2f1a170",9118:"c8862eea",9142:"4b1569d6",9242:"c29bedb9",9310:"d1cef389",9323:"f1e649df",9326:"c844b82d",9354:"943a7d8e",9357:"2dcd9e41",9377:"dbe0b734",9417:"a3d6bdf1",9438:"dc36452a",9450:"2e801cce",9462:"f3277db2",9467:"ffe1d649",9481:"b5698685",9497:"0d852ea0",9514:"1be78505",9519:"7676fa9c",9529:"70d3c5f0",9563:"9d4c58e5",9582:"ea10567b",9583:"1f182e80",9606:"ef5b2427",9627:"1c01e504",9671:"0e384e19",9713:"1bb997fc",9727:"537b82b2",9734:"fd355a6b",9745:"7188ba4d",9792:"895a9c33",9817:"14eb3368",9840:"7995b933",9858:"53abb968",9926:"de6bae66",9953:"67f9c5aa",9984:"cc24784a",9987:"c41fbd98",9993:"417cb48d",9996:"203a6f4f"}[e]||e)+"."+{9:"c344abd0",53:"7dbb118c",56:"353ee7fa",100:"494f5c87",161:"491fbc3d",172:"0aad6d97",199:"13f5fcdf",211:"ae755df8",224:"2a5ad376",229:"2e399a0c",240:"7eb98aef",286:"e139eb8d",288:"12220033",292:"f013d68e",299:"a17fe3ab",321:"c2deb5f1",324:"889d6f8b",367:"6de7e70f",424:"8a0c8746",454:"d26cb1a5",461:"86c70532",471:"c0f7ea6c",498:"1b851b18",582:"857d1705",682:"e1c1df6d",719:"345bbed1",721:"460dedba",808:"13062fb1",843:"ad07df2a",949:"edf3b966",964:"b848eb9e",970:"5b3a80df",988:"f7438749",994:"1ecdd543",998:"c6604207",1002:"aed4b325",1028:"7632d9a3",1065:"b07159c0",1098:"9db7de8a",1109:"bb75ed99",1119:"36ae86d5",1120:"ed67e245",1144:"7e67fa18",1185:"35ae04c8",1236:"1f022807",1318:"b6766e56",1363:"292351fb",1386:"cfc964ad",1389:"93321c57",1416:"80e6f303",1417:"e06fce9c",1436:"d567b968",1467:"927e215d",1483:"9ca8fb8e",1501:"a9773c97",1511:"55cf768a",1566:"d3416106",1567:"ea56c93f",1587:"4a5cba2d",1627:"98cda7a2",1651:"fe384266",1655:"b7fcfeca",1664:"7dbfa2b3",1704:"79a83585",1722:"b74d841a",1772:"3415acc8",1803:"299e216a",1826:"dd91651e",1851:"37fe0fb0",1866:"53b23d7b",1877:"04caccc1",1894:"b2e9b340",1955:"69083ff1",1971:"fc91bad7",2001:"e00bcdc9",2040:"390f4f69",2041:"6a9cdb18",2064:"a1eafc78",2098:"921b57b6",2155:"f1f7821e",2160:"809bc7ea",2203:"31944c97",2256:"f88cf52e",2262:"4cfcdfe5",2288:"c2831bb5",2289:"00a0ee80",2334:"7c0894bb",2340:"07ceff48",2346:"b23df587",2420:"60e4bf52",2460:"b2cd9183",2463:"8e8f9e8d",2468:"3260781d",2476:"42858c30",2504:"0f6926f5",2516:"872eb4b7",2529:"42822631",2535:"5ce84827",2582:"0f920857",2595:"edb09eec",2616:"3e3e8afc",2619:"36967c23",2670:"265dc885",2717:"82eb2a90",2730:"51de0861",2752:"475a3c89",2753:"9c61f48a",2758:"283da8bb",2772:"07997860",2821:"18a39f9e",2859:"c682064a",2889:"f5f69db9",2897:"62692369",2922:"98e68a56",2950:"cdecbd73",2955:"1432bcbd",2997:"65c50095",3025:"6beec333",3063:"67a5613e",3079:"68c8352c",3085:"de0a24e4",3089:"b158668a",3091:"abdf9319",3146:"aa2ba35d",3165:"3881a3fd",3193:"8670a78d",3237:"23eb0085",3252:"a11557e8",3293:"a17a260b",3341:"1074ce83",3354:"97985843",3367:"faad8fb9",3426:"437c55c5",3433:"58ad179a",3467:"f659d38e",3495:"303dac6e",3553:"83d5d90b",3560:"a464831f",3608:"3ab11b8d",3650:"dfe90dc6",3654:"bc99872d",3667:"7e03a255",3688:"7b2ea426",3694:"36fc3dde",3724:"3930965e",3739:"9a8511f0",3759:"04b2e9dd",3760:"07ae25cc",3792:"7fe49081",3796:"b4dbff93",3823:"c2c20875",3828:"3ddcb8f0",3842:"fb12a445",3851:"5df36ea7",3861:"6ec62fef",3888:"45455a1e",3907:"37a4b741",3936:"4fe1f474",3959:"2a05729b",3997:"9279e5b1",3999:"4badc016",4013:"ad6273d4",4014:"c61467cc",4089:"506658d5",4149:"96a02a9a",4170:"42cb4952",4171:"68f47c00",4191:"f50b356e",4193:"d1d63e80",4194:"8c0ffbc9",4200:"2b0d7cc2",4201:"87799e17",4202:"d997fd03",4234:"1fffd3d8",4273:"e6ca178c",4335:"a0426a6c",4337:"836c5106",4344:"f410688b",4349:"d74e62ac",4358:"1f12693e",4361:"1ea42860",4388:"66c29fe9",4395:"1e4da950",4402:"19e6cdd0",4446:"ec3e9cfd",4468:"f54519c5",4471:"22121436",4496:"d1cc53b0",4568:"58e2d470",4573:"649fd3fc",4607:"ad181021",4689:"94b2ae10",4695:"c0d9d74b",4708:"8970d15e",4759:"46c7d80d",4768:"096d27d2",4800:"368d3f6f",4820:"6cba59c0",4837:"dcb4a461",4877:"169ab758",4889:"2094fc80",4940:"b4fe7414",4953:"08612623",4957:"e5fdad62",4972:"179070ba",4999:"c448436f",5006:"1eac4f3c",5026:"41619737",5045:"3891161f",5061:"6c16d091",5088:"f928e18c",5102:"ad5853a5",5159:"5d8371fc",5160:"6b664d6a",5161:"6c1b9ca1",5187:"0c106841",5205:"75b957f2",5208:"765610c8",5256:"03638e79",5306:"6ffe37fd",5308:"161c1957",5331:"97b230b2",5391:"85b95023",5392:"9042f409",5407:"e3d3d866",5439:"33570cae",5465:"ef423a18",5478:"64ff7a1d",5501:"ee4c0de6",5570:"da146d27",5573:"654e4339",5583:"5be1c2bf",5589:"3534a716",5593:"3451aa38",5610:"16e43df5",5612:"6444c96b",5632:"0ab7320a",5634:"230bf113",5650:"111a4733",5666:"08f8e38c",5669:"0ca62ddf",5670:"76621e15",5721:"747d2367",5722:"99343f2d",5772:"f304b8c8",5783:"da95d4d1",5797:"41c5044d",5802:"3f21705f",5853:"c83fd92b",5877:"c40aceaa",5883:"6f824bd4",5922:"fe770004",5963:"f321d6bd",5964:"66280048",5966:"57aea329",5983:"ed5d7a76",5987:"482a11b7",5991:"9104697a",6017:"a6d42b70",6035:"8410045a",6048:"e116bec8",6103:"4fe095e6",6123:"780c2f1e",6148:"f064477d",6173:"a590b436",6184:"4b38f8cb",6205:"2faf2ff8",6261:"367b755d",6293:"07c2abd4",6316:"48ea0a83",6325:"3957b2a9",6332:"f4eac952",6337:"116dc8c1",6404:"939952df",6412:"e574a499",6430:"7261fea7",6432:"d75d83d3",6445:"bb92a9de",6447:"7f2fdf1a",6491:"cd1ff234",6504:"a9e36de2",6508:"a35ff3fb",6518:"ebce0f6d",6524:"207ee58b",6525:"d12af83b",6526:"04ba7cfe",6530:"82fab025",6572:"5e95d99f",6573:"41ee2873",6735:"1e41d25f",6755:"9d08a472",6789:"9fe1dcd9",6794:"fdcd1137",6815:"6400937c",6823:"e6987e37",6831:"1bc52a0b",6837:"fd91e3bf",6871:"a2446d95",6885:"22e4d37e",6887:"25ecdfe3",6897:"d1c21210",6919:"a6b8a8ef",6984:"fa583df2",7009:"9ac8cf97",7056:"548b5f97",7059:"cd1c81c7",7064:"63507c28",7088:"eff1fb05",7116:"a245bdd4",7145:"f5731efc",7157:"7a532699",7216:"0ce04934",7218:"7027fde5",7310:"89888e46",7339:"848ab7e4",7342:"71867401",7346:"d6dd1e26",7373:"4a256193",7412:"095de08e",7414:"e9867ead",7511:"9b023b28",7516:"7868ea90",7534:"b3d66b42",7555:"790d6c60",7581:"2548bc16",7599:"69afbaf8",7605:"77295117",7640:"671b7643",7654:"ce775806",7697:"aedd8fc2",7724:"fa70f2a1",7725:"257e62ad",7766:"4d223b22",7769:"21d5a984",7779:"b4a23f17",7806:"0aaba4e8",7891:"eac25364",7901:"00873d96",7918:"4e53a1a6",7949:"b7ef60fb",7975:"6bf42a8a",8035:"4a142f8a",8053:"caeaf9bc",8060:"5b07ad37",8071:"30ee727c",8113:"a1f7acef",8117:"ca99c0ee",8207:"272ea7f4",8209:"66e787b3",8276:"43ef3b08",8283:"a34bf3a1",8311:"a8809d07",8351:"40a39f50",8355:"2aaaafdd",8385:"3dc43011",8419:"347e7610",8428:"511eb768",8450:"651a86b2",8461:"f2b48995",8502:"25dbe475",8534:"b7f87f8d",8561:"4ac46c63",8566:"9673399f",8586:"5c520248",8610:"3c6dcc8a",8659:"94988e89",8667:"71b3cda2",8684:"230c9f8e",8692:"6456ad41",8718:"f7ad87ef",8721:"2d5af889",8740:"6ae22027",8744:"9a149bfa",8758:"a6c9de59",8786:"d05357bd",8787:"9c0d8e56",8788:"668838e4",8818:"5519c4e7",8875:"84f55692",8882:"61b78995",8921:"47358e39",8979:"728f01ca",9008:"dec253ff",9020:"55394400",9039:"7212b3c5",9056:"6540dc1a",9059:"3deffc09",9062:"eb22593f",9107:"2964b719",9118:"56e42199",9142:"8cdc6c87",9242:"03588e81",9310:"e02ec7cc",9323:"c0341581",9326:"82a1c072",9354:"d08557f0",9357:"34c9bdbe",9377:"adf4007a",9417:"daf7025d",9438:"211e5168",9450:"448d8484",9462:"c183a035",9467:"c19b9af6",9481:"e40b0cf3",9487:"d37a310c",9497:"ea1724d7",9514:"8cac7ad7",9519:"e7571b84",9529:"866292f6",9563:"70094647",9582:"60ff361f",9583:"f5e02180",9606:"4d34b891",9627:"2ff85ed2",9671:"15396710",9713:"7e414064",9727:"ee6848bc",9734:"7a86b357",9745:"dbde2d95",9792:"89e12578",9817:"3edb9f9c",9840:"68c09fa4",9858:"639666c7",9926:"1d9e59b5",9953:"4633dedc",9984:"7c499f3f",9987:"7ff08f24",9993:"5da28777",9996:"f06de3d9"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,f)=>Object.prototype.hasOwnProperty.call(e,f),c={},b="car-ffeine:",r.l=(e,f,a,d)=>{if(c[e])c[e].push(f);else{var t,o;if(void 0!==a)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var b=c[e];if(delete c[e],t.parentNode&&t.parentNode.removeChild(t),b&&b.forEach((e=>e(a))),f)return f(a)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=l.bind(null,t.onerror),t.onload=l.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"7918",28773698:"9008",49859754:"4468",71016178:"1119",79422113:"8718",87847907:"2821",e4e8611b:"9","935f2afb":"53",f9ed38fd:"56",d50fd269:"100",b0e32a59:"161","96adae60":"172","67d8056c":"199","55347c97":"211",c5c65120:"224",c6a127bd:"229","4959fc42":"240","1893cb59":"286","7a9bf44c":"288","8bcbaad2":"292","029258fc":"299","0c071de2":"321","280572f1":"324","75945c6d":"367",fae4bc46:"424","1a665c6f":"454","7853a999":"461","38d8699e":"471","6ba49b42":"498",c4c26f23:"582","28177e2f":"682",b88b1bad:"719","2cec5164":"721","2487d3de":"808",e4ebfe18:"843",f4738242:"949",c573638f:"964","32b2299c":"970","754fb852":"988","6d7fbd92":"994",f4af9f40:"998","9c25eec0":"1002","9a953f96":"1028","69c28c32":"1065",d4122def:"1109","2620e7b9":"1120",aa137ad6:"1144",cc909c85:"1185",d2611248:"1236","600a7791":"1318",c6b4d86c:"1363",b3597569:"1386","7baa2934":"1389","207a8efc":"1416",fc04ac13:"1417","8e498bb6":"1436","601fd4f0":"1467","8f4b370c":"1483","64868a43":"1501","8e21b108":"1511","1252d667":"1566","49f50ebd":"1567","9dbc8adf":"1587",b3706a4c:"1627","163d37ca":"1651",ab705a1f:"1655","803fa4b0":"1664","0faaa152":"1704","22c62d68":"1722","5cfd338e":"1772","4e8d87e8":"1803","5924da1d":"1826",a30cd2a8:"1851",f1a9275a:"1866","8b74b8e0":"1877","2e044ce1":"1894","4d920b72":"1955","3617515a":"1971","7db1f2ec":"2001","37be277e":"2040","0260fa5b":"2041","0f313731":"2064",d350f5d9:"2098",c4d52cca:"2155","1457c284":"2160","807f61b6":"2203","1b7b671a":"2256",ad932f90:"2262","87506be9":"2288",d3ff88aa:"2289","7af1d52f":"2334",fcad85e8:"2340","2514f2ba":"2346","77b86cd7":"2420",f27c2916:"2460",b1bcf66a:"2463","81a656f3":"2468","2832e534":"2476","6c756788":"2504",b5230011:"2516","814f3328":"2535","7aafac26":"2582",d0e4eea8:"2595","3766ff11":"2616","7b1db77a":"2619","4ed0d1cc":"2670",f332d221:"2717",b583d190:"2730","0abff1f0":"2752","7762a24e":"2753",a0ce7679:"2758","153869a1":"2772","18c41134":"2859",ae61c7bf:"2889","007cdc83":"2897",cc9b0e25:"2922","1809876f":"2950","03f1f4ea":"2955",a8ccd094:"2997",f7ae9295:"3025",b18281db:"3063","074793ea":"3079","1f391b9e":"3085",a6aa9e1f:"3089","626b0166":"3091",b7972c94:"3146","8e044d98":"3165",d4497f2f:"3193","1df93b7f":"3237","27097cf2":"3252","37b9d916":"3293","4303a4a6":"3341","97136fd7":"3354",a50e10e9:"3367","547d36c4":"3426","81c2fdf5":"3433",e357b521:"3467",ae6362ae:"3495","210bacf6":"3553","8a0a9511":"3560","9e4087bc":"3608","23922d93":"3650","9ae81301":"3654",bcddcc8f:"3667",bce37a09:"3688","0b70ffa9":"3694","410af8a2":"3724",a0a681be:"3739","8d7fa36c":"3759","8660c6f2":"3760",dff1c289:"3792",cefbed25:"3796","97788a79":"3823",f09c0f77:"3828","1cd7fa68":"3842",b840888d:"3851","06db3492":"3861",d455ae7d:"3888",e63633a5:"3907",c613688f:"3936",a555d30b:"3959","660c0a9e":"3997","8bd490e1":"3999","01a85c17":"4013","0d81c928":"4014",a09e4d68:"4089","8d05b77c":"4149",d52058f1:"4170",b6c52e21:"4171","469dc3ee":"4191",f55d3e7a:"4193",db0d15fb:"4194","1d6afaf2":"4200","90a7e6ea":"4201","38f82c99":"4202","43ea9b4e":"4234","61f14ad3":"4273",a37176f0:"4335","5c31d10c":"4337",f3322a15:"4344","5e5ae3cc":"4349",cace0f84:"4358",fc69ad3c:"4361",c8cc1d07:"4388","9ffe4f1a":"4395","20fcf238":"4402",e7972e79:"4446","494882d1":"4471","02cc5bda":"4496",e613e09c:"4568",a7b32a40:"4573","533a09ca":"4607",eef36cfd:"4689","4fbdb8ff":"4695","034316ba":"4708","6120dc83":"4759",bae98e44:"4768",bbf87d95:"4800",bacd660c:"4820","2d05811a":"4837",a1c4ff9a:"4877","5f81b25c":"4889","857c637e":"4940",eec33099:"4953","483fd5d0":"4957","0be517de":"4999",ca4b344f:"5006",ff4d8b69:"5026",cf597922:"5045","43bed105":"5061","54150be7":"5088",d26080ac:"5102","5133b0c6":"5159",f4948b2c:"5160","9348a89e":"5161","3ce54efd":"5187",fdf3f179:"5205","16d0e52e":"5208",cb4b66ee:"5256",eb7def01:"5306",aec899bc:"5308","26896e6a":"5331",c9630fa2:"5391","6692f06b":"5392",fd250280:"5407","1cc4c623":"5439",d0e4cdf1:"5465","48a67c51":"5478","3ee6368b":"5501",b3b4a184:"5570","18e1dbe5":"5573",fa940ad3:"5583","5c868d36":"5589",d03a6f97:"5593","9899d8a5":"5610",b58cb1a9:"5612",decd4a10:"5632",fc3046f6:"5634",f5eecd74:"5650","337c555a":"5666","00931cc3":"5669",b98794bd:"5670","8ac474ef":"5721",d613ee27:"5722",e9652e75:"5772",f1568e05:"5783","7fbacf84":"5797",d01b1d1b:"5802",ebbab0c1:"5853","0023d7b0":"5877",ae2f64a1:"5883",e6caa061:"5922","93f098ab":"5963","09fbb6bd":"5964","871c1e5a":"5966",d5dc80ab:"5983",be4773d4:"5987",a5557bb9:"5991","6093f82b":"6017","226700de":"6035","1ec645a9":"6048",ccc49370:"6103",f3e308ad:"6123",f7743d3b:"6148",ee7d88ad:"6173",b7a74434:"6184","8e751dff":"6205",df01e1d1:"6261",e947f001:"6293","9bc95288":"6325",ea23132c:"6332","9097e19c":"6337",e726a561:"6404","37b2180d":"6412","181cad37":"6430",af971a0b:"6432",acbf6f0e:"6445",d9c03e5c:"6447","7220f6f9":"6491","822bd8ab":"6504","12cbeba7":"6508",bb25b787:"6518",bfe1055c:"6524",ea88f2a1:"6525","36c3ed9f":"6526",cc21b615:"6530","06f0a746":"6572","23e2728d":"6573","668a56ad":"6735",e44a2883:"6755","6ab5bcea":"6789","89e28a64":"6794","9031057c":"6815","743a3b39":"6823","77310d5d":"6831",fbd57548:"6837",e8622e75:"6871",c185dccb:"6885",f4f49e13:"6887","66e6cd6b":"6897","820d7f62":"6919","274c9143":"6984","54cb095e":"7009",bf03d367:"7056",e8c68abf:"7059","6dd1c948":"7064","9a8c4dbd":"7088","517acb4e":"7116","2728eda2":"7145","3ed04b60":"7157",d7c95adf:"7216","1aff6e78":"7218","852b2c90":"7310",bc0a62f1:"7339",e1d88fa0:"7342","899b6f7f":"7346",c17ec8e8:"7373","70275fcd":"7412","393be207":"7414","75f50328":"7511",f152f207:"7516","9391e08d":"7534","06a46f69":"7555","2e10a69c":"7581","80960b4b":"7599","09d822bf":"7605","76760c9d":"7640","37d538cb":"7654","35293ec4":"7697","9cfe8fd1":"7725",e8e37e6f:"7766",e70a8c2b:"7769",dce2839d:"7779","992b7323":"7806","635a92d5":"7891","2a8faff0":"7901","7085ca87":"7949","270346fa":"7975","389b50e0":"8035",eed983e8:"8053","4cbec242":"8060",e5531274:"8071","258958af":"8113",d0a8fb3e:"8117","9a9cf8cc":"8207","0b036d6e":"8209",dc154858:"8276","2576d4bf":"8283",d097edc6:"8311",e806107b:"8351",fe273484:"8355","5128a070":"8385","41ce545f":"8419","6efb579b":"8428","14759c52":"8450",a064989a:"8461","950dc7df":"8502","0cd70852":"8534","52b9c8f3":"8561",ab37b3d8:"8566","4a412608":"8586","6875c492":"8610",f51c3f5f:"8659",f5bc9f59:"8667","02ee3536":"8684","29bf81f3":"8692","88c8cf4c":"8721",cef71b63:"8740","18de7563":"8744",d5e55b08:"8758","6f6ec9cb":"8786",a05878b0:"8787",ddfb44b9:"8788","1e4232ab":"8818",cd9dc9ea:"8875",f75a8651:"8882",fb4958d9:"8921",d50b0fbf:"8979","73eafdfe":"9020","3ff85ced":"9039","002c05d5":"9056","198f8d8a":"9059",f37f9c20:"9062",e2f1a170:"9107",c8862eea:"9118","4b1569d6":"9142",c29bedb9:"9242",d1cef389:"9310",f1e649df:"9323",c844b82d:"9326","943a7d8e":"9354","2dcd9e41":"9357",dbe0b734:"9377",a3d6bdf1:"9417",dc36452a:"9438","2e801cce":"9450",f3277db2:"9462",ffe1d649:"9467",b5698685:"9481","0d852ea0":"9497","1be78505":"9514","7676fa9c":"9519","70d3c5f0":"9529","9d4c58e5":"9563",ea10567b:"9582","1f182e80":"9583",ef5b2427:"9606","1c01e504":"9627","0e384e19":"9671","1bb997fc":"9713","537b82b2":"9727",fd355a6b:"9734","7188ba4d":"9745","895a9c33":"9792","14eb3368":"9817","7995b933":"9840","53abb968":"9858",de6bae66:"9926","67f9c5aa":"9953",cc24784a:"9984",c41fbd98:"9987","417cb48d":"9993","203a6f4f":"9996"}[e]||e,r.p+r.u(e)},(()=>{var e={1303:0,532:0};r.f.j=(f,a)=>{var c=r.o(e,f)?e[f]:void 0;if(0!==c)if(c)a.push(c[2]);else if(/^(1303|532)$/.test(f))e[f]=0;else{var b=new Promise(((a,b)=>c=e[f]=[a,b]));a.push(c[2]=b);var d=r.p+r.u(f),t=new Error;r.l(d,(a=>{if(r.o(e,f)&&(0!==(c=e[f])&&(e[f]=void 0),c)){var b=a&&("load"===a.type?"missing":a.type),d=a&&a.target&&a.target.src;t.message="Loading chunk "+f+" failed.\n("+b+": "+d+")",t.name="ChunkLoadError",t.type=b,t.request=d,c[1](t)}}),"chunk-"+f,f)}},r.O.j=f=>0===e[f];var f=(f,a)=>{var c,b,d=a[0],t=a[1],o=a[2],n=0;if(d.some((f=>0!==e[f]))){for(c in t)r.o(t,c)&&(r.m[c]=t[c]);if(o)var i=o(r)}for(f&&f(a);n no offset no offset -no offset

    ]]> +no offset +no offset

    ]]> 가브리엘 https://github.com/gabrielyoon7 diff --git a/docs/category/tutorial---basics.html b/docs/category/tutorial---basics.html index 0367760..4367648 100644 --- a/docs/category/tutorial---basics.html +++ b/docs/category/tutorial---basics.html @@ -5,13 +5,13 @@ Tutorial - Basics | CAR-FFEINE - + - + \ No newline at end of file diff --git a/docs/category/tutorial---extras.html b/docs/category/tutorial---extras.html index 15735dc..198506f 100644 --- a/docs/category/tutorial---extras.html +++ b/docs/category/tutorial---extras.html @@ -5,13 +5,13 @@ Tutorial - Extras | CAR-FFEINE - + - + \ No newline at end of file diff --git a/docs/intro.html b/docs/intro.html index 2d944f6..a856b65 100644 --- a/docs/intro.html +++ b/docs/intro.html @@ -5,13 +5,13 @@ Tutorial Intro | CAR-FFEINE - +

    Tutorial Intro

    Let's discover Docusaurus in less than 5 minutes.

    Getting Started

    Get started by creating a new site.

    Or try Docusaurus immediately with docusaurus.new.

    What you'll need

    • Node.js version 16.14 or above:
      • When installing Node.js, you are recommended to check all checkboxes related to dependencies.

    Generate a new site

    Generate a new Docusaurus site using the classic template.

    The classic template will automatically be added to your project after you run the command:

    npm init docusaurus@latest my-website classic

    You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor.

    The command also installs all necessary dependencies you need to run Docusaurus.

    Start your site

    Run the development server:

    cd my-website
    npm run start

    The cd command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there.

    The npm run start command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/.

    Open docs/intro.md (this page) and edit some lines: the site reloads automatically and displays your changes.

    - + \ No newline at end of file diff --git a/docs/tutorial-basics/congratulations.html b/docs/tutorial-basics/congratulations.html index c5b00f7..8ee7296 100644 --- a/docs/tutorial-basics/congratulations.html +++ b/docs/tutorial-basics/congratulations.html @@ -5,13 +5,13 @@ Congratulations! | CAR-FFEINE - +

    Congratulations!

    You have just learned the basics of Docusaurus and made some changes to the initial template.

    Docusaurus has much more to offer!

    Have 5 more minutes? Take a look at versioning and i18n.

    Anything unclear or buggy in this tutorial? Please report it!

    What's next?

    - + \ No newline at end of file diff --git a/docs/tutorial-basics/create-a-blog-post.html b/docs/tutorial-basics/create-a-blog-post.html index 7502170..3e165d0 100644 --- a/docs/tutorial-basics/create-a-blog-post.html +++ b/docs/tutorial-basics/create-a-blog-post.html @@ -5,13 +5,13 @@ Create a Blog Post | CAR-FFEINE - +

    Create a Blog Post

    Docusaurus creates a page for each blog post, but also a blog index page, a tag system, an RSS feed...

    Create your first Post

    Create a file at blog/2021-02-28-greetings.md:

    blog/2021-02-28-greetings.md
    ---
    slug: greetings
    title: Greetings!
    authors:
    - name: Joel Marcey
    title: Co-creator of Docusaurus 1
    url: https://github.com/JoelMarcey
    image_url: https://github.com/JoelMarcey.png
    - name: Sébastien Lorber
    title: Docusaurus maintainer
    url: https://sebastienlorber.com
    image_url: https://github.com/slorber.png
    tags: [greetings]
    ---

    Congratulations, you have made your first post!

    Feel free to play around and edit this post as much you like.

    A new blog post is now available at http://localhost:3000/blog/greetings.

    - + \ No newline at end of file diff --git a/docs/tutorial-basics/create-a-document.html b/docs/tutorial-basics/create-a-document.html index a437e0b..448d50d 100644 --- a/docs/tutorial-basics/create-a-document.html +++ b/docs/tutorial-basics/create-a-document.html @@ -5,13 +5,13 @@ Create a Document | CAR-FFEINE - +

    Create a Document

    Documents are groups of pages connected through:

    • a sidebar
    • previous/next navigation
    • versioning

    Create your first Doc

    Create a Markdown file at docs/hello.md:

    docs/hello.md
    # Hello

    This is my **first Docusaurus document**!

    A new document is now available at http://localhost:3000/docs/hello.

    Configure the Sidebar

    Docusaurus automatically creates a sidebar from the docs folder.

    Add metadata to customize the sidebar label and position:

    docs/hello.md
    ---
    sidebar_label: 'Hi!'
    sidebar_position: 3
    ---

    # Hello

    This is my **first Docusaurus document**!

    It is also possible to create your sidebar explicitly in sidebars.js:

    sidebars.js
    module.exports = {
    tutorialSidebar: [
    'intro',
    'hello',
    {
    type: 'category',
    label: 'Tutorial',
    items: ['tutorial-basics/create-a-document'],
    },
    ],
    };
    - + \ No newline at end of file diff --git a/docs/tutorial-basics/create-a-page.html b/docs/tutorial-basics/create-a-page.html index 11e2236..875105a 100644 --- a/docs/tutorial-basics/create-a-page.html +++ b/docs/tutorial-basics/create-a-page.html @@ -5,13 +5,13 @@ Create a Page | CAR-FFEINE - +

    Create a Page

    Add Markdown or React files to src/pages to create a standalone page:

    • src/pages/index.jslocalhost:3000/
    • src/pages/foo.mdlocalhost:3000/foo
    • src/pages/foo/bar.jslocalhost:3000/foo/bar

    Create your first React Page

    Create a file at src/pages/my-react-page.js:

    src/pages/my-react-page.js
    import React from 'react';
    import Layout from '@theme/Layout';

    export default function MyReactPage() {
    return (
    <Layout>
    <h1>My React page</h1>
    <p>This is a React page</p>
    </Layout>
    );
    }

    A new page is now available at http://localhost:3000/my-react-page.

    Create your first Markdown Page

    Create a file at src/pages/my-markdown-page.md:

    src/pages/my-markdown-page.md
    # My Markdown page

    This is a Markdown page

    A new page is now available at http://localhost:3000/my-markdown-page.

    - + \ No newline at end of file diff --git a/docs/tutorial-basics/deploy-your-site.html b/docs/tutorial-basics/deploy-your-site.html index 88cb6d3..1956767 100644 --- a/docs/tutorial-basics/deploy-your-site.html +++ b/docs/tutorial-basics/deploy-your-site.html @@ -5,13 +5,13 @@ Deploy your site | CAR-FFEINE - +

    Deploy your site

    Docusaurus is a static-site-generator (also called Jamstack).

    It builds your site as simple static HTML, JavaScript and CSS files.

    Build your site

    Build your site for production:

    npm run build

    The static files are generated in the build folder.

    Deploy your site

    Test your production build locally:

    npm run serve

    The build folder is now served at http://localhost:3000/.

    You can now deploy the build folder almost anywhere easily, for free or very small cost (read the Deployment Guide).

    - + \ No newline at end of file diff --git a/docs/tutorial-basics/markdown-features.html b/docs/tutorial-basics/markdown-features.html index 17adc57..c5cd236 100644 --- a/docs/tutorial-basics/markdown-features.html +++ b/docs/tutorial-basics/markdown-features.html @@ -5,13 +5,13 @@ Markdown Features | CAR-FFEINE - +

    Markdown Features

    Docusaurus supports Markdown and a few additional features.

    Front Matter

    Markdown documents have metadata at the top called Front Matter:

    my-doc.md
    ---
    id: my-doc-id
    title: My document title
    description: My document description
    slug: /my-custom-url
    ---

    ## Markdown heading

    Markdown text with [links](./hello.md)

    Regular Markdown links are supported, using url paths or relative file paths.

    Let's see how to [Create a page](/create-a-page).
    Let's see how to [Create a page](./create-a-page.md).

    Result: Let's see how to Create a page.

    Images

    Regular Markdown images are supported.

    You can use absolute paths to reference images in the static directory (static/img/docusaurus.png):

    ![Docusaurus logo](/img/docusaurus.png)

    Docusaurus logo

    You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them:

    ![Docusaurus logo](./img/docusaurus.png)

    Code Blocks

    Markdown code blocks are supported with Syntax highlighting.

    ```jsx title="src/components/HelloDocusaurus.js"
    function HelloDocusaurus() {
    return (
    <h1>Hello, Docusaurus!</h1>
    )
    }
    ```
    src/components/HelloDocusaurus.js
    function HelloDocusaurus() {
    return <h1>Hello, Docusaurus!</h1>;
    }

    Admonitions

    Docusaurus has a special syntax to create admonitions and callouts:

    :::tip My tip

    Use this awesome feature option

    :::

    :::danger Take care

    This action is dangerous

    :::
    My tip

    Use this awesome feature option

    Take care

    This action is dangerous

    MDX and React Components

    MDX can make your documentation more interactive and allows using any React components inside Markdown:

    export const Highlight = ({children, color}) => (
    <span
    style={{
    backgroundColor: color,
    borderRadius: '20px',
    color: '#fff',
    padding: '10px',
    cursor: 'pointer',
    }}
    onClick={() => {
    alert(`You clicked the color ${color} with label ${children}`)
    }}>
    {children}
    </span>
    );

    This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !

    This is <Highlight color="#1877F2">Facebook blue</Highlight> !

    This is Docusaurus green !

    This is Facebook blue !

    - + \ No newline at end of file diff --git a/docs/tutorial-extras/manage-docs-versions.html b/docs/tutorial-extras/manage-docs-versions.html index 4de72ac..da2740a 100644 --- a/docs/tutorial-extras/manage-docs-versions.html +++ b/docs/tutorial-extras/manage-docs-versions.html @@ -5,13 +5,13 @@ Manage Docs Versions | CAR-FFEINE - +

    Manage Docs Versions

    Docusaurus can manage multiple versions of your docs.

    Create a docs version

    Release a version 1.0 of your project:

    npm run docusaurus docs:version 1.0

    The docs folder is copied into versioned_docs/version-1.0 and versions.json is created.

    Your docs now have 2 versions:

    • 1.0 at http://localhost:3000/docs/ for the version 1.0 docs
    • current at http://localhost:3000/docs/next/ for the upcoming, unreleased docs

    Add a Version Dropdown

    To navigate seamlessly across versions, add a version dropdown.

    Modify the docusaurus.config.js file:

    docusaurus.config.js
    module.exports = {
    themeConfig: {
    navbar: {
    items: [
    {
    type: 'docsVersionDropdown',
    },
    ],
    },
    },
    };

    The docs version dropdown appears in your navbar:

    Docs Version Dropdown

    Update an existing version

    It is possible to edit versioned docs in their respective folder:

    • versioned_docs/version-1.0/hello.md updates http://localhost:3000/docs/hello
    • docs/hello.md updates http://localhost:3000/docs/next/hello
    - + \ No newline at end of file diff --git a/docs/tutorial-extras/translate-your-site.html b/docs/tutorial-extras/translate-your-site.html index 174d786..f91e17f 100644 --- a/docs/tutorial-extras/translate-your-site.html +++ b/docs/tutorial-extras/translate-your-site.html @@ -5,13 +5,13 @@ Translate your site | CAR-FFEINE - +

    Translate your site

    Let's translate docs/intro.md to French.

    Configure i18n

    Modify docusaurus.config.js to add support for the fr locale:

    docusaurus.config.js
    module.exports = {
    i18n: {
    defaultLocale: 'en',
    locales: ['en', 'fr'],
    },
    };

    Translate a doc

    Copy the docs/intro.md file to the i18n/fr folder:

    mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/

    cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md

    Translate i18n/fr/docusaurus-plugin-content-docs/current/intro.md in French.

    Start your localized site

    Start your site on the French locale:

    npm run start -- --locale fr

    Your localized site is accessible at http://localhost:3000/fr/ and the Getting Started page is translated.

    주의

    In development, you can only use one locale at a same time.

    Add a Locale Dropdown

    To navigate seamlessly across languages, add a locale dropdown.

    Modify the docusaurus.config.js file:

    docusaurus.config.js
    module.exports = {
    themeConfig: {
    navbar: {
    items: [
    {
    type: 'localeDropdown',
    },
    ],
    },
    },
    };

    The locale dropdown now appears in your navbar:

    Locale Dropdown

    Build your localized site

    Build your site for a specific locale:

    npm run build -- --locale fr

    Or build your site to include all the locales at once:

    npm run build
    - + \ No newline at end of file diff --git a/index.html b/index.html index c93a268..524d58b 100644 --- a/index.html +++ b/index.html @@ -5,13 +5,13 @@ CAR-FFEINE - +
    - + \ No newline at end of file diff --git a/markdown-page.html b/markdown-page.html index ae830ba..d8e013c 100644 --- a/markdown-page.html +++ b/markdown-page.html @@ -5,13 +5,13 @@ Markdown page example | CAR-FFEINE - +

    Markdown page example

    You don't need React to write simple standalone pages.

    - + \ No newline at end of file diff --git a/page/10.html b/page/10.html index 7293f14..43adaf4 100644 --- a/page/10.html +++ b/page/10.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -23,7 +23,7 @@ 따라서 Schedule Thread Pool Size를 늘리도록 하겠습니다.

    @Configuration
    public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
    taskScheduler.setPoolSize(10);
    taskScheduler.setThreadNamePrefix("schedule-task-");
    taskScheduler.initialize();
    taskRegistrar.setTaskScheduler(taskScheduler);
    }
    }

    SchedulingConfigurer 를 구현하여 Thread Pool size를 일단 10개로 정의했습니다.

    success 스레드 풀을 늘렸더니 위와 같이 2의 배수의 시간에 정확히 작동이 되는 것을 확인할 수 있습니다.

    하지만 이렇게 여러 작업을 동시에 실행된다면 데이터베이스에 병목현상이 발생되어 오히려 작업이 더 느리게 끝날 수도 있다고 생각했습니다.

    그래서 해당 부분의 실행을 관리하는 클래스를 생성하여 해당 클래스에서 Schedule의 작업을 관리하도록 구현했습니다.

    @Service
    public class BusinessLogic {

    private final ApplicationEventPublisher applicationEventPublisher;

    @Scheduled(cron = "0/2 * * * * *")
    public void complexJobSchedule() {
    applicationEventPublisher.publishEvent(new SchedulingEvent(this::complexJob, "complexJob", LocalDateTime.now()));
    }

    @Scheduled(cron = "0/4 * * * * *")
    public void moreComplexJobSchedule() {
    applicationEventPublisher.publishEvent(new SchedulingEvent(this::moreComplexJob, "moreComplexJob", LocalDateTime.now()));
    }
    }

    로직이 있는 BusinessLogic 서비스에서 스케줄의 시간마다 실행해야할 메서드를 Event로 발행합니다.

    @Component
    public class ScheduleService {

    private final ExecutorService executorService = Executors.newFixedThreadPool(1);
    private final Queue<SchedulingEvent> scheduleTasks = new ConcurrentLinkedQueue<>();
    private final AtomicBoolean isRunning = new AtomicBoolean(false);

    @EventListener
    public void addTask(SchedulingEvent schedulingEvent) {
    scheduleTasks.add(schedulingEvent);
    }

    @Scheduled(cron = "0/1 * * * * *")
    public void polling() {
    if (!scheduleTasks.isEmpty() || isRunning.compareAndSet(false, true)) {
    SchedulingEvent schedulingEvent = scheduleTasks.poll();
    executorService.execute(() -> execute(schedulingEvent));
    }
    }
    }

    그리고 위와 같은 스케줄을 관리하는 서비스에서는 Schedule Event를 받아 실행하도록 만들었습니다. 해당 클래스에서는 ThreadPool을 새로 생성하여, schedule의 스레드에 영향을 받지 않도록 구현했습니다.

    그리고 1초마다 실행되는 스케줄을 만들어 queue에 작업이 있는지, 현재 작업 중인지 확인하여 그렇지 않다면 queue에서 작업을 꺼내 실행하도록 만들었습니다.

    거의 구현이 끝나갑니다. 이제는 해당 Schedule의 데이터를 저장하고, 작업이 실패했을 시에 다시 작업을 하기 위한 기능만 구현하면 될 것 같습니다.

    @Component
    public class ScheduleService {

    ...

    private void execute(SchedulingEvent schedulingEvent) {
    String jobId = schedulingEvent.jobId();
    LocalDateTime executionTime = schedulingEvent.executionTime();

    if (isJobInProgressOrDone(jobId)) {
    log.info("작업이 실행중입니다. {} {}", executionTime, jobId);
    return;
    }
    ScheduleTask entity = new ScheduleTask(jobId, executionTime, JobStatus.RUNNING);
    scheduleTaskJdbcRepository.save(entity);

    try {
    schedulingEvent.runnable().run();
    scheduleTaskJdbcRepository.updateById(entity.getId(), JobStatus.DONE);
    } catch (Exception e) {
    log.error("{} 작업 실행 중 에러가 발생했습니다.", jobId);
    scheduleTaskJdbcRepository.updateById(entity.getId(), JobStatus.ERROR);
    tasks.add(schedulingEvent);
    }
    }

    private boolean isJobInProgressOrDone(String jobId) {
    Optional<ScheduleTask> taskOptional = scheduleTaskRepository.findById(jobId);
    if (taskOptional.isPresent()) {
    ScheduleTask scheduleTask = taskOptional.get();
    return scheduleTask.getStatus() == JobStatus.RUNNING || scheduleTask.getStatus() == JobStatus.DONE;
    }
    return false;
    }
    }

    이 부분은 간단하게 구현할 수 있습니다. 위와 같이 작업의 실행 시간과, job의 이름으로 데이터베이스에서 조회하고, 없다면 작업을 실행하고 있다면 작업이 ERROR 인지 확인하여 작업을 실행해주면 될 것 같습니다.

    complete

    위와 같이 두 개의 서버를 동시에 띄웠을 때에도 스케줄이 잘 작동하는 것을 확인할 수 있습니다.

    결론

    스케줄을 이렇게 구현할 수도 있지만 환경이 된다면 Message Queue를 사용하는 것이 어떨까요?

    혹시 틀린 부분이 있다면 지적 부탁드립니다.

    - + \ No newline at end of file diff --git a/page/11.html b/page/11.html index 41ffcf6..3837f1d 100644 --- a/page/11.html +++ b/page/11.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -16,7 +16,7 @@ 아래와 같이 간단히 조건을 stream()의 filter()를 사용해서 구현했습니다.

    public class StationCacheRepository {

    private final List<StationInfo> cachedStations;

    public List<StationInfo> findByCoordinate(
    BigDecimal minLatitude,
    BigDecimal maxLatitude,
    BigDecimal minLongitude,
    BigDecimal maxLongitude
    ) {
    return cachedStations.stream()
    .filter(it -> it.latitude().compareTo(minLatitude) >= 0 && it.latitude().compareTo(maxLatitude) <= 0)
    .filter(it -> it.longitude().compareTo(minLongitude) >= 0 && it.longitude().compareTo(maxLongitude) <= 0)
    .toList();
    }
    }

    하지만 해당 방법으로 로컬에서 조회를 테스트 했을 때 캐시를 적용한 것보다 더 느려진 결과가 나왔습니다. 캐싱을 해서 데이터베이스까지 요청을 보내지 않는데 왜 더 느려진 것일까요?

    답은 인덱스 였습니다. Mysql 에서 인덱스는 B Tree로 구성되어 있습니다. 데이터베이스에서는 위도, 경도로 복합 인덱스가 설정되어 있었지만, 현재 어플리케이션 로직에는 해당 부분이 없습니다.

    그래서 filter로 순회하는 시간복잡도가 O(n)이고, 데이터베이스에서는 O(log n)이기 때문에 더 느려진 것입니다. 그렇다고 제가 직접 B tree 자료구조를 직접 구현해야할까요?

    현재 해당 조회 API는 위도 경도로 범위 탐색을 하고 있습니다. 결국엔 station의 정보들이 위도, 경도로 정렬만 되어 있다면 B tree를 직접 구현하지 않더라도 같은 시간복잡도 O(log n)으로 탐색할 수 있습니다. 물론 B tree와 다른 부분은 해당 충전소의 정확한 위도, 경도로 단일 칼럼을 조회할 때는 O(n)이기 때문에 이런 방법이 문제가 될 수 있지만, 해당 캐시 데이터로는 무조건 범위 탐색을 하기 때문에, B tree를 구현하지 않고 이분 탐색으로 조회하는 방식으로 변경해보겠습니다.

        public void initialize(List<StationInfo> stations) {
    cachedStations.addAll(stations);
    cachedStations.sort((o1, o2) -> {
    int latitudeCompare = o1.latitude().compareTo(o2.latitude());
    if (latitudeCompare == 0) {
    return o1.longitude().compareTo(o2.longitude());
    }
    return latitudeCompare;
    });
    }

    private List<StationInfo> findStations(BigDecimal minLatitude, BigDecimal maxLatitude, BigDecimal minLongitude, BigDecimal maxLongitude) {
    int lowerBound = binarySearch(minLatitude, START_INDEX);
    int upperBound = binarySearch(maxLatitude, lowerBound);
    if (lowerBound == -1 || upperBound == -1) {
    return Collections.emptyList();
    }
    return cachedStations.stream()
    .skip(lowerBound)
    .limit(upperBound - lowerBound)
    .filter(station -> station.longitude().compareTo(minLongitude) >= 0 && station.longitude().compareTo(maxLongitude) <= 0)
    .toList();
    }

    private int binarySearch(BigDecimal latitude, int startIndex) {
    int left = startIndex;
    int right = cachedStations.size() - 1;
    int result = -1;
    while (left <= right) {
    int middle = left + (right - left) / 2;
    StationInfo middleStation = cachedStations.get(middle);
    if (middleStation.latitude().compareTo(latitude) >= 0) {
    result = middle;
    right = middle - 1;
    } else {
    left = middle + 1;
    }
    }
    return result;
    }

    먼저 어플리케이션이 실행될 때 cache 데이터를 찾아 저장하는 것 뿐만 아니라, 위도(Latitude)를 기준으로 정렬하도록 만들었습니다. 그리고 위도의 최소, 최대값의 인덱스를 가장 효율적으로 찾아올 수 있도록 binary search를 하는 메서드를 만들었습니다. 이렇게 한다면 O(log n) 으로 위도의 최대 최소 조건에 포함되는 모든 station의 값을 조회할 수 있습니다. 그리고 조회한 데이터들의 개수만큼 filter를 통해 경도(longitude) 가 포함되는지 확인합니다. 해당 방식의 구현은 B tree가 작동하는 방식과 유사할 것입니다.

    이분 탐색을 적용한 결과 로컬에서 응답 속도가 120 ms -> 50 ~ 70 ms로 약 2배 빨라진 것을 확인할 수 있습니다.

    실시간이 중요한 데이터는?

    앞서 말씀드렸다시피 지도로 충전소를 조회할 때, 충전소의 정보들에는 바뀌지 않는 정보뿐만 아니라, 최신화해야하는 충전기의 현재 상태 정보가 있습니다. 이러한 정보들은 캐싱해둘 수 없습니다. 하더라도, 관리 포인트가 늘어나기 때문에 데이터베이스에서 캐싱해둔 충전기 id로 충전기의 상태를 찾아와서 정보를 합쳐 반환하는 식으로 만들 수 있습니다.

        select cs.station_id,
    sum(case
    when cs.charger_condition = 'STANDBY' then 1
    else 0
    end)
    from charger_status cs
    where cs.station_id in (?, ?, ?, ?, ?, ?, ?)
    group by cs.station_id

    위와 같은 쿼리로 해당 충전소의 최신화된 충전기 상태를 가져올 수 있습니다.

    캐싱을 하기전에 데이터베이스를 이용해 데이터를 가져올 때의 쿼리는 아래와 같습니다.

     select
    distinct s.station_id
    from
    charge_station s
    inner join
    charger c
    on (
    c.station_id=s.station_id
    )
    where
    s.latitude>=?
    and s.latitude<=?
    and s.longitude>=?
    and s.longitude<=?
    -------------------------------------------------
    select
    s.station_id,
    s.station_name,
    s.latitude,
    s.longitude,
    s.is_parking_free,
    s.is_private,
    sum(case
    when cs.charger_condition='STANDBY' then 1
    else 0
    end),
    sum(case
    when c.capacity>=50 then 1
    else 0
    end)
    from
    charge_station s
    inner join
    charger c
    on (
    c.station_id=s.station_id
    )
    inner join
    charger_status cs
    on (
    c.station_id=cs.station_id
    and c.charger_id=cs.charger_id
    )
    where
    s.station_id in (
    ?,?,?,?
    )
    group by
    s.station_id

    원래는 위와 같이 여러번의 Join을 하고, 2번의 쿼리가 나갔던 반면 지금은 join을 하지않는 한번의 깔끔한 쿼리로 개선되었습니다.

    그리고 station 테이블의 위도, 경도로 범위 탐색을 위해 생성했던 index도 제거할 수 있게 되었습니다!

    결론

    1. 캐싱할 수 있는 부분은 하는 것도 좋을 것 같습니다
    2. 시간 복잡도를 계산해봅시다.
    3. 성능 개선 재밌습니다.
    - + \ No newline at end of file diff --git a/page/12.html b/page/12.html index 80d3581..6a8d7b3 100644 --- a/page/12.html +++ b/page/12.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -30,7 +30,7 @@ 위와 같은 조회 쿼리가 나왔으므로 인덱스를 아래와 같이 station_id, day_of_week에 걸어주었습니다.

    img 위 실행 속도에서 execution time을 확인해보면 인덱스를 걸고 134ms -> 5ms로 성능이 많이 개선 되었음을 확인할 수 있습니다.

    img 실행 계획도 의도한대로 잘 나오는 것을 보실 수 있습니다.


    정리

    1. DB Partitioning - (day_of_week : 요일)을 기준으로 파티셔닝
    2. 조회 쿼리에 맞게 인덱스 설정
    3. API 수정 (모든 요일의 혼잡도 조회 -> 해당 요일의 혼잡도 조회)

    결과적으로 기존 혼잡도 조회시 511ms가 나왔으나, 요일 별 조회 및 파티셔닝 & 인덱스를 적용하고 execution time = 5ms로 개선

    - + \ No newline at end of file diff --git a/page/13.html b/page/13.html index f56338b..2f4d1b1 100644 --- a/page/13.html +++ b/page/13.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -22,7 +22,7 @@ 소스 서버에서 커밋된 트랜잭션은 바이너리 로그에 기록되고, 레플리카 서버에서는 주기적으로 새로운 트랜잭션에 대한 바이너리 로그를 요청합니다. 이러한 방식은 소스 서버는 레플리카 서버가 제대로 변경 되었는지 알 수 없습니다. 즉 데이터 정합성에 문제가 생긴다는 단점이 있습니다. 하지만 이러한 방식은 소스 서버가 각 트랜잭션에 대해 레플리카 서버로 전송되는 부분을 고려하지 않는다는 점이 속도 측면에서 빠르고, 또 여러 대의 레플리카 서버를 구성하더라도 큰 성능 저하가 없다는 점이서 장점이 있습니다.

    반동기 복제

    반동기 복제는 비동기 복제보다 좀 더 데이터 정합성이 올라갑니다. 소스 서버는 변경된 트랜잭션이 있을 때 레플리카 서버가 다 전송이 되었다는 ACK 신호를 받기 때문에 확실히 알 수 있습니다. 하지만 전송여부만 확인하기 때문에 트랜잭션이 반영이 되었다는 보장은 없습니다. 반동기 복제 방식은 2가지가 있습니다.

    1. After sync: After Sync 방식은 소스 서버에서 트랜잭션을 바이너리 로그에 기록 후 Storage Engine에 바로 커밋하지 않습니다. 먼저 바이너리 로그에 기록 후 레플리카 서버의 ACK 응답을 기다립니다. 그리고 ACK 응답이 도착하면 그제서야 스토리지 엔진을 커밋하여 트랜잭션을 처리하고 결과를 반환합니다.
    2. After commit: After commit은 이름 그대로 커밋을 먼저 하는 것입니다. 트랜잭션이 생기면 먼저 바이너리 로그에 기록 후 소스 서버 스토리지 엔진에 커밋합니다. 그리고 레플리카 서버의 ACK 응답이 내려오면 클라이언트는 처리 결과를 얻고 다음 쿼리를 수행할 수 있습니다.

    먼저 after commit 방식은 소스 서버에 장애가 발생했을 때 팬텀 리드가 발생하게 됩니다. 트랜잭션이 스토리지 엔진 커밋까지된 후 레플리카 서버의 응답을 기다립니다. 이처럼 스토리지 엔진 커밋까지 완료된 데이터는 다른 세션에서도 조회가 가능합니다. 트랜잭션이 커밋되었고, 레플리카 서버로 아직 응답을 기다릴 때, 소스 서버에 장애가 발생한다면 새로운 소스 서버로 승격된 레플리카 서버에서 데이터를 조회할 때 자신이 이전 소스 서버에서 조회했던 데이터를 보지 못할 수도 있습니다.

    그리고 이처럼 레플리카 서버가 승격된 상황에 소스 서버의 장애가 복구되어 재사용할 경우 이미 커밋된 그 트랜잭션을 수동으로 롤백 시켜야만 데이터가 맞는 상황이 생깁니다.

    저희 팀의 복제 동기화 방식

    이러한 장단점으로 저희 팀은 데이터 무결성이 중요하다 판단되어 반동기 복제 방식을 사용하고, After Sync 방식을 적용하였습니다.

    복제 토폴리지

    복제 토폴리지는 여러가지 방식 중 자신의 상황과 가장 맞는 방식을 사용하면 될 것 같습니다. 저희 팀이 고려해야할 문제는 먼저 성능을 올려야 했고, 단일 장애포인트를 개선해야했습니다. 하지만 사용할 수 있는 서버는 2대 뿐이였습니다. 이러한 상황에서 어떤 방식을 택할 수 있을까요?

    싱글 레플리카

    가장 기본적이며 가장 많이 쓰이는 형태입니다. 어플리케이션에서 레플리카 서버에 읽기 요청을 전달하면, 레플리카 서버에 문제가 생겼을 때, 서비스 장애 상황이 발생할 수 있습니다. 그러므로 소스 서버에서 Read, Write를 둘 다 하고, 레플리카 서버는 failover를 위해 대기하는 예비용 서버로 구성합니다. 소스 서버에 장애가 발생했을 때 소스 서버를 대체하거나 데이터를 백업하는 용도로 사용합니다.

    멀티 레플리카

    싱글 레플리카와 비슷한 구성이지만 레플리카 서버가 한 대 더 추가된 구성입니다. 해당 방식은 SPOF 문제가 없기 때문에 레플리카 서버 하나를 읽기 전용 서버로 둘 수 있습니다. 읽기 작업을 분산함으로 어플리케이션의 성능을 향상 시킬 수 있습니다. 아까 말했던 장애 상황이 발생하면 예비용 서버인 Replica2 서버를 Source 서버 혹은 Replica1(읽기 전용) 서버로 대체할 수 있습니다.

    체인 복제

    레플리카 서버가 많아져 소스 서버의 바이너리 로그를 읽는 부하가 많아질 때 할 수 있는 구성입니다. 좀 전에 설명드렸던 멀티 레플리카 방식에서 똑같은 구성을 추가한 방식으로 볼 수 있습니다. Source 1 의 정보를 복제한 Replica 1-1, 1-2 서버는 빠르게 데이터가 반영되지만, Source1의 이벤트를 복제한 Source2를 복제한 Replica 2-1, 2-2 서버는 당연히 늦게 반영되기 때문에 해당 그룹은 예비용으로 사용합니다.

    듀얼 소스 복제

    데이터베이스 둘 다 소스 서버이면서 레플리카 서버인 경우입니다. 이 경우는 Active-Active구성과 Active-Passive 구성으로 나뉩니다

    Active-Active는 서버 둘 다 읽기와 쓰기가 가능한 형태입니다. 즉 부하를 분산시키기 위해 서버 모두 읽고 쓰는 작업을 하는 것입니다. 하지만 이러한 방식은 뻔한 단점이 있습니다. 서로의 이벤트가 동기화 되기 전에는 정합성이 깨질 수 있습니다. 또 동시에 같은 데이터에 대해 쓰기 작업을 수행할 때, 하나의 서버에서 쓰기가 완료되었더라도, 다른 하나의 서버에 늦게 끝난 쓰기가 있다면 마지막 트랜잭션인 늦게 끝난 쓰기 작업이 반영되어 예상하지 못한 결과가 나올 수 있습니다.

    또 다른 문제로는 Auto Increment를 사용할 때입니다. 새로운 데이터가 동시에 생성될 때 Auto Increment가 중복되는 에러가 발생할 수 있기 때문에 해당 토폴로지에서는 ID를 DB에 의존하지 않는 것이 좋습니다.

    Active-Passive 방식은 하나의 서버만 읽기와 쓰기 요청이 되지만, 나머지 서버는 대기하고 있습니다. 두 서버 모두 언제나 쓰기 작업이 가능한 형태이기 때문에 장애 발생 시 빠르게 Faliover할 수 있다는 점이 있습니다.

    멀티 소스 복제

    하나의 레플리카 서버가 다수의 소스 서버를 갖는 구성입니다. 데이터베이스 샤딩을 해뒀는데, 다시 하나의 서버로 통합하고 싶을 때 사용할 수 있습니다. 혹은 서로 다른 데이터를 한 곳에 백업을 할 때도 사용할 수 있습니다.

    저희 팀의 토폴로지 방식

    그럼 이렇게나 많은 구성 중에 저희 팀에서 택할 수 있는 토폴로지 방식은 싱글 레플리카 방식과 듀얼 소스 복제 방식 밖에 없습니다. 왜냐하면 주어진 서버가 2대뿐이기 때문입니다. 하지만 듀얼 소스 방식은 적용하는데 무리가 있는 부분이 있습니다. 일단 저희가 레플리케이션을 적용하려는 가장 큰 이유는 성능 이기 때문에 성능이 변하지 않는 듀얼 소스의 Active-Passive 방식은 제외하겠습니다. 그리고 Active-Active 방식은 부하를 분산시킬 수 있다는 장점이 있지만, 단점으로는 Auto Increment를 사용하는데에 위험이 있다는 점과, 데이터의 정합성 문제가 생길 수 있다는 점에서 듀얼 소스 방식은 제외하도록 했습니다.

    그럼 싱글 레플리카 방식을 적용할 수 밖에 없는데요. 싱글 레플리카의 방식은 가용성 문제를 해결하기 위해 만들어진 방식입니다. 하지만 저희 서비스는 현재 가용성보다 성능을 더 신경써야하는 상황이기때문에 싱글 레플리카 토폴로지를 구성하지만 레플리카 서버를 예비용이 아닌 읽기 전용 방식으로 사용하도록 하고, 가용성 부분을 포기하기로 정했습니다.

    코드에 적용하기

    replication-datasource Github 소스 코드를 참고하시거나, DB 복제, @Transactional에 따라 요청 분리해보기 글을 참고하여 따라하면 금방하실 수 있습니다!

    결론

    데이터베이스 레플리케이션 생각보다 어렵지 않습니다.

    데이터베이스 재밌습니다. 인프라도 재밌습니다.

    참고

    Real Mysql 8.0

    - + \ No newline at end of file diff --git a/page/14.html b/page/14.html index 1db9620..9ab3cec 100644 --- a/page/14.html +++ b/page/14.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -14,7 +14,7 @@ cpu

    조회 성능 개선하기

    먼저 제가 개선하기 위해 사용했던 방법들에 대해 적어보겠습니다.

    DTO 이용하기

    현재 구조는 아래의 JPA를 이용해 아래와 같은 쿼리로 entity로 데이터를 조회합니다.

     select distinct station.station_id,
    charger.charger_id,
    charger.station_id,
    chargerStatus.charger_id,
    chargerStatus.station_id,
    station.created_at,
    station.updated_at,
    station.address,
    station.company_name,
    station.contact,
    station.detail_location,
    station.is_parking_free,
    station.is_private,
    station.latitude,
    station.longitude,
    station.operating_time,
    station.private_reason,
    station.station_name,
    station.station_state,
    charger.created_at,
    charger.updated_at,
    charger.capacity,
    charger.method,
    charger.price,
    charger.type,
    charger.station_id,
    charger.charger_id,
    chargerStatus.created_at,
    chargerStatus.updated_at,
    chargerStatus.charger_condition,
    chargerStatus.latest_update_time
    from charge_station station
    inner join
    charger charger on station.station_id = charger.station_id
    inner join
    charger_status chargerStatus on charger.charger_id = chargerStatus.charger_id
    and charger.station_id = chargerStatus.station_id
    where station.latitude >= 37.5019194727953082567
    and station.latitude <= 37.5092305272047217433
    and station.longitude >= 127.044542269049714936
    and station.longitude <= 127.058071330950285064

    JPA를 통해 이러한 방식으로 조회한다면 아주 편하게 값을 가져오고, fetch join을 통해 하위의 entity들의 정보도 깔끔하게 가져옵니다.

    가져온 값으로 필요한 정보들을 매핑하고 가공하여 응답을 내려줬습니다.

    하지만 조회만을 위해 JPA의 entity를 조회한다는 것은 여러 단점이 존재합니다.

    제일 먼저 응답을 내려줄 때 불필요한 데이터까지 모두 조회를 한다는 부분입니다. 이렇게 많은 필드들이 있습니다. 하지만 응답에서는 대부분의 경우 모든 정보가 필요하지 않습니다. 그리고 모든 정보를 다 보내주는 것도 좋지 않습니다. 대량의 데이터를 조회할 때의 성능이 아주 나빠집니다.

    그래서 필요한 칼럼만 조회하는 것이 좋습니다.

    그리고 또 다른 단점으로는 JPA로 entity를 조회할 때 Hibernate 캐시에 저장한다던가, One To One 에서 N+1 쿼리가 발생하기 때문에 성능적인 이슈가 여러가지 있습니다.

    그래서 조회만 하는 api라면 DTO Projection으로 하는 것이 좋을 것 같습니다. 그리고 아래와 같이 변경하였습니다.

    SELECT s.station_id,
    s.station_name,
    s.latitude,
    s.longitude,
    s.is_parking_free,
    s.is_private,
    sum(case
    when cs.charger_condition = 'STANDBY' then 1
    else 0
    end),
    sum(case
    when c.capacity >= 50 then 1
    else 0
    end)
    FROM charge_station s
    inner join charger c on (c.station_id = s.station_id)
    inner join charger_status cs on (c.charger_id = cs.charger_id and c.station_id = cs.station_id)
    where s.station_id in (?, ?)
    group by s.station_id;

    이렇게 필요한 칼럼만 조회하는 방식으로 변경하여, 선릉역 근처를 조회하는 기준으로 약 450ms -> 350ms로 개선되었습니다.

    하지만 아직도 너무 느린 것을 확인할 수 있습니다. 그래서 실행 계획을 확인했습니다.

    실행 계획 확인하기

    sql의 실행 계획은 아주 중요하고 성능을 개선할 때 아주 유용합니다.

    실행 계획에는 여러가지 정보들이 있습니다.

    1. ID: 실행 계획 내에서 각 작업 또는 단계를 식별하는 일련번호입니다. 실행 계획은 여러 단계로 나뉘며, ID를 통해 이러한 단계를 식별할 수 있습니다.

    2. Select Type: 쿼리의 각 단계(예: SIMPLE, PRIMARY, SUBQUERY)에 대한 실행 유형을 나타냅니다. 이는 MySQL이 데이터를 선택하고 처리하는 방식을 나타냅니다.

    3. Table: 실행 계획에 포함된 테이블의 이름 또는 별칭입니다. 어떤 테이블이 사용되는지를 확인할 수 있습니다.

    4. Type: 테이블 접근 방식을 나타냅니다. 이 값은 인덱스 스캔, 풀 테이블 스캔 등과 같은 값일 수 있으며, 성능에 큰 영향을 미칩니다.

    5. Possible Keys: 사용 가능한 인덱스를 나타냅니다. MySQL이 어떤 인덱스를 사용할 수 있는지 알려줍니다.

    6. Key: 실제로 선택된 인덱스입니다. 이 값은 가능한 인덱스 중에서 실제로 사용되는 인덱스를 나타냅니다.

    7. Key Len: 사용된 인덱스의 길이를 나타냅니다.

    8. Ref: 인덱스를 사용하여 테이블 간의 연결을 나타내는 열입니다.

    9. Rows: 각 단계에서 예상되는 행의 수입니다. 이 값은 성능 평가에 중요한 역할을 합니다.

    10. Extra: 기타 정보를 제공합니다. 이 칼럼에는 추가 정보 및 힌트가 포함될 수 있습니다.

    이렇게 여러 칼럼이 있습니다. 그 중 성능에 큰 영향을 미치는 칼럼 두 가지만 자세히 알아보겠습니다.

    Type

    1. const : 쿼리에 Primary key 혹은 unique key 칼럼을 이용하는 where 조건절을 가지고 있고, 반드시 하나의 데이터를 반환하는 방식이다. (옵티마이저가 해당 부분은 상수로 처리하기 때문에 const라고 한다.)
    2. eq_ref : 조인에서 Primary key 혹은 unique key 칼럼을 이용하는 where 조건절을 가지고 있고, 반드시 하나의 데이터를 반환하는 방식이다. (const와 다른 점은 eq_ref는 조인에서 사용된다는 점이다.)
    3. ref : eq_ref와 다르게 join의 순서와 관계없이 사용된다. 그리고 primary key, unique key도 관계없다. 그냥 인덱스의 종류와 관계없이 = 조건으로 검색할 때 사용된다
    4. fulltext: mysql 전문 검색 인덱스를 사용해서 레코드에 접근하는 방법, 전문 검색할 컬럼에 인덱스가 있어야 한다. "MATCH ... AGAINST ..." 구문을 사용해서 실행된다
    5. range: 인덱스를 이용해서 검색하는데, 검색 조건이 >, >=, <, <=, BETWEEN, IN() 등의 연산자를 사용하는 경우이다. 보통의 인덱스 스캔이라고 하면 range, const, ref를 칭한다
    6. index: 인덱스 풀 스캔이다. 인덱스를 이용해서 테이블의 모든 레코드를 읽는다. 인덱스를 이용해서 테이블을 읽는 것이기 때문에 all보다는 빠르다.
    7. all: 테이블 풀 스캔이다. 테이블의 모든 레코드를 읽는다. 가장 느린 방법이다.

    실행 계획에서 자주 보이는 type들만 성능이 좋은 순으로 정리해봤습니다.

    Extra

    1. using filesort: 정렬을 위해 별도의 파일 정렬을 수행한다. 이는 인덱스를 사용하지 않고 정렬을 수행한다는 의미이다. 이는 성능에 좋지 않다.
    2. using index: 인덱스만으로 쿼리를 처리한다. 이는 인덱스만으로 쿼리를 처리하기 때문에 성능이 좋다.
    3. using join buffer: join이 되는 칼럼은 인덱스를 생성한다. 하지만 driven table에 적절한 인덱스가 없다면 driving table에 있는 모든 레코드를 읽어서 join을 수행한다. 그래서 이걸 보완하기 위해 driving table에 읽은 레코드를 임시 공간에 저장하는데 그 곳이 join buffer이다.
    4. using temporary: 쿼리를 처리하기 위해 임시 테이블을 생성한다. 인덱스를 사용하지 못하는 group by 쿼리가 대표적인 예이다.
    5. using where: mysql 엔진이 별도의 가공, 필터링 작업을 처리한 경우일 때만 나타난다. 범위 조건은 스토리지 엔진에서 처리되어 레코드를 리턴해주지만, 체크 조건은 mysql 엔진에서 처리된다.

    type뿐만 아니라 extra도 쿼리의 문제를 파악하는데 아주 큰 도움을 줍니다. 그 중 자주 보이는 것들에 대해서만 정리해봤습니다.

    그럼 아까 생성한 쿼리의 실행 계획을 확인해봅시다.

    +----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+
    | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    +----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+
    | 1 | SIMPLE | station | NULL | range | PRIMARY,idx_station_coordination | PRIMARY | 1022 | NULL | 2 | 100.00 | Using where; Using temporary |
    | 1 | SIMPLE | charger | NULL | ALL | PRIMARY | NULL | NULL | NULL | 240340 | 10.00 | Using where; Using join buffer (hash join) |
    | 1 | SIMPLE | chargersta | NULL | eq_ref | PRIMARY | PRIMARY | 2044 | charge.charger1_.charger_id,charge.station0_.station_id | 1 | 100.00 | NULL |
    +----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+

    station 테이블에 대해서는 range 스캔, 임시 테이블을 생성하고 있습니다, 그리고 charger에서는 테이블 풀 스캔, join buffer까지 생성하고 있습니다. 다행히도 chargersta 테이블에서는 적당한 조건을 생성한 것 같습니다.

    다시 한번 쿼리를 보고 실행 계획이 이렇게 나온 이유를 알아보겠습니다.

    SELECT
    ...
    FROM charge_station s
    inner join charger c on (c.station_id = s.station_id)
    inner join charger_status cs on (c.charger_id = cs.charger_id and c.station_id = cs.station_id)
    where s.station_id in (?, ?)
    group by s.station_id;

    아까 얘기했던, using temporary와 using join buffer가 발생하는 이유의 공통점을 찾아보면, 인덱스가 문제인 것을 유추할 수 있습니다.

    station과 charger를 join할 때, driven table 즉, charger 테이블에 적절한 인덱스가 없어 성능이 나빠진 것이라 의심하여, 인덱스를 생성하고 다시 한번 실행 계획을 확인했습니다.

    +----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+
    | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    +----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+
    | 1 | SIMPLE | station | NULL | range | PRIMARY,idx_station_coordination | PRIMARY | 1022 | NULL | 2 | 100.00 | Using where |
    | 1 | SIMPLE | charger | NULL | ref | PRIMARY,idx_station_id | idx_station_id | 1022 | charge.s.station_id | 3 | 100.00 | NULL |
    | 1 | SIMPLE | chargersta | NULL | eq_ref | PRIMARY | PRIMARY | 2044 | charge.charger1_.charger_id,charge.station0_.station_id | 1 | 100.00 | NULL |
    +----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+

    이렇게 charger 테이블에 인덱스를 생성한 것만으로도 실행 계획을 깔끔하게 개선했습니다.

    결과

    아래는 인덱스를 생성하기 전 실행 속도입니다.

    개선_전

    아래는 인덱스를 생성한 후 실행 속도입니다.

    개선_후

    315ms -> 24ms 로 약 13배 빨라진 것을 확인할 수 있습니다.

    결론

    실행 계획 확인은 필수입니다!

    참고

    real mysql 책

    - + \ No newline at end of file diff --git a/page/15.html b/page/15.html index 4158fbc..a802632 100644 --- a/page/15.html +++ b/page/15.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -15,7 +15,7 @@ 예를 들면 충전소 회사 명에서 광주시라는 이름이 있었는데, 이 필터는 실제로 두 가지가 존재했습니다.

    하나는 경기도 광주, 하나는 전라도 광주였습니다.

    이런 부분에서 불필요한 지역의 필터까지 걸리게 되는 문제가 있었습니다. 협업하는 과정에서 이를 발견했고, 즉각 조치를 취했습니다.

    조치를 취할 때 서로에게 각자 편한 방법이 있었지만, 단순히 서로에게 편한 작업을 하지 않았고, 팀원과 상의하면서 추후 진행에 문제 없는 방향을 찾고 진행할 수 있었습니다.

    지금 생각해보면 만약 각자에게 편한 방식으로 문제를 수정했다면, 다른 팀원이 다른 작업을 할 때 지장이 갔을 수도 있고 불필요한 작업을 했을 수도 있었을 것 같습니다.

    이 시점을 계기로 저희 팀끼리 예상하지 못한 문제를 작업 중에 발견하더라도 다른 팀원에게 공유하고 서로 짧은 회의를 통해 문제 해결 방안을 같이 찾는 것이 자연스럽게 팀문화로 자리 잡게 되었습니다.

    - + \ No newline at end of file diff --git a/page/16.html b/page/16.html index c7ee1e3..cb4953c 100644 --- a/page/16.html +++ b/page/16.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -24,7 +24,7 @@ 빨간색은 개발자가 직접 건들지 못하지만 간접적으로 사용할 수 있는 영역 노란색은 React 18 엔진의 영역입니다.

    이외에 제공되는 다른 커스텀 훅들도 거의 비슷한 구조를 띄고 있습니다.

    // 추가로 구현할 수 있는 함수들

    export const useSetExternalState = <T>(store: DataObserver<T>) => {
    const { setState } = store;

    return setState;
    };

    export const useExternalValue = <T>(store: DataObserver<T>) => {
    const { subscribe, getState } = store;
    const state = useSyncExternalStore(subscribe, getState);

    return state;
    };

    // 바닐라JS 영역에서 자연스러운 읽기를 지원하는 함수

    export const getStoreSnapshot = <T>(store: DataObserver<T>) => {
    return store.getState();
    };

    더 다양한 예제는 여기에서 확인할 수 있고 작성한 라이브러리 코드 전문은 여기에서 확인할 수 있습니다.

    겨우 파일 수십 줄로 만든 초경량 상태관리 라이브러리였습니다

    - + \ No newline at end of file diff --git a/page/17.html b/page/17.html index 1bcdbcc..89002af 100644 --- a/page/17.html +++ b/page/17.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 18분
    가브리엘

    안녕하세요? 카페인 팀에서 사용한 지도 시스템에 대해서 소개하려고 합니다.

    지도 기능에서 가장 핵심인 기능 두 가지를 뽑자면, 지도 그 자체와 지도 위에 그려지는 마커를 뽑을 수 있을 것입니다. 지도 위에 마커를 그리는 일은 그다지 어렵지 않고, documents 에 있는 예제들을 잘 따라하면 누구나 충분히 구현할 수 있을 것입니다.

    no offset

    하지만 마커의 갯수가 과도하게 많다면 어떤 전략을 세울 수 있을까요?

    카페인 팀에서는요 ...

    카페인 서비스에서 지도는 굉장히 중요한 요소 중 하나였습니다. 사용자들이 궁금한 장소의 주변에 있는 충전소를 시각적으로 제공해주기 위해서는 지도를 잘 제어할 수 있어야 했습니다. 특히 전국에 이미 수만 대의 충전소가 보급이 된 상황에서 충전소 마커를 모두 그려주기 위해서는 많은 제약이 있었고, 마커를 적당한 수준으로 렌더링 하려면 클라이언트와 서버 간에 특별한 작업이 필요했습니다.

    어떤 전략을 펼쳤는지 소개하기에 앞서 미리 말씀드리지만, 저희 팀에서 취한 지도 관리 전략은 모든 프로젝트에 유효하지 않을 것입니다. 지도 위에 한번에 표현할 마커의 갯수가 수백 개 이하라면, 서버에 데이터가 과도하게 많은 것이 아니라면 오히려 이러한 전략이 사용자 경험을 해칠 수 있을 것입니다. (환경이 원활하다면 데이터를 가능한 많이 보여주는 것이 좋을테니깐요.)

    또, 이 글에서는 Google Maps API를 기준으로 설명하고 있지만, 지원하는 기능이 일부 다르더라도 대부분의 지도 API에서 사용이 가능한 전략일 것입니다. 참고로 개인적으로 사용 해본 여러 벤더 사의 지도 API들은 모두 이와 유사한 기능을 제공했습니다.

    좌표란 무엇일까?

    아마 어린 시절부터 우리나라에는 특별히 38선이라는 것이 존재한다는 사실을 교육받기에 좌표계라는 것이 있다는 사실은 누구나 알 것입니다. 하지만 당장 위도와 경도를 구분지으라고 하면 어떤 선이 위선이고 경선인지 헷갈리기에 찍어야 할 것입니다. 따라서 이 선이 어떤 선인지, 어떤 값을 얘기하려는 것인지 사진과 함께 간단히 설명하겠습니다.

    no offset

    사진을 보시면 아시겠지만 위도란, 남북의 위치를 나타내는 데 사용됩니다. 경도는 동서의 위치를 나타내는 데 사용됩니다. 대부분의 공식 문서가 영어로 작성되어있고, 코드에서도 이를 나타내는 것이 중요하기에 영문 표기법까지 소개를 하자면 위도는 Latitude, 경도는 Longitude로 표기합니다. 이유는 모르겠지만 제공되는 변수나 메서드 명으로 lat, lng라고 줄여서 표기하기도 합니다.

    no offset

    위도와 경도만 알면, 지구 위의 어떤 위치를 나타낼 수 있습니다.

    따라서, 어떤 마커를 어떤 위치에 찍을 것인지는 위도와 경도 값으로 결정할 수 있게 되겠죠?

    사용자가 어딜 보고 있을까?

    지도 api에서 제공해주는 메서드를 활용하면 사용자의 디바이스가 어느 위치를 보고 있는지 알 수 있습니다.

    let map = /* 어디선가 생성된 구글 맵 객체 */
    const center = map.getCenter();
    console.log(center.lng()); // 디바이스 중심의 longitude
    console.log(center.lat()); // 디바이스 중심의 latitude

    지도 객체로 부터 중심점을 알게되면 해당 디바이스의 중심의 좌표를 알아낼 수 있게 됩니다.

    no offset

    사용자의 디바이스는 얼마나 넓게 보고 있을까?

    지도 api에서 제공해주는 메서드를 활용하면 사용자의 디바이스가 어떤 영역을 보고 있는지도 알게 됩니다. 지도 api 마다 제공하는 스펙이 다르지만, 대부분은 어떤 식으로든 알려줍니다.

    google maps API에서는 디스플레이의 북동쪽 끝 점의 좌표와, 남서쪽 끝 점의 좌표를 제공해줍니다.

    const map = /* 어디선가 생성된 구글 맵 객체 */
    const bounds = map.getBounds();
    console.log(bounds.getNorthEast().lng(), bounds.getNorthEast().lat()); // 디바이스 1사분면 끝 점의 longitude와 latitude
    console.log(bounds.getSouthWest().lng(), bounds.getSouthWest().lat()); // 디바이스 3사분면 끝 점의 longitude와 latitude

    no offset

    편의상 좌표를 다음과 같이 정의해보겠습니다.

    • 중심 점 p0: (x0, y0)
    • 디바이스의 제 1사분면 끝점 p2: (x2, y2)
    • 디바이스의 제 3사분면 끝점 p1: (x1, y1)
    위 정의는 아래에서도 계속 설명 될 점과 좌표 입니다.

    이렇게 알아낸 값으로 사용자 디바이스의 영역을 알게 됐습니다.

    저희 카페인 팀에서는 이 값을 좀 더 효율적으로 다루기 위해 delta 개념을 도입했습니다.

    화면에서 보고 있는 영역을 확대/축소 하면 어떤 특징을 보일까?

    delta 설명을 앞서, 사용자의 디바이스 영역과 확대 수준에 따른 실제 좌표에 대해 알아보려고 합니다.

    사용자가 화면을 얼마나 넓게 보고 있는지를 쉽게 알기 위해서는 끝점들의 수치를 계산해줄 필요가 있었습니다.

    사진은 사용자가 디바이스를 통해 바라 보고 있는 중심 좌표와 그 끝 점을 의미합니다.

    no offset

    예를 들어 사용자가 지도를 많이 축소한 경우에는 중심 점 p0은 그대로지만 양 끝점 p1, p2의 위치가 점점 중심 점 p0으로 부터 멀어질 것입니다.

    반면에 사용자가 지도를 많이 확대한 경우에는 중심 점 p0은 그대로지만 양 끝점 p1, p2의 위치가 점점 중심점과 가까워질 것입니다.

    no offset

    양 사진 모두 중심 점 p0는 그대로지만, 디바이스의 확대 수준으로 인해 양 끝점인 p1과 p2가 달라진 모습을 보인 것입니다.

    즉, 이런 결론을 내릴 수 있습니다.

    1. 양 끝점 p1, p2가 중심 점 p0으로 부터 멀어질 수록 지도를 축소한 것이다.
    2. 양 끝점 p1, p2가 중심 점 p0으로 부터 가까워 수록 지도를 확대한 것이다.

    이 때 디바이스의 디스플레이가 위도 경도 상으로 얼마나 멀어져있는지를 수치화하면 편하게 다룰 수 있습니다.

    확대 수준을 수치화 할 수 없을까?

    사용자의 디스플레이의 중심 점 p0을 기준으로 하여 양 끝점 p1, p2이 얼마나 멀어져있는지에 따라 지도의 영역 뿐만 아니라 얼마나 많이 확대 되었는지 여부를 알게 됐습니다.

    그렇다면 이를 좀 더 효율적인 방법으로 나타내려면 어떤 전략을 취할 수 있을까요?

    사용자 디스플레이를 조금 더 자세히 살펴보겠습니다.

    no offset

    중학교 시절 배웠던 좌표 평면계를 떠올려보면 화면에서 얻을 수 있는 좌표들은 위와 같습니다. 여기에서 각 점의 수직/수평의 변화량인 delta를 알아보면 어떨까요?

    경도 델타 (longitudeDelta)

    p2와 p0의 경도 거리, 그리고 p1과 p0의 경도 거리는 같습니다.

    즉, x2 - x0 === x0 - x1 이라는 결론을 얻을 수 있습니다.

    이를 longitudeDelta로 정의하겠습니다.

    위도 델타 (latitudeDelta)

    p2와 p0의 위도 거리, 그리고 p1과 p0의 위도 거리는 같습니다.

    즉, y2 - y0 === y0 - y1 이라는 결론을 얻을 수 있습니다.

    이를 latitudeDelta로 정의하겠습니다.

    no offset

    코드로 알아보면 다음과 같습니다.

    const map = /* 어디선가 생성된 구글 맵 객체 */
    const bounds = map.getBounds();
    const longitudeDelta = (bounds.getNorthEast().lng() - bounds.getSouthWest().lng()) / 2; // 경도 변화량
    const latitudeDelta = (bounds.getNorthEast().lat() - bounds.getSouthWest().lat()) / 2; // 위도 변화량

    드디어 클라이언트에서 델타 값을 생성할 수 있게 되었습니다.

    그렇다면 왜 이렇게 굳이 델타 값을 생성한 것일까요?

    delta의 유용한 점 1: 원래 의도한 값을 복원하기 쉽다.

    서버의 입장에서는 중심 좌표와 델타 값만 알면 정확한 영역만큼 데이터를 호출할 수 있게 됩니다.

    예를 들어 클라이언트에서 서버로 다음과 같은 파라미터를 넘겨줬다고 가정해보겠습니다.

    {
    "longitude": 127,
    "latitude": 37,
    "longitudeDelta": 0.1,
    "longitudeDelta": 0.2,
    }

    그렇다면 서버에서는 다음과 같이 해석할 수 있게 됩니다.

    const maxLongitude = longitude + longitudeDelta;
    const minLongitude = longitude - longitudeDelta;
    const maxLatitude = latitude + latitudeDelta;
    const minLatitude = latitude - latitudeDelta;

    (javascript 기준으로 작성했습니다.)

    이렇게 알아낸 경계 값을 가지고 다음과 같은 sql문을 작성할 수 있게 될 것입니다.

    SELECT * FROM stations WHERE latitude >= :minLatitude AND latitude <= :maxLatitude AND longitude >= :minLongitude AND longitude <= :maxLongitude;

    no offset

    즉, 위 그림처럼, 원하는 영역만큼만 정확하게 데이터를 호출할 수 있게 됩니다.

    delta의 유용한 점 2: 델타가 무분별하게 커지는 것을 막기 쉽다.

    예를 들어 사용자가 지도를 축소하여 한반도를 디스플레이에 가득 채운다면 서버가 어떻게 될까요?

    이러한 행위를 막는 가장 쉬운 방법은 지도 api에서 지원하는 줌 레벨을 제한 하는 것입니다. 후술하겠지만 줌 레벨은 디스플레이의 해상도를 고려하지 못합니다.

    따라서 근본적으로 델타가 일정 값 이상 요청되지 못하도록, 혹은 연산되지 못하도록 막게 할 수 있습니다.

    물론 델타가 없더라도 델타 값을 추정하여 연산할 수 있겠지만, 이를 수치화 해서 관리한다면 클라이언트와 서버 모두 지도를 손쉽게 통제하는 것이 가능하게 됩니다.

    예를 들어 다음과 같이 델타 값을 고정하여 요청 영역을 제한할(요청을 보내지 않거나 고정된 사이즈로만 요청을 보낼) 수 있습니다.

    {
    longitude,
    latitude,
    longitudeDelta: longitudeDelta < 0.008 ? longitudeDelta : 0.008,
    latitudeDelta: latitudeDelta < 0.004 ? latitudeDelta : 0.004,
    }

    특정 수치를 넘기지 못하게 처리할 때 눈에 보이는 변수로 취급하기 쉽습니다. (즉, 매번 계산하지 않아도 됩니다.)

    디바이스 크기 관련 문제도 있습니다.

    분명히 같은 줌 레벨이지만, 디바이스의 크기나 해상도에 따라 지도가 보여지는 정도가 다릅니다.

    no offset

    위 사진은 구글에서 제공하는 zoom 레벨을 동일하게 맞춘 후, 여러 디바이스에서 호출한 것입니다.

    줌 레벨을 통해서 요청을 제한하다보면 여러 해상도를 제어하기 어렵습니다.

    no offset

    실제로 카페인 팀에서는 고해상도 모니터를 대응하기 위해 델타 값이 너무 크게 되면 요청의 제한을 하고 있습니다. 사진에서 보시다시피 고해상도 모니터의 경우, 너무 넓은 범위를 요청한다 싶으면 중심점으로 부터 일정 거리만 보여주도록 하고 있습니다.

    (참고로 줌 레벨에 따른 요청도 덤으로 제한하고 있어서 멀리서 호출하는 행위도 금지하고 있습니다.)

    delta의 유용한 점 3: 적당한 범위를 정해주기 편하다

    위 예제에서는 정확한 범위만큼 요청하는 것을 예제로 하지만, 프로젝트에 따라서 조금 더 넓은 영역을 호출하고 싶을 때가 있을 것입니다.

    no offset

    예를 들어 현재 사용자의 디바이스 크기보다 살짝 큰 범위의 데이터를 미리 로드해 놓으면 사용자가 좁은 움직임을 보일 때 불필요한 재 렌더링을 줄여서 더 빠른 렌더링이 가능하게 됩니다.

    사실 이 기법은 프로젝트마다 다르겠지만, 카페인 팀에서는 한번 불러온 마커를 매번 해제 하지 않고 이전 요청 데이터와 다음 요청 데이터를 비교하여 달라진 마커만을 정확하게 탈부착하는 작업을 진행하고 있습니다.

    이런 기법을 활용하면 사용자가 좁은 범위에서 움직임을 보였을 때, 기존에 불러온 마커를 메모리에서 탈락시키지 않으므로 사용자 경험을 개선할 수도 있을 것입니다.

    마커를 상태에 연동하여 정확하게 메모리에서 탈부착 시키는 전략에 대한 글은 이후에 작성할 예정입니다.

    긴 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git a/page/18.html b/page/18.html index e873faf..21a180f 100644 --- a/page/18.html +++ b/page/18.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -17,7 +17,7 @@ 기존 dev 서버는 총 4개의 서버를 배포하고 있었고 배포하는 서버는 다음과 같습니다. prod-BE, prod-FE, dev-BE, dev-FE

    그리고, 기존 dev 서버에서는 환경을 분리해주기 위해서 Nginx를 통해서 포트 포워딩은 다음과 같이 해주었습니다.

    • prod-BE = 8080
    • prod-FE = 3031
    • dev-BE = 8081
    • dev-FE = 3031

    카페인 팀에서는 dev, prod 환경이 분리되지 않아서 인스턴스의 사용량이 높았고, 이에 따라 추가적인 EC2 인스턴스가 필요했습니다.


    문제 해결

    다행히도 카페인 팀에서 추가적인 EC2 인스턴스를 받았고, 저희는 배포 환경을 분리할 수 있었습니다.

    dev-prod-server

    이와 같이 기존 dev 서버 한 개가 infra 서버와 연결되어 있었는데, 두 갈래로 나뉜 것을 확인하실 수 있습니다.

    먼저 배포는 다음과 같이 진행됩니다.

    release branch에 push가 일어나면 dev서버에 배포 작업이 이뤄집니다. prod branch에 push가 일어나면 prod서버에 배포 작업이 이뤄집니다.

    또한 기존 dev 서버에서 4개의 포트포워딩 또한 굳이 그럴 필요가 없어졌습니다. 새로운 서버가 추가됨에 따라 dev, prod 서버 각각 Nginx에서 포트포워딩을 동일하게 FE:3000, BE:8080 으로 변경하였습니다.

    이렇게 카페인 팀에서는 dev, prod 환경을 분리했습니다.

    감사합니다!

    - + \ No newline at end of file diff --git a/page/19.html b/page/19.html index e41d39a..6656d46 100644 --- a/page/19.html +++ b/page/19.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -22,7 +22,7 @@ 하지만 Storybook을 이용하면 특정 컴포넌트를 Storybook 위에 올려놓고 테스트를 할 수 있어 빠르게 작업이 가능합니다. 인터렉션이나 웹접근성을 확인해주는 플러그인도 존재하여 프론트엔드 개발에서 굉장히 중요한 역할로 부상했습니다.

    저희 팀은 이외에 Cypress를 사용하는 것도 고려하였으나, 지도와 결합된 애플리케이션을 테스트하기에 다소 어려움이 있어 위 라이브러리들을 개발에 활용했습니다.

    저희는 위 테스팅 라이브러리들을 원활히 활용하기 위해 테스트 자동화를 구축했습니다.

    Jest와 React Testing Library 테스트 자동화

    name: frontend-test

    on:
    pull_request:
    branches:
    - main
    - develop
    paths:
    - frontend/**
    - .github/**

    permissions:
    contents: read

    jobs:
    test:
    name: test-when-pull-request
    runs-on: ubuntu-latest
    environment: test
    defaults:
    run:
    working-directory: ./frontend
    steps:
    - name: Checkout PR
    uses: actions/checkout@v2
    - name: Install dependencies
    run: npm install
    - name: Test
    run: npm run test

    이벤트 트리거 설정

    pull_request 이벤트가 발생하였을 때, 해당 이벤트가 main 브랜치와 develop 브랜치에서만 동작합니다.

    변경 사항 경로 제한

    테스트를 실행할 때는 frontend 디렉토리와 .github 디렉토리 내의 파일들을 고려하도록 했습니다. 백엔드와의 환경 분리를 위해 이러한 접근 제한을 했습니다.

    권한 설정

    permissions은 읽기 권한만 설정되어 있어 코드나 파일을 변경을 방지합니다.

    작업(Job) 설정

    test라는 이름의 작업을 정의하였고, 이 작업에서는 Ubuntu 환경에서 테스트를 실행합니다. test라는 이름의 환경 변수를 사용합니다. 테스트는 (카페인 팀 레포지토리의) frontend 디렉토리에서 작업하도록 하였습니다.

    스텝(Step) 설정

    코드를 체크아웃하고, 의존성을 설치하며, 테스트를 실행하는 세 가지 단계로 구성되어 있습니다.

    이러한 설정을 통해 PR에 코드가 올라올 때 자동으로 프론트엔드 테스트가 실행됩니다.

    이러한 테스트 자동화 전략은 프론트엔드 애플리케이션을 안정적이게 개발하고 유지할 수 있도록 도와줍니다.

    Storybook의 빌드 자동화

    name: storybook-deploy

    on:
    pull_request:
    branches:
    - develop
    paths:
    - frontend/**
    - .github/**

    jobs:
    build:
    runs-on: ubuntu-22.04
    defaults:
    run:
    working-directory: ./frontend
    steps:
    - name: Setup Repository
    uses: actions/checkout@v3

    - name: Set up Node
    uses: actions/setup-node@v3
    with:
    node-version: 18.16.0

    - name: Install dependencies
    run: npm install

    - name: Cache node_modules
    id: cache
    uses: actions/cache@v3
    with:
    path: '**/node_modules'
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
    ${{ runner.os }}-node-

    - name: storybook build
    run: npm run build-storybook

    - name: Upload storybook build files to temp artifact
    uses: actions/upload-artifact@v3
    with:
    name: Storybook
    path: frontend/storybook-static
    deploy:
    needs: build
    runs-on: self-hosted
    steps:
    - name: Remove previous version app
    working-directory: .
    run: rm -rf dist

    - name: Download the built file to AWS
    uses: actions/download-artifact@v3
    with:
    name: Storybook
    path: frontend/dev/dist

    - name: Move folder
    working-directory: frontend/dev/
    run: |
    rm -rf /home/ubuntu/dist/*
    cp -r ./dist /home/ubuntu

    - name: comment PR
    uses: thollander/actions-comment-pull-request@v1
    env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    with:
    message: '🚀storybook: https://storybook.carffe.in/'

    비슷한 코드이지만, 매번 PR이 열릴 때 마다 스토리북이 자동으로 빌드 및 배포됩니다. 배포가 완료되면 배포된 URL을 알려 코드 리뷰할 때 참고할 수 있도록 돕습니다.

    이상 카페인 팀에서 사용하고 있는 테스팅 라이브러리와 테스트 자동화 방법을 알아봤습니다.

    - + \ No newline at end of file diff --git a/page/2.html b/page/2.html index 9308714..a94024b 100644 --- a/page/2.html +++ b/page/2.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -13,7 +13,7 @@

    · 약 3분

    사용자 피드백

    image

    저희 서비스를 배포하고 사용자에게 피드백을 받았는데, 축소했을 때가 많이 불편하다는 피드백이 대부분이였습니다.

    이유는 아래 화면과 같습니다

    asis

    이런 서비스를 본 적도 없고, 이런 서비스를 사용하고 싶지도 않을 것 입니다. 해당 부분의 문제를 알고 있었지만 어떻게 표현해주는 것이 좋고, 구현할 수 있는 방법이 떠오르지 않아 6차 데모데이까지 미루게 되었습니다.

    열심히 팀 회의를 한 결과 화면에 보이는 사이즈만큼 일정 범위로 나눠 충전소 개수를 보여주는 클러스터링 기능을 추가하기로 정했습니다.

    클러스터 기능 추가

    해당 기능을 간단하게 설명드리면 화면의 일정 범위로 나눠 충전소의 개수를 보여주도록 서버에서 계산하여 클라이언트로 전달하도록 했습니다. 하지만 전달한 클러스터링 마커들의 위치가 아래와 같이 예쁘게 보이지 않았습니다.

    image (5)

    화면의 크기에 비해 마커가 몇개 없는 것을 볼 수 있습니다. 이렇게 된다면 사용자는 그렇기에 클라이언트에 해당 기능을 담당한 가브리엘, 센트가 좀 더 유연하게 마커를 보여주는 것이 UX 관점에서 좋다고 얘기하여

    서버 API와 로직을 변경하여 동적으로 화면의 충전소를 클러스터하도록 변경하였습니다. 그렇게 하여 아래와 같은 화면을 제공하도록 하였습니다.

    final

    이상 협업 일화 였습니다.

    - + \ No newline at end of file diff --git a/page/20.html b/page/20.html index 8c34efe..3ff760f 100644 --- a/page/20.html +++ b/page/20.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -21,7 +21,7 @@ 거기에 file을 만듭니다. 파일 이름이 중요한데요 V1__init.sql 이러한 방식으로 V{version 숫자}__{어떠한 파일인지에 대한 이름}.sql 언더스코어 2개는 필수로 작성해야합니다.

    create table member(
    id bigint auto_increment primary key,
    name varchar(255) null,
    );

    이렇게 V1__init.sql에 대한 파일을 작성했습니다. 이제는 email을 추가한다는 요구사항을 반영해보겠습니다.

    ALTER TABLE member
    ADD COLUMN email varchar(255);

    이렇게 새로운 파일을 만들어서 해당 스크립트를 작성했습니다. 파일명이 중요한데요, 이전 파일의 숫자보다 +1 이 되는 숫자를 V 뒤에 붙입니다.

    따라서 이번 파일은 V2__add_column_email.sql 이라고 만들었습니다.

    그럼 이제 또 시간이 지나 회원이 많아졌습니다. 하지만 email이 없는 사용자도 많습니다. 이 상황에서 email을 not null로 변경해야한다는 요구사항이 생겼습니다.

    그러면 아래와 같이 반영할 수 있습니다.

    ALTER TABLE member
    MODIFY email VARCHAR(20) NOT NULL default 'default'

    이렇게 V3__add_constraints.sql 파일을 만들었습니다. 그러면 null이 있던 row들은 email이 default가 되고 not null 제약조건이 활성화 된 것을 볼 수 있습니다.

    그러면 주어진 요구사항은 모두 만족할 수 있습니다. 거기에다 v1, v2, v3 가 나뉘어져있어서 어느 커밋부터 해당 sql이 추가되었는지도 확인할 수 있습니다.

    그리고 ddl-auto update를 사용하면 반영되지 않았던 제약조건의 추가도 확인할 수 있습니다. 그러면 ddl-auto의 속성을 validate로 변경하여, db schema와 entity의 필드가 다르면 어플리케이션이 실행되지 않도록 해서 좀 더 안전한 개발을 할 수 있습니다.

    결론

    flyway는 roll back을 하는 것이 유료라서, production 서버에서 혹은 롤백을 해야하는 일이 있는 서버에서는 사용하는 것이 좋지 않지만, 이와 같이 데이터를 drop 할 수 없는 상황이라면, 사용하지 않을 이유가 없어보이는 좋은 도구입니다.

    짧은 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git a/page/21.html b/page/21.html index d46e633..d4ec0a5 100644 --- a/page/21.html +++ b/page/21.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -31,7 +31,7 @@ 하지만 직접 확인해보기 전까지는 확신할 수 없으니 간단히 Runtime 클래스에서 제공해주는 totalMemory(), freeMemory() 메서드를 통해 알아보겠습니다.

        @Test
    void 페이징을_사용한_조회() {
    List<Station> stations = stationRepository.findAllByOrder(Pageable.ofSize(1000));

    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();
    System.out.println("paging 사용 중인 메모리: " + ((total - free) / 1024 / 1024) + "MB");
    }

    @Test
    void 페이징을_사용하지_않고_조회() {
    List<Station> stations = stationRepository.findAllFetch();

    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();

    System.out.println("findAll() 사용 중인 메모리: " + ((total - free) / 1024 / 1024) + "MB");
    }

    findAll paging 확연히 차이가 나는 것을 확인할 수 있습니다.

    물론 테스트코드에서는 23만건의 API 요청은 같은 조건이니 배제하고 확인했습니다.

    이로써 하나의 문제가 또 해결된 것 같습니다.

    아직 배우는 단계라 혹시 틀린 점이 있다면 지적 부탁드리겠습니다.

    Reference

    - + \ No newline at end of file diff --git a/page/22.html b/page/22.html index 28d68a9..59363e5 100644 --- a/page/22.html +++ b/page/22.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -26,7 +26,7 @@ 트랜잭션을 오래 가지고 있으면 Lock을 가지고 있는 시간이 오래걸립니다. 그래서 트랜잭션을 작게 분리할 수 있습니다. 페이징을 통해 트랜잭션을 작게 분리하다보면 쿼리가 여러번 나가 성능상 문제가 생길 수 있을 것 같습니다.
  • INSERT ~~ ON DUPLICATE KEY UPDATE ~~ 사용하지 않기 해당 sql이 아닌 INSERT IGNORE을 사용하여 추가된 정보만 넣고, update는 다른 작업으로 분리하기
  • 이런 방법들을 사용하면 될 것 같았습니다. 그 중 저는 현재는 간단하게 2번째 방법이 제일 나을 것 같다는 생각에 쿼리를 수정했습니다.

    그리고 문제를 해결했습니다. 해당 문제가 발생하게 되어 좀 더 재밌는 것들을 고민하고 공부할 수 있는 저희 팀에게 감사하고 모르는 키워드를 많이 알려준 누누에게 감사합니다.

    아직 배우는 단계라 정확한 정보가 아닐 수 있습니다. 부족한 부분에 대해 많은 지적 부탁드립니다.

    - + \ No newline at end of file diff --git a/page/23.html b/page/23.html index 55bc6b8..591cbc2 100644 --- a/page/23.html +++ b/page/23.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -18,7 +18,7 @@ 평균적으로 0.63초가 나왔습니다. 약 25 ~ 30%의 조회 속도가 개선되었습니다.

    아직 이 부분은 개선이 더 필요해보입니다.

    그래도 개선이 됐고, 삽입과 갱신에는 큰 지장이 없어서 일단 이정도로 마무리 하고, 추후에 개선을 해보도록 하겠습니다.

    이미지 추가적으로 충전기 조회는 굉장히 빨라졌습니다!

    배우는 단계이다보니 미숙하고 틀린 부분이 있을 수 있습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git a/page/24.html b/page/24.html index eb82de0..4c7191e 100644 --- a/page/24.html +++ b/page/24.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 2분
    야미

    왜 styled-components인가?


    여러 CSS-in-JS 중 styled-components를 선택한 이유는 다음과 같다.

    1. 컴포넌트 안에 관련 CSS를 작성할 수 있어 컴포넌트별 디자인 코드 확인 및 수정이 용이하다.

    2. 혹자는 코드 가독성이 안 좋아진다고도 하지만, 개인적으로는 태그를 더 시맨틱 하게 작성할 수 있어서 좋다고 느꼈다.

    3. 팀원들 모두 styled-components가 익숙하다.

    4. 지금까지 사용하면서 불편한 점을 못 느꼈다.


    styled-components와 emotion은 기능도, 작성법도 상당히 유사하다.

    그래서 이번에는 styled-components 대신 emotion을 써볼까도 생각했었다.

    하지만 emotion에서만 사용 가능하던 *CSS Props라는 편리한 기능을

    styled-components(v5.2.0 이상)에서 쓸 수 있게 되기도 했고,

    '새로운 기술 공부를 해보면 좋을 것 같다'는 이유를 제외하고는

    딱히 emotion을 사용할 필요성을 못 느껴 styled-components를 채택했다.

    // *CSS Props 예시

    const buttonStyle = css`
    font-size: 18px;
    color: white;
    background: black;
    `;

    const ClickButton = styled.button<{ css: CSSProp }>`
    width: 100px;

    ${({ css }) => css}
    `;

    <ClickButton css={buttonStyle}>Click me!</ClickButton>;
    - + \ No newline at end of file diff --git a/page/25.html b/page/25.html index be7b523..15e75fb 100644 --- a/page/25.html +++ b/page/25.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 9분
    가브리엘

    안녕하세요? 카페인 팀 FE에서 상태관리 라이브러리를 어떻게 해야할 지 고민 끝에 서드파티 라이브러리가 필요하게 되어 글을 작성하게됐습니다.

    서버 상태와 클라이언트 상태의 구분

    서버상태와 UI상태를 이해하는 것은 굉장히 중요했습니다. 데이터를 송수신하는 작업과 상태를 관리하는 작업은 유기적으로 동작해야했습니다. 기존에는 상태와 데이터 송수신 과정을 분리해서 생각했다면, 현대의 react 프로젝트들은 서버와 동기화를 해야할 상태그렇지 않은 상태로 분리해서 생각해야 합니다.

    React에서 어떤 데이터를 상태로 다뤄야 하는가에 대해서는 여러 의견이 나올 수 있다고 생각하지만 상태가 특성을 가지고 있는가에 대해서는 대부분 특성이 있다고 동의할 것입니다. 이 글에서는 React의 상태란 무엇인가?에 대해서 다루지 않고 React의 상태의 특성에 대해서만 언급을 하려고 합니다.

    상태의 특성으로는 크게 두 가지가 있습니다.

    클라이언트 상태

    클라이언트 상태는 컴포넌트들 간에 어떤 값을 공유해야하면서 오로지 React DOM 내부에서만 CRUD가 일어나는 상태를 의미합니다. 이 상태들은 React DOM 외부 세계와 크게 관련이 없으며 동기적으로 반영됩니다. 대표적으로는 UI를 조작하는 상태들이 될 것입니다. 클라이언트 상태들은 대부분 장기적으로 유지될 필요가 없기에 화면을 벗어나거나 세션이 끊기는 경우 사라져도 괜찮은 경우가 많습니다.

    서버 상태

    서버 상태는 React의 바깥 세상(서버)에 존재하는 데이터가 React의 상태 관리와 비동기적으로 동기화 된 것을 의미합니다. 어떤 상태가 외부에서 관리되는 데이터와 반드시 연동되어야 한다면 이는 곧 서버 상태임을 의미합니다. React의 상태를 CRUD 하는 것 뿐만 아닌, 서버에서도 항상 같은 일이 일어나야 합니다. 서버 상태는 장기적으로 유지되어야 하며, 세션에서 벗어나더라도 서버로 부터 복구를 해야 합니다.

    기존의 상태 관리 라이브러리들은 리액트의 전역에서 상태를 조작하는 것에 특화되어있고, 비동기적인 상태 관리도 지원하여 서버와의 통신이 가능합니다. 하지만 대부분의 라이브러리들은 클라이언트 상태를 조작하는 것에 초점이 맞춰져있습니다.

    더군다나 클라이언트 상태와 서버 상태가 하는 일이 명확하게 다른 상황에서 이 둘을 한 곳에서 관리하는 것 보다는 완벽하게 분리하는 것이 더 나을 것입니다. 따라서 서버 상태를 관리하는 것에 중점을 둔 라이브러리들이 등장하였습니다. 대표적인 라이브러리로는 RTK Query, Tanstack Query, SWR 등이 있습니다.

    왜 Tanstack Query였나?

    vs RTK Query

    RTK Query는 RTK를 반드시 사용해야 하는 것은 아니지만 RTK를 타겟으로 나온 서버 상태 관리 라이브러리입니다. 카페인 팀에서는 클라이언트 상태를 관리하기 위해 라이브러리를 사용하지 않습니다. 더욱이 Redux의 복잡한 코드 구성과 방대한 보일러 플레이트는 매력적이지 않았습니다. tanstack query에서는 무한 데이터 페칭을 지원하기 위해 Infinite Queries가 있지만 RTK Query는 그렇지 않았습니다.

    vs SWR

    SWR도 하나의 좋은 선택지였지만, 전역 상태 관리 라이브러리들이 범용적으로 지원하는 셀렉터 기능을 지원하지 않았습니다. 또, 가비지 컬렉터의 부재도 아쉬웠습니다. 재요청을 하기 위한 stale time 설정이나 쿼리 취소 기능이 없는 점도 매력적이지 않았습니다.

    카페인 팀에서 하려는 일은요…

    저희 카페인 팀의 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 로 지도 기반의 프로젝트입니다. 서버 상태를 적극적으로 다뤄야 하는 상황에서 Tanstack Query를 서버 상태 관리 라이브러리로 선정하게 됐습니다.

    메인 기능 중 Tanstack Query가 핵심으로 사용될 것 같은 기능은 다음과 같습니다.

    • 지도에서 충전소 조회
      • 현재 접속한 클라이언트에 렌더링 된 지도 화면(디스플레이)의 크기에 따른 GPS좌표를 알아내어 서버로 부터 충전소 정보를 수신 받습니다. 즉, 화면이 이동하게 되면 사용자가 바라보고 있는 영역이 변하므로 새로운 요청을 보내게 됩니다.
      • 서버에서 수신한 충전소 정보는 실시간 사용 현황도 반영되어있으므로 주기적인 업데이트도 필요합니다.
      • 빈번한 데이터의 변화가 필요하며 그만큼 통신 실패 등 에러가 발생할 가능성도 많아지게 됩니다.
      • 사용자의 빠른 지도 이동이 발생하는 경우를 대응할 수 있어야 합니다.
    • 전국 충전소 검색기
      • 원하는 충전소 검색을 하는 기능을 지원합니다. 전국 단위로 검색 결과를 수신하는 기능입니다.
      • 네이버와 구글 검색창 처럼 사용자가 input 창에 검색어를 입력할 때 마다 검색 결과가 동적으로 표시되어야 합니다.
      • 빈번한 데이터의 변화가 필요하고, 사용자의 빠른 타이핑으로 인해 잦은 검색이 발생하는 경우를 대응할 수 있어야 합니다.
      • 이를 위해 데이터를 캐싱할 필요도 있다고 생각합니다.

    프로젝트에서 클라이언트와 서버와의 통신이 어쩌다 한번 일어난다면 굳이 라이브러리가 필요가 없겠지만, 서버의 데이터 전적으로 의존해야 하는 저희 프로젝트 특성상 Tanstack Query의 여러 기능이 생산성에 많은 도움이 될 것으로 기대합니다.

    - + \ No newline at end of file diff --git a/page/26.html b/page/26.html index bcd5494..cdfcf9d 100644 --- a/page/26.html +++ b/page/26.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -24,7 +24,7 @@ Map<String, Object>로 지정해준 이유는, 플랫폼마다 반환되는 JSON 타입이 다르기 때문에 그런 부분에 대해 중복을 제거하기 위해 이러한 형태로 만들었습니다.

    그리고 아까 yml에 작성했던 정보들을 가져와야합니다. @Value 어노테이션으로도 가져올 수 있습니다.

            @Value("oauth.provider.google.id")
    private String id;
    @Value("oauth.provider.google.secret")
    private String secret;

    ...

    하지만 이렇게 계속 binding을 해줘야한다는 점이 아주 귀찮고 보기도 안좋습니다.

    build.gradle
    annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"

    하지만 위의 의존성을 추가해준다면 아주 편하게 property를 가져올 수 있습니다.

    OAuthProviderProperties.java
    @Component
    @ConfigurationProperties(prefix = "oauth2")
    public class OAuthProviderProperties {
    // prefix oauth2 기준으로 알아서 google이 이름인 Provider Enum을 찾아서 Key로 바인딩
    private final Map<Provider, OAuthProviderProperty> provider = new EnumMap<>(Provider.class);

    public OAuthProviderProperty getProviderProperties(Provider provider) {
    return this.provider.get(provider);
    }

    @Getter
    @Setter
    public static class OAuthProviderProperty {
    // 그리고 provider 하위 정보들은 아래의 필드에 바인딩
    private String id;
    private String secret;
    private String redirectUrl;
    private String tokenUrl;
    private String infoUrl;
    }
    }

    이렇게 되면 구조적인 준비는 끝났습니다.

    이제는 해당 플랫폼에 정보를 요청하는 작업만 하면 됩니다. 그럼 아까 말씀드렸던 순서로 요청을 해보겠습니다.

    RestTemplateOAuthRequester.java
    public class RestTemplateOAuthRequester implements OAuthRequester {

    @Override
    public OAuthMember login(OAuthLoginRequest request) {
    // frontend에서 받아온 로그인 platform
    Provider provider = Provider.from(request.provider());
    // 해당 Platform에 맞는 정보 찾음
    OAuthProviderProperty property = oAuthProviderProperties.getProviderProperties(provider);
    // frontend에서 받아온 code와 등록해놓은 property로 Access Token 요청
    OAuthTokenResponse token = requestAccessToken(property, requet.getCode());
    // 받아온 Token으로 해당 Resource Owner의 정보 요청
    Map<String, Object> userAttributes = getUserAttributes(property, token);
    return provider.getOAuthProvider(userAttributes);
    }

    private OAuthTokenResponse requestAccessToken(OAuthProviderProperty property, String code) {
    HttpHeaders headers = new HttpHeaders();
    headers.setBasicAuth(property.getId(), property.getSecret());
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers);
    URI tokenUri = getTokenUri(property, code);
    return restTemplate.postForEntity(tokenUri, request, OAuthTokenResponse.class).getBody();
    }

    private URI getTokenUri(OAuthProviderProperty property, String code) {
    return UriComponentsBuilder.fromUriString(property.getTokenUrl())
    .queryParam(CODE, URLDecoder.decode(code, StandardCharsets.UTF_8))
    .queryParam(GRANT_TYPE, AUTHORIZATION_CODE)
    .queryParam(REDIRECT_URI, property.getRedirectUrl())
    .build()
    .toUri();
    }

    private Map<String, Object> getUserAttributes(OAuthProviderProperty property, OAuthTokenResponse tokenResponse) {
    HttpHeaders headers = new HttpHeaders();
    headers.setBearerAuth(tokenResponse.accessToken());
    headers.setContentType(MediaType.APPLICATION_JSON);
    URI uri = URI.create(property.getInfoUrl());
    RequestEntity<?> requestEntity = new RequestEntity<>(headers, HttpMethod.GET, uri);
    ResponseEntity<Map<String, Object>> responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<>() {
    });
    return responseEntity.getBody();
    }
    }

    이렇게만 한다면 그 어려워 보이던 OAuth 인증도 간단하게 해결할 수 있습니다. (물론 제 코드가 정답이 아닙니다)

    Reference

    https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-16

    https://developers.google.com/identity/protocols/oauth2?hl=ko

    - + \ No newline at end of file diff --git a/page/27.html b/page/27.html index f4e0a65..554f7b4 100644 --- a/page/27.html +++ b/page/27.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 11분
    누누

    어떤 문제가 있었나요?

    우아한테크코스에서 private 서브넷에 db 인스턴스를 두고, 보안을 위해 외부에서 접속을 차단하려고 했습니다.

    이 과정에서 총 2가지의 문제점이 있었습니다.

    1. private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었습니다.
    2. public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안되었습니다.

    이 부분을 어떻게 해결했는지 알아보도록 하겠습니다.

    아래의 모든 설명은 AWS 를 기준으로 합니다.

    private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었다.

    해결 방법

    public ip 자동할당을 해주지 않아서, 인터넷에 연결이 안 되었습니다.

    이를 해결하기 위해 public ip 자동할당을 해주었습니다.

    왜 public ip를 할당했더니 문제가 해결되었을까요?

    private 서브넷이란?

    정말 간단하게 설명했을 때

    private 서브넷은 인터넷에 연결되지 않은 서브넷입니다.

    조금 자세하게 들어가 보도록 하겠습니다

    private 서브넷은 인터넷 게이트웨이가 연결되지 않은 서브넷입니다.

    aws 공식문서에서 사진을 통해 보면 아래와 같이 되어있습니다

    private subnet

    public 서브넷에만 인터넷 게이트웨이가 연결되어 있고, private 서브넷에는 인터넷 게이트웨이가 연결되어있지 않습니다.

    private 서브넷에 인터넷 게이트웨이가 연결되어 있지 않다고 했을 때, 기본적으로 인터넷에 접속이 안됩니다.

    mysql을 설치할 때도, 인터넷에 접속을 해야하는데, 인터넷에 접속이 안되니 설치가 안되는 것입니다.

    어? 인터넷 자체가 접근이 안되면 어떻게 설치하나요?

    정말 원시적으로 해결하기 위해서는 public 서브넷에 인스턴스를 하나 더 만들어서, mysql 을 압축해서 scp를 통해 private 서브넷에 있는 인스턴스에 전송하고, 압축을 풀어서 설치하는 방법이 있습니다.

    하지만 이 방법은 너무 원시적이고, 비효율적입니다.

    그래서 인터넷으로 요청을 보낼 수 있도록 만드는 과정이 필요합니다.

    인터넷으로 요청을 보낼 수 있도록 만드는 과정

    인터넷으로 요청을 보낼 수 있도록 만드는 과정은 크게 2가지가 있습니다.

    private 서브넷을 public 서브넷으로 바꾸기

    보안을 위해서 private 서브넷에 두려고 했던 것을 public 서브넷으로 바꾼다는 부분은 매우 위험합니다.

    그래서 이 방법은 보통 사용하지 않습니다.

    NAT 인스턴스(Gateway) 만들기

    NAT 인스턴스는 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들어주는 인스턴스입니다.

    인터넷에 접속을 하기 위해서는 public ip 가 필요합니다.

    따라서 NAT 인스턴스, NAT 게이트웨이는 public 서브넷에 존재해야 합니다.

    어? NAT 인스턴스를 통해서 바로 통신이 가능하면 왜 private 서브넷이 필요한가요? 그냥 다 public 서브넷에 두면 되지 않나요?

    NAT 인스턴스, NAT Gateway는 내부에서 출발한 트래픽만 통과할 수 있도록 설정이 되어있습니다.

    예를 들면 private 서브넷에 인스턴스에 접속해서 직접 mysql download 요청을 했을 때만 허용이 됩니다.

    외부에서 바로 private 인스턴스로 접근할 수는 없습니다.

    NAT 인스턴스만 설정을 하면 바로 연결이 되나요?

    public ip도 자동 할당을 해줘야 합니다

    public ip 가 필요한 이유

    NAT 인스턴스를 통해서 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들었는데, 왜 public ip 가 필요할까요?

    외부 인터넷과 통신을 할 때 public ip 가 필요합니다.

    NAT 인스턴스 혹은 NAT 게이트웨이가 인터넷과 통신할 때, NAT 인스턴스의 public ip + private ip를 통해서 통신을 하지 않습니다.

    내부 인스턴스의 public ip 를 통해서 통신을 하게 되어있습니다.

    따라서 NAT 인스턴스와 내부 인스턴스 모두 public ip 가 필요합니다.

    이 과정을 통해서 1번 문제를 해결할 수 있었습니다.

    이제 2번째 문제를 해결해 보도록 하겠습니다.

    public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안 되는 문제

    public 서브넷에 있는 서버가 private 서브넷에 있는 서버에 접속을 하려고 했는데, 접속이 안 되는 문제가 있었습니다.

    해결 방법

    해결 방법에는 2가지 과정이 있습니다.

    public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해 주기

    기본적으로 public 서브넷에 있는 인스턴스의 보안 그룹에는 private 서브넷에 있는 인스턴스의 보안 그룹이 추가되어있지 않습니다.

    따라서 public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.

    private ip를 통해서 접속하기

    public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속할 때, public ip 를 통해서 접속을 하면 안 됩니다.

    public ip를 통해서 접속하는 과정을 자세하게 알아보겠습니다.

    1. public 서브넷에 있는 인스턴스가 public ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
    2. 라우팅 테이블에서 public ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
    3. 라우터를 통해서 외부 인터넷으로 나가게 됩니다.
    4. 트래픽이 NAT 인스턴스에 도착합니다.
    5. NAT 인스턴스는 내부에서 출발한 트래픽이 아니기 때문에, 트래픽을 거부합니다.

    이 과정이 일어나기에, public ip 를 통해서 접속을 하면 안 됩니다.

    private ip를 통해서 접근하면 어떻게 되는지 알아보겠습니다

    1. public 서브넷에 있는 인스턴스가 private ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
    2. 라우팅 테이블에서 private ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
    3. 라우터를 거쳐서 private 서브넷의 라우터로 이동합니다.
    4. private 서브넷의 라우터는 private 서브넷에 있는 인스턴스에게 트래픽을 전달합니다.
    5. private 서브넷에 있는 인스턴스는 트래픽을 받아서 처리합니다.

    이 과정을 통해서 2번 문제를 해결할 수 있었습니다.

    요약

    1. private 서브넷에 있는 인스턴스가 인터넷에 접속을 하려면 NAT 인스턴스 혹은 NAT 게이트웨이가 필요합니다.
    2. private 서브넷에 있는 인스턴스도 public ip 가 필요합니다.
    3. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 하려면 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.
    4. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 할 때, private ip 를 통해서 접속을 해야 합니다.
    - + \ No newline at end of file diff --git a/page/28.html b/page/28.html index 339bd2c..aac14ea 100644 --- a/page/28.html +++ b/page/28.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -20,7 +20,7 @@ 위에 사진과 같이 설정을 해주시면 됩니다.

    그리고 이를 yml에서 사용하기 위해선 secrets.Key이름으로 사용해주시면 됩니다.


    이제 마지막으로 Dockerfile을 만들어줍니다.

    저희는 /backend/ 경로에 만들어주었습니다.

    FROM amazoncorretto:17-alpine-jdk
    ARG JAR_FILE=./backend/build/libs/carffeine-0.0.1-SNAPSHOT.jar
    COPY ${JAR_FILE} app.jar
    ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-jar","/app.jar"]

    저희는 위처럼 절대 경로를 기준으로 JAR_FILE 위치를 지정하고, profiles는 dev로 설정해서 만들어주었습니다.


    3. 배포하기

    트리거를 작동시켜서 저희가 yml 파일에서 지정해준 것들이 잘 작동하는지 확인합니다.

    jobSuccess 위에 사진처럼 모든 Job이 성공적으로 통과하는 것을 보실 수 있습니다.

    dockerPs 이렇게 인프라 서버에서 배포 서버로 들어가서 성공적으로 서버를 도커로 띄운 것을 보실 수 있습니다.

    EC2 배포 서버에서 docker ps를 입력했을 때에도 잘 실행이 되네요!


    CD 배포 과정 요약

    지속적 배포 과정을 요약 하자면 다음과 같습니다.

    1. Self Hosted Runner를 EC2 인프라 서버에 등록해준다.
    2. yml 파일과 Dockerfile을 만들어준다.
    3. 트리거를 작동시켜서 Github Actions의 태스크가 모두 잘 되는지 확인한다.
    4. 잘 됐다면 EC2 배포 서버에 Docker image가 성공적으로 띄워진다.
    - + \ No newline at end of file diff --git a/page/29.html b/page/29.html index a9e99f8..a50ae41 100644 --- a/page/29.html +++ b/page/29.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -22,7 +22,7 @@ 아주 간단하게 entity의 isNew()를 호출한다고 적혀있습니다. 하지만 Persistable 인터페이스를 구현한 Entity의 isNew() 를 호출하는 것 입니다.

    그럼 남은 하나의 클래스를 확인하겠습니다.

    info-support

    위 사진처럼 이 클래스가 Entity 마다 Persistable 구현 유무에 따라 동적으로 구현체를 변경해주고 있었습니다.

    그럼 답이 나온 것 같습니다. ID를 직접 할당하는 Entity에 Persistable을 구현해주면 됩니다.

    Persistable 구현하기

    @Entity
    public class ChargeStation implements Pesistable{

    @Id
    private String stationId;

    private String stationName;

    @CreatedDate
    private LocalDateTime createdTime;

    ...

    @Override
    public Object getId() {
    return getStationId();
    }

    @Override
    public boolean isNew() {
    return createdTime == null;
    }
    }

    간단히 만들어봤습니다. @CreatedDate는 Entity가 처음 영속화될 때 동작하기 때문에 이 Entity의 CreateTime 필드가 null 이면 새로운 Entity라고 확신할 수 있습니다. 그럼 이렇게 인터페이스를 구현하고 아까 실행했던 테스트를 다시 실행해보겠습니다.

    solved

    깔끔하게 구현된 것을 확인할 수 있었습니다. 원하던대로 쿼리가 2번 발생합니다. 이런 Persistable@MappedSuperClass를 통해 더 깔끔하게 구현할 수 있습니다. 하지만 따로 설명드리지는 않겠습니다.

    결론

    JPA는 많은 편의 기능을 제공해주는 것 같아보입니다. 쫄지맙시다.

    - + \ No newline at end of file diff --git a/page/3.html b/page/3.html index ecac1fc..cfa9260 100644 --- a/page/3.html +++ b/page/3.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -14,8 +14,9 @@ no offset no offset no offset -no offset

    - +no offset +no offset

    + \ No newline at end of file diff --git a/page/30.html b/page/30.html index bbcf79b..d8b877d 100644 --- a/page/30.html +++ b/page/30.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -26,7 +26,7 @@ 이를 통해서 Ver2에 비해서 1초정도 줄었습니다.

    Ver4. 이전 방식 + Fetch Join 사용하기 (약 6초)

    마지막 방법은 조회 과정의 시간 단축입니다.

    처음에 Stations를 findAll()하는 쿼리를 확인해보니 N+1 문제가 발생하고 있었습니다. 그 이유는 Station에서 Chargers를 지연로딩으로 설정 했는데, 이를 그대로 get 메서드를 통해 조회해서 해당 문제가 발생했습니다.

    List<ChargeStation> findAll(); // 기존

    @Query("SELECT DISTINCT c FROM ChargeStation c JOIN FETCH c.chargers"); // Fetch Join 적용
    List<ChargeStation> findAll();

    따라서 위에 코드와 같이 Fetch Join을 이용해서 처음에 데이터를 가져왔습니다. 이렇게 효율적인 조회로 변경하면서 시간을 많이 줄일 수 있었습니다.

    지금까지의 방법을 정리를 하자면

    Ver1 과 같은 방식에서는 업데이트 과정에서 JPA의 식별자에 따른 처리 방식으로 인해 [SELECT + UPDATE] or [SELECT + INSERT] 와 같이 쿼리가 두 번씩 나갔습니다.

    그래서 Ver3까지 개선을 하기 위해서 저장과 업데이트를 한 번에 JDBC를 이용해서 Batch로 처리해주는 방식을 선택했고,

    변경 감지 + 배치 데이터를 모으기 위해서 자료구조를 이용해서 시간을 조금씩 단축 했습니다.

    마지막으로 Ver4에서는 findAll()에서 발생하는 N+1의 문제를 해결하면서 시간을 단축했습니다.

    이런 과정을 통해서 동일 작업을 14초에서 6초 정도로 줄일 수 있었습니다!

    - + \ No newline at end of file diff --git a/page/31.html b/page/31.html index 91f5712..10e70a4 100644 --- a/page/31.html +++ b/page/31.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 11분
    누누

    안녕하세요 우아한테크코스 카페인팀 누누입니다

    이번에 카페인 팀에서 배포 아키텍처를 결정하게 되었던 과정에 대해서 정리를 해보고 싶어서 글을 쓰게 되었습니다.

    아키텍처와 서버가 배포되는 과정을 보여드리면서 시작하도록 하겠습니다

    배포 아키텍처

    서버가 배포되는 과정은 다음과 같습니다.

    server image

    우아한테크코스 인스턴스에 대한 소개

    우테코에서 선택할 수 있는 인스턴스는 총 2가지 종류입니다.

    1. 퍼블릭 서브넷에 있는 인스턴스
      • 캠퍼스에서만 SSH 접근이 가능한 인스턴스입니다.
      • 미리 열려있는 포트들만 허용이 되어 있습니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다
    2. 프라이빗 서브넷에 있는 인스턴스
      • 퍼블릭 서브넷에 있는 인스턴스를 통해서만 접근이 가능합니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다.

    1번 인스턴스를 2개 사용 가능하고, 2번 인스턴스를 1개 사용 가능합니다.

    권장되는 환경에서 1개는 db 서버로 사용하고, 나머지 2개는 자유롭게 사용이 가능했습니다.

    그전에 알면 좋아요

    여기서는 Self Hosted Runner를 사용했는데요.

    Self Hosted Runner에 대한 내용은 여기 에 잘 나와있습니다.

    외부 IP로부터 SSH 접근이 불가능하기에, Self Hosted Runner 나, Jenkins 같은 방법을 사용할 수 있었는데, 러닝 커브를 고려해서 Self Hosted Runner를 선택하게 되었습니다.

    배포 아키텍처에 대한 고민

    저희 팀이 이번 아키텍처를 만들기 위해서 고민했던 점들은 다음과 같습니다.

    1. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
    3. 2차 데모데이까지의 과제인 개발 서버를 어떻게 구성할 수 있을까?

    여기서 1번을 가장 먼저 생각한 아키텍처를 구성하게 되었는데, 다음과 같습니다.

    선택의 기준이 되었던 것은 총 3가지였습니다.

    1. DB는 프라이빗 서브넷에 위치시키고, 우리 인스턴스를 거쳐서만 접근이 가능하게 한다.
      • 이 부분은 보안을 위해서 어쩔 수 없이 선택하게 된 부분입니다.이 부분을 고려하다 보니, 최소한으로 구성할 수 있는 구조가 db 용 private 인스턴스 1개, 그리고 우리가 사용할 public 인스턴스 1개가 됩니다
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
      • 개발용 인스턴스에 CD 툴이나, 모니터링 툴을 설치하게 되면, 운영 서버에도 동일하게 작업을 해야 합니다.
      • 이 부분을 최소화하기 위해서, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 되었습니다.
    3. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
      • 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 않는다면 개발용 인스턴스에서 장애가 발생했을 때, CD 툴과 모니터링 툴에도 영향을 미치게 됩니다. 이 부분을 생각했을 때도, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리해야 한다고 결정하게 되었습니다
      • 한 부분의 장애가 다른 툴까지 사용할 수 없게 만들게 되어서, 롤백이나, 상황 파악을 하기 힘들게 만들게 됩니다.

    이런 과정들을 생각했을 때, 인스턴스 1개를 개발 서버용으로, 인스턴스 1개를 CD 툴과 모니터링 툴을 설치한 인스턴스로 사용하게 되었습니다.

    실제 내부 구성은 어떻게 될까요?

    개발 서버

    이 인스턴스에는 총 2가지 기능이 들어가 있습니다.

    1. 프론트 서버
      • react로 되어있는 프론트엔드 코드를 사용자에게 전달해 주는 역할을 합니다.
    2. 백엔드 서버
      • spring으로 되어있는 api 서버입니다.

    물론, 이렇게 하면 두 곳 중 한 곳에 장애가 발생했을 때, 프론트 서버와 백엔드 서버가 모두 영향을 받게 됩니다.

    같이 관리하게 된 첫 번째 이유로 비용이 들기 때문에 비용의 문제를 고려하게 되었습니다. 개발 서버에서 프론트 서버와 백엔드 서버를 관리하게 되었습니다.

    두 번째 이유로는, 아직 프로젝트 초창기 이기 때문에, 백엔드에서 장애가 났을 때, 프론트에서 일정 이상의 에러 처리가 불가능했습니다.

    프로젝트가 많이 진행되었다면, 프론트엔드만으로 혹은 장애가 나지 않은 서버를 활용해 에러 처리를 할 수 있지만, 아직은 그런 기능을 구현하지 못했습니다.

    이와는 별개로 실행 시 편의를 위해서 도커를 사용해 개발 서버를 관리하고 있습니다.

    CD 툴과 모니터링 툴

    이 인스턴스에는 총 3가지 기능이 들어가 있습니다.

    1. CD 툴
      • 위에서 설명드린 것처럼, self hosted runner 가 동작하게 되어있습니다
    2. 보안을 위한 리버스 프록시
      • 저희 프로젝트에서 구글 지도를 사용하게 되는데, 이때 API 키를 사용하게 됩니다. 이렇게 하면, API 키를 노출시키지 않고, 사용할 수 있습니다.
      • 이 API 키를 노출시키지 않기 위해서, 리버스 프록시를 하나 두고, 여기서 API 키를 추가해 요청을 보내는 방식으로 구성하게 되었습니다.
    3. 모니터링 툴
      • 저희 프로젝트에서 아직 도입하지 않았지만, 현재 이슈로는 올라가 있는 상태입니다.
      • Actuator, 프로메테우스, 그라파나 이 3가지를 활용해서 모니터링 툴을 구성하게 될 예정입니다

    위 기능들이 한 인스턴스에 모여있기에, 위의 기능들은 추후에 운영 서버가 추가되었을 때, 중복으로 관리하지 않아도 됩니다.

    배포 과정 더 자세히 알아보기

    아래에 사진에서 보이는 과정을 통해서 배포를 진행하고 있는데요

    server image

    1. 사용자가 push를 하면, github actions에서 도커 빌드를 진행하고, 도커 허브에 이미지를 올립니다.
    2. 도커 허브에 이미지가 올라간 이후에, self hosted runner 가 작동을 시작합니다.
    3. 개발용 인스턴스에 접근해서, 이미지를 받고, 컨테이너를 실행합니다.

    이런 과정을 통해서, 개발용 인스턴스에 배포를 진행하고 있습니다.

    느낀 점

    좋은 아키텍처를 설계하기 위해서는 고려해야 할 점들이 정말 많다는 것을 다시 한번 느꼈습니다.

    운영 서버가 추가된다던가, 인스턴스가 늘어나고, 줄어드는 상황에 유연하게 대처할 수 있도록 설계를 해야 한다는 것을 다시 한번 느꼈습니다.

    중복으로 관리될 포인트를 줄여야 한다는 것도 다시 한번 느낄 수 있었고요

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/page/32.html b/page/32.html index 1917406..ed6c3c0 100644 --- a/page/32.html +++ b/page/32.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 18분
    센트

    Untitled

    위 이미지는 현재까지 구현한 지도의 모습이다. 구현된 기능은 다음과 같다.

    • 충전소 정보를 서버에 요청해 받아온 충전소 정보를 바탕으로 화면에 마커를 표시하는 기능
    • 화면이 이동하거나 줌인, 줌 아웃을 할 시 화면의 마커 정보가 최신화 되는 기능
    • 마커 정보를 최신화 할 때 화면에서 사라진 마커를 dom에서 제거하는 기능
    • 마커 정보를 최신화 할 때 이전 화면에서도 있었던 마커를 재생성 하지 않는 기능
    • 마커를 클릭했을 시 해당 마커에 대한 간단 정보를 모달로 띄워주는 기능
    • 화면에 표시된 마커들에 대한 충전소 정보를 리스트로 보여주는 기능

    이번에 새로 추가하고자 한 기능은 다음과 같다.

    • 충전소 리스트에서 충전소를 선택하면 화면의 중심이 선택한 충전소 마커로 이동하고, 충전소의 간단 정보를 모달로 띄워주는 기능

    위 기능을 구현하기 위해선 google maps api의 InfoWindow객체를 이용해야 한다. 사용 방식은 다음과 같다.

    const infowindow = new google.maps.InfoWindow({
    content: contentString,
    ariaLabel: 'Uluru',
    });

    const marker = new google.maps.Marker({
    position: uluru,
    map,
    title: 'Uluru (Ayers Rock)',
    });

    infowindow.open({
    anchor: marker,
    map,
    });

    간단하게 요약하자면 다음과 같다.

    • InfoWindow 생성자 함수를 통해 infoWindow 인스턴스를 생성한다.
      • 생성시 dom 요소 혹은 string을 전달해 infoWindow가 생성될 dom위치를 지정해준다.
    • marker 인스턴스를 infoWindow 인스턴스의 open 메서드에 인자로 전달한다.
    • infoWindow 생성 시 전달했던 dom요소의 위치가 marker의 위치로 고정되면서 화면에 그려진다.

    Untitled

    충전소 정보를 보여주는 위 StationList 컴포넌트는 충전소 정보에 접근할 때 react-query를 통해 서버 상태를 직접 내려 받아 컴포넌트 내부 리스트를 렌더링 한다.

    또한, StationMarkersContainer에서도 충전소 정보를 react-query의 서버 상태에서 참조해 마커를 렌더링 하고 있다.

    따라서 StationList 컴포넌트와 StationMarkersContainer는 각각 따로 서버 상태에 접근해 렌더링을 수행하고 있으므로 둘 사이에는 어떠한 연결 고리가 없다.

    여기서 문제가 발생하게 되었다.


    현재까지의 코드에서는 infoWindow인스턴스를 StationMarkersContainer컴포넌트에서 생성한다. 이를 하위 컴포넌트인 StationMarker에 내려주고, 이 컴포넌트 내부에서 marker인스턴스를 생성한다.

    이번에 구현하기로 한 기능은 StationList의 항목 중 하나를 선택했을 시 선택된 충전소에 해당하는 마커에 간단 정보 모달이 뜨며 화면을 해당 마커가 중심으로 오도록 이동 시키는 것이었다.

    하지만 지금의 코드 구조상 StationListStationMarkersContainer사이에는 어떠한 연결 고리도 없으므로 infoWindowmarkerStationList는 접근할 수 없는 상태가 된다.

    이를 해결하기 위해서 다음과 같은 방법을 사용하기로 했다.

    • infoWindow인스턴스를 root 단에서 생성해 전역적으로 관리한다.
    • 생성될 marker 인스턴스들을 배열 형태의 전역 상태로 관리한다.

    위 내용을 말로만 본다면 별로 어려울 것 없어 보이지만 실제 구현을 진행해보니 내부적으로 큰 문제가 두 가지 존재했다.

    1. 따로 모듈을 분리해 infoWindow를 생성할 수 없다.
    2. marker인스턴스를 생성하는 주체가 StationMarkersContainer가 되어서는 안된다.

    각각의 문제점을 살펴보자.


    1. 따로 모듈을 분리해 infoWindow를 생성할 수 없다.

    infoWinodw를 전역 상태로 만들어 사용하기 위해 처음으로 했던 생각은 infoWindowStore.ts로 모듈을 분리하여 infoWindow를 생성해 store의 초기값으로 지정하는 것이었다.

    위 생각을 가지고 그대로 구현해보았더니 google을 참조할 수 없다는 에러가 발생했다. InfoWindow생성자 함수는 google.maps.InfoWindow를 통해 접근할 수 있기 때문에 해당 에러는 infoWindow인스턴스를 생성할 수 없다는 것을 의미했다.

    google을 참조할 수 없는지 이유를 분석해보니 이유는 다음과 같았다.

    우리 팀이 구글 지도 로드를 위해 선택한 라이브러리는 @googlemaps/react-wrapper이다. 이 라이브러리의 동작을 살펴보면 다음과 같다.

    • Wrapper컴포넌트가 @googlemaps/js-loader라이브러리의 Loader생성자 함수를 호출한다.
    • 생성된 loader인스턴스의 load메서드를 실행시켜 지도의 로딩 작업을 시작한다.
      • load 메서드는 최종적으로 Promise<typeof google>을 반환하는데, 지도 로드에 성공하면 resolve(window.google) 을 실행시켜 google을 전역적으로 사용 가능하도록 만들어준다.
    • 지도의 로딩이 완료되면 Wrapperrender props를 통해 받은 콜백 함수를 실행시킨다.
      • render콜백 함수는 로딩 상태를 나타내는 Status를 파라미터로 넘겨 받아 호출된다.

    최종적으로 render를 실행 시켰을 때 반환 되는 컴포넌트에서는 google 로딩 되어 전역적으로 접근이 가능함을 보장할 수 있으므로 이때부터 google에 접근이 가능해진다. → 따라서 Wrapper를 통해 반환되는 컴포넌트의 하위 컴포넌트에서 google.maps.Map생성자 함수를 사용해 지도를 생성할 수 있게 된다.

    infoWindow를 생성하기 위해 만든 새로운 모듈은 첫 import시기에 평가될 것이기 때문에 Wrapper의 하위 컴포넌트에서 import를 수행한다면 로드가 완료된 이후 시점일 것이므로 window.google이 등록되어 google에 접근이 가능할 것으로 예상했다.

    하지만 웹팩을 통한 번들링 과정에서 모듈이 뒤섞여 파일의 평가 시기를 보장할 수 없어져 새로 만든 모듈에서는 google에 대한 접근이 불가능해지게 되었다. 웹팩을 좀 더 공부해본다면 이 문제를 해결할 수 있을 것 같았지만, 너무 지엽적인 부분에서 많은 시간을 들이기 보단 기존에 개발하던 방식을 통해 문제를 해결해보기로 결정했다.

    최종적으로 문제를 해결한 방식은 다음과 같다.

    • InfoWindow생성자 함수를 호출할 CarFfeineInfoWindowInitializer컴포넌트를 만든다.
    • Wrapper로 감싸진 컴포넌트 하위에 CarFfeineInfoWindowInitializer 컴포넌트를 추가한다.
    • google에 접근이 가능한 상태를 보장받은 CarFfeineInfoWindowInitializer내부에서 infoWindow인스턴스를 생성한다.
    • storeinfoWindow인스턴스를 set해주어 전역적으로 infoWindow를 사용 가능하도록 한다.

    2. marker인스턴스를 생성하는 주체가 StationMarkersContainer가 되어서는 안된다.

    이번 팀 프로젝트에서 지도를 구현하기 위해 google maps api를 사용하게 되었다. 뜬금없이 이 이야기를 한 이유는 다음과 같다.

    • google maps api는 바닐라 자바스크립트를 기반으로 동작한다.
    • 이번 팀 프로젝트는 리액트를 기반으로 개발을 진행할 것이다.
    • 지도를 그리기 위해서 바닐라 자바스크립트와 리액트의 적절한 조화가 필요하다.
    • 다소 혼란스러울 수 있는 지도의 조작 방식을 리액트와 조화롭게 사용하기 위해서 컴포넌트 설계시 컴포넌트의 책임을 확실하게 구분해야겠다는 생각을 하게 되었다.

    이 컴포넌트의 책임에 대한 문제로 인해 marker 인스턴스를 생성하는 주체에 대해 많은 고민을 하게 되었다.

    일단 원래 코드 구조에서 마커를 그리기 위해 컴포넌트를 다음과 같이 추상화 했다.

    • StationMarkersContainer 컴포넌트
      • 리액트 쿼리를 통해 받아온 서버 상태(충전소 정보 배열)로 StationMarker를 호출한다.
    • StationMarker 컴포넌트
      • 상위에서 내려받은 충전소 정보 props를 통해 marker 인스턴스를 생성한다. (google maps api에서는 인스턴스 생성이 곧 렌더링을 의미한다)
      • 생성한 marker 인스턴스에 infoWindow 인스턴스의 open 메서드를 트리거 하는 클릭 이벤트 리스너를 추가해준다.
      • useEffect의 클린업 함수를 이용해 충전소 정보가 최신화 되었을 때 마커가 더이상 화면에 보이지 않는다면 marker 인스턴스의 setMap(null) 메서드를 호출해 google maps api에서 마커를 지우도록 한다. (마커 렌더링 최적화)

    간략히 설명하자면 StationMarkersContainer 컴포넌트는 충전소 정보를 서버에서 받아 StationMarker를 호출하는 역할만을 수행하고, 마커에 대한 모든 세부 로직은 StationMarker가 수행하도록 컴포넌트를 추상화 해보았다.

    이름에서도 드러나듯 StationMarker 컴포넌트가 marker 인스턴스를 생성하는 주체가 되어야 바닐라 자바스크립트와 리액트의 혼종인 이 프로젝트의 코드를 추후 유지보수 할 때 문제가 없으리라 판단했다.

    하지만 이렇게 추상화 된 컴포넌트들은 marker 인스턴스를 배열 형식의 전역 상태에 담아 관리하고자 할 때 문제가 되었다.


    일단 먼저 서버에서 내려 받은 충전소 정보를 station이라고 하자, 우리는 이 station을 통해 marker 인스턴스를 생성하고자 한다.

    이때 생각 할 수 있는 가장 간단한 방법은 station에서 map 메서드를 통해 marker 인스턴스를 생성하여 이 marker 인스턴스를 하위 컴포넌트인 StationMarker에 넘겨주는 방식일 것이다.

    하지만 이 방식은 인스턴스를 생성하는 것이 곧 화면에 렌더링을 발생시키는 것을 의미하는 google maps api의 특성상 우리가 처음 설계한 컴포넌트의 책임을 반하는 구조를 만들어내게 된다.

    자세히 설명해보자면 마커의 렌더링은 StationMarkersContainer가 수행하고 있는데 화면에 보이지 않는 마커를 지우는 역할은 StationMarker컴포넌트가 수행하고 있고, 이벤트 핸들러의 추가 역시 마커가 생성된 이후에 하위 컴포넌트에서 이를 수행하는 괴상한 코드가 만들어지게 된다.

    추후 코드의 유지보수성을 위해선 피해야 할 방식임이 명확했다.

    해결 방식을 고민해보다가 다음과 같은 해결 방안을 생각하게 되었다.

    StationMarker 컴포넌트의 역할

    • marker 인스턴스를 생성한다.
    • marker 인스턴스의 이벤트 핸들러를 추가한다.
    • 생성된 marker 인스턴스를 배열 형식의 전역 상태에 추가한다.
    • 충전소 정보가 최신화 되었을 때 마커가 화면에 보이지 않는 상태가 되었다면 marker 인스턴스를 전역 상태에서 삭제한다.

    위와 같이 StationMarker 의 역할을 잡게 되면 기존의 컴포넌트 설계 구조를 해치지 않으면서 전역 상태에 marker인스턴스를 잘 추가할 수 있게 된다. 하지만 이렇게 되면 StationMarker 컴포넌트는 다음의 큰 문제들을 가지게 된다.

    1. marker들을 가지는 전역 상태를 구독하고 있는 컴포넌트가 새로 생성되는 마커의 개수만큼 리렌더링 된다.
    2. 현재 사용하고 있는 전역 상태 관리 도구의 특성상 이전 상태를 참조해와야 marker를 추가할 수 있게 되는데, 이 때 이전 상태가 최신의 상태임을 보장하지 못할 수 있다.

    이 두 문제를 해결할 방식을 고민해보았을 때 다음과 같은 결론에 도달하게 되었다.

    • 현재 사용하고 있는 전역 상태 관리 도구는 React 18에 새로 추가된 useSyncExternalState 훅을 기반으로 recoil과 비슷하게 사용할 수 있도록 계층을 분리하여 만든 도구이다.
    • 기존에 사용하던 전역 상태 관리 도구의 메서드 useExternalState, useExternalValue, useSetExternalState 이외에 store 인스턴스에 직접 접근하여 최신의 상태를 참조하는 getStoreSnapShot 메서드를 추가한다.
    • store에 직접 접근해 받아온 최신의 상태는 바닐라 자바스크립트 객체 이므로 리액트의 리렌더링을 발생 시키지 않는다.
    • 리렌더링으로 인한 문제점들을 getStoreSnapShot 메서드를 추가함으로써 해결할 수 있다.

    새로운 기능 추가를 위해 마주했던 앞선 두 가지의 문제와 해결 방식을 살펴 보았다. 그래서 최종적으로 이전까지 계속해서 고민해왔던 문제를 해결한 과정을 간추려보자면 다음과 같다.

    • 충전소 정보를 서버에서 받아와 렌더링 하는 StationList 컴포넌트에서 marker 인스턴스 배열을 저장하고 있는 store인스턴스에 직접 접근해 최신의 marker인스턴스들을 가져온다.
    • 충전소 목록에서 사용자가 충전소를 클릭했을 때 전역으로 관리되는 infoWindow 인스턴스의 open메서드에 marker 인스턴스들 중 선택된 marker를 전달해 간단 정보 모달을 띄워준다.
    - + \ No newline at end of file diff --git a/page/33.html b/page/33.html index 2eb9ab6..33d8ede 100644 --- a/page/33.html +++ b/page/33.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 9분
    가브리엘

    지도 api 벤더 선택 이유

    국내 서비스 중인 지도 서비스로는 google, naver, kakao가 있습니다.

    이 중에서도 google maps api는 css로 지도의 테마를 직접 스타일링할 수 있는 기능이 있어서 선택하게 됐습니다.

    google maps api를 사용하기 위해서 별도의 라이브러리 사용이 필수는 아니지만

    저희 팀에서 대중적인 라이브러리들과 기본 환경 설정법을 모두 테스트 했을 때, 반드시 사용하고 싶은 라이브러리가 존재하여 비교를 기록으로 남기게 됐습니다.

    google maps api 관련 라이브러리

    (선택한 라이브러리들은 ✅으로 표시했습니다.)

    google maps API

    https://github.com/tomchentw/react-google-maps

    이 라이브러리는 구글에서 공식으로 제공하는 지도 api로, HTML DOM에 구글 지도를 부착하고, 사용(조작)할 수 있도록 도와줍니다. 이 라이브러리는 vanilla Javascript 기반으로 동작합니다.

    @types/google.maps

    https://www.npmjs.com/package/@types/google.maps

    TypeScript에서 구글 지도를 사용할 때 타입을 제공해주는 역할을 합니다.

    @googlemaps/js-api-loader

    https://www.npmjs.com/package/@googlemaps/js-api-loader

    이 라이브러리는 구글에서 공식으로 제공하는 지도 호출 api로, api key만 넘겨주더라도 구글 지도를 스크립트 형태로 불러와주는 역할을 하는 라이브러리입니다. 별도로 html 조작 없이 불러온 라이브러리에서 구글 지도를 꺼내서 동적으로 사용할 수 있습니다. vanilla Javascript 기반으로 동작하여 어디에서나 사용이 가능합니다.

    대중적인 라이브러리 비교

    react-google-maps@react-google-maps/api@googlemaps/react-wrapper
    링크https://www.npmjs.com/package/react-google-mapshttps://www.npmjs.com/package/@react-google-maps/apihttps://www.npmjs.com/package/@googlemaps/react-wrapper
    설명이 라이브러리는 개인이 만든 라이브러리로, google maps API를 react DOM 위에 올려서 사용하게 돕습니다.
    구글 지도와 마커를 react component 처럼 사용하여 react스럽게 렌더링 하는 것을 지원합니다.
    react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리였지만 2018년 이후로 업데이트가 끊겼습니다.
    이 라이브러리도 개인이 만든 라이브러리로 앞서 소개한 react-google-maps를 개량하여 만든 라이브러리입니다.
    이 라이브러리 역시 react에 지도나 마커 컴포넌트를 호출해서 사용이 가능합니다.
    현재 react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리 입니다.
    이 라이브러리는 구글에서 공식으로 제공하는 react용 라이브러리입니다.
    이 라이브러리는 앞서 소개한 js-api-loader를 활용하여 만든 Wrapper 컴포넌트를 제공하는데, 구글 지도를 호출하는 과정에서 수신중, 실패, 성공에 따라 지도를 보여줄 지, 로딩중 컴포넌트를 보여줄 지, 에러 컴포넌트를 보여줄 지 결정하는 기능이 있습니다.
    이외에는 기존의 js-api-loader의 기능과 완벽하게 동일합니다. (라이브러리를 열어서 직접 확인해봤습니다.)
    선택여부

    라이브러리 선택 이유

    저희 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 다보니 지도 위에 띄워줘야 할 마커를 최적화 하는 과정이 굉장히 중요합니다.

    1. 전국 6만여 개의 마커를 전부 보여줄 수 없다.
    2. 현재 디스플레이 영역의 마커만을 호출해야한다.
    3. 그 마커들의 렌더링 과정을 저수준에서 다룰 수 있어야 한다.

    이런 원칙을 가지고 있기에 대중적인 라이브러리들(react-google-maps, @react-google-maps/api)은 저희의 선택지에 없었습니다.

    따라서 구글 지도는 오로지 vanilla로 제공되는 상태에서 직접 제어하기로 결정하였고, 마커를 관리하는 주체 또한 구글 지도에서 직접 컨트롤을 하려고 합니다.

    따라서 구글 지도를 호출하는 작업은 @googlemaps/react-wrapper에 맡기고, 불러온 구글 지도는 vanilla로 통제하기로 했습니다.

    지도의 조작, 지도에 마커를 찍는 과정을 모두 공식 문서에 나와있는 방법대로 통제하려고 합니다.

    기존의 라이브러리들은 마커나 지도를 컴포넌트화 한 상태이기에 최적화 과정에서 저희가 제어할 수 없는 부분들이 있다고 생각합니다. 따라서 트러블슈팅 과정에서 마커의 호출 시점, 메모리에서 해제하는 시점, 렌더링하는 시점 등의 작업들을 훨씬 더 세밀하게 하려면 google maps api을 있는 그대로 사용할 수 있어야 합니다. 따라서 지도에 관련된 기능은 react DOM 위에서가 아닌 vanilla 환경에서 작업을 할 것입니다.

    구글 지도 제어 전략

    1. 구글 지도와 마커는 항상 바닐라 환경(react DOM 바깥)에서 동작하게 한다.
    2. 바닐라 환경에서만 동작하게 하여 리액트 컴포넌트에서의 재 렌더링을 일절 방지한다.
    3. 마커나 지도의 동작 이벤트에 의해 UI를 조작해야하는 경우에는 react DOM 조작을 하도록 한다.
    4. 바닐라 환경인 google maps api와 react DOM 사이의 제어 과정에는 useSyncExternalStore 훅을 이용하여 리액트 UI를 강제로 동기화 시킬 수 있도록 한다.

    구글 지도는 바닐라 환경에서, 각종 UI 통제는 리액트에서 통합하여 사용하는 환경을 구상하고 있습니다.

    시중에 나와있는 대부분의 라이브러리들을 활용하여 비교하고 테스트한 결과 @googlemaps/react-wrapper를 선택하는 것이 최적화와 생산성, 앱 안정성을 모두 확보할 수 있는 선택이라고 생각했습니다.

    현재 카페인 팀에서 사용중인 지도 제어에 관한 방법은 이후에 작성 될 글에서 상세하게 설명하겠습니다.

    - + \ No newline at end of file diff --git a/page/34.html b/page/34.html index 5746de5..bcca28e 100644 --- a/page/34.html +++ b/page/34.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 6분
    키아라

    서론

    안녕하세요 카페인팀 키아라입니다.

    이번 프로젝트를 시작하면서 프로퍼티를 암호화하는 방법으로 jasypt를 알게되어

    사용하는 방법을 익혀 저희 프로젝트에 적용해볼 계획입니다.

    프로퍼티 암호화는 왜 필요할까?

    spring:
    datasource:
    url: 데이터베이스 url
    username: 계정
    password: 비밀번호

    프로젝트를 진행하면서 yml 파일에 DB 연결 URL이나 계정, 비밀번호 같이 노출되어선 안 되는 민감한 정보들이 많습니다.

    git의 public repository와 CI/CD를 연동해 어플리케이션을 배포한다면 중요한 정보가 탈취될 가능성이 있죠.

    Jasypt 라이브러리를 사용하면 평문으로 된 데이터베이스 접속 정보를 암호화 하여 방어막을 한 겹 쌓을 수 있게 됩니다.

    간략하게 라이브러리를 소개하고 사용 방법을 알아볼까요?

    jasypt는 뭐지?

    Jasypt이란 쉽게 암호화 기능을 사용할 수 있도록 제공하는 Java 라이브러리입니다.

    민감한 평문 정보를 암호화하고, 아래처럼 설정 값을 지정하면 어플리케이션이 실행될 때 자동으로 이를 복호화하여 사용합니다.

    사용자가 편하게 암호화 기능을 사용할 수 있도록 제공하는 Java 라이브러리로

    공식 홈페이지는 http://www.jasypt.org/ 에 가면 더 자세한 정보를 확인할 수 있습니다.

    사용 방법

    정말 간단하게 라이브러리 추가, key값 넘겨주기, 암호화 세 가지 단계로 프로퍼티를 암호화하여 관리할 수 있습니다.

    1. 라이브러리 추가 (= 의존성 추가)

    implementation "com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.3"

    2. Jasypt 설정 및 Bean 등록

    key를 사용해서 Bean을 등록하는 기본 설정입니다. 여기서 Bean의 이름을 jasyptEncryptor라고 설정했다면 프로퍼티 등록해야 합니다.

    @Configuration
    public class JasyptConfig {

    private String ENCRYPT_KEY = "hello";

    @Bean(name = "jasyptEncryptor")
    public StringEncryptor stringEncryptor() {
    PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();

    SimpleStringPBEConfig config = new SimpleStringPBEConfig();

    config.setPassword(ENCRYPT_KEY);
    config.setAlgorithm("PBEWithMD5AndDES");
    config.setKeyObtentionIterations(1000);
    config.setPoolSize(1);
    config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
    config.setStringOutputType("base64");
    encryptor.setConfig(config);
    return encryptor;
    }
    }
    jasypt:
    encryptor:
    bean: jasyptEncryptor

    3. 암호화

    라이브러리를 사용할 준비는 거의 다 끝났습니다. 이제 암호화하여 프로퍼티에 작성합니다.

    이때 암호화 하는 방법은, 아래 사이트에 접속해 평문과 키를 입력한 후 나온 암호문을 프로퍼티 파일에 'ENC(암호문)' 로 작성합니다.

    암복호화 사이트

    평문

      datasource:
    url: 데이터베이스 url
    username: 계정
    password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)

    나머지도 마저 암호화해줍시다.

      datasource:
    url: ENC(j94r94hQbd1SfFHGCUeweg+GGDosfnxP8dL0FQxfXtE=)
    username: ENC(vp3Gw8kLpwDZhmMMqf88/Q==)
    password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)

    실행

    올바른 암호문을 입력했다면 정상적으로 실행이 됩니다.

    그러나 이때 임의로 암호문을 수정한다면 다음과 같이 빌드를 실패합니다.

    실행 실패

    그런데 뭔가 이상하지 않나요?

    프로퍼티는 분명 암호화 했는데 키가 코드에 그대로 노출되어 있습니다.

    Git의 public Repository에 배포하면 다른 사람들도 볼 수 있습니다.

    그럼 이 키를 어디에 숨길 수 있을까요?

    저는 처음에 일반 file에 키를 넣어놓고 파일을 읽어오는 식으로 키를 관리하려고 했습니다. 당연히 해당 파일은 .gitignore로 커밋 대상에서 제외해야겠죠.

    그런데 이것보다 더 쉽고 빠른 방법이 있습니다.

    바로 환경변수를 설정하는 것이죠.

    + 환경변수 설정

    private String ENCRYPT_KEY = "hello";

    기존의 키를 관리하는 방식이었습니다.

    우선 이 키를 프로퍼티에서 관리하도록 설정해볼까요?

    // JasyptConfig.class
    @Value("${jasypt.encryptor.password}")
    private String ENCRYPT_KEY;
    // application.yml
    jasypt:
    encryptor:
    password: hello

    이제 환경변수를 설정해봅시다.

    Run > Edit Configurations... 경로로 들어가면

    Run/Debug Configurations 창이 나오는데

    Environment variables: 부분에 ENCRYPT_KEY=hello

    라고 적어주세요.

    그 후 다시 yml 파일로 돌아와 기존 hello로 되어있는 부분을 ${ENCRYPT_KEY}로 변경하고 실행한다면 정상적으로 작동됩니다.

    jasypt:
    encryptor:
    password: ${ENCRYPT_KEY}

    긴 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git a/page/35.html b/page/35.html index a480fe9..179199b 100644 --- a/page/35.html +++ b/page/35.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -17,7 +17,7 @@ 아까 environment 속성을 보면 test라고 설정해놓은 것을 볼 수 있습니다. 해당 환경이 여기에 적용됩니다.

    Branch rule 정의하기

    이번에는 해당 Repository의 Settings -> Branches 탭으로 들어갑니다. 그리고 원하는 branch에 들어가 edit 버튼을 누릅니다.

    그리고 사진과 같이 Require deployments to succeed before merging 속성을 클릭합니다. 그리고 아래와 같이 어떤 환경을 적용할 것인지 선택할 수 있습니다.

    이 속성은 해당 배포가 성공해야 merge 할 수 있도록 브랜치를 보호하는 기능입니다.

    그리고 저희는 frontend와 backend Job의 환경을 둘 다 test라는 이름으로 정의했기 때문에 하나의 environment만 선택해도 둘 다 적용되는 효과를 볼 수 있습니다. branch rule

    적용 후

    아래와 같이 merge가 안된다는 글과 빨간색으로 경고 표시를 해주고 있습니다. blocked

    결론

    간단한 github action을 통해서 생산성을 많이 올릴 수 있는 좋은 기능인 것 같습니다. 다른 팀들도 이 기능을 도입하여 사용하는 것을 추천드립니다.

    - + \ No newline at end of file diff --git a/page/36.html b/page/36.html index c691082..cf1f301 100644 --- a/page/36.html +++ b/page/36.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 5분
    센트

    웹팩에서 msw 설정

    이번 팀 프로젝트는 CRA와 같은 보일러 플레이트 코드를 사용하지 못하게 제한이 있다. 또한 요즘 많이 사용된다는 Vite의 사용도 제한이 있고, 웹팩으로 프로젝트를 시작하도록 강제하고 있다.

    팀원 모두 한 번도 웹팩을 통해 프로젝트를 시작해본 경험이 없어 프론트엔드 팀원 각자 개인 레포에서 웹팩 공부를 진행한 후 어느정도 진척이 있을 때 팀 레포에 프로젝트를 시작하기로 했다.

    다행히 웹팩으로 시작하는 프로젝트에 대한 많은 참고 자료들이 있어 첫 리액트 프로젝트 화면을 띄우는데 까지는 그리 오랜 시간이 걸리지 않았다. 그렇게 모든 팀원이 첫 웹팩 프로젝트를 성공시킨 후 모여 팀 프로젝트 초기 설정을 시작해보았다.

    eslint, prettier, 웹팩 등등 여러 설정들을 하고 필요한 패키지를 설치하는데 문제가 발생했다. 큰 데이터를 다루는 백엔드의 개발 속도를 고려해 프론트엔드 개발을 진행하기 위해서 미션중에 배웠던 MSW 라이브러리를 사용하기로 결정했는데, 이 라이브러리가 우리 팀의 개발 환경에서 동작하지 않았다.

    왜 동작하지 않는지 원인을 찾아보니 MSW service worker 파일을 찾을 수 없다는 오류 메세지가 나오는 것을 확인할 수 있었다. 원인을 더 자세히 알아보니 public 폴더에 있는 파일들은 웹팩이 번들링을 진행할 때 포함이 되지 않는다는 것을 알 수 있었고, 이를 어떻게 해결할 지 팀원들과 방법을 찾아보았다.

    약 한시간쯤 지났을 무렵 copy-webpack-plugin 패키지를 통해 public 경로에 있는 파일들도 빌드 폴더에 포함시킬 수 있다는 것을 알게 되었다. 하지만 이 copy-webpack-plugin에 대한 사용법이 미숙해 public 폴더에 있는 mockServiceWorker.js 파일만 빌드 폴더로 옮겼어야 했는데 index.html과 같은 다른 파일들 까지 한꺼번에 빌드 폴더로 옮겨지게 되었다.

    이런 저런 방법들을 시도해보다 webpack.config.js 파일의 plugins에 아래와 같은 설정을 추가 해주어 MSW를 프로젝트에 적용할 수 있게 되었다.

    new CopyWebpackPlugin({
    patterns: [
    { from: 'public/mockServiceWorker.js', to: '.' }, // msw service worker
    ],
    }),

    설정을 간단히 보면 public 경로에 있는 mockServiceWorker.js 파일을 빌드 후 폴더의 루트 디렉토리에 추가해준다는 설정이다.

    문제 상황과 해결 방법을 간단하게 다시 정리해보면 다음과 같다.

    1. MSW를 적용해보려고 함.
    2. 웹팩에서 개발 서버를 열었을 때 MSW 실행을 위해 필요한 mockServiceWorker.js 파일을 찾을 수 없다는 오류가 발생함.
    3. 문제의 원인은 웹팩에서 번들링을 진행할 때 public 폴더 하위 경로에 있는 파일들을 무시하기 때문이었음.
    4. 문제를 해결하기 위해 public 경로에 있는 mockServiceWorker.js 파일을 번들링 후 폴더의 루트 디렉토리에 저장하도록 하는 설정을 추가해줌.
    - + \ No newline at end of file diff --git a/page/37.html b/page/37.html index ad26c65..66cf972 100644 --- a/page/37.html +++ b/page/37.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 12분
    누누

    안녕하세요 카페인팀 nunu입니다.

    오늘은 스프링에서 발생한 에러 로그를 슬랙으로 모니터링하는 방법에 대해서 알아보려고 합니다.

    목차는 다음과 같습니다.

    1. 스프링에서 로그를 남기는 방법
    2. Slf4 j의 동작원리
    3. Logback의 동작원리
    4. Logback을 사용해서 슬랙으로 에러 로그를 모니터링하는 방법

    스프링에서 로그는 어떻게 찍을까?

    스프링에서 로그를 찍는 방법은 여러 가지가 있지만, 가장 간단한 방법은 System.out.println()을 사용하는 것입니다.

    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test() {
    System.out.println("test");
    return "test";
    }
    }

    당연하지만, 성능이 안 좋아서 실제 서비스에서는 사용하지 않습니다.

    스프링에서는 Slf4 j를 통해서 로그를 남길 수 있습니다.

    @Slf4j // private final Logger log = LoggerFactory.getLogger(this.getClass()); 와 같다.
    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test() {
    log.info("test");
    return "test";
    }
    }

    이 코드를 통해서 로그를 남길 수 있는데, 자동으로 콘솔에 출력이 됩니다.

    스프링에서 로깅은 어떻게 작동하는 거지?

    스프링 4까지는 Commons Logging을 사용했었습니다.

    Commons LoggingJCL이라고도 불리며, JDK Logging, Log4 j, Logback 등 다양한 로깅 프레임워크를 지원합니다.

    JCL 은 런타임에 어떤 로깅 프레임워크를 사용할지 결정할 수 있습니다.

    런타임에 어떤 로깅 프레임워크를 사용할지 결정하는 방식으로 클래스 로더에게 질의를 하는 방식으로 작동하게 되는데

    클래스 로더에게 질의를 했을 경우에 몇 가지 문제점이 생깁니다

    1. 클래스 로더에 명확한 표준이 없고, 부모 자식 모델이 있어서, 클래스 로더에 따라서 다른 결과가 나올 수 있습니다. 참고
    2. 클래스로더는 gc의 동작에 방해를 일으켜서 메모리 누수를 발생시킬 수 있습니다. 참고

    @Slf4j 어노테이션을 붙이면, 컴파일 시점에 private final Logger log = LoggerFactory.getLogger(this.getClass()); 와 같은 코드로 변환됩니다.

    스프링 5에서는 Slf4j 가 사용하는 것처럼, 컴파일 타임에 어떤 로깅 프레임워크를 사용할지 결정하는 기능을 작성했고, Commons Logging을 사용하지 않게 되었습니다.

    spring 5에서 변경되었다는 링크

    Slf4 j에 대해서 알아보자

    Slf4 j는 로깅을 위한 인터페이스를 제공하는 프레임워크입니다.(Simple Logging Facade for Java)

    컴파일 타임에, 어떤 로그 라이브러리를 사용할지 결정하는 기능을 제공합니다.

    로그 라이브러리를 바꾸려고 했을 때, 기존 코드는 하나도 건드리지 않고, 로그 라이브러리만 바꿔주면 되도록 해줍니다.

    조금 더 자세한 동작 원리를 알아보자

    only slf4j

    Slf4 j 만을 사용했을 경우 위 사진 같은 형태로 요청이 처리가 됩니다.

    Slf4 j 라는 인터페이스를 통해서 로그를 남기고, 어떤 로그 라이브러리를 사용할지는 Slf4j binding이라는 것을 통해서 결정합니다.

    Slf4j bindingSlf4j의 인터페이스를 구현하고 있지 않은 라이브러리의 구현체를 연결해 주는 역할을 합니다.

    그 구현체로 Slf4j-log4 j12-{version}. jar 같은 것이 있다.

    이와는 다르게 Logback 은 Slf4 j 를 구현하고 있기에, Slf4j binding 을 사용하지 않아도 됩니다.

    logback example

    위 사진처럼 Slf4j binding 을 사용하지 않고, Logback 바로 사용하는 것도 가능합니다.

    그렇다면 Slf4 j를 바로 사용하지 않은 코드에서 Slf4j 를 사용하려면 어떻게 해야 할까요?

    slf4j working principle

    위 사진처럼 Slf4j bridge 를 통해서 외부 라이브러리를 사용하는 것처럼 갈아 끼울 수 있습니다.

    Log4j2 를 사용하는 코드를 전혀 바꾸지 않아도, BridgeSlf4j 를 통해 Logback으로 자연스럽게 로그를 남길 수 있도록 해줍니다.

    Logback에 대해서 알아보자

    Logback 은 스프링에서 기본으로 사용될 만큼 인기 있는 로그 라이브러리입니다.

    logback 동작 과정

    공식문서에서 아주 핵심적인 동작원리를 설명해주고 있는 사진이라서 가져왔습니다.

    너무 어려워 보여서, 조금 자세하게 각각의 구성요소에 대해서 알아보도록 하겠습니다

    이에 대해 알아보도록 하겠습니다

    로그백의 구성요소

    Appender

    Appender는 로그를 어디에 출력할지를 결정하는 역할을 합니다.

    외부로부터 어떤 데이터를 받아서, 어떤 방식으로 처리할지에 대해서 전체적으로 설정할 수 있습니다.

    기본적으로 수많은 Appender 가 제공되고 있습니다.

    • ConsoleAppender
    • FileAppender
    • RollingFileAppender
    • AsyncAppender
    • DBAppender
    • SMTPAppender
    • SocketAppender
    • SyslogAppender

    저희는 Slack에 알림을 주는 것이 목적이기 때문에, SlackAppender를 사용하면 될 것 같습니다.

    하지만 SlackAppender는 제공되고 있지 않기에 직접 구현을 해야 하는데요

    이를 구현했을 때, Slack API 가 끝날 때까지, 계속 기다리고 있을 필요가 없기에, AsyncAppender를 사용하는 것이 좋을 것 같습니다.

    사용 방법은 다음과 같습니다. xml 기반으로 가능한데요

    <configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>myapp.log</file>
    <encoder>
    <pattern>%logger{35} -%kvp -%msg%n</pattern>
    </encoder>
    </appender>

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE" />
    </appender>

    <root level="DEBUG">
    <appender-ref ref="ASYNC" />
    </root>
    </configuration>

    만약 여기에 있는 기능들로 부족하다면, 직접 Appender 를 구현해서 사용할 수도 있습니다.

    직접 구현하려면 AppenderBase를 상속받아서 구현하면 됩니다.

    이 클래스는 필요한 부분이 대부분 구현되어 있고, appender 만 구현하면 바로 사용할 수 있습니다. 당연하지만 필요하다면 override 도 가능하죠

    Layout

    Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 합니다.

    Appender는 로그를 어디에 출력할지를 결정하는 역할을 하고, Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 하도록 하는 것이 이상적이지만

    Logback 은 Appender에서 Layout 을 직접 지정할 수 있도록 해주고 있습니다.

    따라서, 직접 Layout 을 만들지 않고, Appender 에서 기존에 이미 있는 패턴만 사용하려고 합니다

    Encoder

    Encoder는 Layout 과 비슷한 역할을 합니다.

    Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 하고, Encoder 는 실제 byte 형태로 변환하는 역할을 합니다.

    Slack의 webhook을 사용할 것이지만, AppenderBase를 사용하기에, 이번에는 사용할 수 없습니다.

    Filter

    Filter는 로그를 어떤 조건에 따라서 출력할지를 결정하는 역할을 합니다.

    Filter 는 Appender를 등록하며 같이 등록할 수 있는데요

    이번 프로젝트에서는 Level 이 ERROR 이상인 것만 출력하도록 하고 싶기에, LevelFilter를 사용하면 좋을 것 같습니다.

    <configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>INFO</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>
    <encoder>
    <pattern>
    %-4relative [%thread] %-5level %logger{30} -%kvp -%msg%n
    </pattern>
    </encoder>
    </appender>
    <root level="DEBUG">
    <appender-ref ref="CONSOLE" />
    </root>
    </configuration>

    와 비슷하게 사용할 수 있어 보입니다.

    그러면 실제로 프로젝트에서 error 발생 시 slack으로 알림을 주는 것을 구현해 보도록 하겠습니다.

    슬랙에 추가하는 방법

    이 블로그를 보고서 작성했습니다

    실제 구현

    구현된 결과물은 아래와 같습니다

    slack appender

    SlackAppender 구현하기

    public class SlackAppender extends AppenderBase<ILoggingEvent> {

    @Override
    protected void append(final ILoggingEvent eventObject) {
    final var restTemplate = new RestTemplate();
    final var url = "https://hooks.slack.com/services/";
    final Map<String, Object> body = createSlackErrorBody(eventObject);
    restTemplate.postForEntity(url, body, String.class);
    }

    private Map<String, Object> createSlackErrorBody(final ILoggingEvent eventObject) {
    final String message = createMessage(eventObject);
    return Map.of(
    "attachments", List.of(
    Map.of(
    "fallback", "요청을 실패했어요 :cry:",
    "color", "#2eb886",
    "pretext", "에러가 발생했어요 확인해주세요 :cry:",
    "author_name", "car-ffeine",
    "text", message,
    "fields", List.of(
    Map.of(
    "title", "우선순위",
    "value", "High",
    "short", false
    ),
    Map.of(
    "title", "서버 환경",
    "value", "local",
    "short", false
    )
    ),
    "ts", eventObject.getTimeStamp()
    )
    )
    );
    }

    private String createMessage(final ILoggingEvent eventObject) {
    final String baseMessage = "에러가 발생했습니다.\n";
    final String pattern = baseMessage + "```%s %s %s [%s] - %s```";
    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    return String.format(pattern,
    simpleDateFormat.format(eventObject.getTimeStamp()),
    eventObject.getLevel(),
    eventObject.getThreadName(),
    eventObject.getLoggerName(),
    eventObject.getFormattedMessage());
    }
    }

    이 과정에서 url을 직접 입력하시면 됩니다.

    그리고, 이렇게 만든 SlackAppender를 logback-spring.xml 에 등록하면 됩니다.

    <?xml version="1.0" encoding="UTF-8"?>

    <configuration scan="true" scanPeriod="60 seconds">
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <include resource="org/springframework/boot/logging/logback/file-appender.xml"/>
    <root level="INFO">
    <appender-ref ref="FILE"/>
    <appender-ref ref="CONSOLE"/>
    </root>
    <appender name="SLACK_APPENDER" class="racingcar.SlackAppender">
    </appender>
    <appender name="ASYNC_SLACK_APPENDER" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="SLACK_APPENDER"/>
    </appender>
    <logger name="racingcar" level="ERROR" additivity="true">
    <appender-ref ref="ASYNC_SLACK_APPENDER"/>

    </logger>

    </configuration>

    이렇게 하면, racingcar 패키지에서 에러가 발생할 때만 slack으로 알림을 받을 수 있습니다.

    결론

    slack appender

    이번 글에서는 log 레벨에 따라 slack 으로 알림을 받는 방법을 알아보았습니다.

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/page/38.html b/page/38.html index 8867dc3..80e97e0 100644 --- a/page/38.html +++ b/page/38.html @@ -5,14 +5,14 @@ Blog | CAR-FFEINE - +

    · 약 3분
    야미

    프로젝트 브랜치명 컨벤션이 feat/이슈번호여서, 브랜치명에서 이슈번호만 가져온 다음 커밋할 때마다 커밋 메시지 아래단(footer)에 이슈 번호를 자동으로 입력해주고 싶었다. 자동으로 입력된다면 깜빡하고 이슈 번호를 안 적는 일도 없고, 시간도 단축할 수 있기 때문이다.

    아래 순서대로 진행한다면 이슈 번호 POSTFIX 자동화를 할 수 있다.

    1) 프로젝트 폴더에 .githooks 폴더 생성

    2) .githooks 폴더에 commit-msg 파일 생성

    #!/bin/bash

    COMMIT_MESSAGE_FILE_PATH=$1
    MESSAGE=$(cat "$COMMIT_MESSAGE_FILE_PATH")

    # 커밋 메시지가 없을 때, 커밋 방지
    if [[ $(head -1 "$COMMIT_MESSAGE_FILE_PATH") == '' ]]; then
    exit 0
    fi

    # 브랜치명에서 이슈 번호만 추출 ('/' 뒤에 있는 문자만 추출)
    POSTFIX=$(git branch | grep '\*' | sed 's/* //' | sed 's/^.*\///' | sed 's/^\([^-]*-[^-]*\).*/\1/')

    COMMIT_SOURCE=$2
    CURRENT_BRANCH=$(git branch --show-current)

    # [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] 👉🏻 현재 브랜치명과 POSTFIX가 똑같으면 POSTFIX 입력 방지
    # [ "$COMMIT_SOURCE" != "merge" ] 👉🏻 merge할 때, POSTFIX 입력 방지
    # [[ "$MESSAGE" != *"[#$POSTFIX]"* ]] 👉🏻 이미 POSTFIX가 존재할 때, POSTFIX 중복 입력 방지
    if [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] && [ "$COMMIT_SOURCE" != "merge" ] && [[ "$MESSAGE" != *"[#$POSTFIX]"* ]]; then
    printf "%s\n\n[#%s]" "$MESSAGE" "$POSTFIX" > "$COMMIT_MESSAGE_FILE_PATH"
    fi

    🧐 이슈 번호 추출에 사용된 명령어 설명

    • grep '*' 👉 * 표시된 브랜치(현재 위치의 브랜치)를 가져온다.
    • sed 's/_ //' 👉 * 제거
    • sed 's/(/)./\1/' 👉 / 이후의 문자만 추출
    • sed 's/^(---)._/\1/' 👉 하나의 이슈에 여러 브랜치를 만들면서 feat/10-1 이런 형태로 브랜치를 만들 경우, 첫 번째 '-' 앞 뒤만 추출 (ex. 10-1)

    3) 프로젝트 폴더에 Makefile 파일 생성

    init:
    git config core.hooksPath .githooks
    chmod +x .githooks/commit-msg
    git update-index --chmod=+x .githooks/commit-msg

    # chmod +x .githooks/commit-msg 👉🏻 macOS, 리눅스에서 스크립트 권한 부여
    # git update-index --chmod=+x .githooks/commit-msg
    # 👉 macOS, 리눅스에서 브랜치가 바뀔 때마다 스크립트 실행시켜줘야 하는 문제 해결

    4) 아래 코드 실행

    새로 git clone을 할 때마다 아래 코드를 실행시켜줘야 한다. 한 번만 실행시키면 계속 적용된다. (window 기준)

    git config core.hooksPath .githooks

    ❗macOS는 git clone 할 때마다 아래 코드를 실행시켜줘야 한다.

    make

    참고 블로그 https://blog.deering.co/commit-convention/

    - + \ No newline at end of file diff --git a/page/39.html b/page/39.html index 1e13367..c19688d 100644 --- a/page/39.html +++ b/page/39.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 9분
    누누
    박스터

    안녕하세요 카페인팀 누누입니다

    이번에는 대량의 데이터를 DB에 넣는 과정을 최적화하는 과정에서 알게 된 내용을 공유하려고 합니다

    이번 최적화의 목표

    전기차 충전소에 대한 공공 데이터를 가져오고, 그 데이터를 DB 에 넣는 과정을 최적화해보자

    대량의 데이터를 삽입하는 과정

    저희 팀의 요구사항을 간단하게 정리하면 다음과 같습니다

    1. 대량의 데이터를 공공 데이터에서 전기차 충전소와 전기차 충전기에 대한 데이터를 가져온다
      • 충전소는 6만 개, 충전기는 23만 개의 데이터가 존재한다.
      • 한 번에 가져올 수 있는 양은 9999개 까지다.
    2. 이 데이터를 DB에 넣는다
      • 충전소와 충전기는 1:N 관계이다

    최적화 전은 어떤 상황이었는데?

    before_optimize

    위 사진을 잘 보시면 아실 수 있으시겠지만, 2000개를 저장하는데, 231.762 초가 사용되었습니다.

    물론 출력을 위한 시간도 포함되었기에, 230초 정도라고 생각하셔도 좋습니다

    1만 개라면? 231.762초 * 5 = 1,158.81초

    23만 개라면? 1158.81 * 23 = 26,652.63초

    시간으로 바꿔보면 7.4 시간이 걸린다는 것을 볼 수 있습니다

    이 과정에서 볼 수 있는 문제점

    1. 데이터를 저장할 때마다, 새로운 Transaction 이 생성된다.

    어떻게 개선할 수 있을까?

    데이터를 저장할 때마다, 새로운 Transaction 이 생성되는 것을 방지하기 위해, 전체를 하나의 트랜잭션으로 묶는다

    전체를 한 트랜잭션으로 묶은 버전

    all_in_transaction

    이 과정에서 2000개를 저장하는데 65초 가 사용되었습니다.

    1만 개라면? 65초 * 5 = 325초

    23만 개라면? 325초 * 23 = 7,475초

    시간으로 바꿔보면 2시간이 걸린다는 것을 볼 수 있습니다

    전체적으로 3배 정도 빨라졌습니다

    이 과정에서 볼 수 있는 문제점

    1. 23만 개의 저장이 모두 한 트랜잭션이 되어서, 하나가 실패하면 23만개를 새로 저장해야 하는 상황에 처한다

    어떻게 개선할 수 있을까?

    23만개의 저장이 모두 한 트랜잭션이 되는 것을 방지하기 위해, 1만 개씩 영속화시킨다

    1만 개가 한 트랜잭션으로 묶인 버전

    separateTransaction

    성능상으로 개선한 부분은 그렇게 크지 않지만, 실패했을 때, 1만 개만 다시 저장하면 되기에, 훨씬 빠르게 복구가 가능합니다.

    여기서 PageNo라는 클래스는, i를 바로 참조했을 경우, effectively final을 보장할 수 없어서 만들었습니다.

    성능은 전체를 한 트랜잭션으로 묶은 버전과 큰 차이가 나지 않습니다.

    이 과정에서 볼 수 있는 문제점

    1. id 생성 전략이 GenerationType.IDENTITY 이기에, 데이터를 저장할 때마다, DB에서 id를 생성해야 한다.

    JPA에 있는 쓰기 지연을 전혀 활용할 수 없고, DB에서 id를 생성하기 위해, DB와 매번 통신을 해야 한다.

    어떻게 개선할 수 있을까?

    id를 미리 생성해서, DB 에서 id 를 생성하는 과정을 생략한다

    ID 생성 전략을 GenerationType.Table의 형태로 바꿔서, DB에서 id를 생성하는 과정을 줄여서, 성능을 개선한다

    1만 개가 한 트랜잭션으로 묶이고, id를 미리 생성한 버전

    이때 batch size를 1000 단위로 설정해서 1000개씩 id 가 늘어나도록 설정했다

    charger_generatorstation_generator

    spring.jdbc.template.fetch-size=10000

    10000batch_size

    1자리 숫자는 앞에서부터 n(만개)를 의미하고, 2번째 숫자는 1만 개를 저장하는 데 걸린 시간(ms)을 의미합니다.

    처음 1만 개는 142초가 걸리고, 2만 개는 285초가 걸렸습니다.

    23만 개라면? 142 * 26 = 3,266초

    처음과 비교하자면 7.4시간이 걸리는 것에서 54분 정도 걸리는 것으로 개선되었습니다.

    이 과정에서 볼 수 있는 문제점

    하나의 스레드에서만 동작하기에, 성능이 개선되었지만, 여전히 느립니다.

    하나의 스레드에서만 동작하기에, 하나의 커넥션을 사용하게 됩니다.

    어떻게 개선할 수 있을까?

    여러 스레드에서 동작하게 하고, 여러 커넥션을 사용하게 합니다.

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전

    multi_thread

    이 버전에서 89991 개를 저장하는데 총 157초가 걸렸습니다.

    23만 개라면? 157 * 3 = 471초

    시간으로 바꿔보면 5분도 채 걸리지 않는 시간이죠

    이 과정에서 볼 수 있는 문제점

    hikari connection pool 사이즈를 10으로 설정했는데, 10개의 커넥션을 사용하면서 저장을 하다 보니, 10개의 커넥션을 모두 사용하고 나서, 11번째부터는 커넥션을 가져오기 위해, 기다려야 하는 상황이 발생합니다.

    어떻게 개선할 수 있을까?

    hikari connection pool 사이즈를 25로 설정해서, 25개의 커넥션을 사용하도록 합니다.

    spring.datasource.hikari.maximum-pool-size=25

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전 2

    multi_thread2

    총 13만 개의 데이터를 저장하는데, 147초가 걸리고, db 인스턴스의 cpu 사용률이 100%에 가까워져서 ec2 가 다운되었습니다.

    이 과정에서 볼 수 있는 문제점

    db의 cpu 사용량을 고려하지 않고, 23만 개가 조금 넘는 데이터를 25개의 커넥션을 활용해 저장하려고 했습니다

    결론

    1. 데이터를 저장할 때마다, transaction을 사용하지 말자
    2. 데이터를 저장할 때마다, id를 생성하지 말자
    3. 여러 스레드에서 동작하고, 여러 커넥션을 사용하자
    4. db의 cpu 사용량을 고려하자

    긴 글 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/page/4.html b/page/4.html index 02bab19..5fa309c 100644 --- a/page/4.html +++ b/page/4.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -13,7 +13,7 @@

    · 약 9분
    제이

    안녕하세요! 카페인팀의 제이입니다.

    저희 카페인 팀에서 무중단 배포를 진행했습니다. 어떤 과정으로 진행을 했는지 작성해보도록 하겠습니다!


    기존 배포 방식과 문제점

    먼저 카페인 팀의 기존 배포 방식은 다음과 같습니다.

    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근하여서 기존에 띄워진 서버를 다운 시킵니다.
    4. Docker Hub에 업로드한 Docker image를 pull해서 서버를 가동시킵니다.

    이런 과정으로 배포 스크립트가 작성되어 있습니다. 하지만 이 방법은 기존 서버를 다운 시키고 새로운 서버를 띄울 때 다운 타임이 존재한다는 문제점이 있습니다.

    사용자 입장에서는 잘 사용하고 있는데 갑자기 서비스가 작동되지 않는다면 서비스에 대한 신뢰성이 낮아질 수도 있고 이런 이유로 이탈할 수도 있습니다.

    기존 문제를 해결하기

    저희는 먼저 제한된 EC2 인스턴스로 인해 롤링 배포의 장점을 가져갈 수 없었고, 카나리 방식 또한 저희 서비스에서 필요로한 전략이 아니기 때문에 비교적 롤백도 빠른 Blue/Green 전략을 선택하였습니다.

    저희의 Blue/Green 무중단 배포 시나리오는 다음과 같습니다. 편의를 위해서 [기존 서버(기존 포트) / 새로운 서버(새로운 포트)] 라는 명칭을 사용하겠습니다.


    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub 에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근해서 Docker Hub에 업로드한 새로운 버전의 Image를 pull 해옵니다.
    4. 만약 8080 포트에 기존 서버가 띄워져 있으면 8081 포트를 새로운 서버가 띄워질 포트로 지정해주고, 반대로 8081 포트에 기존 서버가 띄워져 있으면 8080 포트에 새로운 서버가 띄워질 포트로 지정해줍니다.
    5. 미리 Docker Hub에 업로드한 Docker image를 [image+port]라는 네이밍으로 pull을 한 후 새로운 포트로 서버를 가동시킵니다.
    6. 새로운 서버가 제대로 가동 됐는지 확인하기 위해서 헬스 체크를 진행합니다. 20번 동안 서버가 정상 동작하는지 Spring Actuactor를 통해서 확인을 합니다.
    7. 정상 작동이 됐음을 확인하면 현재 인스턴스에는 2대의 서버가 띄워져있고 요청은 여전히 기존 서버로 들어가게 됩니다. 따라서 Nginx를 통해 포트포워딩을 새로운 서버의 포트로 지정해주고 기존 서버는 내려줍니다.

    여기까지가 카페인 팀의 시나리오입니다. 그렇다면 하나씩 스크립트를 확인해보겠습니다. 설명은 주석으로 달아두겠습니다 :)

    backend-deploy.yml

    (Github Actions에서 사용)

    name: deploy

    # 1. prod/backend branch에 push 작업이 일어나면 해당 작업을 수행한다
    on:
    push:
    branches:
    - prod/backend

    jobs:
    docker-build:
    runs-on: ubuntu-latest
    defaults:
    run:
    working-directory: ./backend

    steps:
    # 2. 도커 허브에 로그인
    - name: Log in to Docker Hub
    uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
    with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_PASSWORD }}
    - uses: actions/checkout@v3

    # 3. JDK 17 설치 및 빌드 (프로젝트 Java version)
    - name: Set up JDK 17
    uses: actions/setup-java@v3
    with:
    java-version: '17'
    distribution: 'adopt'

    - name: Gradle Caching
    uses: actions/cache@v3
    with:
    path: |
    ~/.gradle/caches
    ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
    ${{ runner.os }}-gradle-

    - name: Grant execute permission for gradlew
    run: chmod +x gradlew
    - name: Build for asciiDoc
    run: ./gradlew bootjar

    - name: Build with Gradle
    run: ./gradlew bootjar

    # 4. 산출물을 Image로 빌드 후 Docker Hub에 Image Push하기
    - name: Extract metadata (tags, labels) for Docker
    id: meta
    uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
    with:
    images: woowacarffeine/backend

    - name: Build and push Docker image
    uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
    with:
    context: .
    file: ./backend/Dockerfile
    push: true
    platforms: linux/arm64
    tags: woowacarffeine/backend:latest
    labels: ${{ steps.meta.outputs.labels }}


    deploy:
    # 5. Self-hosted 작동 -> infra 인스턴스에서 작동됨
    runs-on: self-hosted
    if: ${{ needs.docker-build.result == 'success' }}
    needs: [ docker-build ]
    steps:

    # 6. infra 인스턴스에서 prod 인스턴스로 접근 (아래부터는 prod 서버 내에서 작업)
    - name: Join EC2 prod server
    uses: appleboy/ssh-action@master
    env:
    JASYPT_KEY: ${{ secrets.JASYPT_KEY }}
    DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
    DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
    with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USERNAME }}
    key: ${{ secrets.SERVER_KEY }}
    port: ${{ secrets.SERVER_PORT }}
    envs: JASYPT_KEY, DATABASE_USERNAME, DATABASE_PASSWORD

    script: |

    # 7. Docker Hub에서 Image를 pull해온다
    sudo docker pull woowacarffeine/backend:latest

    # 8. 만약 8080 포트가 켜져 있으면 새로운 서버의 포트는 8081로 설정
    if sudo docker ps | grep ":8080"; then
    export BEFORE_PORT=8080
    export NEW_PORT=8081
    export NEW_ACTUATOR_PORT=8089

    # 9. 만약 8081 포트가 켜져 있으면 새로운 서버의 포트는 8080로 설정
    else
    export BEFORE_PORT=8081
    export NEW_PORT=8080
    export NEW_ACTUATOR_PORT=8088
    fi

    # 10. Docker로 새로운 서버를 띄운다.
    sudo docker run -d -p $NEW_PORT:8080 -p $NEW_ACTUATOR_PORT:8088 \
    -e "SPRING_PROFILE=prod" \
    -e "ENCRYPT_KEY=${{secrets.JASYPT_KEY}}" \
    -e "DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}}" \
    -e "DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}}" \
    -e "REPLICA_DATABASE_USERNAME=${{secrets.REPLICA_DB_USER_NAME}}" \
    -e "REPLICA_DATABASE_PASSWORD=${{secrets.REPLICA_DB_USER_PASSWORD}}" \
    -e "SLACK_WEBHOOK_URL=${{secrets.SLACK_WEBHOOK_URL}}" \
    --name backend$NEW_PORT \
    woowacarffeine/backend:latest

    # 11. prod 인스턴스에 있는 bluegreen.sh 를 작동한다. (이 때 port 값을 같이 넣어준다.)
    sudo sh /home/ubuntu/bluegreen.sh $BEFORE_PORT $NEW_PORT $NEW_ACTUATOR_PORT



    bluegreen.sh

    (prod 인스턴스 내부에 존재)

    #!/bin/bash

    # 1. Github Actions를 통해 넘겨 받은 환경변수 값
    BEFORE_PORT=$1
    NEW_PORT=$2
    NEW_ACTUATOR_PORT=$3

    echo "기존 포트 : $BEFORE_PORT"
    echo "새로운 포트: $NEW_PORT"
    echo "새로운 ACTUATOR_PORT: $NEW_ACTUATOR_PORT"


    # 2. 20번 동안 헬스 체크를 진행
    count=0
    for count in {0..20}
    do
    echo "서버 상태 확인(${count}/20)";

    # 3. 새로운 서버가 작동되는지 Actuator를 통해 값을 받아옴
    STATUS=$(curl -s http://127.0.0.1:${NEW_ACTUATOR_PORT}/actuator/health-check)

    # 4. Actuator를 통해 성공적으로 서버가 띄워지지 않은 경우
    if [ "${STATUS}" != '{"status":"up"}' ]
    then
    # 5. 10초를 기다린 후 다시 헬스 체크를 진행한다.
    sleep 10
    continue
    else
    # 6. 헬스 체크를 통해 새로운 서버가 성공적으로 작동된다면 멈춘다.
    break
    fi
    done


    # 7. 20번의 헬스 체크를 하는 동안 새로운 서버가 제대로 작동되지 않은 경우 종료
    if [ $count -eq 20 ]
    then
    echo "새로운 서버 배포를 실패했습니다."
    exit 1
    fi


    # 8. 새로운 서버가 성공적으로 작동한 경우
    # Nginx를 통해 포트포워딩을 기존 포트에서 새로운 포트로 변경해준다.
    # 이 부분은 .inc 파일을 통해 Nginx에서 주입 받아서 포트만 변경해도 됩니다!
    export BACKEND_PORT=$NEW_PORT
    envsubst '${BACKEND_PORT}' < backend.template > backend.conf
    sudo mv backend.conf /etc/nginx/conf.d/
    sudo nginx -s reload


    # 9. 기존 서버를 내려주고, 도커 리소스를 정리해준다
    docker stop backend$BEFORE_PORT
    sudo docker container prune -f

    이렇게 카페인 팀에서는 무중단 배포를 도입할 수 있었습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git a/page/40.html b/page/40.html index 4614b13..8f3b92a 100644 --- a/page/40.html +++ b/page/40.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 4분
    누누

    안녕하세요 우테코 카페인팀 누누입니다

    빠르게 결과부터 보고 가시죠.

    어떤 결과가 나왔나요?

    pr의 본문 끝에, 연관된 이슈 번호를 달아주는 기능을 만들었습니다.

    밑에 사진을 보시면 쉽게 이해하실 수 있을 것 같습니다.

    imgimg

    github에서 issue 번호가 pr에 담겨있다면 2가지 장점이 생기는데요.

    1. issue를 클릭했을 때, 자동으로 그 issue로 넘어갈 수 있습니다. (호버만으로 이슈에 대한 간단한 정보를 볼 수 있습니다)
    2. pr 이 merge 되었을 때, 자동으로 issue 가 close 됩니다.

    이 과정을 손으로 진행하는 것보다, 자동으로 진행하게 되면 실수도 줄어들고, 개발 과정이 편해질 것 같아서 이 기능을 제작하게 되었는데요

    중요한 점

    이 과정을 진행하려면 밑에서 소개해드릴 브랜치 네이밍 규칙이 필요합니다.

    브랜치 이름 규칙

    • 브랜치 이름은 타입/이슈번호 으로 구성합니다. ex) feat/1
    • 타입은 feat, fix, docs, refactor, test 등 여러 가지가 있을 수 있습니다.

    이렇게 했을 때, 이슈 번호를 브랜치 명에서부터 가져올 수 있기에, 자동화를 할 수 있습니다.

    이런 규칙이 아닌, feat/action 같은 형태가 된다면 issue 번호를 찾기 어렵겠죠?

    사용 방법

    작성된 코드부터 보시고, 설명을 드리겠습니다.

    아래에 작성된 코드를. github/workflows/assign_issue_number_to_pr_body.yml로 저장하시면 끝입니다.

    name: assign_issue_number_to_pr_body

    on:
    pull_request:
    types: [ opened ]
    branches-ignore:
    - develop

    jobs:
    append_issue_number_to_pr_body:
    runs-on: ubuntu-latest
    steps:
    - name: append feature number to pr body pr branch = feat/(issueNumber)
    uses: actions/github-script@v4
    with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    script: |
    const pr = await github.pulls.get({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number
    });
    const body = pr.data.body;
    const issueNumber= pr.data.head.ref.split('/')[1];
    const newBody = body + "\n\n" + "close #" + issueNumber;
    await github.pulls.update({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number,
    body: newBody
    });

    진행 과정

    1. pr 이 생성되면, pr에 대한 정보를 가져옵니다.
    2. pr의 본문을 가져옵니다.
    3. pr의 브랜치 이름에서 이슈 번호를 가져옵니다.
    4. pr의 본문에 이슈 번호를 추가합니다.
    5. pr의 본문을 업데이트합니다.

    이 과정을 통해서, 직접 pr의 본문을 수정하지 않아도, 자동으로 이슈 번호가 추가되기에, 실수를 줄일 수 있으니, 한 번 시도해 보세요

    - + \ No newline at end of file diff --git a/page/41.html b/page/41.html index 564b586..bec2c5a 100644 --- a/page/41.html +++ b/page/41.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -21,7 +21,7 @@ 물론 이는 계획이고 공부하지 않은 다른 내용이 있을 수 있기 때문에 언제든 바뀔 수 있습니다.

    무중단 배포 아키텍처 적용

    이 또한 아직은 먼 이야기지만, 고려해 볼 상황이라서 적어봤습니다.

    사용자가 이용하고 있는 서비스가 갑자기 중단된다면 어떨까요? 저는 화가 많이 날 것 같습니다.

    피치 못할 사정으로 서버가 터져도, 사용자가 서비스를 계속 이용할 방법이 없을까요?

    이런 고민을 해결하기 위해서 나온 개념이 무중단 배포입니다.

    카나리아 배포, Blue/Green 배포, 롤링등 무중단 배포를 위한 여러가지 전략은 이미 존재합니다. 이 부분은 아직은 서버의 명세가 정확하지 않아서 어떤 방식으로 어떻게 처리할 것인지에 대해서는 아직 정할 수는 없습니다.

    이는 명세가 확실하게 정해진 후 팀원과 장단점을 상의하며 결정할 일이기 때문에 현재까지는 "이 정도를 고려하고 있다." 정도만 알면 될 것 같습니다.

    - + \ No newline at end of file diff --git a/page/42.html b/page/42.html index d559593..344e35f 100644 --- a/page/42.html +++ b/page/42.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 6분
    누누

    우아한테크코스에서 자바 11을 사용하는 것이 너무 익숙해진 상황이어서, java 11 대신 java 17을 쓰려면 쓰는 대신, 왜 java 17을 쓰면 좋은지에 대해서 설득을 하는 시간이 있어야 하는데요

    처음에는 단순히 record 클래스가 좋아요, collect(Collectors.toList()); 대신 toList() 만으로 해결할 수 있어서 좋아요

    까지밖에 설명할 수 없었습니다.

    이것만으로 동의를 해줘서 일단 java 17 을 사용하기로 했지만, 이번 기회에 조금 더 자세하게 알아보려고 합니다

    Java 17 과 Java 11의 중요한 차이들

    기능적인 부분과, 숨겨진 부분을 나누어볼 수 있을 것 같습니다.

    기능적인 차이점

    언제나 직접 차이를 보면 더 직관적이기 때문에, 직접 코드를 보면서 설명을 해보려고 합니다

    record 클래스

    간단한 dto 클래스를 만들었을 때 코드가 정말 간단해지는 것을 확인할 수 있습니다

    Java 11

    public class Dto {
    private final int data;

    public Dto(int data) {
    this.data = data;
    }

    public int getData() {
    return data;
    }
    }

    lombok 을 사용했을 때


    @Getter
    @AllArgsConstructor
    public class Dto {
    private final int data;
    }

    Java17

    public record Record(int data) {
    }

    이렇게 보면 훨씬 간단해진 것을 볼 수 있습니다

    예상되는 문제점

    objectMapper를 사용하면 어떻게 되나요? noArgsConstructor 가 필요하지 않나요?

    class RecordTest {

    @Test
    void objectMapper_로_변환() throws JsonProcessingException {
    // given
    ObjectMapper objectMapper = new ObjectMapper();
    Record record = new Record(1);

    // when
    String json = objectMapper.writeValueAsString(record);

    // then
    assertEquals("{\"data\":1}", json);
    }

    @Test
    void string_에서_객체로_변환() throws JsonProcessingException {
    // given
    String json = "{\"data\":1}";
    ObjectMapper objectMapper = new ObjectMapper();

    // when
    Record record = objectMapper.readValue(json, Record.class);

    // then
    assertEquals(1, record.data());
    }
    }

    이 테스트에서 볼 수 있는 것처럼 성공적으로 deserialize, serialize 가 가능합니다

    toList() method

    Java 11

    이 부분도 정말 편의성이 높다고 생각하는 부분 중 하나인데요

    Collectors.toList() 대신 toList() 만으로도 사용이 가능합니다

    public class ToListWith11 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .collect(Collectors.toList());
    System.out.println(result);
    }
    }

    Java 17

    public class ToListWith17 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .toList();
    System.out.println(result);
    }
    }

    switch expression

    Java 11

    우테코에서는 switch, case 를 싫어하기에 볼 수는 없겠지만

    switch 문에도 정말 편하게 바뀌었는데요

    public class SwitchWith11 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = 0;
    switch (day) {
    case "Monday":
    result = 1;
    break;
    case "Tuesday":
    result = 2;
    break;
    case "Wednesday":
    result = 3;
    break;
    case "Thursday":
    result = 4;
    break;
    case "Friday":
    result = 5;
    break;
    case "Saturday":
    result = 6;
    break;
    case "Sunday":
    result = 7;
    break;
    }
    System.out.println(result);
    }
    }

    Java 17

    public class SwitchWith17 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = switch (day) {
    case "Monday" -> 1;
    case "Tuesday" -> 2;
    case "Wednesday" -> 3;
    case "Thursday" -> 4;
    case "Friday" -> 5;
    case "Saturday" -> 6;
    case "Sunday" -> 7;
    default -> 0;
    };
    System.out.println(result);
    }
    }

    코드 량이 엄청 줄어든 것을 확인하실 수 있습니다

    instanceof pattern matching

    물론 instanceof 를 사용할 경우가 많은가? 하면 많지는 않겠지만

    아래와 같이 변경되었습니다

    Java 11

    public class InstanceOfWith11 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.toUpperCase());
    }
    }
    }

    Java 17

    public class InstanceOfWith17 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String str) {
    System.out.println(str.toUpperCase());
    }
    }
    }

    number format

    이 기능은 12에 나왔는데요

    언어별로 숫자를 표현하는 방식이 다르지만, 쉽게 표현할 수 있도록 도와주는 기능입니다

    Java 17

    public class NumberFormatterWith11 {
    public static void main(String[] args) {
    int number = 1_000_000;

    String result = NumberFormat.getCompactNumberInstance(Locale.KOREA, NumberFormat.Style.LONG).format(number);

    System.out.println(result.equals("100만"));
    }
    }

    나머지 부분은 사실 그렇게 큰 역할을 할 것 같지는 않아서 생략하겠습니다

    숨겨진 부분들

    gc throughput

    위의 사진은 gc 의 버전별 처리량입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 15% 정도 향상되었고, java 11과는 10% 정도 향상되었습니다.

    gc latency

    위의 사진은 gc의 버전별 지연시간입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 30% 정도 향상되었고, java 11과는 25% 정도 향상되었습니다.

    이와 같이, 단순하게 새로운 기능만 추가되는 것이 아니라 꾸준히 성능도 향상되고 있습니다.

    이런 부분을 고려했을 때, Java 17을 사용하는 것이 좋을 것 같습니다.

    참고

    - + \ No newline at end of file diff --git a/page/43.html b/page/43.html index 138d598..b2c902b 100644 --- a/page/43.html +++ b/page/43.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 11분
    누누

    현재 상황은 어떤데?

    현재 우아한테크코스에서는 프론트 코드와 백엔드 코드가 같은 레포지토리를 사용하고 있습니다.

    프론트와 백엔드가 같이 작업하기에, 의도치 않은 충돌이 자주 생길 수 있는 구조이기에, 이를 git branch 전략으로 충돌을 줄이고자 합니다

    Git Branch 전략이란?

    git을 사용해서 소프트웨어 개발을 관리하는 방법입니다.

    여러 개발자가 동시에 작업하고 코드를 통합할 때 생기는 충돌을 효율적으로 조정하기 위한 방법입니다.

    왜 git branch 전략이 중요한데?

    아래에 있는 4가지를 제외하고도 훨씬 많은 장점이 있을 수 있습니다.

    1. 동시 작업이 편하다

    여러 사람이 독립적으로 작업하고, 커밋을 할 때, 자신의 브랜치에서 변경 사항을 커밋하게 됩니다.

    브랜치가 병합될 때만 충돌을 해결하면 되니, 아무 규칙이 없는 것보다 충돌 시점이 명확해지기에 생산성을 높일 수 있습니다.

    2. 목적이 명확한 브랜치

    애플리케이션의 상태에 몇 가지가 있는데, 안정된 프로덕션, 테스트 환경, 기능 추가 환경... 등이 있습니다

    여러 기능별 브랜치(안정된 버전의 코드만이 관리되는 브랜치, 테스트 환경을 위한 브랜치, 기능 추가를 위한 브랜치)를

    네이밍을 통해 구분하면 각각의 브랜치에 대해서 추가적인 설명을 할 필요 없이 명확하게 관리할 수 있습니다.

    3. 배포 파이프라인 관리가 편함

    브랜치가 네이밍으로 명확하게 구분이 되어있다면, 조건을 설정하기 쉽습니다.

    특정 타입의 브랜치에 push 되었을 때, pull request를 만들었을 때 같은 조건에 따른 스크립트를 만들어둔다면 CI/CD를 구축하기 쉽습니다.

    4. 버전 관리가 편리하다

    서버에 문제가 생겼을 때, 어떤 브랜치로 돌아가서 롤백을 해야 하는지에 대한 것들이 명확합니다.

    안정된 브랜치가 어떤 것인지 명확하기에, 롤백 과정에 대한 의사결정을 줄일 수 있습니다.

    그러면 어떤 종류가 있는지 더 자세하게 알아보도록 하겠습니다.

    Git Branch 전략의 종류는?

    총 3가지의 전략이 있습니다.

    1. Github Flow

    2. Gitlab Flow

    3. Git Flow

    git을 사용하기에, Git Flow라는 네이밍이 가장 직관적이고 유명한데요. 

    3가지 전략 중에서 가장 복잡하기에, 쉬운 순서대로 진행해 보도록 하겠습니다.

    1. Github Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img

    img2

    브랜치는 총 2가지 종류가 존재합니다

    1. master 브랜치

    여기에 머지가 되면 배포가 되도록 CD를 연결해 놓은 경우가 많습니다.

    안정된 버전의 코드가 관리되는 브랜치입니다.

    2. feature 브랜치

    기능 추가, 버그 수정 등 모든 작업은 feature 브랜치에서 일어납니다.

    master 브랜치에서 새로운 브랜치를 만들어서, 마스터로 머지되는 단순한 사이클을 가지고 있습니다.

    장점

    위에서 볼 수 있는 것처럼 2종류의 브랜치만 있기에, 정말 간단합니다.

    학습 과정까지의 러닝 커브가 거의 없다시피 하기에, 간단한 프로젝트에 적용하기 정말 좋습니다.

    릴리즈 되지 않은 코드가 최소화됩니다. 최신 버전의 코드와 최대한 빠르게 동기화를 계속해서 시킬 수 있습니다

    단점

    모든 코드는 다 master 브랜치에 머지가 되어야 한다는 점이 개발 서버와, 운영서버를 나누기 애매할 수 있습니다.

    개발 서버에 배포를 하고 싶은 상황이라면, master에 머지가 되어야 합니다.

    머지가 된 이후에 cd 파이프라인을 통해서 개발 서버와 운영 서버 모두에 배포가 됩니다.

    여러 환경을 나누고 관리를 하고 싶으시다면 다음에 소개해드릴 전략을 사용해 보셔도 좋을 것 같습니다

    2. Gitlab Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img2

    밑에 환경은 총 2개의 서버가 존재할 때를 가정하고 있습니다.

    1. pre-production 서버

    2. production 서버

    편의를 위해 main에 머지되는 과정은 간단하게 표현했습니다.

    img3

    브랜치 종류

    총 3가지 브랜치가 필요하고, 추가에 따라서 더 추가할 수 있습니다.

    1. main(or develop) 브랜치

    기능에 대한 개발이 완료되었지만, 여기에 머지되어도 바로 배포되지는 않습니다.

    2. feature브랜치

    기능을 개발하는 브랜치입니다. Github Flow 와도 유사합니다.

    3. production 브랜치

    실제 배포가 일어나는 브랜치입니다. 

    여기에 머지가 되는 순간 배포가 일어납니다.

    위 사진에 있는 것처럼, 필요에 따라서 pre-production이나, staging 같은 환경에 따른 브랜치를 추가할 수 있습니다.

    특징

    1. 무조건 단방향으로 머지가 일어납니다.

    긴급하게 라이브 서버에 수정을 해야 할 때, production 부터 시작하는 것이 아닌, main 부터 차근차근 올라가야 합니다

    2. 환경에 따라 브랜치 종류가 늘어날 수 있습니다.

    위 사진에서는 pre-production 이 그 예시가 되겠네요.

    장점

    1. Github Flow에서 환경별 브랜치를 통해서 개발 서버나 pre-production 서버에 버전을 깔끔하게 관리할 수 있습니다.

    3. Git Flow

    브랜치 전략 중 가장 처음으로 유명해진 브랜치 전략입니다.

    배포가 특정 주기를 가지고 있는 애플리케이션일 때, 가장 적합합니다.

    가장 복잡한 전략을 가지고 있어서, 모두가 브랜치 전략에 대해서 이해하고 있다면 역할에 따른 깔끔한 분리가 가능합니다

    그림으로 보고 가도록 하겠습니다

    img4

    가장 유명한 브랜치 전략이지만, 가장 어려운 전략이기도 합니다.

    특징

    1. 브랜치에 대해서 양방향으로 머지가 일어납니다

    release 브랜치에서 버그 수정이 일어나면, develop 브랜치에도 머지해줘야 합니다.

    hotfix 브랜치를 main 브랜치뿐만 아니라, develop 브랜치에도 머지해줘야 합니다

    브랜치의 종류가 5가지나 됩니다

    1. main

    production 이 배포되었을 때, 이 브랜치에 머지되는 것이 기준이 됩니다.

    2. develop 

    위에서 설명드렸던 브랜치들과 큰 차이가 없이 배포 전 브랜치입니다.

    3. feature

    기능을 개발할 때 사용하는 브랜치입니다. 이것도 위와 큰 차이가 없습니다

    4. release

    Gitlab Flow에서 pre-production에 해당한다고 봐도 무방합니다.

    여기서 버그 수정이 일어났을 경우에,  develop에 머지하는 것을 까먹으면 안 됩니다.

    5. hotfix

    main 브랜치에서 생성된 브랜치로, 긴급한 변경사항을 처리합니다.

    이때, develop에 머지하는 것을 깜빡하면 안 됩니다.

    더 자세하게 알아보실 분은 아래 링크들을 확인해 보세요

    우리 프로젝트에는 어떤 것이 적절할까?

    나중에 개발 서버 혹은 스테이징 서버를 두고 싶기에, 이 부분에 대한 처리가 부족한 Github Flow는 적절하지 않습니다.

    Git Flow는 깔끔하게 처리할 수 있지만, 러닝 커브가 Gitlab Flow 보다 약간 더 있어서, 빠르게 개발하는 취지에 맞지 않아 보였습니다.

    이런 과정을 통해서 Gitlab Flow를 사용하려고 합니다 

    참고

    https://techblog.woowahan.com/2553/

    https://docs.gitlab.com/ee/topics/gitlab_flow.html

    - + \ No newline at end of file diff --git a/page/44.html b/page/44.html index 4766f88..7247fec 100644 --- a/page/44.html +++ b/page/44.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - + - + \ No newline at end of file diff --git a/page/5.html b/page/5.html index 08cf837..bc412b7 100644 --- a/page/5.html +++ b/page/5.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -13,7 +13,7 @@

    · 약 15분
    가브리엘
    센트

    안녕하세요? 센트와 가브리엘 입니다.

    저희 카페인 팀에서는 지난번 카페인 서비스 1차 체험 진행 이후 일부 기능 개선이 있었습니다. 기능 개선의 유용성을 판별하고자 카페인 서비스 2차 체험을 다녀왔습니다.

    저희 팀에서 1차 체험 이후 개선한 사항은 다음과 같습니다.

    1. 지역검색

    no offset

    • 이제는 검색어를 입력하는 경우, 전국 도시의 주소가 같이 제공됩니다.

    2. 충전소 마커를 확인할 수 있는 지도 영역 확장

    no offset

    (기존에는 위 사진보다 좁은 영역만을 호출하는 것이 허용되었다.)

    • 모바일에서 좀 더 넓은 영역을 호출하는 것을 허용했습니다. 원래는 디바이스 너비를 고려하지 않고 줌 레벨 기준으로 요청을 제한했으나, 이제는 사용자 디바이스에 보이는 지도의 영역 크기를 기반으로 요청을 제한하는 방식을 도입했습니다.
    • 기존에 사용하던 마커의 단점은, 그 크기가 너무 크다는 것이었습니다. 이로인해 더 넓은 영역을 보여주는 경우에 마커들이 겹치는 현상이 있었는데요, 이를 수정하기 위해 특정 영역 크기 이상에서는 마커를 좀 더 간소화 된 디자인으로 보이도록 개선하였습니다.
    • 마커 사이즈가 작아지면서 사용 가능한 충전기 개수가 더이상 들어갈 공간이 없어졌습니다. 따라서 마커 색상은 그대로 유지를 하되, 인포 윈도우에 현재 사용 가능한 충전기 개수를 보여주는 방식으로 디자인을 개선하였습니다.

    체험 규칙 설정

    개선한 기능이 실제로 유용한지 확인해보기 위해 저희는 카페인 서비스 2차 체험의 규칙을 다음과 같이 설정했습니다.

    저희는 좀 더 의미있는 경험을 하기위해 1차 체험 때 정했던 규칙에 더해서 다음과 같은 추가 규칙을 설정하였습니다.

    중간에 목표 지점이 많이 변경된다

    지난 카페인 서비스 1차 체험에서는 지역 검색이 없어 목표 지점을 찾는 것이 불편했습니다. 1차 체험 이후 지역 검색이 추가 되었으므로 이 기능이 얼마나 유용한지 경험해보고자 이 규칙을 설정했습니다.

    추가로 목표 지점 주변의 충전소를 확인할 때 새로 추가된 지도 영역 확장이 얼마나 유용한지도 경험해보고자 했습니다.

    체험 개요

    no offset

    1. 잠실역 출발
    2. 하남 만두집
    3. 다음 목적지 설정
    4. 판교

    체험 후기

    잠실역 출발

    no offset

    쏘카에서 EV6를 대여해서 가브리엘, 센트, 키아라가 잠실역에서 출발하였습니다. 저녁 퇴근 이후에 남이섬을 가려고 목적지를 설정하였으나 배가 너무 고파서 가는 길에 식사를 하자고 얘기가 나왔습니다.

    하남 만두집

    따라서 진정한 처음 목적지는 스타필드였으나, 가브리엘은 동네 주민이라 스타필드를 너무 잘 알고 있었습니다. 따라서 스타필드에 전기차 충전소가 어디에 있는지도 알고있으므로 목적지를 급하게 변경하기로 했습니다. 이 때 목적지 변경을 위해 주변 식당을 둘러보던 중에 괜찮은 식당을 발견해서 해당 식당을 기준으로 주변 충전소를 확인해보기로 했습니다.

    no offset

    식당 주변을 가기 위해 지역 검색을 처음으로 사용하여 식당과 가까운 지역을 탐색할 수 있었습니다.

    이 과정에서 식당에는 충전소가 없다는 사실을 알게되어, 근처 충전소를 찾아보기 위해서 지도를 축소했더니 1차 체험때와는 달리 더 넓은 영역을 보여줬습니다. 이전에는 마커 자체가 보이지 않아 답답하였으나, 이제는 더 넓은 영역을 조회할 수 있게 되어 편리했습니다.

    지난 체험 이후로 피드백을 자체 수집하여 개발한 기능들이 편하다는 것을 식당에 가는 길에 느낄 수 있었습니다.

    다음 목적지 설정

    no offset

    하남 만두집에서 식사를 하다가 알게된 사실은, 남이섬은 생각보다 너무 멀다는 것이었습니다. 식사를 마치고 남이섬에 가면, 충전도 제대로 못하고 돌아올 판이었습니다.

    식사를 하면서 다른 목적지를 알아봤는데, 가브리엘이 예전에 가봤던 곳 중에서 남양주의 물의 정원이 시간을 떼우기 좋다는 소리를 하였습니다. 따라서 물의 정원을 검색해보았습니다.

    놀랍게도 물의정원은 검색결과에 없었습니다!

    어쩔 수 없이 카카오 지도로 물의 정원 위치를 확인하여 주소를 알아내었고, 이 주소를 카페인 검색창에 넣었습니다. 저희는 이 과정에서 카페인 서비스는 업체명 조회가 안된다는 것이 치명적인 단점이라고 생각했습니다. 다만, 이 기능은 검색 할 때마다 많은 비용이 청구되어 현실적으로 지금 당장 기능을 넣는 것은 어렵다고 판단했습니다.

    결국 주소 검색을 통해 물의 정원과 가장 가까운 충전소를 알아내었습니다.

    그런데! 지도를 축소해서 확인해 보니 해당 충전소는 물의 정원과 생각보다 멀었습니다.

    no offset

    no offset

    무려 걸어서 30분이나 걸리는 충전소였습니다!

    전기차 충전을 위해 왕복 1시간이나 걸리는 거리를 걸을 수 없다고 생각하였습니다.

    물론 지난 체험에서 전기차가 생각보다 배터리가 오래간다는 사실을 알고 있었지만, 만약 저희처럼 충전이 급한 사용자라면 목적지를 포기할 수 밖에 없겠구나 라는 생각이 들었습니다.

    마지막으로 정한 목적지는, 의외의 결정이었습니다.

    굉장히 발전된 첨단 도시로 알려진 판교였습니다!

    사실은 앞으로 갈지도 모르는 판교를 미리 구경이나 해보자는게 이유였지만 비밀입니다(?)

    일단 판교역은 IT서비스 회사들이 많이 몰려있는 곳이었습니다.

    따라서 저희는 판교역을 카페인 검색창에 검색했습니다.

    no offset

    지도를 판교역으로 이동하여 외부인 개방인 충전소를 찾았는데, 판교공영주차장이 보여서 해당 충전소를 목적지로 잡고 출발했습니다.

    판교

    하남에서 판교를 가기 위해서는 서하남IC를 지나야했습니다.

    가는 길에 우리 서비스에 나오는 정보와 실제 정보가 일치하는지 점검차 서하남 간이 휴게소를 들려봤습니다.

    이 휴게소에도 충전소가 있다고 검색이 되었기 때문입니다!

    no offset

    검색 당시에는 2대의 충전기가 있다고 나왔고, 둘다 사용이 가능하다고 되어있었는데 실제로 확인해보니 일치하는 것을 확인했습니다.

    먼 길을 달려 판교에 도착하였습니다.

    주차장에 들어오기 전, 카페인 서비스를 확인해보니 판교공영주차장의 충전기 총 12기 중 10기가 사용가능한 상태였습니다.

    정작 들어와서 보니 입구부터 너무 많은 전기차들이 충전기를 사용중이었습니다.

    뭔가 이상하다 싶었지만, 아직 서버에 반영이 안된건가? 하면서 비어있는 충전기를 찾았습니다.

    no offset no offset

    충전기를 꽂고 나서 알게된 것은 카페인 서비스에 나온 충전소 회사명과 방금 꽂은 충전기 회사명이 다르다는 것이었습니다.

    알고보니 음성 인식으로 네비에 검색한 충전소는 판교공영주차장이 아닌 판교역 환승 주차장이라 엉뚱한 곳으로 온 것이었습니다!!!

    다행인 점은 우리 서비스에서 제공하는 충전기 사용 여부 정보가 잘못된 것이 아니었다는 것이었습니다.

    그래서 애초에 가고자 했던 판교공영주자창에 대한 카페인 서비스의 정보가 실제와 동일한지 확인해보러 걸어서 이동했습니다. (바로 앞에 있었기 때문입니다.)

    no offset no offset

    도착해보니 1층의 충전기들이 모두 공사중이었고, 서비스의 정보가 실제로도 불일치 하는 줄 알았습니다. 다시 상세 정보를 보니 3~6층에 충전기들에 대한 정보라는 것이 명시되어 있었고, 실제로도 이와 동일한 것을 확인했습니다.

    no offset

    저희는 시간이 너무 흘러 다시 잠실로 돌아와 차를 반납하고 체험을 마무리 했습니다.

    결론

    불편했던 점

    • 디바이스에 보여지는 지도 영역 확장시에 원하는 정보를 볼 수 없는 것이 불편했다.
      • 지도를 확대해주세요 모달이 뜨고, 원래 있던 충전소 마커가 전부 사라진다.
    • 현재 나의 위치를 알아볼 수 있는 수단이 없어 불편했다.
      • 현위치를 나타내는 핀 (1차 체험기에서도 언급했던 부분)
      • 내 위치를 상대적으로 알 수 있는 랜드마크의 부족
    • 특정 장소(매장명) 검색이 안돼서 카페인 서비스만으로 목적지를 찾아가기 불편했다.
      • 카카오맵 등을 활용해 특정 장소 검색을 진행해야 했다.

    다음 목표

    앞선 불편했던점을 개선하기 위해 다음과 같은 기능 개선을 추가로 진행할 예정입니다.

    • 디바이스에 보여지는 지도 영역 확장에 제한이 생기지 않게 충전소 마커 클러스터링을 우선적으로 도입한다.
    • 현재 나의 위치를 알아볼 수 있도록 지하철 역과 같은 랜드마커를 지웠던 것을 롤백한다.

    카페인 서비스만으로 목적지를 찾아갈 수 있도록 하기 위해서 특정 장소 검색을 추가하고 싶지만, 해당 기능을 구현하기 위해선 검색당 비용이 많이 청구되는 장소 검색 API를 추가해야 했기에 현실적으로 지금 당장 구현하기 어렵다고 판단했습니다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git a/page/6.html b/page/6.html index 8830184..337ec91 100644 --- a/page/6.html +++ b/page/6.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -27,7 +27,7 @@ 차주 분과 인터뷰 하고 싶었지만, 차 내부에서 너무 바빠보이셔서 그럴 수 없었습니다.

    전기차 충전을 기다리면서 무엇을 할 수 있을까요? 이 분은 다행히도 업무를 보고 계셨지만, 다른 차주들은 무엇을 하고 보낼지 궁금해졌습니다.

    no offset

    휴게소에는 충전소가 하나 더 있었습니다.

    한 곳은 사용중이지만, 다른 한 곳은 사용할 수 있었습니다.

    저희는 이 충전소를 사용해보기로 했습니다.

    no offset

    사용할 수 있으니깐 들어가봐야지! 하고 도착한 순간 아차 싶었습니다.

    "아, 충전소가 외부인 사용 금지일 수 있었지?"

    저희는 분명히 서비스를 직접 개발했으니깐 다 알고 있던 사항이었지만, 전혀 생각치 못했습니다.

    서비스를 개발하는 내내 외부인 개방 충전소에 대한 중요성을 간파하였고, 이 기능을 넣었으면서도 사용하지 않고 충전소를 방문한 것이었습니다.

    바로 앞에 있어서 다행이었지만, 어찌됐든 이 충전소를 사용할 수 없었습니다.

    따라서 저희는 휴게소를 떠나는 내내 이 문제에 대해서 토론을 할 수 밖에 없었습니다.

    분명 우리가 만든 서비스인데 왜 놓쳤을까?

    맛있는 점심

    no offset

    파주닭국수 본점에서 맛있는 식사를 했습니다.

    비록 식당에는 전기차 충전소가 없었지만, 인근에 충전소가 있어 실험을 하나 해볼 수 있었습니다.

    인근 충전소와 식당의 거리가 가까워 보이는데, 과연 걸어갈 수 있을까?

    실제로 걷지는 않았습니다만 차 타면서 지나가면서 확인해본 결과 직접 걸을 수 없는 거리였습니다. (굉장히 걷기 싫은 수준의 먼 거리였습니다.)

    집에 있는 PHEV를 탈 기회가 많아 전기차 충전소를 자주 방문했던 저는 이런 점을 잘 알고 있었습니다.

    다행히 이 부분을 잘 알고 있었기에 저희는 이 부분을 서비스에 반영하였고, 모든 데이터를 포기하지 않았던 것이 옳은 선택이었다는 것을 확인하게 되었습니다.

    no offset

    식사가 끝나고 드디어 마장호수로 출발하게 되었습니다.

    마장호수 도착

    마장호수에 도착하자마자 충전소에 방문했습니다.

    no offset

    통계에서는 사용률이 적을 것이라고 하였는데 저희만 있었습니다.

    no offset no offset

    2기 중 1곳을 저희가 사용하였고, 마장호수를 돌았습니다.

    no offset

    약 50분 간 산책을 하고, 돌아와보니 충전기 다 되어있었습니다.

    사실 마장호수 까지 오는 내내 든 생각이었지만, 전기차의 배터리가 생각보다 오래 간다는 생각이 들었습니다.

    일부러 회생제동 기능도 끄고, 에어컨을 강하게 틀어서 배터리를 소진하려고 하였으나, 85km를 주행하는 동안 겨우 20%를 소모하였습니다.

    충전기를 꽂을 때 50%였으나, 호수를 한바퀴 돌고 오니 이미 100%가 되어있었습니다.

    여담이지만, 저희가 돌아왔을 때 옆 자리에는 전기 화물차가 있어 충전소가 가득 찼습니다.

    또, 앱에서도 충전기 사용 여부가 업데이트 되는 것을 확인했습니다.

    no offset

    배터리 성능에는 좋지 않고 가격도 비싸서 이를 자주 사용하는 것은 좋지 않겠지만, 급한 사람들은 급속 충전기를 사용하면 되겠구나 싶었습니다.

    따라서 급속과 완속은 더더욱 다른 개념으로 봐야겠다는 생각이 들었습니다.

    제가 그동안 경험했던 전기차 충전소는 완속 기준이었기에 신선한 경험이었습니다.

    선릉으로 돌아오다

    no offset

    선릉으로 돌아와서 차량을 반납하였습니다.

    저희는 이번 여정을 통해 카페인 서비스에서 어떤 점을 개선해야할지 좀 더 명확하게 알게되었습니다.

    1. 현재 서비스에서 제공하는 기능들로 충전소를 검색하는 것은 가능하며, 충전소의 위치를 정확하게 파악하는 것도 가능하다.
    2. 하지만 충전소가 없는 목적지는 검색할 수 없고, 현 위치가 어디인지 가늠하기가 어려워진다.
    3. 충전소를 사용할 수 있다고 표기되어 있더라도 외부인 개방이 아닐 수 있다. 정보가 정확히 제공됨에도 불구하고 이를 단번에 눈치채기 어렵다.
    4. 이러한 문제를 예상하여 외부인 개방 여부를 필터링 할 수 있는 기능을 제공하고 있음에도 불구하고 사용하지 않았다.
    5. 충전소의 통계 자료의 적중률은 높았으나, 좀 더 많은 충전소를 들려 확인해봐야 할 것 같았다.
    6. 전기자동차는 생각보다 오래가고 상품성이 있었다. 주행 능력도 충분하고, 인프라가 잘 되어있다. 이걸 왜 욕하지? 라는 생각이 들었다.
    7. 지도 확대 허용 범위가 너무 좁아서 사용하는데 불편한건 실제 상황에서 더 불편했다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git a/page/7.html b/page/7.html index dba793d..aabbd39 100644 --- a/page/7.html +++ b/page/7.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -21,7 +21,7 @@ 이 정보를 제외하고 마커를 띄우기 위해 필요한 최소한의 정보를 조회하도록 수정해 서버의 부하를 낮췄습니다.

    이러한 변경으로 인해 충전소 조회 API의 성능이 개선되었습니다. 필요한 정보만을 조회하므로써 데이터베이스의 부하를 줄이고 응답 시간을 단축할 수 있게 되었습니다. 또한, 프론트엔드에서는 필요한 정보만을 호출하여 불필요한 데이터를 받아오지 않아도 되므로 클라이언트 측의 성능도 향상되었습니다.

    - + \ No newline at end of file diff --git a/page/8.html b/page/8.html index 05f48f0..4f15b7b 100644 --- a/page/8.html +++ b/page/8.html @@ -5,7 +5,7 @@ Blog | CAR-FFEINE - + @@ -20,7 +20,7 @@ no offset no offset no offset

    집계 된 자료처럼 방문자들이 단순 방문만 한 것이 아니라, 수 많은 이벤트를 발생시키고 평균 참여 시간도 상당 부분 확보했음을 확인할 수 있습니다.

    - + \ No newline at end of file diff --git a/page/9.html b/page/9.html index dd435df..0e711f9 100644 --- a/page/9.html +++ b/page/9.html @@ -5,13 +5,13 @@ Blog | CAR-FFEINE - +

    · 약 13분
    센트

    1. 개요

    기존의 구조에서는 마커 하나를 렌더링하기 위해 다음과 같은 과정을 거쳤다.

    1. StationMarkersContainer 컴포넌트에서 충전소 정보 요청
    2. 충전소 정보를 props로 넘겨 Marker 컴포넌트 호출
    3. 지도에 부착될 DOM요소 생성
    4. createRoot를 통해 리액트 root 생성
    5. 2번에서 생성한 DOM 요소를 전달해 구글 지도 api의 Marker 생성자 함수 호출
    6. 3번에서 생성했던 root의 render 메서드 호출
    7. 마커 인스턴스 전역 상태에 새로 생성한 마커 추가

    위 과정을 거쳤을 때의 마커 렌더링 모습을 보면 다음과 같다.

    before

    마커들이 한번에 렌더링 되는 것이 아니라 산발적으로 렌더링 되는 모습을 확인할 수 있다.

    2. 문제 원인 분석

    마커를 렌더링 하기 위해 거치는 과정을 분석해 보았다.

    1 ~ 3 과정에서는 성능에 크게 영향을 끼칠 요소가 없지만 4번 과정은 일반적인 리액트 프로젝트를 개발할 때 겪는 과정이 아니다. 따라서 createRoot를 통해 많은 개수의 루트를 생성했을 때의 영향에 대해 알아보았다.

    image

    리액트 공식 문서를 보니 페이지의 일부에 리액트를 뿌려서 사용하는 경우에는 루트를 필요한 만큼 생성해도 된다는 이야기가 포함되어 있었다. 따라서 4번 과정 또한 문제의 원인이라고 볼 수 없었다.

    5번 과정은 구글 지도에 마커를 특정 위도 경도에 위치시키기 위해서 어쩔 수 없이 거쳐야 하는 과정이므로 이 과정은 문제가 있더라도 개선이 불가능해 일단 고려하지 않았다.

    6번 과정은 4번 과정에서 생성했던 리액트 루트의 render 메서드를 호출해 실제로 화면에 리액트 컴포넌트를 그리도록 하는 과정이다. 이 과정 또한 리액트 컴포넌트를 화면에 렌더링하기 위해선 어쩔 수 없이 거쳐야 하는 과정이므로 고려하지 않았다.

    하지만 6번 과정에서 리액트 컴포넌트를 직접 그리는 것이 아니라 구글 지도 api의 기본 마커를 사용하면 성능을 향상시킬 수 있지 않냐고 반문할 수도 있을 것이다. 이전에는 이러한 방식을 사용해 마커를 렌더링 했었다. 우리의 서비스는 현재 사용 가능한 충전소 개수를 마커를 통해서도 전달하기 때문에 이를 고려해 기본 마커를 사용할 때 다음의 두 가지 문제가 생긴다.

    1. 사용 가능한 충전소 개수를 기본 마커에 렌더링 할 때 성능이 매우 좋지 않다.
    2. 마커의 디자인을 바꾸고자 할 때 변경에 대응하기 어렵다.

    따라서 마커는 리액트 루트의 render 메서드를 호출해 리액트 컴포넌트를 렌더링하는 것으로 결정했다.

    마지막으로 남은 7번 과정에서는 useSyncExternalState 훅을 사용해 전역적으로 관리하고 있던 상태에 수정을 가하는 연산을 수행한다. 이 과정은 이전에도 성능 저하를 유발할 것으로 예상되던 부분이었다. (하단 링크 참고)

    useSyncExternalStore 훅을 통해 구독한 state가 한번에 업데이트 되는 이유

    요청의 결과로 받아온 마커 정보의 개수가 100개라고 가정해보자. 우리는 이제 마커를 렌더링 할 것이다. 첫 번째 마커의 렌더링을 위해 1번 ~ 6번의 과정을 거친 후 7번 과정을 수행한다. 그러면 리액트 입장에서는 리액트 루트의 render 메서드 호출에 대한 동작을 수행해야 하고, 새로운 마커 인스턴스에 대한 전역 상태를 변경시키는 동작을 수행해야 한다. 리액트가 이 과정을 100번 반복하고 나면 우리는 비로소 모든 마커가 화면에 렌더링 된 모습을 볼 수 있을 것이다.

    나는 이 부분에서 성능 저하의 요소가 있다고 생각했다. 리액트에서의 상태 변화는 곧 리액트 내부의 렌더링을 위한 로직이 수행되게 함을 의미하고, 이 과정을 개선 이전에는 마커의 개수만큼 반복하고 있었던 것이다. 여기까지 생각해보니 전역 상태 변화에 대해 리액트가 렌더링을 위한 연산을 진행할 동안에는 마커의 렌더링(render 메서드 호출)이 멈추는 것이 아닐까 하는 생각이 들었다.

    그래서 크롬 개발자 도구의 퍼포먼스 탭을 들어가 보니 산발적으로 발생하던 마커 렌더링의 문제 원인이 짐작했던 그 원인임을 확인할 수 있었다.

    image

    프레임 이미지 하단을 보면 산발적인 마커 렌더링이 수행될 때마다 수반되는 어떤 함수 호출이 있음을 확인할 수 있다.

    image

    이 부분이 문제의 함수 호출 부분이다. 자세히 살펴보면 상단에 performWorkUntilDeadline이란 함수가 호출됨을 볼 수 있다.

    image

    performWorkUntilDeadline 라는 함수를 조금 알아보니 해당 함수는 간단히 말해 리액트에서 state의 변경이 한번에 많이 발생할 때 5ms의 데드라인 시간을 줄 때 사용하는 함수라는 것을 알게 되었다. 문제의 원인이라고 생각했던 마커 개수 만큼의 전역 상태 변화가 실제로 마커 렌더링을 잠시 중단하게 만들고 있음을 알게 되었다.

    3. 문제 해결

    앞서 분석한 문제를 개선해보고자 마커 렌더링에 필요한 충전소 정보 배열을 부모 컴포넌트에서 받아와 각 충전소 정보를 자식 컴포넌트에 넘겨주고, 자식 컴포넌트에서 마커 생성과 렌더링 로직을 수행하던 기존의 방식을 부수고 부모 컴포넌트에서 모든 것을 일괄 처리하는 방식으로 고쳐보았다.

    고치는 과정에서 기존 방식에서는 리액트 생명 주기에 의존하여 화면에 보여지지 않는 마커를 지워주던 로직을 이제는 모두 직접 구현해야 했다.

    이전의 영역과 겹치는 부분에 있는 충전소는 다시 그리지 않고, 영역 밖의 충전소를 나타내는 마커는 지워주고, 이전의 영역과 겹치지 않는 새로 받아온 충전소는 그리도록 다음과 같이 메서드를 분리해보았다.

    • 기존과 겹치지 않는 새로운 영역에 대한 마커를 생성하는 메서드
    • 기존과 겹쳐지는 영역에 대한 마커들을 반환하는 메서드
    • 새로운 영역 밖에 있는 마커들을 지워주는 메서드
    • 새롭게 생성된 마커를 화면에 렌더링하는 메서드

    이 메서드들을 커스텀 훅으로 분리해 부모 컴포넌트에서 이를 활용하도록 하여 다소 복잡할 수 있는 마커 렌더링 로직을 선언적으로 구현할 수 있도록 했다.

    결과적으로 기존에 사용되던 기능들을 그대로 사용할 수 있으면서 화면에 마커가 산발적으로 렌더링 되던 문제가 해결 되었고, 부가적인 효과로 전체 마커의 렌더링 시점도 앞당길 수 있게 되었다. + 기존에는 구조적인 문제로 연산량이 너무 많아 클러스터링이 늦어져 이를 도입할 수 없었던 문제를 구조 수정으로 인해 적용할 수 있게 되었다.

    작업한 PR

    https://github.com/woowacourse-teams/2023-car-ffeine/pull/737

    결과 분석 (performance 탭 활용)

    before

    마커 조회 요청이 종료된 시점: 약 2499ms

    image

    첫 마커 렌더링 시점: 3093ms

    image

    모든 마커 렌더링 종료 시점: 약 3611ms

    image

    처음으로 마커가 렌더링 될 때까지 소요된 시간: 594ms

    모든 마커 렌더링에 소요된 시간: 1112ms

    after

    마커 조회 요청의 시작점: 약 1875ms

    image

    모든 마커 렌더링 종료 시점: 2395ms

    image

    처음으로 마커가 렌더링 될 때까지 소요된 시간: 519ms

    모든 마커 렌더링에 소요된 시간: 519ms

    개선 결과

    처음으로 마커가 렌더링 되는 시점은 두 방식 모두 비슷한 결과를 보인다. 하지만 개선 후 방식은 한번에 모든 마커가 렌더링 되는 방식이고, 개선 이전의 방식은 산발적으로 마커가 렌더링 되는 방식이므로 개선 후의 방식에서 전체 마커를 렌더링 하는 시점이 훨씬 빨라지게 되었다.

    결과적으로 전체 마커가 렌더링 되는 속도 약 55.6% 단축하게 되었다. 이 결과는 마커가 늘어날 수록 더욱 차이가 극적으로 벌어질 것으로 예상된다.

    before

    before

    after

    after

    - + \ No newline at end of file diff --git a/rss.xml b/rss.xml index 6facc10..622c128 100644 --- a/rss.xml +++ b/rss.xml @@ -42,7 +42,8 @@ no offset no offset no offset -no offset

    ]]> +no offset +no offset

    ]]> 카페인 서비스 경험 피드백 diff --git a/tags.html b/tags.html index d7aa49b..5a0e0b9 100644 --- a/tags.html +++ b/tags.html @@ -5,13 +5,13 @@ 태그 | CAR-FFEINE - + - + \ No newline at end of file diff --git a/tags/action.html b/tags/action.html index fe1c946..ef625d7 100644 --- a/tags/action.html +++ b/tags/action.html @@ -5,7 +5,7 @@ "action" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -17,7 +17,7 @@ 아까 environment 속성을 보면 test라고 설정해놓은 것을 볼 수 있습니다. 해당 환경이 여기에 적용됩니다.

    Branch rule 정의하기

    이번에는 해당 Repository의 Settings -> Branches 탭으로 들어갑니다. 그리고 원하는 branch에 들어가 edit 버튼을 누릅니다.

    그리고 사진과 같이 Require deployments to succeed before merging 속성을 클릭합니다. 그리고 아래와 같이 어떤 환경을 적용할 것인지 선택할 수 있습니다.

    이 속성은 해당 배포가 성공해야 merge 할 수 있도록 브랜치를 보호하는 기능입니다.

    그리고 저희는 frontend와 backend Job의 환경을 둘 다 test라는 이름으로 정의했기 때문에 하나의 environment만 선택해도 둘 다 적용되는 효과를 볼 수 있습니다. branch rule

    적용 후

    아래와 같이 merge가 안된다는 글과 빨간색으로 경고 표시를 해주고 있습니다. blocked

    결론

    간단한 github action을 통해서 생산성을 많이 올릴 수 있는 좋은 기능인 것 같습니다. 다른 팀들도 이 기능을 도입하여 사용하는 것을 추천드립니다.

    - + \ No newline at end of file diff --git a/tags/action/page/2.html b/tags/action/page/2.html index 33d6039..32fb672 100644 --- a/tags/action/page/2.html +++ b/tags/action/page/2.html @@ -5,13 +5,13 @@ "action" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "action" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 4분
    누누

    안녕하세요 우테코 카페인팀 누누입니다

    빠르게 결과부터 보고 가시죠.

    어떤 결과가 나왔나요?

    pr의 본문 끝에, 연관된 이슈 번호를 달아주는 기능을 만들었습니다.

    밑에 사진을 보시면 쉽게 이해하실 수 있을 것 같습니다.

    imgimg

    github에서 issue 번호가 pr에 담겨있다면 2가지 장점이 생기는데요.

    1. issue를 클릭했을 때, 자동으로 그 issue로 넘어갈 수 있습니다. (호버만으로 이슈에 대한 간단한 정보를 볼 수 있습니다)
    2. pr 이 merge 되었을 때, 자동으로 issue 가 close 됩니다.

    이 과정을 손으로 진행하는 것보다, 자동으로 진행하게 되면 실수도 줄어들고, 개발 과정이 편해질 것 같아서 이 기능을 제작하게 되었는데요

    중요한 점

    이 과정을 진행하려면 밑에서 소개해드릴 브랜치 네이밍 규칙이 필요합니다.

    브랜치 이름 규칙

    • 브랜치 이름은 타입/이슈번호 으로 구성합니다. ex) feat/1
    • 타입은 feat, fix, docs, refactor, test 등 여러 가지가 있을 수 있습니다.

    이렇게 했을 때, 이슈 번호를 브랜치 명에서부터 가져올 수 있기에, 자동화를 할 수 있습니다.

    이런 규칙이 아닌, feat/action 같은 형태가 된다면 issue 번호를 찾기 어렵겠죠?

    사용 방법

    작성된 코드부터 보시고, 설명을 드리겠습니다.

    아래에 작성된 코드를. github/workflows/assign_issue_number_to_pr_body.yml로 저장하시면 끝입니다.

    name: assign_issue_number_to_pr_body

    on:
    pull_request:
    types: [ opened ]
    branches-ignore:
    - develop

    jobs:
    append_issue_number_to_pr_body:
    runs-on: ubuntu-latest
    steps:
    - name: append feature number to pr body pr branch = feat/(issueNumber)
    uses: actions/github-script@v4
    with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    script: |
    const pr = await github.pulls.get({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number
    });
    const body = pr.data.body;
    const issueNumber= pr.data.head.ref.split('/')[1];
    const newBody = body + "\n\n" + "close #" + issueNumber;
    await github.pulls.update({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number,
    body: newBody
    });

    진행 과정

    1. pr 이 생성되면, pr에 대한 정보를 가져옵니다.
    2. pr의 본문을 가져옵니다.
    3. pr의 브랜치 이름에서 이슈 번호를 가져옵니다.
    4. pr의 본문에 이슈 번호를 추가합니다.
    5. pr의 본문을 업데이트합니다.

    이 과정을 통해서, 직접 pr의 본문을 수정하지 않아도, 자동으로 이슈 번호가 추가되기에, 실수를 줄일 수 있으니, 한 번 시도해 보세요

    - + \ No newline at end of file diff --git a/tags/auto.html b/tags/auto.html index c936b37..610bc4b 100644 --- a/tags/auto.html +++ b/tags/auto.html @@ -5,14 +5,14 @@ "auto" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "auto" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 3분
    야미

    프로젝트 브랜치명 컨벤션이 feat/이슈번호여서, 브랜치명에서 이슈번호만 가져온 다음 커밋할 때마다 커밋 메시지 아래단(footer)에 이슈 번호를 자동으로 입력해주고 싶었다. 자동으로 입력된다면 깜빡하고 이슈 번호를 안 적는 일도 없고, 시간도 단축할 수 있기 때문이다.

    아래 순서대로 진행한다면 이슈 번호 POSTFIX 자동화를 할 수 있다.

    1) 프로젝트 폴더에 .githooks 폴더 생성

    2) .githooks 폴더에 commit-msg 파일 생성

    #!/bin/bash

    COMMIT_MESSAGE_FILE_PATH=$1
    MESSAGE=$(cat "$COMMIT_MESSAGE_FILE_PATH")

    # 커밋 메시지가 없을 때, 커밋 방지
    if [[ $(head -1 "$COMMIT_MESSAGE_FILE_PATH") == '' ]]; then
    exit 0
    fi

    # 브랜치명에서 이슈 번호만 추출 ('/' 뒤에 있는 문자만 추출)
    POSTFIX=$(git branch | grep '\*' | sed 's/* //' | sed 's/^.*\///' | sed 's/^\([^-]*-[^-]*\).*/\1/')

    COMMIT_SOURCE=$2
    CURRENT_BRANCH=$(git branch --show-current)

    # [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] 👉🏻 현재 브랜치명과 POSTFIX가 똑같으면 POSTFIX 입력 방지
    # [ "$COMMIT_SOURCE" != "merge" ] 👉🏻 merge할 때, POSTFIX 입력 방지
    # [[ "$MESSAGE" != *"[#$POSTFIX]"* ]] 👉🏻 이미 POSTFIX가 존재할 때, POSTFIX 중복 입력 방지
    if [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] && [ "$COMMIT_SOURCE" != "merge" ] && [[ "$MESSAGE" != *"[#$POSTFIX]"* ]]; then
    printf "%s\n\n[#%s]" "$MESSAGE" "$POSTFIX" > "$COMMIT_MESSAGE_FILE_PATH"
    fi

    🧐 이슈 번호 추출에 사용된 명령어 설명

    • grep '*' 👉 * 표시된 브랜치(현재 위치의 브랜치)를 가져온다.
    • sed 's/_ //' 👉 * 제거
    • sed 's/(/)./\1/' 👉 / 이후의 문자만 추출
    • sed 's/^(---)._/\1/' 👉 하나의 이슈에 여러 브랜치를 만들면서 feat/10-1 이런 형태로 브랜치를 만들 경우, 첫 번째 '-' 앞 뒤만 추출 (ex. 10-1)

    3) 프로젝트 폴더에 Makefile 파일 생성

    init:
    git config core.hooksPath .githooks
    chmod +x .githooks/commit-msg
    git update-index --chmod=+x .githooks/commit-msg

    # chmod +x .githooks/commit-msg 👉🏻 macOS, 리눅스에서 스크립트 권한 부여
    # git update-index --chmod=+x .githooks/commit-msg
    # 👉 macOS, 리눅스에서 브랜치가 바뀔 때마다 스크립트 실행시켜줘야 하는 문제 해결

    4) 아래 코드 실행

    새로 git clone을 할 때마다 아래 코드를 실행시켜줘야 한다. 한 번만 실행시키면 계속 적용된다. (window 기준)

    git config core.hooksPath .githooks

    ❗macOS는 git clone 할 때마다 아래 코드를 실행시켜줘야 한다.

    make

    참고 블로그 https://blog.deering.co/commit-convention/

    - + \ No newline at end of file diff --git a/tags/aws.html b/tags/aws.html index 6968d1e..35e1a0c 100644 --- a/tags/aws.html +++ b/tags/aws.html @@ -5,7 +5,7 @@ "aws" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "aws" 태그로 연결된 4개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    제이

    안녕하세요! 카페인팀의 제이입니다.

    저희 카페인 팀에서 무중단 배포를 진행했습니다. 어떤 과정으로 진행을 했는지 작성해보도록 하겠습니다!


    기존 배포 방식과 문제점

    먼저 카페인 팀의 기존 배포 방식은 다음과 같습니다.

    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근하여서 기존에 띄워진 서버를 다운 시킵니다.
    4. Docker Hub에 업로드한 Docker image를 pull해서 서버를 가동시킵니다.

    이런 과정으로 배포 스크립트가 작성되어 있습니다. 하지만 이 방법은 기존 서버를 다운 시키고 새로운 서버를 띄울 때 다운 타임이 존재한다는 문제점이 있습니다.

    사용자 입장에서는 잘 사용하고 있는데 갑자기 서비스가 작동되지 않는다면 서비스에 대한 신뢰성이 낮아질 수도 있고 이런 이유로 이탈할 수도 있습니다.

    기존 문제를 해결하기

    저희는 먼저 제한된 EC2 인스턴스로 인해 롤링 배포의 장점을 가져갈 수 없었고, 카나리 방식 또한 저희 서비스에서 필요로한 전략이 아니기 때문에 비교적 롤백도 빠른 Blue/Green 전략을 선택하였습니다.

    저희의 Blue/Green 무중단 배포 시나리오는 다음과 같습니다. 편의를 위해서 [기존 서버(기존 포트) / 새로운 서버(새로운 포트)] 라는 명칭을 사용하겠습니다.


    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub 에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근해서 Docker Hub에 업로드한 새로운 버전의 Image를 pull 해옵니다.
    4. 만약 8080 포트에 기존 서버가 띄워져 있으면 8081 포트를 새로운 서버가 띄워질 포트로 지정해주고, 반대로 8081 포트에 기존 서버가 띄워져 있으면 8080 포트에 새로운 서버가 띄워질 포트로 지정해줍니다.
    5. 미리 Docker Hub에 업로드한 Docker image를 [image+port]라는 네이밍으로 pull을 한 후 새로운 포트로 서버를 가동시킵니다.
    6. 새로운 서버가 제대로 가동 됐는지 확인하기 위해서 헬스 체크를 진행합니다. 20번 동안 서버가 정상 동작하는지 Spring Actuactor를 통해서 확인을 합니다.
    7. 정상 작동이 됐음을 확인하면 현재 인스턴스에는 2대의 서버가 띄워져있고 요청은 여전히 기존 서버로 들어가게 됩니다. 따라서 Nginx를 통해 포트포워딩을 새로운 서버의 포트로 지정해주고 기존 서버는 내려줍니다.

    여기까지가 카페인 팀의 시나리오입니다. 그렇다면 하나씩 스크립트를 확인해보겠습니다. 설명은 주석으로 달아두겠습니다 :)

    backend-deploy.yml

    (Github Actions에서 사용)

    name: deploy

    # 1. prod/backend branch에 push 작업이 일어나면 해당 작업을 수행한다
    on:
    push:
    branches:
    - prod/backend

    jobs:
    docker-build:
    runs-on: ubuntu-latest
    defaults:
    run:
    working-directory: ./backend

    steps:
    # 2. 도커 허브에 로그인
    - name: Log in to Docker Hub
    uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
    with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_PASSWORD }}
    - uses: actions/checkout@v3

    # 3. JDK 17 설치 및 빌드 (프로젝트 Java version)
    - name: Set up JDK 17
    uses: actions/setup-java@v3
    with:
    java-version: '17'
    distribution: 'adopt'

    - name: Gradle Caching
    uses: actions/cache@v3
    with:
    path: |
    ~/.gradle/caches
    ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
    ${{ runner.os }}-gradle-

    - name: Grant execute permission for gradlew
    run: chmod +x gradlew
    - name: Build for asciiDoc
    run: ./gradlew bootjar

    - name: Build with Gradle
    run: ./gradlew bootjar

    # 4. 산출물을 Image로 빌드 후 Docker Hub에 Image Push하기
    - name: Extract metadata (tags, labels) for Docker
    id: meta
    uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
    with:
    images: woowacarffeine/backend

    - name: Build and push Docker image
    uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
    with:
    context: .
    file: ./backend/Dockerfile
    push: true
    platforms: linux/arm64
    tags: woowacarffeine/backend:latest
    labels: ${{ steps.meta.outputs.labels }}


    deploy:
    # 5. Self-hosted 작동 -> infra 인스턴스에서 작동됨
    runs-on: self-hosted
    if: ${{ needs.docker-build.result == 'success' }}
    needs: [ docker-build ]
    steps:

    # 6. infra 인스턴스에서 prod 인스턴스로 접근 (아래부터는 prod 서버 내에서 작업)
    - name: Join EC2 prod server
    uses: appleboy/ssh-action@master
    env:
    JASYPT_KEY: ${{ secrets.JASYPT_KEY }}
    DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
    DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
    with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USERNAME }}
    key: ${{ secrets.SERVER_KEY }}
    port: ${{ secrets.SERVER_PORT }}
    envs: JASYPT_KEY, DATABASE_USERNAME, DATABASE_PASSWORD

    script: |

    # 7. Docker Hub에서 Image를 pull해온다
    sudo docker pull woowacarffeine/backend:latest

    # 8. 만약 8080 포트가 켜져 있으면 새로운 서버의 포트는 8081로 설정
    if sudo docker ps | grep ":8080"; then
    export BEFORE_PORT=8080
    export NEW_PORT=8081
    export NEW_ACTUATOR_PORT=8089

    # 9. 만약 8081 포트가 켜져 있으면 새로운 서버의 포트는 8080로 설정
    else
    export BEFORE_PORT=8081
    export NEW_PORT=8080
    export NEW_ACTUATOR_PORT=8088
    fi

    # 10. Docker로 새로운 서버를 띄운다.
    sudo docker run -d -p $NEW_PORT:8080 -p $NEW_ACTUATOR_PORT:8088 \
    -e "SPRING_PROFILE=prod" \
    -e "ENCRYPT_KEY=${{secrets.JASYPT_KEY}}" \
    -e "DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}}" \
    -e "DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}}" \
    -e "REPLICA_DATABASE_USERNAME=${{secrets.REPLICA_DB_USER_NAME}}" \
    -e "REPLICA_DATABASE_PASSWORD=${{secrets.REPLICA_DB_USER_PASSWORD}}" \
    -e "SLACK_WEBHOOK_URL=${{secrets.SLACK_WEBHOOK_URL}}" \
    --name backend$NEW_PORT \
    woowacarffeine/backend:latest

    # 11. prod 인스턴스에 있는 bluegreen.sh 를 작동한다. (이 때 port 값을 같이 넣어준다.)
    sudo sh /home/ubuntu/bluegreen.sh $BEFORE_PORT $NEW_PORT $NEW_ACTUATOR_PORT



    bluegreen.sh

    (prod 인스턴스 내부에 존재)

    #!/bin/bash

    # 1. Github Actions를 통해 넘겨 받은 환경변수 값
    BEFORE_PORT=$1
    NEW_PORT=$2
    NEW_ACTUATOR_PORT=$3

    echo "기존 포트 : $BEFORE_PORT"
    echo "새로운 포트: $NEW_PORT"
    echo "새로운 ACTUATOR_PORT: $NEW_ACTUATOR_PORT"


    # 2. 20번 동안 헬스 체크를 진행
    count=0
    for count in {0..20}
    do
    echo "서버 상태 확인(${count}/20)";

    # 3. 새로운 서버가 작동되는지 Actuator를 통해 값을 받아옴
    STATUS=$(curl -s http://127.0.0.1:${NEW_ACTUATOR_PORT}/actuator/health-check)

    # 4. Actuator를 통해 성공적으로 서버가 띄워지지 않은 경우
    if [ "${STATUS}" != '{"status":"up"}' ]
    then
    # 5. 10초를 기다린 후 다시 헬스 체크를 진행한다.
    sleep 10
    continue
    else
    # 6. 헬스 체크를 통해 새로운 서버가 성공적으로 작동된다면 멈춘다.
    break
    fi
    done


    # 7. 20번의 헬스 체크를 하는 동안 새로운 서버가 제대로 작동되지 않은 경우 종료
    if [ $count -eq 20 ]
    then
    echo "새로운 서버 배포를 실패했습니다."
    exit 1
    fi


    # 8. 새로운 서버가 성공적으로 작동한 경우
    # Nginx를 통해 포트포워딩을 기존 포트에서 새로운 포트로 변경해준다.
    # 이 부분은 .inc 파일을 통해 Nginx에서 주입 받아서 포트만 변경해도 됩니다!
    export BACKEND_PORT=$NEW_PORT
    envsubst '${BACKEND_PORT}' < backend.template > backend.conf
    sudo mv backend.conf /etc/nginx/conf.d/
    sudo nginx -s reload


    # 9. 기존 서버를 내려주고, 도커 리소스를 정리해준다
    docker stop backend$BEFORE_PORT
    sudo docker container prune -f

    이렇게 카페인 팀에서는 무중단 배포를 도입할 수 있었습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git a/tags/aws/page/2.html b/tags/aws/page/2.html index d07658a..fc8e203 100644 --- a/tags/aws/page/2.html +++ b/tags/aws/page/2.html @@ -5,13 +5,13 @@ "aws" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "aws" 태그로 연결된 4개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    어떤 문제가 있었나요?

    우아한테크코스에서 private 서브넷에 db 인스턴스를 두고, 보안을 위해 외부에서 접속을 차단하려고 했습니다.

    이 과정에서 총 2가지의 문제점이 있었습니다.

    1. private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었습니다.
    2. public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안되었습니다.

    이 부분을 어떻게 해결했는지 알아보도록 하겠습니다.

    아래의 모든 설명은 AWS 를 기준으로 합니다.

    private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었다.

    해결 방법

    public ip 자동할당을 해주지 않아서, 인터넷에 연결이 안 되었습니다.

    이를 해결하기 위해 public ip 자동할당을 해주었습니다.

    왜 public ip를 할당했더니 문제가 해결되었을까요?

    private 서브넷이란?

    정말 간단하게 설명했을 때

    private 서브넷은 인터넷에 연결되지 않은 서브넷입니다.

    조금 자세하게 들어가 보도록 하겠습니다

    private 서브넷은 인터넷 게이트웨이가 연결되지 않은 서브넷입니다.

    aws 공식문서에서 사진을 통해 보면 아래와 같이 되어있습니다

    private subnet

    public 서브넷에만 인터넷 게이트웨이가 연결되어 있고, private 서브넷에는 인터넷 게이트웨이가 연결되어있지 않습니다.

    private 서브넷에 인터넷 게이트웨이가 연결되어 있지 않다고 했을 때, 기본적으로 인터넷에 접속이 안됩니다.

    mysql을 설치할 때도, 인터넷에 접속을 해야하는데, 인터넷에 접속이 안되니 설치가 안되는 것입니다.

    어? 인터넷 자체가 접근이 안되면 어떻게 설치하나요?

    정말 원시적으로 해결하기 위해서는 public 서브넷에 인스턴스를 하나 더 만들어서, mysql 을 압축해서 scp를 통해 private 서브넷에 있는 인스턴스에 전송하고, 압축을 풀어서 설치하는 방법이 있습니다.

    하지만 이 방법은 너무 원시적이고, 비효율적입니다.

    그래서 인터넷으로 요청을 보낼 수 있도록 만드는 과정이 필요합니다.

    인터넷으로 요청을 보낼 수 있도록 만드는 과정

    인터넷으로 요청을 보낼 수 있도록 만드는 과정은 크게 2가지가 있습니다.

    private 서브넷을 public 서브넷으로 바꾸기

    보안을 위해서 private 서브넷에 두려고 했던 것을 public 서브넷으로 바꾼다는 부분은 매우 위험합니다.

    그래서 이 방법은 보통 사용하지 않습니다.

    NAT 인스턴스(Gateway) 만들기

    NAT 인스턴스는 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들어주는 인스턴스입니다.

    인터넷에 접속을 하기 위해서는 public ip 가 필요합니다.

    따라서 NAT 인스턴스, NAT 게이트웨이는 public 서브넷에 존재해야 합니다.

    어? NAT 인스턴스를 통해서 바로 통신이 가능하면 왜 private 서브넷이 필요한가요? 그냥 다 public 서브넷에 두면 되지 않나요?

    NAT 인스턴스, NAT Gateway는 내부에서 출발한 트래픽만 통과할 수 있도록 설정이 되어있습니다.

    예를 들면 private 서브넷에 인스턴스에 접속해서 직접 mysql download 요청을 했을 때만 허용이 됩니다.

    외부에서 바로 private 인스턴스로 접근할 수는 없습니다.

    NAT 인스턴스만 설정을 하면 바로 연결이 되나요?

    public ip도 자동 할당을 해줘야 합니다

    public ip 가 필요한 이유

    NAT 인스턴스를 통해서 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들었는데, 왜 public ip 가 필요할까요?

    외부 인터넷과 통신을 할 때 public ip 가 필요합니다.

    NAT 인스턴스 혹은 NAT 게이트웨이가 인터넷과 통신할 때, NAT 인스턴스의 public ip + private ip를 통해서 통신을 하지 않습니다.

    내부 인스턴스의 public ip 를 통해서 통신을 하게 되어있습니다.

    따라서 NAT 인스턴스와 내부 인스턴스 모두 public ip 가 필요합니다.

    이 과정을 통해서 1번 문제를 해결할 수 있었습니다.

    이제 2번째 문제를 해결해 보도록 하겠습니다.

    public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안 되는 문제

    public 서브넷에 있는 서버가 private 서브넷에 있는 서버에 접속을 하려고 했는데, 접속이 안 되는 문제가 있었습니다.

    해결 방법

    해결 방법에는 2가지 과정이 있습니다.

    public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해 주기

    기본적으로 public 서브넷에 있는 인스턴스의 보안 그룹에는 private 서브넷에 있는 인스턴스의 보안 그룹이 추가되어있지 않습니다.

    따라서 public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.

    private ip를 통해서 접속하기

    public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속할 때, public ip 를 통해서 접속을 하면 안 됩니다.

    public ip를 통해서 접속하는 과정을 자세하게 알아보겠습니다.

    1. public 서브넷에 있는 인스턴스가 public ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
    2. 라우팅 테이블에서 public ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
    3. 라우터를 통해서 외부 인터넷으로 나가게 됩니다.
    4. 트래픽이 NAT 인스턴스에 도착합니다.
    5. NAT 인스턴스는 내부에서 출발한 트래픽이 아니기 때문에, 트래픽을 거부합니다.

    이 과정이 일어나기에, public ip 를 통해서 접속을 하면 안 됩니다.

    private ip를 통해서 접근하면 어떻게 되는지 알아보겠습니다

    1. public 서브넷에 있는 인스턴스가 private ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
    2. 라우팅 테이블에서 private ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
    3. 라우터를 거쳐서 private 서브넷의 라우터로 이동합니다.
    4. private 서브넷의 라우터는 private 서브넷에 있는 인스턴스에게 트래픽을 전달합니다.
    5. private 서브넷에 있는 인스턴스는 트래픽을 받아서 처리합니다.

    이 과정을 통해서 2번 문제를 해결할 수 있었습니다.

    요약

    1. private 서브넷에 있는 인스턴스가 인터넷에 접속을 하려면 NAT 인스턴스 혹은 NAT 게이트웨이가 필요합니다.
    2. private 서브넷에 있는 인스턴스도 public ip 가 필요합니다.
    3. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 하려면 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.
    4. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 할 때, private ip 를 통해서 접속을 해야 합니다.
    - + \ No newline at end of file diff --git a/tags/aws/page/3.html b/tags/aws/page/3.html index d454dbb..b447e62 100644 --- a/tags/aws/page/3.html +++ b/tags/aws/page/3.html @@ -5,13 +5,13 @@ "aws" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "aws" 태그로 연결된 4개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    안녕하세요 우아한테크코스 카페인팀 누누입니다

    이번에 카페인 팀에서 배포 아키텍처를 결정하게 되었던 과정에 대해서 정리를 해보고 싶어서 글을 쓰게 되었습니다.

    아키텍처와 서버가 배포되는 과정을 보여드리면서 시작하도록 하겠습니다

    배포 아키텍처

    서버가 배포되는 과정은 다음과 같습니다.

    server image

    우아한테크코스 인스턴스에 대한 소개

    우테코에서 선택할 수 있는 인스턴스는 총 2가지 종류입니다.

    1. 퍼블릭 서브넷에 있는 인스턴스
      • 캠퍼스에서만 SSH 접근이 가능한 인스턴스입니다.
      • 미리 열려있는 포트들만 허용이 되어 있습니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다
    2. 프라이빗 서브넷에 있는 인스턴스
      • 퍼블릭 서브넷에 있는 인스턴스를 통해서만 접근이 가능합니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다.

    1번 인스턴스를 2개 사용 가능하고, 2번 인스턴스를 1개 사용 가능합니다.

    권장되는 환경에서 1개는 db 서버로 사용하고, 나머지 2개는 자유롭게 사용이 가능했습니다.

    그전에 알면 좋아요

    여기서는 Self Hosted Runner를 사용했는데요.

    Self Hosted Runner에 대한 내용은 여기 에 잘 나와있습니다.

    외부 IP로부터 SSH 접근이 불가능하기에, Self Hosted Runner 나, Jenkins 같은 방법을 사용할 수 있었는데, 러닝 커브를 고려해서 Self Hosted Runner를 선택하게 되었습니다.

    배포 아키텍처에 대한 고민

    저희 팀이 이번 아키텍처를 만들기 위해서 고민했던 점들은 다음과 같습니다.

    1. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
    3. 2차 데모데이까지의 과제인 개발 서버를 어떻게 구성할 수 있을까?

    여기서 1번을 가장 먼저 생각한 아키텍처를 구성하게 되었는데, 다음과 같습니다.

    선택의 기준이 되었던 것은 총 3가지였습니다.

    1. DB는 프라이빗 서브넷에 위치시키고, 우리 인스턴스를 거쳐서만 접근이 가능하게 한다.
      • 이 부분은 보안을 위해서 어쩔 수 없이 선택하게 된 부분입니다.이 부분을 고려하다 보니, 최소한으로 구성할 수 있는 구조가 db 용 private 인스턴스 1개, 그리고 우리가 사용할 public 인스턴스 1개가 됩니다
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
      • 개발용 인스턴스에 CD 툴이나, 모니터링 툴을 설치하게 되면, 운영 서버에도 동일하게 작업을 해야 합니다.
      • 이 부분을 최소화하기 위해서, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 되었습니다.
    3. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
      • 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 않는다면 개발용 인스턴스에서 장애가 발생했을 때, CD 툴과 모니터링 툴에도 영향을 미치게 됩니다. 이 부분을 생각했을 때도, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리해야 한다고 결정하게 되었습니다
      • 한 부분의 장애가 다른 툴까지 사용할 수 없게 만들게 되어서, 롤백이나, 상황 파악을 하기 힘들게 만들게 됩니다.

    이런 과정들을 생각했을 때, 인스턴스 1개를 개발 서버용으로, 인스턴스 1개를 CD 툴과 모니터링 툴을 설치한 인스턴스로 사용하게 되었습니다.

    실제 내부 구성은 어떻게 될까요?

    개발 서버

    이 인스턴스에는 총 2가지 기능이 들어가 있습니다.

    1. 프론트 서버
      • react로 되어있는 프론트엔드 코드를 사용자에게 전달해 주는 역할을 합니다.
    2. 백엔드 서버
      • spring으로 되어있는 api 서버입니다.

    물론, 이렇게 하면 두 곳 중 한 곳에 장애가 발생했을 때, 프론트 서버와 백엔드 서버가 모두 영향을 받게 됩니다.

    같이 관리하게 된 첫 번째 이유로 비용이 들기 때문에 비용의 문제를 고려하게 되었습니다. 개발 서버에서 프론트 서버와 백엔드 서버를 관리하게 되었습니다.

    두 번째 이유로는, 아직 프로젝트 초창기 이기 때문에, 백엔드에서 장애가 났을 때, 프론트에서 일정 이상의 에러 처리가 불가능했습니다.

    프로젝트가 많이 진행되었다면, 프론트엔드만으로 혹은 장애가 나지 않은 서버를 활용해 에러 처리를 할 수 있지만, 아직은 그런 기능을 구현하지 못했습니다.

    이와는 별개로 실행 시 편의를 위해서 도커를 사용해 개발 서버를 관리하고 있습니다.

    CD 툴과 모니터링 툴

    이 인스턴스에는 총 3가지 기능이 들어가 있습니다.

    1. CD 툴
      • 위에서 설명드린 것처럼, self hosted runner 가 동작하게 되어있습니다
    2. 보안을 위한 리버스 프록시
      • 저희 프로젝트에서 구글 지도를 사용하게 되는데, 이때 API 키를 사용하게 됩니다. 이렇게 하면, API 키를 노출시키지 않고, 사용할 수 있습니다.
      • 이 API 키를 노출시키지 않기 위해서, 리버스 프록시를 하나 두고, 여기서 API 키를 추가해 요청을 보내는 방식으로 구성하게 되었습니다.
    3. 모니터링 툴
      • 저희 프로젝트에서 아직 도입하지 않았지만, 현재 이슈로는 올라가 있는 상태입니다.
      • Actuator, 프로메테우스, 그라파나 이 3가지를 활용해서 모니터링 툴을 구성하게 될 예정입니다

    위 기능들이 한 인스턴스에 모여있기에, 위의 기능들은 추후에 운영 서버가 추가되었을 때, 중복으로 관리하지 않아도 됩니다.

    배포 과정 더 자세히 알아보기

    아래에 사진에서 보이는 과정을 통해서 배포를 진행하고 있는데요

    server image

    1. 사용자가 push를 하면, github actions에서 도커 빌드를 진행하고, 도커 허브에 이미지를 올립니다.
    2. 도커 허브에 이미지가 올라간 이후에, self hosted runner 가 작동을 시작합니다.
    3. 개발용 인스턴스에 접근해서, 이미지를 받고, 컨테이너를 실행합니다.

    이런 과정을 통해서, 개발용 인스턴스에 배포를 진행하고 있습니다.

    느낀 점

    좋은 아키텍처를 설계하기 위해서는 고려해야 할 점들이 정말 많다는 것을 다시 한번 느꼈습니다.

    운영 서버가 추가된다던가, 인스턴스가 늘어나고, 줄어드는 상황에 유연하게 대처할 수 있도록 설계를 해야 한다는 것을 다시 한번 느꼈습니다.

    중복으로 관리될 포인트를 줄여야 한다는 것도 다시 한번 느낄 수 있었고요

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/tags/aws/page/4.html b/tags/aws/page/4.html index 6bd5df9..0c7e847 100644 --- a/tags/aws/page/4.html +++ b/tags/aws/page/4.html @@ -5,7 +5,7 @@ "aws" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -21,7 +21,7 @@ 물론 이는 계획이고 공부하지 않은 다른 내용이 있을 수 있기 때문에 언제든 바뀔 수 있습니다.

    무중단 배포 아키텍처 적용

    이 또한 아직은 먼 이야기지만, 고려해 볼 상황이라서 적어봤습니다.

    사용자가 이용하고 있는 서비스가 갑자기 중단된다면 어떨까요? 저는 화가 많이 날 것 같습니다.

    피치 못할 사정으로 서버가 터져도, 사용자가 서비스를 계속 이용할 방법이 없을까요?

    이런 고민을 해결하기 위해서 나온 개념이 무중단 배포입니다.

    카나리아 배포, Blue/Green 배포, 롤링등 무중단 배포를 위한 여러가지 전략은 이미 존재합니다. 이 부분은 아직은 서버의 명세가 정확하지 않아서 어떤 방식으로 어떻게 처리할 것인지에 대해서는 아직 정할 수는 없습니다.

    이는 명세가 확실하게 정해진 후 팀원과 장단점을 상의하며 결정할 일이기 때문에 현재까지는 "이 정도를 고려하고 있다." 정도만 알면 될 것 같습니다.

    - + \ No newline at end of file diff --git a/tags/blue-green.html b/tags/blue-green.html index c45bea1..f0945ec 100644 --- a/tags/blue-green.html +++ b/tags/blue-green.html @@ -5,7 +5,7 @@ "blue-green" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "blue-green" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    제이

    안녕하세요! 카페인팀의 제이입니다.

    저희 카페인 팀에서 무중단 배포를 진행했습니다. 어떤 과정으로 진행을 했는지 작성해보도록 하겠습니다!


    기존 배포 방식과 문제점

    먼저 카페인 팀의 기존 배포 방식은 다음과 같습니다.

    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근하여서 기존에 띄워진 서버를 다운 시킵니다.
    4. Docker Hub에 업로드한 Docker image를 pull해서 서버를 가동시킵니다.

    이런 과정으로 배포 스크립트가 작성되어 있습니다. 하지만 이 방법은 기존 서버를 다운 시키고 새로운 서버를 띄울 때 다운 타임이 존재한다는 문제점이 있습니다.

    사용자 입장에서는 잘 사용하고 있는데 갑자기 서비스가 작동되지 않는다면 서비스에 대한 신뢰성이 낮아질 수도 있고 이런 이유로 이탈할 수도 있습니다.

    기존 문제를 해결하기

    저희는 먼저 제한된 EC2 인스턴스로 인해 롤링 배포의 장점을 가져갈 수 없었고, 카나리 방식 또한 저희 서비스에서 필요로한 전략이 아니기 때문에 비교적 롤백도 빠른 Blue/Green 전략을 선택하였습니다.

    저희의 Blue/Green 무중단 배포 시나리오는 다음과 같습니다. 편의를 위해서 [기존 서버(기존 포트) / 새로운 서버(새로운 포트)] 라는 명칭을 사용하겠습니다.


    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub 에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근해서 Docker Hub에 업로드한 새로운 버전의 Image를 pull 해옵니다.
    4. 만약 8080 포트에 기존 서버가 띄워져 있으면 8081 포트를 새로운 서버가 띄워질 포트로 지정해주고, 반대로 8081 포트에 기존 서버가 띄워져 있으면 8080 포트에 새로운 서버가 띄워질 포트로 지정해줍니다.
    5. 미리 Docker Hub에 업로드한 Docker image를 [image+port]라는 네이밍으로 pull을 한 후 새로운 포트로 서버를 가동시킵니다.
    6. 새로운 서버가 제대로 가동 됐는지 확인하기 위해서 헬스 체크를 진행합니다. 20번 동안 서버가 정상 동작하는지 Spring Actuactor를 통해서 확인을 합니다.
    7. 정상 작동이 됐음을 확인하면 현재 인스턴스에는 2대의 서버가 띄워져있고 요청은 여전히 기존 서버로 들어가게 됩니다. 따라서 Nginx를 통해 포트포워딩을 새로운 서버의 포트로 지정해주고 기존 서버는 내려줍니다.

    여기까지가 카페인 팀의 시나리오입니다. 그렇다면 하나씩 스크립트를 확인해보겠습니다. 설명은 주석으로 달아두겠습니다 :)

    backend-deploy.yml

    (Github Actions에서 사용)

    name: deploy

    # 1. prod/backend branch에 push 작업이 일어나면 해당 작업을 수행한다
    on:
    push:
    branches:
    - prod/backend

    jobs:
    docker-build:
    runs-on: ubuntu-latest
    defaults:
    run:
    working-directory: ./backend

    steps:
    # 2. 도커 허브에 로그인
    - name: Log in to Docker Hub
    uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
    with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_PASSWORD }}
    - uses: actions/checkout@v3

    # 3. JDK 17 설치 및 빌드 (프로젝트 Java version)
    - name: Set up JDK 17
    uses: actions/setup-java@v3
    with:
    java-version: '17'
    distribution: 'adopt'

    - name: Gradle Caching
    uses: actions/cache@v3
    with:
    path: |
    ~/.gradle/caches
    ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
    ${{ runner.os }}-gradle-

    - name: Grant execute permission for gradlew
    run: chmod +x gradlew
    - name: Build for asciiDoc
    run: ./gradlew bootjar

    - name: Build with Gradle
    run: ./gradlew bootjar

    # 4. 산출물을 Image로 빌드 후 Docker Hub에 Image Push하기
    - name: Extract metadata (tags, labels) for Docker
    id: meta
    uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
    with:
    images: woowacarffeine/backend

    - name: Build and push Docker image
    uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
    with:
    context: .
    file: ./backend/Dockerfile
    push: true
    platforms: linux/arm64
    tags: woowacarffeine/backend:latest
    labels: ${{ steps.meta.outputs.labels }}


    deploy:
    # 5. Self-hosted 작동 -> infra 인스턴스에서 작동됨
    runs-on: self-hosted
    if: ${{ needs.docker-build.result == 'success' }}
    needs: [ docker-build ]
    steps:

    # 6. infra 인스턴스에서 prod 인스턴스로 접근 (아래부터는 prod 서버 내에서 작업)
    - name: Join EC2 prod server
    uses: appleboy/ssh-action@master
    env:
    JASYPT_KEY: ${{ secrets.JASYPT_KEY }}
    DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
    DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
    with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USERNAME }}
    key: ${{ secrets.SERVER_KEY }}
    port: ${{ secrets.SERVER_PORT }}
    envs: JASYPT_KEY, DATABASE_USERNAME, DATABASE_PASSWORD

    script: |

    # 7. Docker Hub에서 Image를 pull해온다
    sudo docker pull woowacarffeine/backend:latest

    # 8. 만약 8080 포트가 켜져 있으면 새로운 서버의 포트는 8081로 설정
    if sudo docker ps | grep ":8080"; then
    export BEFORE_PORT=8080
    export NEW_PORT=8081
    export NEW_ACTUATOR_PORT=8089

    # 9. 만약 8081 포트가 켜져 있으면 새로운 서버의 포트는 8080로 설정
    else
    export BEFORE_PORT=8081
    export NEW_PORT=8080
    export NEW_ACTUATOR_PORT=8088
    fi

    # 10. Docker로 새로운 서버를 띄운다.
    sudo docker run -d -p $NEW_PORT:8080 -p $NEW_ACTUATOR_PORT:8088 \
    -e "SPRING_PROFILE=prod" \
    -e "ENCRYPT_KEY=${{secrets.JASYPT_KEY}}" \
    -e "DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}}" \
    -e "DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}}" \
    -e "REPLICA_DATABASE_USERNAME=${{secrets.REPLICA_DB_USER_NAME}}" \
    -e "REPLICA_DATABASE_PASSWORD=${{secrets.REPLICA_DB_USER_PASSWORD}}" \
    -e "SLACK_WEBHOOK_URL=${{secrets.SLACK_WEBHOOK_URL}}" \
    --name backend$NEW_PORT \
    woowacarffeine/backend:latest

    # 11. prod 인스턴스에 있는 bluegreen.sh 를 작동한다. (이 때 port 값을 같이 넣어준다.)
    sudo sh /home/ubuntu/bluegreen.sh $BEFORE_PORT $NEW_PORT $NEW_ACTUATOR_PORT



    bluegreen.sh

    (prod 인스턴스 내부에 존재)

    #!/bin/bash

    # 1. Github Actions를 통해 넘겨 받은 환경변수 값
    BEFORE_PORT=$1
    NEW_PORT=$2
    NEW_ACTUATOR_PORT=$3

    echo "기존 포트 : $BEFORE_PORT"
    echo "새로운 포트: $NEW_PORT"
    echo "새로운 ACTUATOR_PORT: $NEW_ACTUATOR_PORT"


    # 2. 20번 동안 헬스 체크를 진행
    count=0
    for count in {0..20}
    do
    echo "서버 상태 확인(${count}/20)";

    # 3. 새로운 서버가 작동되는지 Actuator를 통해 값을 받아옴
    STATUS=$(curl -s http://127.0.0.1:${NEW_ACTUATOR_PORT}/actuator/health-check)

    # 4. Actuator를 통해 성공적으로 서버가 띄워지지 않은 경우
    if [ "${STATUS}" != '{"status":"up"}' ]
    then
    # 5. 10초를 기다린 후 다시 헬스 체크를 진행한다.
    sleep 10
    continue
    else
    # 6. 헬스 체크를 통해 새로운 서버가 성공적으로 작동된다면 멈춘다.
    break
    fi
    done


    # 7. 20번의 헬스 체크를 하는 동안 새로운 서버가 제대로 작동되지 않은 경우 종료
    if [ $count -eq 20 ]
    then
    echo "새로운 서버 배포를 실패했습니다."
    exit 1
    fi


    # 8. 새로운 서버가 성공적으로 작동한 경우
    # Nginx를 통해 포트포워딩을 기존 포트에서 새로운 포트로 변경해준다.
    # 이 부분은 .inc 파일을 통해 Nginx에서 주입 받아서 포트만 변경해도 됩니다!
    export BACKEND_PORT=$NEW_PORT
    envsubst '${BACKEND_PORT}' < backend.template > backend.conf
    sudo mv backend.conf /etc/nginx/conf.d/
    sudo nginx -s reload


    # 9. 기존 서버를 내려주고, 도커 리소스를 정리해준다
    docker stop backend$BEFORE_PORT
    sudo docker container prune -f

    이렇게 카페인 팀에서는 무중단 배포를 도입할 수 있었습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git a/tags/branch.html b/tags/branch.html index 2dc75c8..7a74aca 100644 --- a/tags/branch.html +++ b/tags/branch.html @@ -5,13 +5,13 @@ "branch" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "branch" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    현재 상황은 어떤데?

    현재 우아한테크코스에서는 프론트 코드와 백엔드 코드가 같은 레포지토리를 사용하고 있습니다.

    프론트와 백엔드가 같이 작업하기에, 의도치 않은 충돌이 자주 생길 수 있는 구조이기에, 이를 git branch 전략으로 충돌을 줄이고자 합니다

    Git Branch 전략이란?

    git을 사용해서 소프트웨어 개발을 관리하는 방법입니다.

    여러 개발자가 동시에 작업하고 코드를 통합할 때 생기는 충돌을 효율적으로 조정하기 위한 방법입니다.

    왜 git branch 전략이 중요한데?

    아래에 있는 4가지를 제외하고도 훨씬 많은 장점이 있을 수 있습니다.

    1. 동시 작업이 편하다

    여러 사람이 독립적으로 작업하고, 커밋을 할 때, 자신의 브랜치에서 변경 사항을 커밋하게 됩니다.

    브랜치가 병합될 때만 충돌을 해결하면 되니, 아무 규칙이 없는 것보다 충돌 시점이 명확해지기에 생산성을 높일 수 있습니다.

    2. 목적이 명확한 브랜치

    애플리케이션의 상태에 몇 가지가 있는데, 안정된 프로덕션, 테스트 환경, 기능 추가 환경... 등이 있습니다

    여러 기능별 브랜치(안정된 버전의 코드만이 관리되는 브랜치, 테스트 환경을 위한 브랜치, 기능 추가를 위한 브랜치)를

    네이밍을 통해 구분하면 각각의 브랜치에 대해서 추가적인 설명을 할 필요 없이 명확하게 관리할 수 있습니다.

    3. 배포 파이프라인 관리가 편함

    브랜치가 네이밍으로 명확하게 구분이 되어있다면, 조건을 설정하기 쉽습니다.

    특정 타입의 브랜치에 push 되었을 때, pull request를 만들었을 때 같은 조건에 따른 스크립트를 만들어둔다면 CI/CD를 구축하기 쉽습니다.

    4. 버전 관리가 편리하다

    서버에 문제가 생겼을 때, 어떤 브랜치로 돌아가서 롤백을 해야 하는지에 대한 것들이 명확합니다.

    안정된 브랜치가 어떤 것인지 명확하기에, 롤백 과정에 대한 의사결정을 줄일 수 있습니다.

    그러면 어떤 종류가 있는지 더 자세하게 알아보도록 하겠습니다.

    Git Branch 전략의 종류는?

    총 3가지의 전략이 있습니다.

    1. Github Flow

    2. Gitlab Flow

    3. Git Flow

    git을 사용하기에, Git Flow라는 네이밍이 가장 직관적이고 유명한데요. 

    3가지 전략 중에서 가장 복잡하기에, 쉬운 순서대로 진행해 보도록 하겠습니다.

    1. Github Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img

    img2

    브랜치는 총 2가지 종류가 존재합니다

    1. master 브랜치

    여기에 머지가 되면 배포가 되도록 CD를 연결해 놓은 경우가 많습니다.

    안정된 버전의 코드가 관리되는 브랜치입니다.

    2. feature 브랜치

    기능 추가, 버그 수정 등 모든 작업은 feature 브랜치에서 일어납니다.

    master 브랜치에서 새로운 브랜치를 만들어서, 마스터로 머지되는 단순한 사이클을 가지고 있습니다.

    장점

    위에서 볼 수 있는 것처럼 2종류의 브랜치만 있기에, 정말 간단합니다.

    학습 과정까지의 러닝 커브가 거의 없다시피 하기에, 간단한 프로젝트에 적용하기 정말 좋습니다.

    릴리즈 되지 않은 코드가 최소화됩니다. 최신 버전의 코드와 최대한 빠르게 동기화를 계속해서 시킬 수 있습니다

    단점

    모든 코드는 다 master 브랜치에 머지가 되어야 한다는 점이 개발 서버와, 운영서버를 나누기 애매할 수 있습니다.

    개발 서버에 배포를 하고 싶은 상황이라면, master에 머지가 되어야 합니다.

    머지가 된 이후에 cd 파이프라인을 통해서 개발 서버와 운영 서버 모두에 배포가 됩니다.

    여러 환경을 나누고 관리를 하고 싶으시다면 다음에 소개해드릴 전략을 사용해 보셔도 좋을 것 같습니다

    2. Gitlab Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img2

    밑에 환경은 총 2개의 서버가 존재할 때를 가정하고 있습니다.

    1. pre-production 서버

    2. production 서버

    편의를 위해 main에 머지되는 과정은 간단하게 표현했습니다.

    img3

    브랜치 종류

    총 3가지 브랜치가 필요하고, 추가에 따라서 더 추가할 수 있습니다.

    1. main(or develop) 브랜치

    기능에 대한 개발이 완료되었지만, 여기에 머지되어도 바로 배포되지는 않습니다.

    2. feature브랜치

    기능을 개발하는 브랜치입니다. Github Flow 와도 유사합니다.

    3. production 브랜치

    실제 배포가 일어나는 브랜치입니다. 

    여기에 머지가 되는 순간 배포가 일어납니다.

    위 사진에 있는 것처럼, 필요에 따라서 pre-production이나, staging 같은 환경에 따른 브랜치를 추가할 수 있습니다.

    특징

    1. 무조건 단방향으로 머지가 일어납니다.

    긴급하게 라이브 서버에 수정을 해야 할 때, production 부터 시작하는 것이 아닌, main 부터 차근차근 올라가야 합니다

    2. 환경에 따라 브랜치 종류가 늘어날 수 있습니다.

    위 사진에서는 pre-production 이 그 예시가 되겠네요.

    장점

    1. Github Flow에서 환경별 브랜치를 통해서 개발 서버나 pre-production 서버에 버전을 깔끔하게 관리할 수 있습니다.

    3. Git Flow

    브랜치 전략 중 가장 처음으로 유명해진 브랜치 전략입니다.

    배포가 특정 주기를 가지고 있는 애플리케이션일 때, 가장 적합합니다.

    가장 복잡한 전략을 가지고 있어서, 모두가 브랜치 전략에 대해서 이해하고 있다면 역할에 따른 깔끔한 분리가 가능합니다

    그림으로 보고 가도록 하겠습니다

    img4

    가장 유명한 브랜치 전략이지만, 가장 어려운 전략이기도 합니다.

    특징

    1. 브랜치에 대해서 양방향으로 머지가 일어납니다

    release 브랜치에서 버그 수정이 일어나면, develop 브랜치에도 머지해줘야 합니다.

    hotfix 브랜치를 main 브랜치뿐만 아니라, develop 브랜치에도 머지해줘야 합니다

    브랜치의 종류가 5가지나 됩니다

    1. main

    production 이 배포되었을 때, 이 브랜치에 머지되는 것이 기준이 됩니다.

    2. develop 

    위에서 설명드렸던 브랜치들과 큰 차이가 없이 배포 전 브랜치입니다.

    3. feature

    기능을 개발할 때 사용하는 브랜치입니다. 이것도 위와 큰 차이가 없습니다

    4. release

    Gitlab Flow에서 pre-production에 해당한다고 봐도 무방합니다.

    여기서 버그 수정이 일어났을 경우에,  develop에 머지하는 것을 까먹으면 안 됩니다.

    5. hotfix

    main 브랜치에서 생성된 브랜치로, 긴급한 변경사항을 처리합니다.

    이때, develop에 머지하는 것을 깜빡하면 안 됩니다.

    더 자세하게 알아보실 분은 아래 링크들을 확인해 보세요

    우리 프로젝트에는 어떤 것이 적절할까?

    나중에 개발 서버 혹은 스테이징 서버를 두고 싶기에, 이 부분에 대한 처리가 부족한 Github Flow는 적절하지 않습니다.

    Git Flow는 깔끔하게 처리할 수 있지만, 러닝 커브가 Gitlab Flow 보다 약간 더 있어서, 빠르게 개발하는 취지에 맞지 않아 보였습니다.

    이런 과정을 통해서 Gitlab Flow를 사용하려고 합니다 

    참고

    https://techblog.woowahan.com/2553/

    https://docs.gitlab.com/ee/topics/gitlab_flow.html

    - + \ No newline at end of file diff --git a/tags/cd.html b/tags/cd.html index 54224f6..99d0aa5 100644 --- a/tags/cd.html +++ b/tags/cd.html @@ -5,7 +5,7 @@ "cd" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "cd" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    제이

    안녕하세요! 카페인팀의 제이입니다.

    저희 카페인 팀에서 무중단 배포를 진행했습니다. 어떤 과정으로 진행을 했는지 작성해보도록 하겠습니다!


    기존 배포 방식과 문제점

    먼저 카페인 팀의 기존 배포 방식은 다음과 같습니다.

    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근하여서 기존에 띄워진 서버를 다운 시킵니다.
    4. Docker Hub에 업로드한 Docker image를 pull해서 서버를 가동시킵니다.

    이런 과정으로 배포 스크립트가 작성되어 있습니다. 하지만 이 방법은 기존 서버를 다운 시키고 새로운 서버를 띄울 때 다운 타임이 존재한다는 문제점이 있습니다.

    사용자 입장에서는 잘 사용하고 있는데 갑자기 서비스가 작동되지 않는다면 서비스에 대한 신뢰성이 낮아질 수도 있고 이런 이유로 이탈할 수도 있습니다.

    기존 문제를 해결하기

    저희는 먼저 제한된 EC2 인스턴스로 인해 롤링 배포의 장점을 가져갈 수 없었고, 카나리 방식 또한 저희 서비스에서 필요로한 전략이 아니기 때문에 비교적 롤백도 빠른 Blue/Green 전략을 선택하였습니다.

    저희의 Blue/Green 무중단 배포 시나리오는 다음과 같습니다. 편의를 위해서 [기존 서버(기존 포트) / 새로운 서버(새로운 포트)] 라는 명칭을 사용하겠습니다.


    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub 에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근해서 Docker Hub에 업로드한 새로운 버전의 Image를 pull 해옵니다.
    4. 만약 8080 포트에 기존 서버가 띄워져 있으면 8081 포트를 새로운 서버가 띄워질 포트로 지정해주고, 반대로 8081 포트에 기존 서버가 띄워져 있으면 8080 포트에 새로운 서버가 띄워질 포트로 지정해줍니다.
    5. 미리 Docker Hub에 업로드한 Docker image를 [image+port]라는 네이밍으로 pull을 한 후 새로운 포트로 서버를 가동시킵니다.
    6. 새로운 서버가 제대로 가동 됐는지 확인하기 위해서 헬스 체크를 진행합니다. 20번 동안 서버가 정상 동작하는지 Spring Actuactor를 통해서 확인을 합니다.
    7. 정상 작동이 됐음을 확인하면 현재 인스턴스에는 2대의 서버가 띄워져있고 요청은 여전히 기존 서버로 들어가게 됩니다. 따라서 Nginx를 통해 포트포워딩을 새로운 서버의 포트로 지정해주고 기존 서버는 내려줍니다.

    여기까지가 카페인 팀의 시나리오입니다. 그렇다면 하나씩 스크립트를 확인해보겠습니다. 설명은 주석으로 달아두겠습니다 :)

    backend-deploy.yml

    (Github Actions에서 사용)

    name: deploy

    # 1. prod/backend branch에 push 작업이 일어나면 해당 작업을 수행한다
    on:
    push:
    branches:
    - prod/backend

    jobs:
    docker-build:
    runs-on: ubuntu-latest
    defaults:
    run:
    working-directory: ./backend

    steps:
    # 2. 도커 허브에 로그인
    - name: Log in to Docker Hub
    uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
    with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_PASSWORD }}
    - uses: actions/checkout@v3

    # 3. JDK 17 설치 및 빌드 (프로젝트 Java version)
    - name: Set up JDK 17
    uses: actions/setup-java@v3
    with:
    java-version: '17'
    distribution: 'adopt'

    - name: Gradle Caching
    uses: actions/cache@v3
    with:
    path: |
    ~/.gradle/caches
    ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
    ${{ runner.os }}-gradle-

    - name: Grant execute permission for gradlew
    run: chmod +x gradlew
    - name: Build for asciiDoc
    run: ./gradlew bootjar

    - name: Build with Gradle
    run: ./gradlew bootjar

    # 4. 산출물을 Image로 빌드 후 Docker Hub에 Image Push하기
    - name: Extract metadata (tags, labels) for Docker
    id: meta
    uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
    with:
    images: woowacarffeine/backend

    - name: Build and push Docker image
    uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
    with:
    context: .
    file: ./backend/Dockerfile
    push: true
    platforms: linux/arm64
    tags: woowacarffeine/backend:latest
    labels: ${{ steps.meta.outputs.labels }}


    deploy:
    # 5. Self-hosted 작동 -> infra 인스턴스에서 작동됨
    runs-on: self-hosted
    if: ${{ needs.docker-build.result == 'success' }}
    needs: [ docker-build ]
    steps:

    # 6. infra 인스턴스에서 prod 인스턴스로 접근 (아래부터는 prod 서버 내에서 작업)
    - name: Join EC2 prod server
    uses: appleboy/ssh-action@master
    env:
    JASYPT_KEY: ${{ secrets.JASYPT_KEY }}
    DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
    DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
    with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USERNAME }}
    key: ${{ secrets.SERVER_KEY }}
    port: ${{ secrets.SERVER_PORT }}
    envs: JASYPT_KEY, DATABASE_USERNAME, DATABASE_PASSWORD

    script: |

    # 7. Docker Hub에서 Image를 pull해온다
    sudo docker pull woowacarffeine/backend:latest

    # 8. 만약 8080 포트가 켜져 있으면 새로운 서버의 포트는 8081로 설정
    if sudo docker ps | grep ":8080"; then
    export BEFORE_PORT=8080
    export NEW_PORT=8081
    export NEW_ACTUATOR_PORT=8089

    # 9. 만약 8081 포트가 켜져 있으면 새로운 서버의 포트는 8080로 설정
    else
    export BEFORE_PORT=8081
    export NEW_PORT=8080
    export NEW_ACTUATOR_PORT=8088
    fi

    # 10. Docker로 새로운 서버를 띄운다.
    sudo docker run -d -p $NEW_PORT:8080 -p $NEW_ACTUATOR_PORT:8088 \
    -e "SPRING_PROFILE=prod" \
    -e "ENCRYPT_KEY=${{secrets.JASYPT_KEY}}" \
    -e "DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}}" \
    -e "DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}}" \
    -e "REPLICA_DATABASE_USERNAME=${{secrets.REPLICA_DB_USER_NAME}}" \
    -e "REPLICA_DATABASE_PASSWORD=${{secrets.REPLICA_DB_USER_PASSWORD}}" \
    -e "SLACK_WEBHOOK_URL=${{secrets.SLACK_WEBHOOK_URL}}" \
    --name backend$NEW_PORT \
    woowacarffeine/backend:latest

    # 11. prod 인스턴스에 있는 bluegreen.sh 를 작동한다. (이 때 port 값을 같이 넣어준다.)
    sudo sh /home/ubuntu/bluegreen.sh $BEFORE_PORT $NEW_PORT $NEW_ACTUATOR_PORT



    bluegreen.sh

    (prod 인스턴스 내부에 존재)

    #!/bin/bash

    # 1. Github Actions를 통해 넘겨 받은 환경변수 값
    BEFORE_PORT=$1
    NEW_PORT=$2
    NEW_ACTUATOR_PORT=$3

    echo "기존 포트 : $BEFORE_PORT"
    echo "새로운 포트: $NEW_PORT"
    echo "새로운 ACTUATOR_PORT: $NEW_ACTUATOR_PORT"


    # 2. 20번 동안 헬스 체크를 진행
    count=0
    for count in {0..20}
    do
    echo "서버 상태 확인(${count}/20)";

    # 3. 새로운 서버가 작동되는지 Actuator를 통해 값을 받아옴
    STATUS=$(curl -s http://127.0.0.1:${NEW_ACTUATOR_PORT}/actuator/health-check)

    # 4. Actuator를 통해 성공적으로 서버가 띄워지지 않은 경우
    if [ "${STATUS}" != '{"status":"up"}' ]
    then
    # 5. 10초를 기다린 후 다시 헬스 체크를 진행한다.
    sleep 10
    continue
    else
    # 6. 헬스 체크를 통해 새로운 서버가 성공적으로 작동된다면 멈춘다.
    break
    fi
    done


    # 7. 20번의 헬스 체크를 하는 동안 새로운 서버가 제대로 작동되지 않은 경우 종료
    if [ $count -eq 20 ]
    then
    echo "새로운 서버 배포를 실패했습니다."
    exit 1
    fi


    # 8. 새로운 서버가 성공적으로 작동한 경우
    # Nginx를 통해 포트포워딩을 기존 포트에서 새로운 포트로 변경해준다.
    # 이 부분은 .inc 파일을 통해 Nginx에서 주입 받아서 포트만 변경해도 됩니다!
    export BACKEND_PORT=$NEW_PORT
    envsubst '${BACKEND_PORT}' < backend.template > backend.conf
    sudo mv backend.conf /etc/nginx/conf.d/
    sudo nginx -s reload


    # 9. 기존 서버를 내려주고, 도커 리소스를 정리해준다
    docker stop backend$BEFORE_PORT
    sudo docker container prune -f

    이렇게 카페인 팀에서는 무중단 배포를 도입할 수 있었습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git a/tags/cd/page/2.html b/tags/cd/page/2.html index 4c8b68a..f3ebce1 100644 --- a/tags/cd/page/2.html +++ b/tags/cd/page/2.html @@ -5,7 +5,7 @@ "cd" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -20,7 +20,7 @@ 위에 사진과 같이 설정을 해주시면 됩니다.

    그리고 이를 yml에서 사용하기 위해선 secrets.Key이름으로 사용해주시면 됩니다.


    이제 마지막으로 Dockerfile을 만들어줍니다.

    저희는 /backend/ 경로에 만들어주었습니다.

    FROM amazoncorretto:17-alpine-jdk
    ARG JAR_FILE=./backend/build/libs/carffeine-0.0.1-SNAPSHOT.jar
    COPY ${JAR_FILE} app.jar
    ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-jar","/app.jar"]

    저희는 위처럼 절대 경로를 기준으로 JAR_FILE 위치를 지정하고, profiles는 dev로 설정해서 만들어주었습니다.


    3. 배포하기

    트리거를 작동시켜서 저희가 yml 파일에서 지정해준 것들이 잘 작동하는지 확인합니다.

    jobSuccess 위에 사진처럼 모든 Job이 성공적으로 통과하는 것을 보실 수 있습니다.

    dockerPs 이렇게 인프라 서버에서 배포 서버로 들어가서 성공적으로 서버를 도커로 띄운 것을 보실 수 있습니다.

    EC2 배포 서버에서 docker ps를 입력했을 때에도 잘 실행이 되네요!


    CD 배포 과정 요약

    지속적 배포 과정을 요약 하자면 다음과 같습니다.

    1. Self Hosted Runner를 EC2 인프라 서버에 등록해준다.
    2. yml 파일과 Dockerfile을 만들어준다.
    3. 트리거를 작동시켜서 Github Actions의 태스크가 모두 잘 되는지 확인한다.
    4. 잘 됐다면 EC2 배포 서버에 Docker image가 성공적으로 띄워진다.
    - + \ No newline at end of file diff --git a/tags/cd/page/3.html b/tags/cd/page/3.html index bf377ef..a6126a5 100644 --- a/tags/cd/page/3.html +++ b/tags/cd/page/3.html @@ -5,7 +5,7 @@ "cd" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -21,7 +21,7 @@ 물론 이는 계획이고 공부하지 않은 다른 내용이 있을 수 있기 때문에 언제든 바뀔 수 있습니다.

    무중단 배포 아키텍처 적용

    이 또한 아직은 먼 이야기지만, 고려해 볼 상황이라서 적어봤습니다.

    사용자가 이용하고 있는 서비스가 갑자기 중단된다면 어떨까요? 저는 화가 많이 날 것 같습니다.

    피치 못할 사정으로 서버가 터져도, 사용자가 서비스를 계속 이용할 방법이 없을까요?

    이런 고민을 해결하기 위해서 나온 개념이 무중단 배포입니다.

    카나리아 배포, Blue/Green 배포, 롤링등 무중단 배포를 위한 여러가지 전략은 이미 존재합니다. 이 부분은 아직은 서버의 명세가 정확하지 않아서 어떤 방식으로 어떻게 처리할 것인지에 대해서는 아직 정할 수는 없습니다.

    이는 명세가 확실하게 정해진 후 팀원과 장단점을 상의하며 결정할 일이기 때문에 현재까지는 "이 정도를 고려하고 있다." 정도만 알면 될 것 같습니다.

    - + \ No newline at end of file diff --git a/tags/ci.html b/tags/ci.html index ccc5236..d174d85 100644 --- a/tags/ci.html +++ b/tags/ci.html @@ -5,7 +5,7 @@ "CI" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -20,7 +20,7 @@ 위에 사진과 같이 설정을 해주시면 됩니다.

    그리고 이를 yml에서 사용하기 위해선 secrets.Key이름으로 사용해주시면 됩니다.


    이제 마지막으로 Dockerfile을 만들어줍니다.

    저희는 /backend/ 경로에 만들어주었습니다.

    FROM amazoncorretto:17-alpine-jdk
    ARG JAR_FILE=./backend/build/libs/carffeine-0.0.1-SNAPSHOT.jar
    COPY ${JAR_FILE} app.jar
    ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-jar","/app.jar"]

    저희는 위처럼 절대 경로를 기준으로 JAR_FILE 위치를 지정하고, profiles는 dev로 설정해서 만들어주었습니다.


    3. 배포하기

    트리거를 작동시켜서 저희가 yml 파일에서 지정해준 것들이 잘 작동하는지 확인합니다.

    jobSuccess 위에 사진처럼 모든 Job이 성공적으로 통과하는 것을 보실 수 있습니다.

    dockerPs 이렇게 인프라 서버에서 배포 서버로 들어가서 성공적으로 서버를 도커로 띄운 것을 보실 수 있습니다.

    EC2 배포 서버에서 docker ps를 입력했을 때에도 잘 실행이 되네요!


    CD 배포 과정 요약

    지속적 배포 과정을 요약 하자면 다음과 같습니다.

    1. Self Hosted Runner를 EC2 인프라 서버에 등록해준다.
    2. yml 파일과 Dockerfile을 만들어준다.
    3. 트리거를 작동시켜서 Github Actions의 태스크가 모두 잘 되는지 확인한다.
    4. 잘 됐다면 EC2 배포 서버에 Docker image가 성공적으로 띄워진다.
    - + \ No newline at end of file diff --git a/tags/ci/page/2.html b/tags/ci/page/2.html index e10ed79..e55acb5 100644 --- a/tags/ci/page/2.html +++ b/tags/ci/page/2.html @@ -5,7 +5,7 @@ "CI" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -21,7 +21,7 @@ 물론 이는 계획이고 공부하지 않은 다른 내용이 있을 수 있기 때문에 언제든 바뀔 수 있습니다.

    무중단 배포 아키텍처 적용

    이 또한 아직은 먼 이야기지만, 고려해 볼 상황이라서 적어봤습니다.

    사용자가 이용하고 있는 서비스가 갑자기 중단된다면 어떨까요? 저는 화가 많이 날 것 같습니다.

    피치 못할 사정으로 서버가 터져도, 사용자가 서비스를 계속 이용할 방법이 없을까요?

    이런 고민을 해결하기 위해서 나온 개념이 무중단 배포입니다.

    카나리아 배포, Blue/Green 배포, 롤링등 무중단 배포를 위한 여러가지 전략은 이미 존재합니다. 이 부분은 아직은 서버의 명세가 정확하지 않아서 어떤 방식으로 어떻게 처리할 것인지에 대해서는 아직 정할 수는 없습니다.

    이는 명세가 확실하게 정해진 후 팀원과 장단점을 상의하며 결정할 일이기 때문에 현재까지는 "이 정도를 고려하고 있다." 정도만 알면 될 것 같습니다.

    - + \ No newline at end of file diff --git a/tags/collaboration.html b/tags/collaboration.html index bebebbd..59b08f1 100644 --- a/tags/collaboration.html +++ b/tags/collaboration.html @@ -5,7 +5,7 @@ "collaboration" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "collaboration" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 3분

    사용자 피드백

    image

    저희 서비스를 배포하고 사용자에게 피드백을 받았는데, 축소했을 때가 많이 불편하다는 피드백이 대부분이였습니다.

    이유는 아래 화면과 같습니다

    asis

    이런 서비스를 본 적도 없고, 이런 서비스를 사용하고 싶지도 않을 것 입니다. 해당 부분의 문제를 알고 있었지만 어떻게 표현해주는 것이 좋고, 구현할 수 있는 방법이 떠오르지 않아 6차 데모데이까지 미루게 되었습니다.

    열심히 팀 회의를 한 결과 화면에 보이는 사이즈만큼 일정 범위로 나눠 충전소 개수를 보여주는 클러스터링 기능을 추가하기로 정했습니다.

    클러스터 기능 추가

    해당 기능을 간단하게 설명드리면 화면의 일정 범위로 나눠 충전소의 개수를 보여주도록 서버에서 계산하여 클라이언트로 전달하도록 했습니다. 하지만 전달한 클러스터링 마커들의 위치가 아래와 같이 예쁘게 보이지 않았습니다.

    image (5)

    화면의 크기에 비해 마커가 몇개 없는 것을 볼 수 있습니다. 이렇게 된다면 사용자는 그렇기에 클라이언트에 해당 기능을 담당한 가브리엘, 센트가 좀 더 유연하게 마커를 보여주는 것이 UX 관점에서 좋다고 얘기하여

    서버 API와 로직을 변경하여 동적으로 화면의 충전소를 클러스터하도록 변경하였습니다. 그렇게 하여 아래와 같은 화면을 제공하도록 하였습니다.

    final

    이상 협업 일화 였습니다.

    - + \ No newline at end of file diff --git a/tags/commit.html b/tags/commit.html index c75568c..7f04e47 100644 --- a/tags/commit.html +++ b/tags/commit.html @@ -5,14 +5,14 @@ "commit" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "commit" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 3분
    야미

    프로젝트 브랜치명 컨벤션이 feat/이슈번호여서, 브랜치명에서 이슈번호만 가져온 다음 커밋할 때마다 커밋 메시지 아래단(footer)에 이슈 번호를 자동으로 입력해주고 싶었다. 자동으로 입력된다면 깜빡하고 이슈 번호를 안 적는 일도 없고, 시간도 단축할 수 있기 때문이다.

    아래 순서대로 진행한다면 이슈 번호 POSTFIX 자동화를 할 수 있다.

    1) 프로젝트 폴더에 .githooks 폴더 생성

    2) .githooks 폴더에 commit-msg 파일 생성

    #!/bin/bash

    COMMIT_MESSAGE_FILE_PATH=$1
    MESSAGE=$(cat "$COMMIT_MESSAGE_FILE_PATH")

    # 커밋 메시지가 없을 때, 커밋 방지
    if [[ $(head -1 "$COMMIT_MESSAGE_FILE_PATH") == '' ]]; then
    exit 0
    fi

    # 브랜치명에서 이슈 번호만 추출 ('/' 뒤에 있는 문자만 추출)
    POSTFIX=$(git branch | grep '\*' | sed 's/* //' | sed 's/^.*\///' | sed 's/^\([^-]*-[^-]*\).*/\1/')

    COMMIT_SOURCE=$2
    CURRENT_BRANCH=$(git branch --show-current)

    # [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] 👉🏻 현재 브랜치명과 POSTFIX가 똑같으면 POSTFIX 입력 방지
    # [ "$COMMIT_SOURCE" != "merge" ] 👉🏻 merge할 때, POSTFIX 입력 방지
    # [[ "$MESSAGE" != *"[#$POSTFIX]"* ]] 👉🏻 이미 POSTFIX가 존재할 때, POSTFIX 중복 입력 방지
    if [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] && [ "$COMMIT_SOURCE" != "merge" ] && [[ "$MESSAGE" != *"[#$POSTFIX]"* ]]; then
    printf "%s\n\n[#%s]" "$MESSAGE" "$POSTFIX" > "$COMMIT_MESSAGE_FILE_PATH"
    fi

    🧐 이슈 번호 추출에 사용된 명령어 설명

    • grep '*' 👉 * 표시된 브랜치(현재 위치의 브랜치)를 가져온다.
    • sed 's/_ //' 👉 * 제거
    • sed 's/(/)./\1/' 👉 / 이후의 문자만 추출
    • sed 's/^(---)._/\1/' 👉 하나의 이슈에 여러 브랜치를 만들면서 feat/10-1 이런 형태로 브랜치를 만들 경우, 첫 번째 '-' 앞 뒤만 추출 (ex. 10-1)

    3) 프로젝트 폴더에 Makefile 파일 생성

    init:
    git config core.hooksPath .githooks
    chmod +x .githooks/commit-msg
    git update-index --chmod=+x .githooks/commit-msg

    # chmod +x .githooks/commit-msg 👉🏻 macOS, 리눅스에서 스크립트 권한 부여
    # git update-index --chmod=+x .githooks/commit-msg
    # 👉 macOS, 리눅스에서 브랜치가 바뀔 때마다 스크립트 실행시켜줘야 하는 문제 해결

    4) 아래 코드 실행

    새로 git clone을 할 때마다 아래 코드를 실행시켜줘야 한다. 한 번만 실행시키면 계속 적용된다. (window 기준)

    git config core.hooksPath .githooks

    ❗macOS는 git clone 할 때마다 아래 코드를 실행시켜줘야 한다.

    make

    참고 블로그 https://blog.deering.co/commit-convention/

    - + \ No newline at end of file diff --git a/tags/css-in-js.html b/tags/css-in-js.html index 44b7c66..5afb6f4 100644 --- a/tags/css-in-js.html +++ b/tags/css-in-js.html @@ -5,13 +5,13 @@ "css in js" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "css in js" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 2분
    야미

    왜 styled-components인가?


    여러 CSS-in-JS 중 styled-components를 선택한 이유는 다음과 같다.

    1. 컴포넌트 안에 관련 CSS를 작성할 수 있어 컴포넌트별 디자인 코드 확인 및 수정이 용이하다.

    2. 혹자는 코드 가독성이 안 좋아진다고도 하지만, 개인적으로는 태그를 더 시맨틱 하게 작성할 수 있어서 좋다고 느꼈다.

    3. 팀원들 모두 styled-components가 익숙하다.

    4. 지금까지 사용하면서 불편한 점을 못 느꼈다.


    styled-components와 emotion은 기능도, 작성법도 상당히 유사하다.

    그래서 이번에는 styled-components 대신 emotion을 써볼까도 생각했었다.

    하지만 emotion에서만 사용 가능하던 *CSS Props라는 편리한 기능을

    styled-components(v5.2.0 이상)에서 쓸 수 있게 되기도 했고,

    '새로운 기술 공부를 해보면 좋을 것 같다'는 이유를 제외하고는

    딱히 emotion을 사용할 필요성을 못 느껴 styled-components를 채택했다.

    // *CSS Props 예시

    const buttonStyle = css`
    font-size: 18px;
    color: white;
    background: black;
    `;

    const ClickButton = styled.button<{ css: CSSProp }>`
    width: 100px;

    ${({ css }) => css}
    `;

    <ClickButton css={buttonStyle}>Click me!</ClickButton>;
    - + \ No newline at end of file diff --git a/tags/css.html b/tags/css.html index 32a333a..f2b8a1f 100644 --- a/tags/css.html +++ b/tags/css.html @@ -5,13 +5,13 @@ "css" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "css" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 2분
    야미

    왜 styled-components인가?


    여러 CSS-in-JS 중 styled-components를 선택한 이유는 다음과 같다.

    1. 컴포넌트 안에 관련 CSS를 작성할 수 있어 컴포넌트별 디자인 코드 확인 및 수정이 용이하다.

    2. 혹자는 코드 가독성이 안 좋아진다고도 하지만, 개인적으로는 태그를 더 시맨틱 하게 작성할 수 있어서 좋다고 느꼈다.

    3. 팀원들 모두 styled-components가 익숙하다.

    4. 지금까지 사용하면서 불편한 점을 못 느꼈다.


    styled-components와 emotion은 기능도, 작성법도 상당히 유사하다.

    그래서 이번에는 styled-components 대신 emotion을 써볼까도 생각했었다.

    하지만 emotion에서만 사용 가능하던 *CSS Props라는 편리한 기능을

    styled-components(v5.2.0 이상)에서 쓸 수 있게 되기도 했고,

    '새로운 기술 공부를 해보면 좋을 것 같다'는 이유를 제외하고는

    딱히 emotion을 사용할 필요성을 못 느껴 styled-components를 채택했다.

    // *CSS Props 예시

    const buttonStyle = css`
    font-size: 18px;
    color: white;
    background: black;
    `;

    const ClickButton = styled.button<{ css: CSSProp }>`
    width: 100px;

    ${({ css }) => css}
    `;

    <ClickButton css={buttonStyle}>Click me!</ClickButton>;
    - + \ No newline at end of file diff --git a/tags/db.html b/tags/db.html index 7a4aa37..02ad2de 100644 --- a/tags/db.html +++ b/tags/db.html @@ -5,13 +5,13 @@ "DB" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "DB" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    누누
    박스터

    안녕하세요 카페인팀 누누입니다

    이번에는 대량의 데이터를 DB에 넣는 과정을 최적화하는 과정에서 알게 된 내용을 공유하려고 합니다

    이번 최적화의 목표

    전기차 충전소에 대한 공공 데이터를 가져오고, 그 데이터를 DB 에 넣는 과정을 최적화해보자

    대량의 데이터를 삽입하는 과정

    저희 팀의 요구사항을 간단하게 정리하면 다음과 같습니다

    1. 대량의 데이터를 공공 데이터에서 전기차 충전소와 전기차 충전기에 대한 데이터를 가져온다
      • 충전소는 6만 개, 충전기는 23만 개의 데이터가 존재한다.
      • 한 번에 가져올 수 있는 양은 9999개 까지다.
    2. 이 데이터를 DB에 넣는다
      • 충전소와 충전기는 1:N 관계이다

    최적화 전은 어떤 상황이었는데?

    before_optimize

    위 사진을 잘 보시면 아실 수 있으시겠지만, 2000개를 저장하는데, 231.762 초가 사용되었습니다.

    물론 출력을 위한 시간도 포함되었기에, 230초 정도라고 생각하셔도 좋습니다

    1만 개라면? 231.762초 * 5 = 1,158.81초

    23만 개라면? 1158.81 * 23 = 26,652.63초

    시간으로 바꿔보면 7.4 시간이 걸린다는 것을 볼 수 있습니다

    이 과정에서 볼 수 있는 문제점

    1. 데이터를 저장할 때마다, 새로운 Transaction 이 생성된다.

    어떻게 개선할 수 있을까?

    데이터를 저장할 때마다, 새로운 Transaction 이 생성되는 것을 방지하기 위해, 전체를 하나의 트랜잭션으로 묶는다

    전체를 한 트랜잭션으로 묶은 버전

    all_in_transaction

    이 과정에서 2000개를 저장하는데 65초 가 사용되었습니다.

    1만 개라면? 65초 * 5 = 325초

    23만 개라면? 325초 * 23 = 7,475초

    시간으로 바꿔보면 2시간이 걸린다는 것을 볼 수 있습니다

    전체적으로 3배 정도 빨라졌습니다

    이 과정에서 볼 수 있는 문제점

    1. 23만 개의 저장이 모두 한 트랜잭션이 되어서, 하나가 실패하면 23만개를 새로 저장해야 하는 상황에 처한다

    어떻게 개선할 수 있을까?

    23만개의 저장이 모두 한 트랜잭션이 되는 것을 방지하기 위해, 1만 개씩 영속화시킨다

    1만 개가 한 트랜잭션으로 묶인 버전

    separateTransaction

    성능상으로 개선한 부분은 그렇게 크지 않지만, 실패했을 때, 1만 개만 다시 저장하면 되기에, 훨씬 빠르게 복구가 가능합니다.

    여기서 PageNo라는 클래스는, i를 바로 참조했을 경우, effectively final을 보장할 수 없어서 만들었습니다.

    성능은 전체를 한 트랜잭션으로 묶은 버전과 큰 차이가 나지 않습니다.

    이 과정에서 볼 수 있는 문제점

    1. id 생성 전략이 GenerationType.IDENTITY 이기에, 데이터를 저장할 때마다, DB에서 id를 생성해야 한다.

    JPA에 있는 쓰기 지연을 전혀 활용할 수 없고, DB에서 id를 생성하기 위해, DB와 매번 통신을 해야 한다.

    어떻게 개선할 수 있을까?

    id를 미리 생성해서, DB 에서 id 를 생성하는 과정을 생략한다

    ID 생성 전략을 GenerationType.Table의 형태로 바꿔서, DB에서 id를 생성하는 과정을 줄여서, 성능을 개선한다

    1만 개가 한 트랜잭션으로 묶이고, id를 미리 생성한 버전

    이때 batch size를 1000 단위로 설정해서 1000개씩 id 가 늘어나도록 설정했다

    charger_generatorstation_generator

    spring.jdbc.template.fetch-size=10000

    10000batch_size

    1자리 숫자는 앞에서부터 n(만개)를 의미하고, 2번째 숫자는 1만 개를 저장하는 데 걸린 시간(ms)을 의미합니다.

    처음 1만 개는 142초가 걸리고, 2만 개는 285초가 걸렸습니다.

    23만 개라면? 142 * 26 = 3,266초

    처음과 비교하자면 7.4시간이 걸리는 것에서 54분 정도 걸리는 것으로 개선되었습니다.

    이 과정에서 볼 수 있는 문제점

    하나의 스레드에서만 동작하기에, 성능이 개선되었지만, 여전히 느립니다.

    하나의 스레드에서만 동작하기에, 하나의 커넥션을 사용하게 됩니다.

    어떻게 개선할 수 있을까?

    여러 스레드에서 동작하게 하고, 여러 커넥션을 사용하게 합니다.

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전

    multi_thread

    이 버전에서 89991 개를 저장하는데 총 157초가 걸렸습니다.

    23만 개라면? 157 * 3 = 471초

    시간으로 바꿔보면 5분도 채 걸리지 않는 시간이죠

    이 과정에서 볼 수 있는 문제점

    hikari connection pool 사이즈를 10으로 설정했는데, 10개의 커넥션을 사용하면서 저장을 하다 보니, 10개의 커넥션을 모두 사용하고 나서, 11번째부터는 커넥션을 가져오기 위해, 기다려야 하는 상황이 발생합니다.

    어떻게 개선할 수 있을까?

    hikari connection pool 사이즈를 25로 설정해서, 25개의 커넥션을 사용하도록 합니다.

    spring.datasource.hikari.maximum-pool-size=25

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전 2

    multi_thread2

    총 13만 개의 데이터를 저장하는데, 147초가 걸리고, db 인스턴스의 cpu 사용률이 100%에 가까워져서 ec2 가 다운되었습니다.

    이 과정에서 볼 수 있는 문제점

    db의 cpu 사용량을 고려하지 않고, 23만 개가 조금 넘는 데이터를 25개의 커넥션을 활용해 저장하려고 했습니다

    결론

    1. 데이터를 저장할 때마다, transaction을 사용하지 말자
    2. 데이터를 저장할 때마다, id를 생성하지 말자
    3. 여러 스레드에서 동작하고, 여러 커넥션을 사용하자
    4. db의 cpu 사용량을 고려하자

    긴 글 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/tags/deadlock.html b/tags/deadlock.html index 55f7976..69720f9 100644 --- a/tags/deadlock.html +++ b/tags/deadlock.html @@ -5,7 +5,7 @@ "deadlock" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -26,7 +26,7 @@ 트랜잭션을 오래 가지고 있으면 Lock을 가지고 있는 시간이 오래걸립니다. 그래서 트랜잭션을 작게 분리할 수 있습니다. 페이징을 통해 트랜잭션을 작게 분리하다보면 쿼리가 여러번 나가 성능상 문제가 생길 수 있을 것 같습니다.
  • INSERT ~~ ON DUPLICATE KEY UPDATE ~~ 사용하지 않기 해당 sql이 아닌 INSERT IGNORE을 사용하여 추가된 정보만 넣고, update는 다른 작업으로 분리하기
  • 이런 방법들을 사용하면 될 것 같았습니다. 그 중 저는 현재는 간단하게 2번째 방법이 제일 나을 것 같다는 생각에 쿼리를 수정했습니다.

    그리고 문제를 해결했습니다. 해당 문제가 발생하게 되어 좀 더 재밌는 것들을 고민하고 공부할 수 있는 저희 팀에게 감사하고 모르는 키워드를 많이 알려준 누누에게 감사합니다.

    아직 배우는 단계라 정확한 정보가 아닐 수 있습니다. 부족한 부분에 대해 많은 지적 부탁드립니다.

    - + \ No newline at end of file diff --git a/tags/dev.html b/tags/dev.html index 98a4c78..74dfbd0 100644 --- a/tags/dev.html +++ b/tags/dev.html @@ -5,7 +5,7 @@ "dev" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -17,7 +17,7 @@ 기존 dev 서버는 총 4개의 서버를 배포하고 있었고 배포하는 서버는 다음과 같습니다. prod-BE, prod-FE, dev-BE, dev-FE

    그리고, 기존 dev 서버에서는 환경을 분리해주기 위해서 Nginx를 통해서 포트 포워딩은 다음과 같이 해주었습니다.

    • prod-BE = 8080
    • prod-FE = 3031
    • dev-BE = 8081
    • dev-FE = 3031

    카페인 팀에서는 dev, prod 환경이 분리되지 않아서 인스턴스의 사용량이 높았고, 이에 따라 추가적인 EC2 인스턴스가 필요했습니다.


    문제 해결

    다행히도 카페인 팀에서 추가적인 EC2 인스턴스를 받았고, 저희는 배포 환경을 분리할 수 있었습니다.

    dev-prod-server

    이와 같이 기존 dev 서버 한 개가 infra 서버와 연결되어 있었는데, 두 갈래로 나뉜 것을 확인하실 수 있습니다.

    먼저 배포는 다음과 같이 진행됩니다.

    release branch에 push가 일어나면 dev서버에 배포 작업이 이뤄집니다. prod branch에 push가 일어나면 prod서버에 배포 작업이 이뤄집니다.

    또한 기존 dev 서버에서 4개의 포트포워딩 또한 굳이 그럴 필요가 없어졌습니다. 새로운 서버가 추가됨에 따라 dev, prod 서버 각각 Nginx에서 포트포워딩을 동일하게 FE:3000, BE:8080 으로 변경하였습니다.

    이렇게 카페인 팀에서는 dev, prod 환경을 분리했습니다.

    감사합니다!

    - + \ No newline at end of file diff --git a/tags/ec-2.html b/tags/ec-2.html index cb2d622..f744fa4 100644 --- a/tags/ec-2.html +++ b/tags/ec-2.html @@ -5,7 +5,7 @@ "ec2" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "ec2" 태그로 연결된 4개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    제이

    안녕하세요! 카페인팀의 제이입니다.

    저희 카페인 팀에서 무중단 배포를 진행했습니다. 어떤 과정으로 진행을 했는지 작성해보도록 하겠습니다!


    기존 배포 방식과 문제점

    먼저 카페인 팀의 기존 배포 방식은 다음과 같습니다.

    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근하여서 기존에 띄워진 서버를 다운 시킵니다.
    4. Docker Hub에 업로드한 Docker image를 pull해서 서버를 가동시킵니다.

    이런 과정으로 배포 스크립트가 작성되어 있습니다. 하지만 이 방법은 기존 서버를 다운 시키고 새로운 서버를 띄울 때 다운 타임이 존재한다는 문제점이 있습니다.

    사용자 입장에서는 잘 사용하고 있는데 갑자기 서비스가 작동되지 않는다면 서비스에 대한 신뢰성이 낮아질 수도 있고 이런 이유로 이탈할 수도 있습니다.

    기존 문제를 해결하기

    저희는 먼저 제한된 EC2 인스턴스로 인해 롤링 배포의 장점을 가져갈 수 없었고, 카나리 방식 또한 저희 서비스에서 필요로한 전략이 아니기 때문에 비교적 롤백도 빠른 Blue/Green 전략을 선택하였습니다.

    저희의 Blue/Green 무중단 배포 시나리오는 다음과 같습니다. 편의를 위해서 [기존 서버(기존 포트) / 새로운 서버(새로운 포트)] 라는 명칭을 사용하겠습니다.


    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub 에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근해서 Docker Hub에 업로드한 새로운 버전의 Image를 pull 해옵니다.
    4. 만약 8080 포트에 기존 서버가 띄워져 있으면 8081 포트를 새로운 서버가 띄워질 포트로 지정해주고, 반대로 8081 포트에 기존 서버가 띄워져 있으면 8080 포트에 새로운 서버가 띄워질 포트로 지정해줍니다.
    5. 미리 Docker Hub에 업로드한 Docker image를 [image+port]라는 네이밍으로 pull을 한 후 새로운 포트로 서버를 가동시킵니다.
    6. 새로운 서버가 제대로 가동 됐는지 확인하기 위해서 헬스 체크를 진행합니다. 20번 동안 서버가 정상 동작하는지 Spring Actuactor를 통해서 확인을 합니다.
    7. 정상 작동이 됐음을 확인하면 현재 인스턴스에는 2대의 서버가 띄워져있고 요청은 여전히 기존 서버로 들어가게 됩니다. 따라서 Nginx를 통해 포트포워딩을 새로운 서버의 포트로 지정해주고 기존 서버는 내려줍니다.

    여기까지가 카페인 팀의 시나리오입니다. 그렇다면 하나씩 스크립트를 확인해보겠습니다. 설명은 주석으로 달아두겠습니다 :)

    backend-deploy.yml

    (Github Actions에서 사용)

    name: deploy

    # 1. prod/backend branch에 push 작업이 일어나면 해당 작업을 수행한다
    on:
    push:
    branches:
    - prod/backend

    jobs:
    docker-build:
    runs-on: ubuntu-latest
    defaults:
    run:
    working-directory: ./backend

    steps:
    # 2. 도커 허브에 로그인
    - name: Log in to Docker Hub
    uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
    with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_PASSWORD }}
    - uses: actions/checkout@v3

    # 3. JDK 17 설치 및 빌드 (프로젝트 Java version)
    - name: Set up JDK 17
    uses: actions/setup-java@v3
    with:
    java-version: '17'
    distribution: 'adopt'

    - name: Gradle Caching
    uses: actions/cache@v3
    with:
    path: |
    ~/.gradle/caches
    ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
    ${{ runner.os }}-gradle-

    - name: Grant execute permission for gradlew
    run: chmod +x gradlew
    - name: Build for asciiDoc
    run: ./gradlew bootjar

    - name: Build with Gradle
    run: ./gradlew bootjar

    # 4. 산출물을 Image로 빌드 후 Docker Hub에 Image Push하기
    - name: Extract metadata (tags, labels) for Docker
    id: meta
    uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
    with:
    images: woowacarffeine/backend

    - name: Build and push Docker image
    uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
    with:
    context: .
    file: ./backend/Dockerfile
    push: true
    platforms: linux/arm64
    tags: woowacarffeine/backend:latest
    labels: ${{ steps.meta.outputs.labels }}


    deploy:
    # 5. Self-hosted 작동 -> infra 인스턴스에서 작동됨
    runs-on: self-hosted
    if: ${{ needs.docker-build.result == 'success' }}
    needs: [ docker-build ]
    steps:

    # 6. infra 인스턴스에서 prod 인스턴스로 접근 (아래부터는 prod 서버 내에서 작업)
    - name: Join EC2 prod server
    uses: appleboy/ssh-action@master
    env:
    JASYPT_KEY: ${{ secrets.JASYPT_KEY }}
    DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
    DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
    with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USERNAME }}
    key: ${{ secrets.SERVER_KEY }}
    port: ${{ secrets.SERVER_PORT }}
    envs: JASYPT_KEY, DATABASE_USERNAME, DATABASE_PASSWORD

    script: |

    # 7. Docker Hub에서 Image를 pull해온다
    sudo docker pull woowacarffeine/backend:latest

    # 8. 만약 8080 포트가 켜져 있으면 새로운 서버의 포트는 8081로 설정
    if sudo docker ps | grep ":8080"; then
    export BEFORE_PORT=8080
    export NEW_PORT=8081
    export NEW_ACTUATOR_PORT=8089

    # 9. 만약 8081 포트가 켜져 있으면 새로운 서버의 포트는 8080로 설정
    else
    export BEFORE_PORT=8081
    export NEW_PORT=8080
    export NEW_ACTUATOR_PORT=8088
    fi

    # 10. Docker로 새로운 서버를 띄운다.
    sudo docker run -d -p $NEW_PORT:8080 -p $NEW_ACTUATOR_PORT:8088 \
    -e "SPRING_PROFILE=prod" \
    -e "ENCRYPT_KEY=${{secrets.JASYPT_KEY}}" \
    -e "DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}}" \
    -e "DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}}" \
    -e "REPLICA_DATABASE_USERNAME=${{secrets.REPLICA_DB_USER_NAME}}" \
    -e "REPLICA_DATABASE_PASSWORD=${{secrets.REPLICA_DB_USER_PASSWORD}}" \
    -e "SLACK_WEBHOOK_URL=${{secrets.SLACK_WEBHOOK_URL}}" \
    --name backend$NEW_PORT \
    woowacarffeine/backend:latest

    # 11. prod 인스턴스에 있는 bluegreen.sh 를 작동한다. (이 때 port 값을 같이 넣어준다.)
    sudo sh /home/ubuntu/bluegreen.sh $BEFORE_PORT $NEW_PORT $NEW_ACTUATOR_PORT



    bluegreen.sh

    (prod 인스턴스 내부에 존재)

    #!/bin/bash

    # 1. Github Actions를 통해 넘겨 받은 환경변수 값
    BEFORE_PORT=$1
    NEW_PORT=$2
    NEW_ACTUATOR_PORT=$3

    echo "기존 포트 : $BEFORE_PORT"
    echo "새로운 포트: $NEW_PORT"
    echo "새로운 ACTUATOR_PORT: $NEW_ACTUATOR_PORT"


    # 2. 20번 동안 헬스 체크를 진행
    count=0
    for count in {0..20}
    do
    echo "서버 상태 확인(${count}/20)";

    # 3. 새로운 서버가 작동되는지 Actuator를 통해 값을 받아옴
    STATUS=$(curl -s http://127.0.0.1:${NEW_ACTUATOR_PORT}/actuator/health-check)

    # 4. Actuator를 통해 성공적으로 서버가 띄워지지 않은 경우
    if [ "${STATUS}" != '{"status":"up"}' ]
    then
    # 5. 10초를 기다린 후 다시 헬스 체크를 진행한다.
    sleep 10
    continue
    else
    # 6. 헬스 체크를 통해 새로운 서버가 성공적으로 작동된다면 멈춘다.
    break
    fi
    done


    # 7. 20번의 헬스 체크를 하는 동안 새로운 서버가 제대로 작동되지 않은 경우 종료
    if [ $count -eq 20 ]
    then
    echo "새로운 서버 배포를 실패했습니다."
    exit 1
    fi


    # 8. 새로운 서버가 성공적으로 작동한 경우
    # Nginx를 통해 포트포워딩을 기존 포트에서 새로운 포트로 변경해준다.
    # 이 부분은 .inc 파일을 통해 Nginx에서 주입 받아서 포트만 변경해도 됩니다!
    export BACKEND_PORT=$NEW_PORT
    envsubst '${BACKEND_PORT}' < backend.template > backend.conf
    sudo mv backend.conf /etc/nginx/conf.d/
    sudo nginx -s reload


    # 9. 기존 서버를 내려주고, 도커 리소스를 정리해준다
    docker stop backend$BEFORE_PORT
    sudo docker container prune -f

    이렇게 카페인 팀에서는 무중단 배포를 도입할 수 있었습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git a/tags/ec-2/page/2.html b/tags/ec-2/page/2.html index aac2b88..7f874ce 100644 --- a/tags/ec-2/page/2.html +++ b/tags/ec-2/page/2.html @@ -5,7 +5,7 @@ "ec2" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -17,7 +17,7 @@ 기존 dev 서버는 총 4개의 서버를 배포하고 있었고 배포하는 서버는 다음과 같습니다. prod-BE, prod-FE, dev-BE, dev-FE

    그리고, 기존 dev 서버에서는 환경을 분리해주기 위해서 Nginx를 통해서 포트 포워딩은 다음과 같이 해주었습니다.

    • prod-BE = 8080
    • prod-FE = 3031
    • dev-BE = 8081
    • dev-FE = 3031

    카페인 팀에서는 dev, prod 환경이 분리되지 않아서 인스턴스의 사용량이 높았고, 이에 따라 추가적인 EC2 인스턴스가 필요했습니다.


    문제 해결

    다행히도 카페인 팀에서 추가적인 EC2 인스턴스를 받았고, 저희는 배포 환경을 분리할 수 있었습니다.

    dev-prod-server

    이와 같이 기존 dev 서버 한 개가 infra 서버와 연결되어 있었는데, 두 갈래로 나뉜 것을 확인하실 수 있습니다.

    먼저 배포는 다음과 같이 진행됩니다.

    release branch에 push가 일어나면 dev서버에 배포 작업이 이뤄집니다. prod branch에 push가 일어나면 prod서버에 배포 작업이 이뤄집니다.

    또한 기존 dev 서버에서 4개의 포트포워딩 또한 굳이 그럴 필요가 없어졌습니다. 새로운 서버가 추가됨에 따라 dev, prod 서버 각각 Nginx에서 포트포워딩을 동일하게 FE:3000, BE:8080 으로 변경하였습니다.

    이렇게 카페인 팀에서는 dev, prod 환경을 분리했습니다.

    감사합니다!

    - + \ No newline at end of file diff --git a/tags/ec-2/page/3.html b/tags/ec-2/page/3.html index a635245..52fca6b 100644 --- a/tags/ec-2/page/3.html +++ b/tags/ec-2/page/3.html @@ -5,13 +5,13 @@ "ec2" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "ec2" 태그로 연결된 4개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    안녕하세요 우아한테크코스 카페인팀 누누입니다

    이번에 카페인 팀에서 배포 아키텍처를 결정하게 되었던 과정에 대해서 정리를 해보고 싶어서 글을 쓰게 되었습니다.

    아키텍처와 서버가 배포되는 과정을 보여드리면서 시작하도록 하겠습니다

    배포 아키텍처

    서버가 배포되는 과정은 다음과 같습니다.

    server image

    우아한테크코스 인스턴스에 대한 소개

    우테코에서 선택할 수 있는 인스턴스는 총 2가지 종류입니다.

    1. 퍼블릭 서브넷에 있는 인스턴스
      • 캠퍼스에서만 SSH 접근이 가능한 인스턴스입니다.
      • 미리 열려있는 포트들만 허용이 되어 있습니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다
    2. 프라이빗 서브넷에 있는 인스턴스
      • 퍼블릭 서브넷에 있는 인스턴스를 통해서만 접근이 가능합니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다.

    1번 인스턴스를 2개 사용 가능하고, 2번 인스턴스를 1개 사용 가능합니다.

    권장되는 환경에서 1개는 db 서버로 사용하고, 나머지 2개는 자유롭게 사용이 가능했습니다.

    그전에 알면 좋아요

    여기서는 Self Hosted Runner를 사용했는데요.

    Self Hosted Runner에 대한 내용은 여기 에 잘 나와있습니다.

    외부 IP로부터 SSH 접근이 불가능하기에, Self Hosted Runner 나, Jenkins 같은 방법을 사용할 수 있었는데, 러닝 커브를 고려해서 Self Hosted Runner를 선택하게 되었습니다.

    배포 아키텍처에 대한 고민

    저희 팀이 이번 아키텍처를 만들기 위해서 고민했던 점들은 다음과 같습니다.

    1. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
    3. 2차 데모데이까지의 과제인 개발 서버를 어떻게 구성할 수 있을까?

    여기서 1번을 가장 먼저 생각한 아키텍처를 구성하게 되었는데, 다음과 같습니다.

    선택의 기준이 되었던 것은 총 3가지였습니다.

    1. DB는 프라이빗 서브넷에 위치시키고, 우리 인스턴스를 거쳐서만 접근이 가능하게 한다.
      • 이 부분은 보안을 위해서 어쩔 수 없이 선택하게 된 부분입니다.이 부분을 고려하다 보니, 최소한으로 구성할 수 있는 구조가 db 용 private 인스턴스 1개, 그리고 우리가 사용할 public 인스턴스 1개가 됩니다
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
      • 개발용 인스턴스에 CD 툴이나, 모니터링 툴을 설치하게 되면, 운영 서버에도 동일하게 작업을 해야 합니다.
      • 이 부분을 최소화하기 위해서, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 되었습니다.
    3. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
      • 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 않는다면 개발용 인스턴스에서 장애가 발생했을 때, CD 툴과 모니터링 툴에도 영향을 미치게 됩니다. 이 부분을 생각했을 때도, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리해야 한다고 결정하게 되었습니다
      • 한 부분의 장애가 다른 툴까지 사용할 수 없게 만들게 되어서, 롤백이나, 상황 파악을 하기 힘들게 만들게 됩니다.

    이런 과정들을 생각했을 때, 인스턴스 1개를 개발 서버용으로, 인스턴스 1개를 CD 툴과 모니터링 툴을 설치한 인스턴스로 사용하게 되었습니다.

    실제 내부 구성은 어떻게 될까요?

    개발 서버

    이 인스턴스에는 총 2가지 기능이 들어가 있습니다.

    1. 프론트 서버
      • react로 되어있는 프론트엔드 코드를 사용자에게 전달해 주는 역할을 합니다.
    2. 백엔드 서버
      • spring으로 되어있는 api 서버입니다.

    물론, 이렇게 하면 두 곳 중 한 곳에 장애가 발생했을 때, 프론트 서버와 백엔드 서버가 모두 영향을 받게 됩니다.

    같이 관리하게 된 첫 번째 이유로 비용이 들기 때문에 비용의 문제를 고려하게 되었습니다. 개발 서버에서 프론트 서버와 백엔드 서버를 관리하게 되었습니다.

    두 번째 이유로는, 아직 프로젝트 초창기 이기 때문에, 백엔드에서 장애가 났을 때, 프론트에서 일정 이상의 에러 처리가 불가능했습니다.

    프로젝트가 많이 진행되었다면, 프론트엔드만으로 혹은 장애가 나지 않은 서버를 활용해 에러 처리를 할 수 있지만, 아직은 그런 기능을 구현하지 못했습니다.

    이와는 별개로 실행 시 편의를 위해서 도커를 사용해 개발 서버를 관리하고 있습니다.

    CD 툴과 모니터링 툴

    이 인스턴스에는 총 3가지 기능이 들어가 있습니다.

    1. CD 툴
      • 위에서 설명드린 것처럼, self hosted runner 가 동작하게 되어있습니다
    2. 보안을 위한 리버스 프록시
      • 저희 프로젝트에서 구글 지도를 사용하게 되는데, 이때 API 키를 사용하게 됩니다. 이렇게 하면, API 키를 노출시키지 않고, 사용할 수 있습니다.
      • 이 API 키를 노출시키지 않기 위해서, 리버스 프록시를 하나 두고, 여기서 API 키를 추가해 요청을 보내는 방식으로 구성하게 되었습니다.
    3. 모니터링 툴
      • 저희 프로젝트에서 아직 도입하지 않았지만, 현재 이슈로는 올라가 있는 상태입니다.
      • Actuator, 프로메테우스, 그라파나 이 3가지를 활용해서 모니터링 툴을 구성하게 될 예정입니다

    위 기능들이 한 인스턴스에 모여있기에, 위의 기능들은 추후에 운영 서버가 추가되었을 때, 중복으로 관리하지 않아도 됩니다.

    배포 과정 더 자세히 알아보기

    아래에 사진에서 보이는 과정을 통해서 배포를 진행하고 있는데요

    server image

    1. 사용자가 push를 하면, github actions에서 도커 빌드를 진행하고, 도커 허브에 이미지를 올립니다.
    2. 도커 허브에 이미지가 올라간 이후에, self hosted runner 가 작동을 시작합니다.
    3. 개발용 인스턴스에 접근해서, 이미지를 받고, 컨테이너를 실행합니다.

    이런 과정을 통해서, 개발용 인스턴스에 배포를 진행하고 있습니다.

    느낀 점

    좋은 아키텍처를 설계하기 위해서는 고려해야 할 점들이 정말 많다는 것을 다시 한번 느꼈습니다.

    운영 서버가 추가된다던가, 인스턴스가 늘어나고, 줄어드는 상황에 유연하게 대처할 수 있도록 설계를 해야 한다는 것을 다시 한번 느꼈습니다.

    중복으로 관리될 포인트를 줄여야 한다는 것도 다시 한번 느낄 수 있었고요

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/tags/ec-2/page/4.html b/tags/ec-2/page/4.html index e832503..0c787a2 100644 --- a/tags/ec-2/page/4.html +++ b/tags/ec-2/page/4.html @@ -5,7 +5,7 @@ "ec2" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -21,7 +21,7 @@ 물론 이는 계획이고 공부하지 않은 다른 내용이 있을 수 있기 때문에 언제든 바뀔 수 있습니다.

    무중단 배포 아키텍처 적용

    이 또한 아직은 먼 이야기지만, 고려해 볼 상황이라서 적어봤습니다.

    사용자가 이용하고 있는 서비스가 갑자기 중단된다면 어떨까요? 저는 화가 많이 날 것 같습니다.

    피치 못할 사정으로 서버가 터져도, 사용자가 서비스를 계속 이용할 방법이 없을까요?

    이런 고민을 해결하기 위해서 나온 개념이 무중단 배포입니다.

    카나리아 배포, Blue/Green 배포, 롤링등 무중단 배포를 위한 여러가지 전략은 이미 존재합니다. 이 부분은 아직은 서버의 명세가 정확하지 않아서 어떤 방식으로 어떻게 처리할 것인지에 대해서는 아직 정할 수는 없습니다.

    이는 명세가 확실하게 정해진 후 팀원과 장단점을 상의하며 결정할 일이기 때문에 현재까지는 "이 정도를 고려하고 있다." 정도만 알면 될 것 같습니다.

    - + \ No newline at end of file diff --git a/tags/error.html b/tags/error.html index 394252b..3a7c6b9 100644 --- a/tags/error.html +++ b/tags/error.html @@ -5,13 +5,13 @@ "error" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "error" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 12분
    누누

    안녕하세요 카페인팀 nunu입니다.

    오늘은 스프링에서 발생한 에러 로그를 슬랙으로 모니터링하는 방법에 대해서 알아보려고 합니다.

    목차는 다음과 같습니다.

    1. 스프링에서 로그를 남기는 방법
    2. Slf4 j의 동작원리
    3. Logback의 동작원리
    4. Logback을 사용해서 슬랙으로 에러 로그를 모니터링하는 방법

    스프링에서 로그는 어떻게 찍을까?

    스프링에서 로그를 찍는 방법은 여러 가지가 있지만, 가장 간단한 방법은 System.out.println()을 사용하는 것입니다.

    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test() {
    System.out.println("test");
    return "test";
    }
    }

    당연하지만, 성능이 안 좋아서 실제 서비스에서는 사용하지 않습니다.

    스프링에서는 Slf4 j를 통해서 로그를 남길 수 있습니다.

    @Slf4j // private final Logger log = LoggerFactory.getLogger(this.getClass()); 와 같다.
    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test() {
    log.info("test");
    return "test";
    }
    }

    이 코드를 통해서 로그를 남길 수 있는데, 자동으로 콘솔에 출력이 됩니다.

    스프링에서 로깅은 어떻게 작동하는 거지?

    스프링 4까지는 Commons Logging을 사용했었습니다.

    Commons LoggingJCL이라고도 불리며, JDK Logging, Log4 j, Logback 등 다양한 로깅 프레임워크를 지원합니다.

    JCL 은 런타임에 어떤 로깅 프레임워크를 사용할지 결정할 수 있습니다.

    런타임에 어떤 로깅 프레임워크를 사용할지 결정하는 방식으로 클래스 로더에게 질의를 하는 방식으로 작동하게 되는데

    클래스 로더에게 질의를 했을 경우에 몇 가지 문제점이 생깁니다

    1. 클래스 로더에 명확한 표준이 없고, 부모 자식 모델이 있어서, 클래스 로더에 따라서 다른 결과가 나올 수 있습니다. 참고
    2. 클래스로더는 gc의 동작에 방해를 일으켜서 메모리 누수를 발생시킬 수 있습니다. 참고

    @Slf4j 어노테이션을 붙이면, 컴파일 시점에 private final Logger log = LoggerFactory.getLogger(this.getClass()); 와 같은 코드로 변환됩니다.

    스프링 5에서는 Slf4j 가 사용하는 것처럼, 컴파일 타임에 어떤 로깅 프레임워크를 사용할지 결정하는 기능을 작성했고, Commons Logging을 사용하지 않게 되었습니다.

    spring 5에서 변경되었다는 링크

    Slf4 j에 대해서 알아보자

    Slf4 j는 로깅을 위한 인터페이스를 제공하는 프레임워크입니다.(Simple Logging Facade for Java)

    컴파일 타임에, 어떤 로그 라이브러리를 사용할지 결정하는 기능을 제공합니다.

    로그 라이브러리를 바꾸려고 했을 때, 기존 코드는 하나도 건드리지 않고, 로그 라이브러리만 바꿔주면 되도록 해줍니다.

    조금 더 자세한 동작 원리를 알아보자

    only slf4j

    Slf4 j 만을 사용했을 경우 위 사진 같은 형태로 요청이 처리가 됩니다.

    Slf4 j 라는 인터페이스를 통해서 로그를 남기고, 어떤 로그 라이브러리를 사용할지는 Slf4j binding이라는 것을 통해서 결정합니다.

    Slf4j bindingSlf4j의 인터페이스를 구현하고 있지 않은 라이브러리의 구현체를 연결해 주는 역할을 합니다.

    그 구현체로 Slf4j-log4 j12-{version}. jar 같은 것이 있다.

    이와는 다르게 Logback 은 Slf4 j 를 구현하고 있기에, Slf4j binding 을 사용하지 않아도 됩니다.

    logback example

    위 사진처럼 Slf4j binding 을 사용하지 않고, Logback 바로 사용하는 것도 가능합니다.

    그렇다면 Slf4 j를 바로 사용하지 않은 코드에서 Slf4j 를 사용하려면 어떻게 해야 할까요?

    slf4j working principle

    위 사진처럼 Slf4j bridge 를 통해서 외부 라이브러리를 사용하는 것처럼 갈아 끼울 수 있습니다.

    Log4j2 를 사용하는 코드를 전혀 바꾸지 않아도, BridgeSlf4j 를 통해 Logback으로 자연스럽게 로그를 남길 수 있도록 해줍니다.

    Logback에 대해서 알아보자

    Logback 은 스프링에서 기본으로 사용될 만큼 인기 있는 로그 라이브러리입니다.

    logback 동작 과정

    공식문서에서 아주 핵심적인 동작원리를 설명해주고 있는 사진이라서 가져왔습니다.

    너무 어려워 보여서, 조금 자세하게 각각의 구성요소에 대해서 알아보도록 하겠습니다

    이에 대해 알아보도록 하겠습니다

    로그백의 구성요소

    Appender

    Appender는 로그를 어디에 출력할지를 결정하는 역할을 합니다.

    외부로부터 어떤 데이터를 받아서, 어떤 방식으로 처리할지에 대해서 전체적으로 설정할 수 있습니다.

    기본적으로 수많은 Appender 가 제공되고 있습니다.

    • ConsoleAppender
    • FileAppender
    • RollingFileAppender
    • AsyncAppender
    • DBAppender
    • SMTPAppender
    • SocketAppender
    • SyslogAppender

    저희는 Slack에 알림을 주는 것이 목적이기 때문에, SlackAppender를 사용하면 될 것 같습니다.

    하지만 SlackAppender는 제공되고 있지 않기에 직접 구현을 해야 하는데요

    이를 구현했을 때, Slack API 가 끝날 때까지, 계속 기다리고 있을 필요가 없기에, AsyncAppender를 사용하는 것이 좋을 것 같습니다.

    사용 방법은 다음과 같습니다. xml 기반으로 가능한데요

    <configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>myapp.log</file>
    <encoder>
    <pattern>%logger{35} -%kvp -%msg%n</pattern>
    </encoder>
    </appender>

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE" />
    </appender>

    <root level="DEBUG">
    <appender-ref ref="ASYNC" />
    </root>
    </configuration>

    만약 여기에 있는 기능들로 부족하다면, 직접 Appender 를 구현해서 사용할 수도 있습니다.

    직접 구현하려면 AppenderBase를 상속받아서 구현하면 됩니다.

    이 클래스는 필요한 부분이 대부분 구현되어 있고, appender 만 구현하면 바로 사용할 수 있습니다. 당연하지만 필요하다면 override 도 가능하죠

    Layout

    Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 합니다.

    Appender는 로그를 어디에 출력할지를 결정하는 역할을 하고, Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 하도록 하는 것이 이상적이지만

    Logback 은 Appender에서 Layout 을 직접 지정할 수 있도록 해주고 있습니다.

    따라서, 직접 Layout 을 만들지 않고, Appender 에서 기존에 이미 있는 패턴만 사용하려고 합니다

    Encoder

    Encoder는 Layout 과 비슷한 역할을 합니다.

    Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 하고, Encoder 는 실제 byte 형태로 변환하는 역할을 합니다.

    Slack의 webhook을 사용할 것이지만, AppenderBase를 사용하기에, 이번에는 사용할 수 없습니다.

    Filter

    Filter는 로그를 어떤 조건에 따라서 출력할지를 결정하는 역할을 합니다.

    Filter 는 Appender를 등록하며 같이 등록할 수 있는데요

    이번 프로젝트에서는 Level 이 ERROR 이상인 것만 출력하도록 하고 싶기에, LevelFilter를 사용하면 좋을 것 같습니다.

    <configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>INFO</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>
    <encoder>
    <pattern>
    %-4relative [%thread] %-5level %logger{30} -%kvp -%msg%n
    </pattern>
    </encoder>
    </appender>
    <root level="DEBUG">
    <appender-ref ref="CONSOLE" />
    </root>
    </configuration>

    와 비슷하게 사용할 수 있어 보입니다.

    그러면 실제로 프로젝트에서 error 발생 시 slack으로 알림을 주는 것을 구현해 보도록 하겠습니다.

    슬랙에 추가하는 방법

    이 블로그를 보고서 작성했습니다

    실제 구현

    구현된 결과물은 아래와 같습니다

    slack appender

    SlackAppender 구현하기

    public class SlackAppender extends AppenderBase<ILoggingEvent> {

    @Override
    protected void append(final ILoggingEvent eventObject) {
    final var restTemplate = new RestTemplate();
    final var url = "https://hooks.slack.com/services/";
    final Map<String, Object> body = createSlackErrorBody(eventObject);
    restTemplate.postForEntity(url, body, String.class);
    }

    private Map<String, Object> createSlackErrorBody(final ILoggingEvent eventObject) {
    final String message = createMessage(eventObject);
    return Map.of(
    "attachments", List.of(
    Map.of(
    "fallback", "요청을 실패했어요 :cry:",
    "color", "#2eb886",
    "pretext", "에러가 발생했어요 확인해주세요 :cry:",
    "author_name", "car-ffeine",
    "text", message,
    "fields", List.of(
    Map.of(
    "title", "우선순위",
    "value", "High",
    "short", false
    ),
    Map.of(
    "title", "서버 환경",
    "value", "local",
    "short", false
    )
    ),
    "ts", eventObject.getTimeStamp()
    )
    )
    );
    }

    private String createMessage(final ILoggingEvent eventObject) {
    final String baseMessage = "에러가 발생했습니다.\n";
    final String pattern = baseMessage + "```%s %s %s [%s] - %s```";
    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    return String.format(pattern,
    simpleDateFormat.format(eventObject.getTimeStamp()),
    eventObject.getLevel(),
    eventObject.getThreadName(),
    eventObject.getLoggerName(),
    eventObject.getFormattedMessage());
    }
    }

    이 과정에서 url을 직접 입력하시면 됩니다.

    그리고, 이렇게 만든 SlackAppender를 logback-spring.xml 에 등록하면 됩니다.

    <?xml version="1.0" encoding="UTF-8"?>

    <configuration scan="true" scanPeriod="60 seconds">
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <include resource="org/springframework/boot/logging/logback/file-appender.xml"/>
    <root level="INFO">
    <appender-ref ref="FILE"/>
    <appender-ref ref="CONSOLE"/>
    </root>
    <appender name="SLACK_APPENDER" class="racingcar.SlackAppender">
    </appender>
    <appender name="ASYNC_SLACK_APPENDER" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="SLACK_APPENDER"/>
    </appender>
    <logger name="racingcar" level="ERROR" additivity="true">
    <appender-ref ref="ASYNC_SLACK_APPENDER"/>

    </logger>

    </configuration>

    이렇게 하면, racingcar 패키지에서 에러가 발생할 때만 slack으로 알림을 받을 수 있습니다.

    결론

    slack appender

    이번 글에서는 log 레벨에 따라 slack 으로 알림을 받는 방법을 알아보았습니다.

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/tags/filter.html b/tags/filter.html index a83394f..d5ed88e 100644 --- a/tags/filter.html +++ b/tags/filter.html @@ -5,7 +5,7 @@ "filter" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -18,7 +18,7 @@ 평균적으로 0.63초가 나왔습니다. 약 25 ~ 30%의 조회 속도가 개선되었습니다.

    아직 이 부분은 개선이 더 필요해보입니다.

    그래도 개선이 됐고, 삽입과 갱신에는 큰 지장이 없어서 일단 이정도로 마무리 하고, 추후에 개선을 해보도록 하겠습니다.

    이미지 추가적으로 충전기 조회는 굉장히 빨라졌습니다!

    배우는 단계이다보니 미숙하고 틀린 부분이 있을 수 있습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git a/tags/ga-4.html b/tags/ga-4.html index 424f32f..1e0a468 100644 --- a/tags/ga-4.html +++ b/tags/ga-4.html @@ -5,7 +5,7 @@ "ga4" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -20,7 +20,7 @@ no offset no offset no offset

    집계 된 자료처럼 방문자들이 단순 방문만 한 것이 아니라, 수 많은 이벤트를 발생시키고 평균 참여 시간도 상당 부분 확보했음을 확인할 수 있습니다.

    - + \ No newline at end of file diff --git a/tags/gc.html b/tags/gc.html index 72351f0..d00dcfa 100644 --- a/tags/gc.html +++ b/tags/gc.html @@ -5,13 +5,13 @@ "gc" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "gc" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 6분
    누누

    우아한테크코스에서 자바 11을 사용하는 것이 너무 익숙해진 상황이어서, java 11 대신 java 17을 쓰려면 쓰는 대신, 왜 java 17을 쓰면 좋은지에 대해서 설득을 하는 시간이 있어야 하는데요

    처음에는 단순히 record 클래스가 좋아요, collect(Collectors.toList()); 대신 toList() 만으로 해결할 수 있어서 좋아요

    까지밖에 설명할 수 없었습니다.

    이것만으로 동의를 해줘서 일단 java 17 을 사용하기로 했지만, 이번 기회에 조금 더 자세하게 알아보려고 합니다

    Java 17 과 Java 11의 중요한 차이들

    기능적인 부분과, 숨겨진 부분을 나누어볼 수 있을 것 같습니다.

    기능적인 차이점

    언제나 직접 차이를 보면 더 직관적이기 때문에, 직접 코드를 보면서 설명을 해보려고 합니다

    record 클래스

    간단한 dto 클래스를 만들었을 때 코드가 정말 간단해지는 것을 확인할 수 있습니다

    Java 11

    public class Dto {
    private final int data;

    public Dto(int data) {
    this.data = data;
    }

    public int getData() {
    return data;
    }
    }

    lombok 을 사용했을 때


    @Getter
    @AllArgsConstructor
    public class Dto {
    private final int data;
    }

    Java17

    public record Record(int data) {
    }

    이렇게 보면 훨씬 간단해진 것을 볼 수 있습니다

    예상되는 문제점

    objectMapper를 사용하면 어떻게 되나요? noArgsConstructor 가 필요하지 않나요?

    class RecordTest {

    @Test
    void objectMapper_로_변환() throws JsonProcessingException {
    // given
    ObjectMapper objectMapper = new ObjectMapper();
    Record record = new Record(1);

    // when
    String json = objectMapper.writeValueAsString(record);

    // then
    assertEquals("{\"data\":1}", json);
    }

    @Test
    void string_에서_객체로_변환() throws JsonProcessingException {
    // given
    String json = "{\"data\":1}";
    ObjectMapper objectMapper = new ObjectMapper();

    // when
    Record record = objectMapper.readValue(json, Record.class);

    // then
    assertEquals(1, record.data());
    }
    }

    이 테스트에서 볼 수 있는 것처럼 성공적으로 deserialize, serialize 가 가능합니다

    toList() method

    Java 11

    이 부분도 정말 편의성이 높다고 생각하는 부분 중 하나인데요

    Collectors.toList() 대신 toList() 만으로도 사용이 가능합니다

    public class ToListWith11 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .collect(Collectors.toList());
    System.out.println(result);
    }
    }

    Java 17

    public class ToListWith17 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .toList();
    System.out.println(result);
    }
    }

    switch expression

    Java 11

    우테코에서는 switch, case 를 싫어하기에 볼 수는 없겠지만

    switch 문에도 정말 편하게 바뀌었는데요

    public class SwitchWith11 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = 0;
    switch (day) {
    case "Monday":
    result = 1;
    break;
    case "Tuesday":
    result = 2;
    break;
    case "Wednesday":
    result = 3;
    break;
    case "Thursday":
    result = 4;
    break;
    case "Friday":
    result = 5;
    break;
    case "Saturday":
    result = 6;
    break;
    case "Sunday":
    result = 7;
    break;
    }
    System.out.println(result);
    }
    }

    Java 17

    public class SwitchWith17 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = switch (day) {
    case "Monday" -> 1;
    case "Tuesday" -> 2;
    case "Wednesday" -> 3;
    case "Thursday" -> 4;
    case "Friday" -> 5;
    case "Saturday" -> 6;
    case "Sunday" -> 7;
    default -> 0;
    };
    System.out.println(result);
    }
    }

    코드 량이 엄청 줄어든 것을 확인하실 수 있습니다

    instanceof pattern matching

    물론 instanceof 를 사용할 경우가 많은가? 하면 많지는 않겠지만

    아래와 같이 변경되었습니다

    Java 11

    public class InstanceOfWith11 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.toUpperCase());
    }
    }
    }

    Java 17

    public class InstanceOfWith17 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String str) {
    System.out.println(str.toUpperCase());
    }
    }
    }

    number format

    이 기능은 12에 나왔는데요

    언어별로 숫자를 표현하는 방식이 다르지만, 쉽게 표현할 수 있도록 도와주는 기능입니다

    Java 17

    public class NumberFormatterWith11 {
    public static void main(String[] args) {
    int number = 1_000_000;

    String result = NumberFormat.getCompactNumberInstance(Locale.KOREA, NumberFormat.Style.LONG).format(number);

    System.out.println(result.equals("100만"));
    }
    }

    나머지 부분은 사실 그렇게 큰 역할을 할 것 같지는 않아서 생략하겠습니다

    숨겨진 부분들

    gc throughput

    위의 사진은 gc 의 버전별 처리량입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 15% 정도 향상되었고, java 11과는 10% 정도 향상되었습니다.

    gc latency

    위의 사진은 gc의 버전별 지연시간입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 30% 정도 향상되었고, java 11과는 25% 정도 향상되었습니다.

    이와 같이, 단순하게 새로운 기능만 추가되는 것이 아니라 꾸준히 성능도 향상되고 있습니다.

    이런 부분을 고려했을 때, Java 17을 사용하는 것이 좋을 것 같습니다.

    참고

    - + \ No newline at end of file diff --git a/tags/git-branch.html b/tags/git-branch.html index 54fba08..a61d1bf 100644 --- a/tags/git-branch.html +++ b/tags/git-branch.html @@ -5,13 +5,13 @@ "git branch" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "git branch" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    현재 상황은 어떤데?

    현재 우아한테크코스에서는 프론트 코드와 백엔드 코드가 같은 레포지토리를 사용하고 있습니다.

    프론트와 백엔드가 같이 작업하기에, 의도치 않은 충돌이 자주 생길 수 있는 구조이기에, 이를 git branch 전략으로 충돌을 줄이고자 합니다

    Git Branch 전략이란?

    git을 사용해서 소프트웨어 개발을 관리하는 방법입니다.

    여러 개발자가 동시에 작업하고 코드를 통합할 때 생기는 충돌을 효율적으로 조정하기 위한 방법입니다.

    왜 git branch 전략이 중요한데?

    아래에 있는 4가지를 제외하고도 훨씬 많은 장점이 있을 수 있습니다.

    1. 동시 작업이 편하다

    여러 사람이 독립적으로 작업하고, 커밋을 할 때, 자신의 브랜치에서 변경 사항을 커밋하게 됩니다.

    브랜치가 병합될 때만 충돌을 해결하면 되니, 아무 규칙이 없는 것보다 충돌 시점이 명확해지기에 생산성을 높일 수 있습니다.

    2. 목적이 명확한 브랜치

    애플리케이션의 상태에 몇 가지가 있는데, 안정된 프로덕션, 테스트 환경, 기능 추가 환경... 등이 있습니다

    여러 기능별 브랜치(안정된 버전의 코드만이 관리되는 브랜치, 테스트 환경을 위한 브랜치, 기능 추가를 위한 브랜치)를

    네이밍을 통해 구분하면 각각의 브랜치에 대해서 추가적인 설명을 할 필요 없이 명확하게 관리할 수 있습니다.

    3. 배포 파이프라인 관리가 편함

    브랜치가 네이밍으로 명확하게 구분이 되어있다면, 조건을 설정하기 쉽습니다.

    특정 타입의 브랜치에 push 되었을 때, pull request를 만들었을 때 같은 조건에 따른 스크립트를 만들어둔다면 CI/CD를 구축하기 쉽습니다.

    4. 버전 관리가 편리하다

    서버에 문제가 생겼을 때, 어떤 브랜치로 돌아가서 롤백을 해야 하는지에 대한 것들이 명확합니다.

    안정된 브랜치가 어떤 것인지 명확하기에, 롤백 과정에 대한 의사결정을 줄일 수 있습니다.

    그러면 어떤 종류가 있는지 더 자세하게 알아보도록 하겠습니다.

    Git Branch 전략의 종류는?

    총 3가지의 전략이 있습니다.

    1. Github Flow

    2. Gitlab Flow

    3. Git Flow

    git을 사용하기에, Git Flow라는 네이밍이 가장 직관적이고 유명한데요. 

    3가지 전략 중에서 가장 복잡하기에, 쉬운 순서대로 진행해 보도록 하겠습니다.

    1. Github Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img

    img2

    브랜치는 총 2가지 종류가 존재합니다

    1. master 브랜치

    여기에 머지가 되면 배포가 되도록 CD를 연결해 놓은 경우가 많습니다.

    안정된 버전의 코드가 관리되는 브랜치입니다.

    2. feature 브랜치

    기능 추가, 버그 수정 등 모든 작업은 feature 브랜치에서 일어납니다.

    master 브랜치에서 새로운 브랜치를 만들어서, 마스터로 머지되는 단순한 사이클을 가지고 있습니다.

    장점

    위에서 볼 수 있는 것처럼 2종류의 브랜치만 있기에, 정말 간단합니다.

    학습 과정까지의 러닝 커브가 거의 없다시피 하기에, 간단한 프로젝트에 적용하기 정말 좋습니다.

    릴리즈 되지 않은 코드가 최소화됩니다. 최신 버전의 코드와 최대한 빠르게 동기화를 계속해서 시킬 수 있습니다

    단점

    모든 코드는 다 master 브랜치에 머지가 되어야 한다는 점이 개발 서버와, 운영서버를 나누기 애매할 수 있습니다.

    개발 서버에 배포를 하고 싶은 상황이라면, master에 머지가 되어야 합니다.

    머지가 된 이후에 cd 파이프라인을 통해서 개발 서버와 운영 서버 모두에 배포가 됩니다.

    여러 환경을 나누고 관리를 하고 싶으시다면 다음에 소개해드릴 전략을 사용해 보셔도 좋을 것 같습니다

    2. Gitlab Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img2

    밑에 환경은 총 2개의 서버가 존재할 때를 가정하고 있습니다.

    1. pre-production 서버

    2. production 서버

    편의를 위해 main에 머지되는 과정은 간단하게 표현했습니다.

    img3

    브랜치 종류

    총 3가지 브랜치가 필요하고, 추가에 따라서 더 추가할 수 있습니다.

    1. main(or develop) 브랜치

    기능에 대한 개발이 완료되었지만, 여기에 머지되어도 바로 배포되지는 않습니다.

    2. feature브랜치

    기능을 개발하는 브랜치입니다. Github Flow 와도 유사합니다.

    3. production 브랜치

    실제 배포가 일어나는 브랜치입니다. 

    여기에 머지가 되는 순간 배포가 일어납니다.

    위 사진에 있는 것처럼, 필요에 따라서 pre-production이나, staging 같은 환경에 따른 브랜치를 추가할 수 있습니다.

    특징

    1. 무조건 단방향으로 머지가 일어납니다.

    긴급하게 라이브 서버에 수정을 해야 할 때, production 부터 시작하는 것이 아닌, main 부터 차근차근 올라가야 합니다

    2. 환경에 따라 브랜치 종류가 늘어날 수 있습니다.

    위 사진에서는 pre-production 이 그 예시가 되겠네요.

    장점

    1. Github Flow에서 환경별 브랜치를 통해서 개발 서버나 pre-production 서버에 버전을 깔끔하게 관리할 수 있습니다.

    3. Git Flow

    브랜치 전략 중 가장 처음으로 유명해진 브랜치 전략입니다.

    배포가 특정 주기를 가지고 있는 애플리케이션일 때, 가장 적합합니다.

    가장 복잡한 전략을 가지고 있어서, 모두가 브랜치 전략에 대해서 이해하고 있다면 역할에 따른 깔끔한 분리가 가능합니다

    그림으로 보고 가도록 하겠습니다

    img4

    가장 유명한 브랜치 전략이지만, 가장 어려운 전략이기도 합니다.

    특징

    1. 브랜치에 대해서 양방향으로 머지가 일어납니다

    release 브랜치에서 버그 수정이 일어나면, develop 브랜치에도 머지해줘야 합니다.

    hotfix 브랜치를 main 브랜치뿐만 아니라, develop 브랜치에도 머지해줘야 합니다

    브랜치의 종류가 5가지나 됩니다

    1. main

    production 이 배포되었을 때, 이 브랜치에 머지되는 것이 기준이 됩니다.

    2. develop 

    위에서 설명드렸던 브랜치들과 큰 차이가 없이 배포 전 브랜치입니다.

    3. feature

    기능을 개발할 때 사용하는 브랜치입니다. 이것도 위와 큰 차이가 없습니다

    4. release

    Gitlab Flow에서 pre-production에 해당한다고 봐도 무방합니다.

    여기서 버그 수정이 일어났을 경우에,  develop에 머지하는 것을 까먹으면 안 됩니다.

    5. hotfix

    main 브랜치에서 생성된 브랜치로, 긴급한 변경사항을 처리합니다.

    이때, develop에 머지하는 것을 깜빡하면 안 됩니다.

    더 자세하게 알아보실 분은 아래 링크들을 확인해 보세요

    우리 프로젝트에는 어떤 것이 적절할까?

    나중에 개발 서버 혹은 스테이징 서버를 두고 싶기에, 이 부분에 대한 처리가 부족한 Github Flow는 적절하지 않습니다.

    Git Flow는 깔끔하게 처리할 수 있지만, 러닝 커브가 Gitlab Flow 보다 약간 더 있어서, 빠르게 개발하는 취지에 맞지 않아 보였습니다.

    이런 과정을 통해서 Gitlab Flow를 사용하려고 합니다 

    참고

    https://techblog.woowahan.com/2553/

    https://docs.gitlab.com/ee/topics/gitlab_flow.html

    - + \ No newline at end of file diff --git a/tags/git-flow.html b/tags/git-flow.html index c4f605c..c756300 100644 --- a/tags/git-flow.html +++ b/tags/git-flow.html @@ -5,13 +5,13 @@ "git flow" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "git flow" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    현재 상황은 어떤데?

    현재 우아한테크코스에서는 프론트 코드와 백엔드 코드가 같은 레포지토리를 사용하고 있습니다.

    프론트와 백엔드가 같이 작업하기에, 의도치 않은 충돌이 자주 생길 수 있는 구조이기에, 이를 git branch 전략으로 충돌을 줄이고자 합니다

    Git Branch 전략이란?

    git을 사용해서 소프트웨어 개발을 관리하는 방법입니다.

    여러 개발자가 동시에 작업하고 코드를 통합할 때 생기는 충돌을 효율적으로 조정하기 위한 방법입니다.

    왜 git branch 전략이 중요한데?

    아래에 있는 4가지를 제외하고도 훨씬 많은 장점이 있을 수 있습니다.

    1. 동시 작업이 편하다

    여러 사람이 독립적으로 작업하고, 커밋을 할 때, 자신의 브랜치에서 변경 사항을 커밋하게 됩니다.

    브랜치가 병합될 때만 충돌을 해결하면 되니, 아무 규칙이 없는 것보다 충돌 시점이 명확해지기에 생산성을 높일 수 있습니다.

    2. 목적이 명확한 브랜치

    애플리케이션의 상태에 몇 가지가 있는데, 안정된 프로덕션, 테스트 환경, 기능 추가 환경... 등이 있습니다

    여러 기능별 브랜치(안정된 버전의 코드만이 관리되는 브랜치, 테스트 환경을 위한 브랜치, 기능 추가를 위한 브랜치)를

    네이밍을 통해 구분하면 각각의 브랜치에 대해서 추가적인 설명을 할 필요 없이 명확하게 관리할 수 있습니다.

    3. 배포 파이프라인 관리가 편함

    브랜치가 네이밍으로 명확하게 구분이 되어있다면, 조건을 설정하기 쉽습니다.

    특정 타입의 브랜치에 push 되었을 때, pull request를 만들었을 때 같은 조건에 따른 스크립트를 만들어둔다면 CI/CD를 구축하기 쉽습니다.

    4. 버전 관리가 편리하다

    서버에 문제가 생겼을 때, 어떤 브랜치로 돌아가서 롤백을 해야 하는지에 대한 것들이 명확합니다.

    안정된 브랜치가 어떤 것인지 명확하기에, 롤백 과정에 대한 의사결정을 줄일 수 있습니다.

    그러면 어떤 종류가 있는지 더 자세하게 알아보도록 하겠습니다.

    Git Branch 전략의 종류는?

    총 3가지의 전략이 있습니다.

    1. Github Flow

    2. Gitlab Flow

    3. Git Flow

    git을 사용하기에, Git Flow라는 네이밍이 가장 직관적이고 유명한데요. 

    3가지 전략 중에서 가장 복잡하기에, 쉬운 순서대로 진행해 보도록 하겠습니다.

    1. Github Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img

    img2

    브랜치는 총 2가지 종류가 존재합니다

    1. master 브랜치

    여기에 머지가 되면 배포가 되도록 CD를 연결해 놓은 경우가 많습니다.

    안정된 버전의 코드가 관리되는 브랜치입니다.

    2. feature 브랜치

    기능 추가, 버그 수정 등 모든 작업은 feature 브랜치에서 일어납니다.

    master 브랜치에서 새로운 브랜치를 만들어서, 마스터로 머지되는 단순한 사이클을 가지고 있습니다.

    장점

    위에서 볼 수 있는 것처럼 2종류의 브랜치만 있기에, 정말 간단합니다.

    학습 과정까지의 러닝 커브가 거의 없다시피 하기에, 간단한 프로젝트에 적용하기 정말 좋습니다.

    릴리즈 되지 않은 코드가 최소화됩니다. 최신 버전의 코드와 최대한 빠르게 동기화를 계속해서 시킬 수 있습니다

    단점

    모든 코드는 다 master 브랜치에 머지가 되어야 한다는 점이 개발 서버와, 운영서버를 나누기 애매할 수 있습니다.

    개발 서버에 배포를 하고 싶은 상황이라면, master에 머지가 되어야 합니다.

    머지가 된 이후에 cd 파이프라인을 통해서 개발 서버와 운영 서버 모두에 배포가 됩니다.

    여러 환경을 나누고 관리를 하고 싶으시다면 다음에 소개해드릴 전략을 사용해 보셔도 좋을 것 같습니다

    2. Gitlab Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img2

    밑에 환경은 총 2개의 서버가 존재할 때를 가정하고 있습니다.

    1. pre-production 서버

    2. production 서버

    편의를 위해 main에 머지되는 과정은 간단하게 표현했습니다.

    img3

    브랜치 종류

    총 3가지 브랜치가 필요하고, 추가에 따라서 더 추가할 수 있습니다.

    1. main(or develop) 브랜치

    기능에 대한 개발이 완료되었지만, 여기에 머지되어도 바로 배포되지는 않습니다.

    2. feature브랜치

    기능을 개발하는 브랜치입니다. Github Flow 와도 유사합니다.

    3. production 브랜치

    실제 배포가 일어나는 브랜치입니다. 

    여기에 머지가 되는 순간 배포가 일어납니다.

    위 사진에 있는 것처럼, 필요에 따라서 pre-production이나, staging 같은 환경에 따른 브랜치를 추가할 수 있습니다.

    특징

    1. 무조건 단방향으로 머지가 일어납니다.

    긴급하게 라이브 서버에 수정을 해야 할 때, production 부터 시작하는 것이 아닌, main 부터 차근차근 올라가야 합니다

    2. 환경에 따라 브랜치 종류가 늘어날 수 있습니다.

    위 사진에서는 pre-production 이 그 예시가 되겠네요.

    장점

    1. Github Flow에서 환경별 브랜치를 통해서 개발 서버나 pre-production 서버에 버전을 깔끔하게 관리할 수 있습니다.

    3. Git Flow

    브랜치 전략 중 가장 처음으로 유명해진 브랜치 전략입니다.

    배포가 특정 주기를 가지고 있는 애플리케이션일 때, 가장 적합합니다.

    가장 복잡한 전략을 가지고 있어서, 모두가 브랜치 전략에 대해서 이해하고 있다면 역할에 따른 깔끔한 분리가 가능합니다

    그림으로 보고 가도록 하겠습니다

    img4

    가장 유명한 브랜치 전략이지만, 가장 어려운 전략이기도 합니다.

    특징

    1. 브랜치에 대해서 양방향으로 머지가 일어납니다

    release 브랜치에서 버그 수정이 일어나면, develop 브랜치에도 머지해줘야 합니다.

    hotfix 브랜치를 main 브랜치뿐만 아니라, develop 브랜치에도 머지해줘야 합니다

    브랜치의 종류가 5가지나 됩니다

    1. main

    production 이 배포되었을 때, 이 브랜치에 머지되는 것이 기준이 됩니다.

    2. develop 

    위에서 설명드렸던 브랜치들과 큰 차이가 없이 배포 전 브랜치입니다.

    3. feature

    기능을 개발할 때 사용하는 브랜치입니다. 이것도 위와 큰 차이가 없습니다

    4. release

    Gitlab Flow에서 pre-production에 해당한다고 봐도 무방합니다.

    여기서 버그 수정이 일어났을 경우에,  develop에 머지하는 것을 까먹으면 안 됩니다.

    5. hotfix

    main 브랜치에서 생성된 브랜치로, 긴급한 변경사항을 처리합니다.

    이때, develop에 머지하는 것을 깜빡하면 안 됩니다.

    더 자세하게 알아보실 분은 아래 링크들을 확인해 보세요

    우리 프로젝트에는 어떤 것이 적절할까?

    나중에 개발 서버 혹은 스테이징 서버를 두고 싶기에, 이 부분에 대한 처리가 부족한 Github Flow는 적절하지 않습니다.

    Git Flow는 깔끔하게 처리할 수 있지만, 러닝 커브가 Gitlab Flow 보다 약간 더 있어서, 빠르게 개발하는 취지에 맞지 않아 보였습니다.

    이런 과정을 통해서 Gitlab Flow를 사용하려고 합니다 

    참고

    https://techblog.woowahan.com/2553/

    https://docs.gitlab.com/ee/topics/gitlab_flow.html

    - + \ No newline at end of file diff --git a/tags/git.html b/tags/git.html index de48924..c06602a 100644 --- a/tags/git.html +++ b/tags/git.html @@ -5,14 +5,14 @@ "git" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "git" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 3분
    야미

    프로젝트 브랜치명 컨벤션이 feat/이슈번호여서, 브랜치명에서 이슈번호만 가져온 다음 커밋할 때마다 커밋 메시지 아래단(footer)에 이슈 번호를 자동으로 입력해주고 싶었다. 자동으로 입력된다면 깜빡하고 이슈 번호를 안 적는 일도 없고, 시간도 단축할 수 있기 때문이다.

    아래 순서대로 진행한다면 이슈 번호 POSTFIX 자동화를 할 수 있다.

    1) 프로젝트 폴더에 .githooks 폴더 생성

    2) .githooks 폴더에 commit-msg 파일 생성

    #!/bin/bash

    COMMIT_MESSAGE_FILE_PATH=$1
    MESSAGE=$(cat "$COMMIT_MESSAGE_FILE_PATH")

    # 커밋 메시지가 없을 때, 커밋 방지
    if [[ $(head -1 "$COMMIT_MESSAGE_FILE_PATH") == '' ]]; then
    exit 0
    fi

    # 브랜치명에서 이슈 번호만 추출 ('/' 뒤에 있는 문자만 추출)
    POSTFIX=$(git branch | grep '\*' | sed 's/* //' | sed 's/^.*\///' | sed 's/^\([^-]*-[^-]*\).*/\1/')

    COMMIT_SOURCE=$2
    CURRENT_BRANCH=$(git branch --show-current)

    # [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] 👉🏻 현재 브랜치명과 POSTFIX가 똑같으면 POSTFIX 입력 방지
    # [ "$COMMIT_SOURCE" != "merge" ] 👉🏻 merge할 때, POSTFIX 입력 방지
    # [[ "$MESSAGE" != *"[#$POSTFIX]"* ]] 👉🏻 이미 POSTFIX가 존재할 때, POSTFIX 중복 입력 방지
    if [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] && [ "$COMMIT_SOURCE" != "merge" ] && [[ "$MESSAGE" != *"[#$POSTFIX]"* ]]; then
    printf "%s\n\n[#%s]" "$MESSAGE" "$POSTFIX" > "$COMMIT_MESSAGE_FILE_PATH"
    fi

    🧐 이슈 번호 추출에 사용된 명령어 설명

    • grep '*' 👉 * 표시된 브랜치(현재 위치의 브랜치)를 가져온다.
    • sed 's/_ //' 👉 * 제거
    • sed 's/(/)./\1/' 👉 / 이후의 문자만 추출
    • sed 's/^(---)._/\1/' 👉 하나의 이슈에 여러 브랜치를 만들면서 feat/10-1 이런 형태로 브랜치를 만들 경우, 첫 번째 '-' 앞 뒤만 추출 (ex. 10-1)

    3) 프로젝트 폴더에 Makefile 파일 생성

    init:
    git config core.hooksPath .githooks
    chmod +x .githooks/commit-msg
    git update-index --chmod=+x .githooks/commit-msg

    # chmod +x .githooks/commit-msg 👉🏻 macOS, 리눅스에서 스크립트 권한 부여
    # git update-index --chmod=+x .githooks/commit-msg
    # 👉 macOS, 리눅스에서 브랜치가 바뀔 때마다 스크립트 실행시켜줘야 하는 문제 해결

    4) 아래 코드 실행

    새로 git clone을 할 때마다 아래 코드를 실행시켜줘야 한다. 한 번만 실행시키면 계속 적용된다. (window 기준)

    git config core.hooksPath .githooks

    ❗macOS는 git clone 할 때마다 아래 코드를 실행시켜줘야 한다.

    make

    참고 블로그 https://blog.deering.co/commit-convention/

    - + \ No newline at end of file diff --git a/tags/git/page/2.html b/tags/git/page/2.html index a7bb275..4e26a96 100644 --- a/tags/git/page/2.html +++ b/tags/git/page/2.html @@ -5,13 +5,13 @@ "git" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "git" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    현재 상황은 어떤데?

    현재 우아한테크코스에서는 프론트 코드와 백엔드 코드가 같은 레포지토리를 사용하고 있습니다.

    프론트와 백엔드가 같이 작업하기에, 의도치 않은 충돌이 자주 생길 수 있는 구조이기에, 이를 git branch 전략으로 충돌을 줄이고자 합니다

    Git Branch 전략이란?

    git을 사용해서 소프트웨어 개발을 관리하는 방법입니다.

    여러 개발자가 동시에 작업하고 코드를 통합할 때 생기는 충돌을 효율적으로 조정하기 위한 방법입니다.

    왜 git branch 전략이 중요한데?

    아래에 있는 4가지를 제외하고도 훨씬 많은 장점이 있을 수 있습니다.

    1. 동시 작업이 편하다

    여러 사람이 독립적으로 작업하고, 커밋을 할 때, 자신의 브랜치에서 변경 사항을 커밋하게 됩니다.

    브랜치가 병합될 때만 충돌을 해결하면 되니, 아무 규칙이 없는 것보다 충돌 시점이 명확해지기에 생산성을 높일 수 있습니다.

    2. 목적이 명확한 브랜치

    애플리케이션의 상태에 몇 가지가 있는데, 안정된 프로덕션, 테스트 환경, 기능 추가 환경... 등이 있습니다

    여러 기능별 브랜치(안정된 버전의 코드만이 관리되는 브랜치, 테스트 환경을 위한 브랜치, 기능 추가를 위한 브랜치)를

    네이밍을 통해 구분하면 각각의 브랜치에 대해서 추가적인 설명을 할 필요 없이 명확하게 관리할 수 있습니다.

    3. 배포 파이프라인 관리가 편함

    브랜치가 네이밍으로 명확하게 구분이 되어있다면, 조건을 설정하기 쉽습니다.

    특정 타입의 브랜치에 push 되었을 때, pull request를 만들었을 때 같은 조건에 따른 스크립트를 만들어둔다면 CI/CD를 구축하기 쉽습니다.

    4. 버전 관리가 편리하다

    서버에 문제가 생겼을 때, 어떤 브랜치로 돌아가서 롤백을 해야 하는지에 대한 것들이 명확합니다.

    안정된 브랜치가 어떤 것인지 명확하기에, 롤백 과정에 대한 의사결정을 줄일 수 있습니다.

    그러면 어떤 종류가 있는지 더 자세하게 알아보도록 하겠습니다.

    Git Branch 전략의 종류는?

    총 3가지의 전략이 있습니다.

    1. Github Flow

    2. Gitlab Flow

    3. Git Flow

    git을 사용하기에, Git Flow라는 네이밍이 가장 직관적이고 유명한데요. 

    3가지 전략 중에서 가장 복잡하기에, 쉬운 순서대로 진행해 보도록 하겠습니다.

    1. Github Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img

    img2

    브랜치는 총 2가지 종류가 존재합니다

    1. master 브랜치

    여기에 머지가 되면 배포가 되도록 CD를 연결해 놓은 경우가 많습니다.

    안정된 버전의 코드가 관리되는 브랜치입니다.

    2. feature 브랜치

    기능 추가, 버그 수정 등 모든 작업은 feature 브랜치에서 일어납니다.

    master 브랜치에서 새로운 브랜치를 만들어서, 마스터로 머지되는 단순한 사이클을 가지고 있습니다.

    장점

    위에서 볼 수 있는 것처럼 2종류의 브랜치만 있기에, 정말 간단합니다.

    학습 과정까지의 러닝 커브가 거의 없다시피 하기에, 간단한 프로젝트에 적용하기 정말 좋습니다.

    릴리즈 되지 않은 코드가 최소화됩니다. 최신 버전의 코드와 최대한 빠르게 동기화를 계속해서 시킬 수 있습니다

    단점

    모든 코드는 다 master 브랜치에 머지가 되어야 한다는 점이 개발 서버와, 운영서버를 나누기 애매할 수 있습니다.

    개발 서버에 배포를 하고 싶은 상황이라면, master에 머지가 되어야 합니다.

    머지가 된 이후에 cd 파이프라인을 통해서 개발 서버와 운영 서버 모두에 배포가 됩니다.

    여러 환경을 나누고 관리를 하고 싶으시다면 다음에 소개해드릴 전략을 사용해 보셔도 좋을 것 같습니다

    2. Gitlab Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img2

    밑에 환경은 총 2개의 서버가 존재할 때를 가정하고 있습니다.

    1. pre-production 서버

    2. production 서버

    편의를 위해 main에 머지되는 과정은 간단하게 표현했습니다.

    img3

    브랜치 종류

    총 3가지 브랜치가 필요하고, 추가에 따라서 더 추가할 수 있습니다.

    1. main(or develop) 브랜치

    기능에 대한 개발이 완료되었지만, 여기에 머지되어도 바로 배포되지는 않습니다.

    2. feature브랜치

    기능을 개발하는 브랜치입니다. Github Flow 와도 유사합니다.

    3. production 브랜치

    실제 배포가 일어나는 브랜치입니다. 

    여기에 머지가 되는 순간 배포가 일어납니다.

    위 사진에 있는 것처럼, 필요에 따라서 pre-production이나, staging 같은 환경에 따른 브랜치를 추가할 수 있습니다.

    특징

    1. 무조건 단방향으로 머지가 일어납니다.

    긴급하게 라이브 서버에 수정을 해야 할 때, production 부터 시작하는 것이 아닌, main 부터 차근차근 올라가야 합니다

    2. 환경에 따라 브랜치 종류가 늘어날 수 있습니다.

    위 사진에서는 pre-production 이 그 예시가 되겠네요.

    장점

    1. Github Flow에서 환경별 브랜치를 통해서 개발 서버나 pre-production 서버에 버전을 깔끔하게 관리할 수 있습니다.

    3. Git Flow

    브랜치 전략 중 가장 처음으로 유명해진 브랜치 전략입니다.

    배포가 특정 주기를 가지고 있는 애플리케이션일 때, 가장 적합합니다.

    가장 복잡한 전략을 가지고 있어서, 모두가 브랜치 전략에 대해서 이해하고 있다면 역할에 따른 깔끔한 분리가 가능합니다

    그림으로 보고 가도록 하겠습니다

    img4

    가장 유명한 브랜치 전략이지만, 가장 어려운 전략이기도 합니다.

    특징

    1. 브랜치에 대해서 양방향으로 머지가 일어납니다

    release 브랜치에서 버그 수정이 일어나면, develop 브랜치에도 머지해줘야 합니다.

    hotfix 브랜치를 main 브랜치뿐만 아니라, develop 브랜치에도 머지해줘야 합니다

    브랜치의 종류가 5가지나 됩니다

    1. main

    production 이 배포되었을 때, 이 브랜치에 머지되는 것이 기준이 됩니다.

    2. develop 

    위에서 설명드렸던 브랜치들과 큰 차이가 없이 배포 전 브랜치입니다.

    3. feature

    기능을 개발할 때 사용하는 브랜치입니다. 이것도 위와 큰 차이가 없습니다

    4. release

    Gitlab Flow에서 pre-production에 해당한다고 봐도 무방합니다.

    여기서 버그 수정이 일어났을 경우에,  develop에 머지하는 것을 까먹으면 안 됩니다.

    5. hotfix

    main 브랜치에서 생성된 브랜치로, 긴급한 변경사항을 처리합니다.

    이때, develop에 머지하는 것을 깜빡하면 안 됩니다.

    더 자세하게 알아보실 분은 아래 링크들을 확인해 보세요

    우리 프로젝트에는 어떤 것이 적절할까?

    나중에 개발 서버 혹은 스테이징 서버를 두고 싶기에, 이 부분에 대한 처리가 부족한 Github Flow는 적절하지 않습니다.

    Git Flow는 깔끔하게 처리할 수 있지만, 러닝 커브가 Gitlab Flow 보다 약간 더 있어서, 빠르게 개발하는 취지에 맞지 않아 보였습니다.

    이런 과정을 통해서 Gitlab Flow를 사용하려고 합니다 

    참고

    https://techblog.woowahan.com/2553/

    https://docs.gitlab.com/ee/topics/gitlab_flow.html

    - + \ No newline at end of file diff --git a/tags/github-flow.html b/tags/github-flow.html index 3d977a9..268b90f 100644 --- a/tags/github-flow.html +++ b/tags/github-flow.html @@ -5,13 +5,13 @@ "github flow" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "github flow" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    현재 상황은 어떤데?

    현재 우아한테크코스에서는 프론트 코드와 백엔드 코드가 같은 레포지토리를 사용하고 있습니다.

    프론트와 백엔드가 같이 작업하기에, 의도치 않은 충돌이 자주 생길 수 있는 구조이기에, 이를 git branch 전략으로 충돌을 줄이고자 합니다

    Git Branch 전략이란?

    git을 사용해서 소프트웨어 개발을 관리하는 방법입니다.

    여러 개발자가 동시에 작업하고 코드를 통합할 때 생기는 충돌을 효율적으로 조정하기 위한 방법입니다.

    왜 git branch 전략이 중요한데?

    아래에 있는 4가지를 제외하고도 훨씬 많은 장점이 있을 수 있습니다.

    1. 동시 작업이 편하다

    여러 사람이 독립적으로 작업하고, 커밋을 할 때, 자신의 브랜치에서 변경 사항을 커밋하게 됩니다.

    브랜치가 병합될 때만 충돌을 해결하면 되니, 아무 규칙이 없는 것보다 충돌 시점이 명확해지기에 생산성을 높일 수 있습니다.

    2. 목적이 명확한 브랜치

    애플리케이션의 상태에 몇 가지가 있는데, 안정된 프로덕션, 테스트 환경, 기능 추가 환경... 등이 있습니다

    여러 기능별 브랜치(안정된 버전의 코드만이 관리되는 브랜치, 테스트 환경을 위한 브랜치, 기능 추가를 위한 브랜치)를

    네이밍을 통해 구분하면 각각의 브랜치에 대해서 추가적인 설명을 할 필요 없이 명확하게 관리할 수 있습니다.

    3. 배포 파이프라인 관리가 편함

    브랜치가 네이밍으로 명확하게 구분이 되어있다면, 조건을 설정하기 쉽습니다.

    특정 타입의 브랜치에 push 되었을 때, pull request를 만들었을 때 같은 조건에 따른 스크립트를 만들어둔다면 CI/CD를 구축하기 쉽습니다.

    4. 버전 관리가 편리하다

    서버에 문제가 생겼을 때, 어떤 브랜치로 돌아가서 롤백을 해야 하는지에 대한 것들이 명확합니다.

    안정된 브랜치가 어떤 것인지 명확하기에, 롤백 과정에 대한 의사결정을 줄일 수 있습니다.

    그러면 어떤 종류가 있는지 더 자세하게 알아보도록 하겠습니다.

    Git Branch 전략의 종류는?

    총 3가지의 전략이 있습니다.

    1. Github Flow

    2. Gitlab Flow

    3. Git Flow

    git을 사용하기에, Git Flow라는 네이밍이 가장 직관적이고 유명한데요. 

    3가지 전략 중에서 가장 복잡하기에, 쉬운 순서대로 진행해 보도록 하겠습니다.

    1. Github Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img

    img2

    브랜치는 총 2가지 종류가 존재합니다

    1. master 브랜치

    여기에 머지가 되면 배포가 되도록 CD를 연결해 놓은 경우가 많습니다.

    안정된 버전의 코드가 관리되는 브랜치입니다.

    2. feature 브랜치

    기능 추가, 버그 수정 등 모든 작업은 feature 브랜치에서 일어납니다.

    master 브랜치에서 새로운 브랜치를 만들어서, 마스터로 머지되는 단순한 사이클을 가지고 있습니다.

    장점

    위에서 볼 수 있는 것처럼 2종류의 브랜치만 있기에, 정말 간단합니다.

    학습 과정까지의 러닝 커브가 거의 없다시피 하기에, 간단한 프로젝트에 적용하기 정말 좋습니다.

    릴리즈 되지 않은 코드가 최소화됩니다. 최신 버전의 코드와 최대한 빠르게 동기화를 계속해서 시킬 수 있습니다

    단점

    모든 코드는 다 master 브랜치에 머지가 되어야 한다는 점이 개발 서버와, 운영서버를 나누기 애매할 수 있습니다.

    개발 서버에 배포를 하고 싶은 상황이라면, master에 머지가 되어야 합니다.

    머지가 된 이후에 cd 파이프라인을 통해서 개발 서버와 운영 서버 모두에 배포가 됩니다.

    여러 환경을 나누고 관리를 하고 싶으시다면 다음에 소개해드릴 전략을 사용해 보셔도 좋을 것 같습니다

    2. Gitlab Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img2

    밑에 환경은 총 2개의 서버가 존재할 때를 가정하고 있습니다.

    1. pre-production 서버

    2. production 서버

    편의를 위해 main에 머지되는 과정은 간단하게 표현했습니다.

    img3

    브랜치 종류

    총 3가지 브랜치가 필요하고, 추가에 따라서 더 추가할 수 있습니다.

    1. main(or develop) 브랜치

    기능에 대한 개발이 완료되었지만, 여기에 머지되어도 바로 배포되지는 않습니다.

    2. feature브랜치

    기능을 개발하는 브랜치입니다. Github Flow 와도 유사합니다.

    3. production 브랜치

    실제 배포가 일어나는 브랜치입니다. 

    여기에 머지가 되는 순간 배포가 일어납니다.

    위 사진에 있는 것처럼, 필요에 따라서 pre-production이나, staging 같은 환경에 따른 브랜치를 추가할 수 있습니다.

    특징

    1. 무조건 단방향으로 머지가 일어납니다.

    긴급하게 라이브 서버에 수정을 해야 할 때, production 부터 시작하는 것이 아닌, main 부터 차근차근 올라가야 합니다

    2. 환경에 따라 브랜치 종류가 늘어날 수 있습니다.

    위 사진에서는 pre-production 이 그 예시가 되겠네요.

    장점

    1. Github Flow에서 환경별 브랜치를 통해서 개발 서버나 pre-production 서버에 버전을 깔끔하게 관리할 수 있습니다.

    3. Git Flow

    브랜치 전략 중 가장 처음으로 유명해진 브랜치 전략입니다.

    배포가 특정 주기를 가지고 있는 애플리케이션일 때, 가장 적합합니다.

    가장 복잡한 전략을 가지고 있어서, 모두가 브랜치 전략에 대해서 이해하고 있다면 역할에 따른 깔끔한 분리가 가능합니다

    그림으로 보고 가도록 하겠습니다

    img4

    가장 유명한 브랜치 전략이지만, 가장 어려운 전략이기도 합니다.

    특징

    1. 브랜치에 대해서 양방향으로 머지가 일어납니다

    release 브랜치에서 버그 수정이 일어나면, develop 브랜치에도 머지해줘야 합니다.

    hotfix 브랜치를 main 브랜치뿐만 아니라, develop 브랜치에도 머지해줘야 합니다

    브랜치의 종류가 5가지나 됩니다

    1. main

    production 이 배포되었을 때, 이 브랜치에 머지되는 것이 기준이 됩니다.

    2. develop 

    위에서 설명드렸던 브랜치들과 큰 차이가 없이 배포 전 브랜치입니다.

    3. feature

    기능을 개발할 때 사용하는 브랜치입니다. 이것도 위와 큰 차이가 없습니다

    4. release

    Gitlab Flow에서 pre-production에 해당한다고 봐도 무방합니다.

    여기서 버그 수정이 일어났을 경우에,  develop에 머지하는 것을 까먹으면 안 됩니다.

    5. hotfix

    main 브랜치에서 생성된 브랜치로, 긴급한 변경사항을 처리합니다.

    이때, develop에 머지하는 것을 깜빡하면 안 됩니다.

    더 자세하게 알아보실 분은 아래 링크들을 확인해 보세요

    우리 프로젝트에는 어떤 것이 적절할까?

    나중에 개발 서버 혹은 스테이징 서버를 두고 싶기에, 이 부분에 대한 처리가 부족한 Github Flow는 적절하지 않습니다.

    Git Flow는 깔끔하게 처리할 수 있지만, 러닝 커브가 Gitlab Flow 보다 약간 더 있어서, 빠르게 개발하는 취지에 맞지 않아 보였습니다.

    이런 과정을 통해서 Gitlab Flow를 사용하려고 합니다 

    참고

    https://techblog.woowahan.com/2553/

    https://docs.gitlab.com/ee/topics/gitlab_flow.html

    - + \ No newline at end of file diff --git a/tags/github.html b/tags/github.html index 0b0f632..7098046 100644 --- a/tags/github.html +++ b/tags/github.html @@ -5,7 +5,7 @@ "github" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -17,7 +17,7 @@ 아까 environment 속성을 보면 test라고 설정해놓은 것을 볼 수 있습니다. 해당 환경이 여기에 적용됩니다.

    Branch rule 정의하기

    이번에는 해당 Repository의 Settings -> Branches 탭으로 들어갑니다. 그리고 원하는 branch에 들어가 edit 버튼을 누릅니다.

    그리고 사진과 같이 Require deployments to succeed before merging 속성을 클릭합니다. 그리고 아래와 같이 어떤 환경을 적용할 것인지 선택할 수 있습니다.

    이 속성은 해당 배포가 성공해야 merge 할 수 있도록 브랜치를 보호하는 기능입니다.

    그리고 저희는 frontend와 backend Job의 환경을 둘 다 test라는 이름으로 정의했기 때문에 하나의 environment만 선택해도 둘 다 적용되는 효과를 볼 수 있습니다. branch rule

    적용 후

    아래와 같이 merge가 안된다는 글과 빨간색으로 경고 표시를 해주고 있습니다. blocked

    결론

    간단한 github action을 통해서 생산성을 많이 올릴 수 있는 좋은 기능인 것 같습니다. 다른 팀들도 이 기능을 도입하여 사용하는 것을 추천드립니다.

    - + \ No newline at end of file diff --git a/tags/github/page/2.html b/tags/github/page/2.html index 32ee5d2..0567627 100644 --- a/tags/github/page/2.html +++ b/tags/github/page/2.html @@ -5,13 +5,13 @@ "github" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "github" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 4분
    누누

    안녕하세요 우테코 카페인팀 누누입니다

    빠르게 결과부터 보고 가시죠.

    어떤 결과가 나왔나요?

    pr의 본문 끝에, 연관된 이슈 번호를 달아주는 기능을 만들었습니다.

    밑에 사진을 보시면 쉽게 이해하실 수 있을 것 같습니다.

    imgimg

    github에서 issue 번호가 pr에 담겨있다면 2가지 장점이 생기는데요.

    1. issue를 클릭했을 때, 자동으로 그 issue로 넘어갈 수 있습니다. (호버만으로 이슈에 대한 간단한 정보를 볼 수 있습니다)
    2. pr 이 merge 되었을 때, 자동으로 issue 가 close 됩니다.

    이 과정을 손으로 진행하는 것보다, 자동으로 진행하게 되면 실수도 줄어들고, 개발 과정이 편해질 것 같아서 이 기능을 제작하게 되었는데요

    중요한 점

    이 과정을 진행하려면 밑에서 소개해드릴 브랜치 네이밍 규칙이 필요합니다.

    브랜치 이름 규칙

    • 브랜치 이름은 타입/이슈번호 으로 구성합니다. ex) feat/1
    • 타입은 feat, fix, docs, refactor, test 등 여러 가지가 있을 수 있습니다.

    이렇게 했을 때, 이슈 번호를 브랜치 명에서부터 가져올 수 있기에, 자동화를 할 수 있습니다.

    이런 규칙이 아닌, feat/action 같은 형태가 된다면 issue 번호를 찾기 어렵겠죠?

    사용 방법

    작성된 코드부터 보시고, 설명을 드리겠습니다.

    아래에 작성된 코드를. github/workflows/assign_issue_number_to_pr_body.yml로 저장하시면 끝입니다.

    name: assign_issue_number_to_pr_body

    on:
    pull_request:
    types: [ opened ]
    branches-ignore:
    - develop

    jobs:
    append_issue_number_to_pr_body:
    runs-on: ubuntu-latest
    steps:
    - name: append feature number to pr body pr branch = feat/(issueNumber)
    uses: actions/github-script@v4
    with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    script: |
    const pr = await github.pulls.get({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number
    });
    const body = pr.data.body;
    const issueNumber= pr.data.head.ref.split('/')[1];
    const newBody = body + "\n\n" + "close #" + issueNumber;
    await github.pulls.update({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number,
    body: newBody
    });

    진행 과정

    1. pr 이 생성되면, pr에 대한 정보를 가져옵니다.
    2. pr의 본문을 가져옵니다.
    3. pr의 브랜치 이름에서 이슈 번호를 가져옵니다.
    4. pr의 본문에 이슈 번호를 추가합니다.
    5. pr의 본문을 업데이트합니다.

    이 과정을 통해서, 직접 pr의 본문을 수정하지 않아도, 자동으로 이슈 번호가 추가되기에, 실수를 줄일 수 있으니, 한 번 시도해 보세요

    - + \ No newline at end of file diff --git a/tags/gitlab-flow.html b/tags/gitlab-flow.html index be244e8..9a3965e 100644 --- a/tags/gitlab-flow.html +++ b/tags/gitlab-flow.html @@ -5,13 +5,13 @@ "gitlab flow" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "gitlab flow" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    현재 상황은 어떤데?

    현재 우아한테크코스에서는 프론트 코드와 백엔드 코드가 같은 레포지토리를 사용하고 있습니다.

    프론트와 백엔드가 같이 작업하기에, 의도치 않은 충돌이 자주 생길 수 있는 구조이기에, 이를 git branch 전략으로 충돌을 줄이고자 합니다

    Git Branch 전략이란?

    git을 사용해서 소프트웨어 개발을 관리하는 방법입니다.

    여러 개발자가 동시에 작업하고 코드를 통합할 때 생기는 충돌을 효율적으로 조정하기 위한 방법입니다.

    왜 git branch 전략이 중요한데?

    아래에 있는 4가지를 제외하고도 훨씬 많은 장점이 있을 수 있습니다.

    1. 동시 작업이 편하다

    여러 사람이 독립적으로 작업하고, 커밋을 할 때, 자신의 브랜치에서 변경 사항을 커밋하게 됩니다.

    브랜치가 병합될 때만 충돌을 해결하면 되니, 아무 규칙이 없는 것보다 충돌 시점이 명확해지기에 생산성을 높일 수 있습니다.

    2. 목적이 명확한 브랜치

    애플리케이션의 상태에 몇 가지가 있는데, 안정된 프로덕션, 테스트 환경, 기능 추가 환경... 등이 있습니다

    여러 기능별 브랜치(안정된 버전의 코드만이 관리되는 브랜치, 테스트 환경을 위한 브랜치, 기능 추가를 위한 브랜치)를

    네이밍을 통해 구분하면 각각의 브랜치에 대해서 추가적인 설명을 할 필요 없이 명확하게 관리할 수 있습니다.

    3. 배포 파이프라인 관리가 편함

    브랜치가 네이밍으로 명확하게 구분이 되어있다면, 조건을 설정하기 쉽습니다.

    특정 타입의 브랜치에 push 되었을 때, pull request를 만들었을 때 같은 조건에 따른 스크립트를 만들어둔다면 CI/CD를 구축하기 쉽습니다.

    4. 버전 관리가 편리하다

    서버에 문제가 생겼을 때, 어떤 브랜치로 돌아가서 롤백을 해야 하는지에 대한 것들이 명확합니다.

    안정된 브랜치가 어떤 것인지 명확하기에, 롤백 과정에 대한 의사결정을 줄일 수 있습니다.

    그러면 어떤 종류가 있는지 더 자세하게 알아보도록 하겠습니다.

    Git Branch 전략의 종류는?

    총 3가지의 전략이 있습니다.

    1. Github Flow

    2. Gitlab Flow

    3. Git Flow

    git을 사용하기에, Git Flow라는 네이밍이 가장 직관적이고 유명한데요. 

    3가지 전략 중에서 가장 복잡하기에, 쉬운 순서대로 진행해 보도록 하겠습니다.

    1. Github Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img

    img2

    브랜치는 총 2가지 종류가 존재합니다

    1. master 브랜치

    여기에 머지가 되면 배포가 되도록 CD를 연결해 놓은 경우가 많습니다.

    안정된 버전의 코드가 관리되는 브랜치입니다.

    2. feature 브랜치

    기능 추가, 버그 수정 등 모든 작업은 feature 브랜치에서 일어납니다.

    master 브랜치에서 새로운 브랜치를 만들어서, 마스터로 머지되는 단순한 사이클을 가지고 있습니다.

    장점

    위에서 볼 수 있는 것처럼 2종류의 브랜치만 있기에, 정말 간단합니다.

    학습 과정까지의 러닝 커브가 거의 없다시피 하기에, 간단한 프로젝트에 적용하기 정말 좋습니다.

    릴리즈 되지 않은 코드가 최소화됩니다. 최신 버전의 코드와 최대한 빠르게 동기화를 계속해서 시킬 수 있습니다

    단점

    모든 코드는 다 master 브랜치에 머지가 되어야 한다는 점이 개발 서버와, 운영서버를 나누기 애매할 수 있습니다.

    개발 서버에 배포를 하고 싶은 상황이라면, master에 머지가 되어야 합니다.

    머지가 된 이후에 cd 파이프라인을 통해서 개발 서버와 운영 서버 모두에 배포가 됩니다.

    여러 환경을 나누고 관리를 하고 싶으시다면 다음에 소개해드릴 전략을 사용해 보셔도 좋을 것 같습니다

    2. Gitlab Flow

    그림으로 flow 간단하게 보고 가도록 하겠습니다.

    img2

    밑에 환경은 총 2개의 서버가 존재할 때를 가정하고 있습니다.

    1. pre-production 서버

    2. production 서버

    편의를 위해 main에 머지되는 과정은 간단하게 표현했습니다.

    img3

    브랜치 종류

    총 3가지 브랜치가 필요하고, 추가에 따라서 더 추가할 수 있습니다.

    1. main(or develop) 브랜치

    기능에 대한 개발이 완료되었지만, 여기에 머지되어도 바로 배포되지는 않습니다.

    2. feature브랜치

    기능을 개발하는 브랜치입니다. Github Flow 와도 유사합니다.

    3. production 브랜치

    실제 배포가 일어나는 브랜치입니다. 

    여기에 머지가 되는 순간 배포가 일어납니다.

    위 사진에 있는 것처럼, 필요에 따라서 pre-production이나, staging 같은 환경에 따른 브랜치를 추가할 수 있습니다.

    특징

    1. 무조건 단방향으로 머지가 일어납니다.

    긴급하게 라이브 서버에 수정을 해야 할 때, production 부터 시작하는 것이 아닌, main 부터 차근차근 올라가야 합니다

    2. 환경에 따라 브랜치 종류가 늘어날 수 있습니다.

    위 사진에서는 pre-production 이 그 예시가 되겠네요.

    장점

    1. Github Flow에서 환경별 브랜치를 통해서 개발 서버나 pre-production 서버에 버전을 깔끔하게 관리할 수 있습니다.

    3. Git Flow

    브랜치 전략 중 가장 처음으로 유명해진 브랜치 전략입니다.

    배포가 특정 주기를 가지고 있는 애플리케이션일 때, 가장 적합합니다.

    가장 복잡한 전략을 가지고 있어서, 모두가 브랜치 전략에 대해서 이해하고 있다면 역할에 따른 깔끔한 분리가 가능합니다

    그림으로 보고 가도록 하겠습니다

    img4

    가장 유명한 브랜치 전략이지만, 가장 어려운 전략이기도 합니다.

    특징

    1. 브랜치에 대해서 양방향으로 머지가 일어납니다

    release 브랜치에서 버그 수정이 일어나면, develop 브랜치에도 머지해줘야 합니다.

    hotfix 브랜치를 main 브랜치뿐만 아니라, develop 브랜치에도 머지해줘야 합니다

    브랜치의 종류가 5가지나 됩니다

    1. main

    production 이 배포되었을 때, 이 브랜치에 머지되는 것이 기준이 됩니다.

    2. develop 

    위에서 설명드렸던 브랜치들과 큰 차이가 없이 배포 전 브랜치입니다.

    3. feature

    기능을 개발할 때 사용하는 브랜치입니다. 이것도 위와 큰 차이가 없습니다

    4. release

    Gitlab Flow에서 pre-production에 해당한다고 봐도 무방합니다.

    여기서 버그 수정이 일어났을 경우에,  develop에 머지하는 것을 까먹으면 안 됩니다.

    5. hotfix

    main 브랜치에서 생성된 브랜치로, 긴급한 변경사항을 처리합니다.

    이때, develop에 머지하는 것을 깜빡하면 안 됩니다.

    더 자세하게 알아보실 분은 아래 링크들을 확인해 보세요

    우리 프로젝트에는 어떤 것이 적절할까?

    나중에 개발 서버 혹은 스테이징 서버를 두고 싶기에, 이 부분에 대한 처리가 부족한 Github Flow는 적절하지 않습니다.

    Git Flow는 깔끔하게 처리할 수 있지만, 러닝 커브가 Gitlab Flow 보다 약간 더 있어서, 빠르게 개발하는 취지에 맞지 않아 보였습니다.

    이런 과정을 통해서 Gitlab Flow를 사용하려고 합니다 

    참고

    https://techblog.woowahan.com/2553/

    https://docs.gitlab.com/ee/topics/gitlab_flow.html

    - + \ No newline at end of file diff --git a/tags/google-analytics-4.html b/tags/google-analytics-4.html index fe68887..e3fcaf7 100644 --- a/tags/google-analytics-4.html +++ b/tags/google-analytics-4.html @@ -5,7 +5,7 @@ "google analytics 4" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -20,7 +20,7 @@ no offset no offset no offset

    집계 된 자료처럼 방문자들이 단순 방문만 한 것이 아니라, 수 많은 이벤트를 발생시키고 평균 참여 시간도 상당 부분 확보했음을 확인할 수 있습니다.

    - + \ No newline at end of file diff --git a/tags/google-map.html b/tags/google-map.html index e350227..75790a9 100644 --- a/tags/google-map.html +++ b/tags/google-map.html @@ -5,13 +5,13 @@ "googleMap" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "googleMap" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 13분
    센트

    1. 개요

    기존의 구조에서는 마커 하나를 렌더링하기 위해 다음과 같은 과정을 거쳤다.

    1. StationMarkersContainer 컴포넌트에서 충전소 정보 요청
    2. 충전소 정보를 props로 넘겨 Marker 컴포넌트 호출
    3. 지도에 부착될 DOM요소 생성
    4. createRoot를 통해 리액트 root 생성
    5. 2번에서 생성한 DOM 요소를 전달해 구글 지도 api의 Marker 생성자 함수 호출
    6. 3번에서 생성했던 root의 render 메서드 호출
    7. 마커 인스턴스 전역 상태에 새로 생성한 마커 추가

    위 과정을 거쳤을 때의 마커 렌더링 모습을 보면 다음과 같다.

    before

    마커들이 한번에 렌더링 되는 것이 아니라 산발적으로 렌더링 되는 모습을 확인할 수 있다.

    2. 문제 원인 분석

    마커를 렌더링 하기 위해 거치는 과정을 분석해 보았다.

    1 ~ 3 과정에서는 성능에 크게 영향을 끼칠 요소가 없지만 4번 과정은 일반적인 리액트 프로젝트를 개발할 때 겪는 과정이 아니다. 따라서 createRoot를 통해 많은 개수의 루트를 생성했을 때의 영향에 대해 알아보았다.

    image

    리액트 공식 문서를 보니 페이지의 일부에 리액트를 뿌려서 사용하는 경우에는 루트를 필요한 만큼 생성해도 된다는 이야기가 포함되어 있었다. 따라서 4번 과정 또한 문제의 원인이라고 볼 수 없었다.

    5번 과정은 구글 지도에 마커를 특정 위도 경도에 위치시키기 위해서 어쩔 수 없이 거쳐야 하는 과정이므로 이 과정은 문제가 있더라도 개선이 불가능해 일단 고려하지 않았다.

    6번 과정은 4번 과정에서 생성했던 리액트 루트의 render 메서드를 호출해 실제로 화면에 리액트 컴포넌트를 그리도록 하는 과정이다. 이 과정 또한 리액트 컴포넌트를 화면에 렌더링하기 위해선 어쩔 수 없이 거쳐야 하는 과정이므로 고려하지 않았다.

    하지만 6번 과정에서 리액트 컴포넌트를 직접 그리는 것이 아니라 구글 지도 api의 기본 마커를 사용하면 성능을 향상시킬 수 있지 않냐고 반문할 수도 있을 것이다. 이전에는 이러한 방식을 사용해 마커를 렌더링 했었다. 우리의 서비스는 현재 사용 가능한 충전소 개수를 마커를 통해서도 전달하기 때문에 이를 고려해 기본 마커를 사용할 때 다음의 두 가지 문제가 생긴다.

    1. 사용 가능한 충전소 개수를 기본 마커에 렌더링 할 때 성능이 매우 좋지 않다.
    2. 마커의 디자인을 바꾸고자 할 때 변경에 대응하기 어렵다.

    따라서 마커는 리액트 루트의 render 메서드를 호출해 리액트 컴포넌트를 렌더링하는 것으로 결정했다.

    마지막으로 남은 7번 과정에서는 useSyncExternalState 훅을 사용해 전역적으로 관리하고 있던 상태에 수정을 가하는 연산을 수행한다. 이 과정은 이전에도 성능 저하를 유발할 것으로 예상되던 부분이었다. (하단 링크 참고)

    useSyncExternalStore 훅을 통해 구독한 state가 한번에 업데이트 되는 이유

    요청의 결과로 받아온 마커 정보의 개수가 100개라고 가정해보자. 우리는 이제 마커를 렌더링 할 것이다. 첫 번째 마커의 렌더링을 위해 1번 ~ 6번의 과정을 거친 후 7번 과정을 수행한다. 그러면 리액트 입장에서는 리액트 루트의 render 메서드 호출에 대한 동작을 수행해야 하고, 새로운 마커 인스턴스에 대한 전역 상태를 변경시키는 동작을 수행해야 한다. 리액트가 이 과정을 100번 반복하고 나면 우리는 비로소 모든 마커가 화면에 렌더링 된 모습을 볼 수 있을 것이다.

    나는 이 부분에서 성능 저하의 요소가 있다고 생각했다. 리액트에서의 상태 변화는 곧 리액트 내부의 렌더링을 위한 로직이 수행되게 함을 의미하고, 이 과정을 개선 이전에는 마커의 개수만큼 반복하고 있었던 것이다. 여기까지 생각해보니 전역 상태 변화에 대해 리액트가 렌더링을 위한 연산을 진행할 동안에는 마커의 렌더링(render 메서드 호출)이 멈추는 것이 아닐까 하는 생각이 들었다.

    그래서 크롬 개발자 도구의 퍼포먼스 탭을 들어가 보니 산발적으로 발생하던 마커 렌더링의 문제 원인이 짐작했던 그 원인임을 확인할 수 있었다.

    image

    프레임 이미지 하단을 보면 산발적인 마커 렌더링이 수행될 때마다 수반되는 어떤 함수 호출이 있음을 확인할 수 있다.

    image

    이 부분이 문제의 함수 호출 부분이다. 자세히 살펴보면 상단에 performWorkUntilDeadline이란 함수가 호출됨을 볼 수 있다.

    image

    performWorkUntilDeadline 라는 함수를 조금 알아보니 해당 함수는 간단히 말해 리액트에서 state의 변경이 한번에 많이 발생할 때 5ms의 데드라인 시간을 줄 때 사용하는 함수라는 것을 알게 되었다. 문제의 원인이라고 생각했던 마커 개수 만큼의 전역 상태 변화가 실제로 마커 렌더링을 잠시 중단하게 만들고 있음을 알게 되었다.

    3. 문제 해결

    앞서 분석한 문제를 개선해보고자 마커 렌더링에 필요한 충전소 정보 배열을 부모 컴포넌트에서 받아와 각 충전소 정보를 자식 컴포넌트에 넘겨주고, 자식 컴포넌트에서 마커 생성과 렌더링 로직을 수행하던 기존의 방식을 부수고 부모 컴포넌트에서 모든 것을 일괄 처리하는 방식으로 고쳐보았다.

    고치는 과정에서 기존 방식에서는 리액트 생명 주기에 의존하여 화면에 보여지지 않는 마커를 지워주던 로직을 이제는 모두 직접 구현해야 했다.

    이전의 영역과 겹치는 부분에 있는 충전소는 다시 그리지 않고, 영역 밖의 충전소를 나타내는 마커는 지워주고, 이전의 영역과 겹치지 않는 새로 받아온 충전소는 그리도록 다음과 같이 메서드를 분리해보았다.

    • 기존과 겹치지 않는 새로운 영역에 대한 마커를 생성하는 메서드
    • 기존과 겹쳐지는 영역에 대한 마커들을 반환하는 메서드
    • 새로운 영역 밖에 있는 마커들을 지워주는 메서드
    • 새롭게 생성된 마커를 화면에 렌더링하는 메서드

    이 메서드들을 커스텀 훅으로 분리해 부모 컴포넌트에서 이를 활용하도록 하여 다소 복잡할 수 있는 마커 렌더링 로직을 선언적으로 구현할 수 있도록 했다.

    결과적으로 기존에 사용되던 기능들을 그대로 사용할 수 있으면서 화면에 마커가 산발적으로 렌더링 되던 문제가 해결 되었고, 부가적인 효과로 전체 마커의 렌더링 시점도 앞당길 수 있게 되었다. + 기존에는 구조적인 문제로 연산량이 너무 많아 클러스터링이 늦어져 이를 도입할 수 없었던 문제를 구조 수정으로 인해 적용할 수 있게 되었다.

    작업한 PR

    https://github.com/woowacourse-teams/2023-car-ffeine/pull/737

    결과 분석 (performance 탭 활용)

    before

    마커 조회 요청이 종료된 시점: 약 2499ms

    image

    첫 마커 렌더링 시점: 3093ms

    image

    모든 마커 렌더링 종료 시점: 약 3611ms

    image

    처음으로 마커가 렌더링 될 때까지 소요된 시간: 594ms

    모든 마커 렌더링에 소요된 시간: 1112ms

    after

    마커 조회 요청의 시작점: 약 1875ms

    image

    모든 마커 렌더링 종료 시점: 2395ms

    image

    처음으로 마커가 렌더링 될 때까지 소요된 시간: 519ms

    모든 마커 렌더링에 소요된 시간: 519ms

    개선 결과

    처음으로 마커가 렌더링 되는 시점은 두 방식 모두 비슷한 결과를 보인다. 하지만 개선 후 방식은 한번에 모든 마커가 렌더링 되는 방식이고, 개선 이전의 방식은 산발적으로 마커가 렌더링 되는 방식이므로 개선 후의 방식에서 전체 마커를 렌더링 하는 시점이 훨씬 빨라지게 되었다.

    결과적으로 전체 마커가 렌더링 되는 속도 약 55.6% 단축하게 되었다. 이 결과는 마커가 늘어날 수록 더욱 차이가 극적으로 벌어질 것으로 예상된다.

    before

    before

    after

    after

    - + \ No newline at end of file diff --git a/tags/google-maps-api.html b/tags/google-maps-api.html index b5f4235..7a08a73 100644 --- a/tags/google-maps-api.html +++ b/tags/google-maps-api.html @@ -5,13 +5,13 @@ "google maps api" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "google maps api" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 18분
    가브리엘

    안녕하세요? 카페인 팀에서 사용한 지도 시스템에 대해서 소개하려고 합니다.

    지도 기능에서 가장 핵심인 기능 두 가지를 뽑자면, 지도 그 자체와 지도 위에 그려지는 마커를 뽑을 수 있을 것입니다. 지도 위에 마커를 그리는 일은 그다지 어렵지 않고, documents 에 있는 예제들을 잘 따라하면 누구나 충분히 구현할 수 있을 것입니다.

    no offset

    하지만 마커의 갯수가 과도하게 많다면 어떤 전략을 세울 수 있을까요?

    카페인 팀에서는요 ...

    카페인 서비스에서 지도는 굉장히 중요한 요소 중 하나였습니다. 사용자들이 궁금한 장소의 주변에 있는 충전소를 시각적으로 제공해주기 위해서는 지도를 잘 제어할 수 있어야 했습니다. 특히 전국에 이미 수만 대의 충전소가 보급이 된 상황에서 충전소 마커를 모두 그려주기 위해서는 많은 제약이 있었고, 마커를 적당한 수준으로 렌더링 하려면 클라이언트와 서버 간에 특별한 작업이 필요했습니다.

    어떤 전략을 펼쳤는지 소개하기에 앞서 미리 말씀드리지만, 저희 팀에서 취한 지도 관리 전략은 모든 프로젝트에 유효하지 않을 것입니다. 지도 위에 한번에 표현할 마커의 갯수가 수백 개 이하라면, 서버에 데이터가 과도하게 많은 것이 아니라면 오히려 이러한 전략이 사용자 경험을 해칠 수 있을 것입니다. (환경이 원활하다면 데이터를 가능한 많이 보여주는 것이 좋을테니깐요.)

    또, 이 글에서는 Google Maps API를 기준으로 설명하고 있지만, 지원하는 기능이 일부 다르더라도 대부분의 지도 API에서 사용이 가능한 전략일 것입니다. 참고로 개인적으로 사용 해본 여러 벤더 사의 지도 API들은 모두 이와 유사한 기능을 제공했습니다.

    좌표란 무엇일까?

    아마 어린 시절부터 우리나라에는 특별히 38선이라는 것이 존재한다는 사실을 교육받기에 좌표계라는 것이 있다는 사실은 누구나 알 것입니다. 하지만 당장 위도와 경도를 구분지으라고 하면 어떤 선이 위선이고 경선인지 헷갈리기에 찍어야 할 것입니다. 따라서 이 선이 어떤 선인지, 어떤 값을 얘기하려는 것인지 사진과 함께 간단히 설명하겠습니다.

    no offset

    사진을 보시면 아시겠지만 위도란, 남북의 위치를 나타내는 데 사용됩니다. 경도는 동서의 위치를 나타내는 데 사용됩니다. 대부분의 공식 문서가 영어로 작성되어있고, 코드에서도 이를 나타내는 것이 중요하기에 영문 표기법까지 소개를 하자면 위도는 Latitude, 경도는 Longitude로 표기합니다. 이유는 모르겠지만 제공되는 변수나 메서드 명으로 lat, lng라고 줄여서 표기하기도 합니다.

    no offset

    위도와 경도만 알면, 지구 위의 어떤 위치를 나타낼 수 있습니다.

    따라서, 어떤 마커를 어떤 위치에 찍을 것인지는 위도와 경도 값으로 결정할 수 있게 되겠죠?

    사용자가 어딜 보고 있을까?

    지도 api에서 제공해주는 메서드를 활용하면 사용자의 디바이스가 어느 위치를 보고 있는지 알 수 있습니다.

    let map = /* 어디선가 생성된 구글 맵 객체 */
    const center = map.getCenter();
    console.log(center.lng()); // 디바이스 중심의 longitude
    console.log(center.lat()); // 디바이스 중심의 latitude

    지도 객체로 부터 중심점을 알게되면 해당 디바이스의 중심의 좌표를 알아낼 수 있게 됩니다.

    no offset

    사용자의 디바이스는 얼마나 넓게 보고 있을까?

    지도 api에서 제공해주는 메서드를 활용하면 사용자의 디바이스가 어떤 영역을 보고 있는지도 알게 됩니다. 지도 api 마다 제공하는 스펙이 다르지만, 대부분은 어떤 식으로든 알려줍니다.

    google maps API에서는 디스플레이의 북동쪽 끝 점의 좌표와, 남서쪽 끝 점의 좌표를 제공해줍니다.

    const map = /* 어디선가 생성된 구글 맵 객체 */
    const bounds = map.getBounds();
    console.log(bounds.getNorthEast().lng(), bounds.getNorthEast().lat()); // 디바이스 1사분면 끝 점의 longitude와 latitude
    console.log(bounds.getSouthWest().lng(), bounds.getSouthWest().lat()); // 디바이스 3사분면 끝 점의 longitude와 latitude

    no offset

    편의상 좌표를 다음과 같이 정의해보겠습니다.

    • 중심 점 p0: (x0, y0)
    • 디바이스의 제 1사분면 끝점 p2: (x2, y2)
    • 디바이스의 제 3사분면 끝점 p1: (x1, y1)
    위 정의는 아래에서도 계속 설명 될 점과 좌표 입니다.

    이렇게 알아낸 값으로 사용자 디바이스의 영역을 알게 됐습니다.

    저희 카페인 팀에서는 이 값을 좀 더 효율적으로 다루기 위해 delta 개념을 도입했습니다.

    화면에서 보고 있는 영역을 확대/축소 하면 어떤 특징을 보일까?

    delta 설명을 앞서, 사용자의 디바이스 영역과 확대 수준에 따른 실제 좌표에 대해 알아보려고 합니다.

    사용자가 화면을 얼마나 넓게 보고 있는지를 쉽게 알기 위해서는 끝점들의 수치를 계산해줄 필요가 있었습니다.

    사진은 사용자가 디바이스를 통해 바라 보고 있는 중심 좌표와 그 끝 점을 의미합니다.

    no offset

    예를 들어 사용자가 지도를 많이 축소한 경우에는 중심 점 p0은 그대로지만 양 끝점 p1, p2의 위치가 점점 중심 점 p0으로 부터 멀어질 것입니다.

    반면에 사용자가 지도를 많이 확대한 경우에는 중심 점 p0은 그대로지만 양 끝점 p1, p2의 위치가 점점 중심점과 가까워질 것입니다.

    no offset

    양 사진 모두 중심 점 p0는 그대로지만, 디바이스의 확대 수준으로 인해 양 끝점인 p1과 p2가 달라진 모습을 보인 것입니다.

    즉, 이런 결론을 내릴 수 있습니다.

    1. 양 끝점 p1, p2가 중심 점 p0으로 부터 멀어질 수록 지도를 축소한 것이다.
    2. 양 끝점 p1, p2가 중심 점 p0으로 부터 가까워 수록 지도를 확대한 것이다.

    이 때 디바이스의 디스플레이가 위도 경도 상으로 얼마나 멀어져있는지를 수치화하면 편하게 다룰 수 있습니다.

    확대 수준을 수치화 할 수 없을까?

    사용자의 디스플레이의 중심 점 p0을 기준으로 하여 양 끝점 p1, p2이 얼마나 멀어져있는지에 따라 지도의 영역 뿐만 아니라 얼마나 많이 확대 되었는지 여부를 알게 됐습니다.

    그렇다면 이를 좀 더 효율적인 방법으로 나타내려면 어떤 전략을 취할 수 있을까요?

    사용자 디스플레이를 조금 더 자세히 살펴보겠습니다.

    no offset

    중학교 시절 배웠던 좌표 평면계를 떠올려보면 화면에서 얻을 수 있는 좌표들은 위와 같습니다. 여기에서 각 점의 수직/수평의 변화량인 delta를 알아보면 어떨까요?

    경도 델타 (longitudeDelta)

    p2와 p0의 경도 거리, 그리고 p1과 p0의 경도 거리는 같습니다.

    즉, x2 - x0 === x0 - x1 이라는 결론을 얻을 수 있습니다.

    이를 longitudeDelta로 정의하겠습니다.

    위도 델타 (latitudeDelta)

    p2와 p0의 위도 거리, 그리고 p1과 p0의 위도 거리는 같습니다.

    즉, y2 - y0 === y0 - y1 이라는 결론을 얻을 수 있습니다.

    이를 latitudeDelta로 정의하겠습니다.

    no offset

    코드로 알아보면 다음과 같습니다.

    const map = /* 어디선가 생성된 구글 맵 객체 */
    const bounds = map.getBounds();
    const longitudeDelta = (bounds.getNorthEast().lng() - bounds.getSouthWest().lng()) / 2; // 경도 변화량
    const latitudeDelta = (bounds.getNorthEast().lat() - bounds.getSouthWest().lat()) / 2; // 위도 변화량

    드디어 클라이언트에서 델타 값을 생성할 수 있게 되었습니다.

    그렇다면 왜 이렇게 굳이 델타 값을 생성한 것일까요?

    delta의 유용한 점 1: 원래 의도한 값을 복원하기 쉽다.

    서버의 입장에서는 중심 좌표와 델타 값만 알면 정확한 영역만큼 데이터를 호출할 수 있게 됩니다.

    예를 들어 클라이언트에서 서버로 다음과 같은 파라미터를 넘겨줬다고 가정해보겠습니다.

    {
    "longitude": 127,
    "latitude": 37,
    "longitudeDelta": 0.1,
    "longitudeDelta": 0.2,
    }

    그렇다면 서버에서는 다음과 같이 해석할 수 있게 됩니다.

    const maxLongitude = longitude + longitudeDelta;
    const minLongitude = longitude - longitudeDelta;
    const maxLatitude = latitude + latitudeDelta;
    const minLatitude = latitude - latitudeDelta;

    (javascript 기준으로 작성했습니다.)

    이렇게 알아낸 경계 값을 가지고 다음과 같은 sql문을 작성할 수 있게 될 것입니다.

    SELECT * FROM stations WHERE latitude >= :minLatitude AND latitude <= :maxLatitude AND longitude >= :minLongitude AND longitude <= :maxLongitude;

    no offset

    즉, 위 그림처럼, 원하는 영역만큼만 정확하게 데이터를 호출할 수 있게 됩니다.

    delta의 유용한 점 2: 델타가 무분별하게 커지는 것을 막기 쉽다.

    예를 들어 사용자가 지도를 축소하여 한반도를 디스플레이에 가득 채운다면 서버가 어떻게 될까요?

    이러한 행위를 막는 가장 쉬운 방법은 지도 api에서 지원하는 줌 레벨을 제한 하는 것입니다. 후술하겠지만 줌 레벨은 디스플레이의 해상도를 고려하지 못합니다.

    따라서 근본적으로 델타가 일정 값 이상 요청되지 못하도록, 혹은 연산되지 못하도록 막게 할 수 있습니다.

    물론 델타가 없더라도 델타 값을 추정하여 연산할 수 있겠지만, 이를 수치화 해서 관리한다면 클라이언트와 서버 모두 지도를 손쉽게 통제하는 것이 가능하게 됩니다.

    예를 들어 다음과 같이 델타 값을 고정하여 요청 영역을 제한할(요청을 보내지 않거나 고정된 사이즈로만 요청을 보낼) 수 있습니다.

    {
    longitude,
    latitude,
    longitudeDelta: longitudeDelta < 0.008 ? longitudeDelta : 0.008,
    latitudeDelta: latitudeDelta < 0.004 ? latitudeDelta : 0.004,
    }

    특정 수치를 넘기지 못하게 처리할 때 눈에 보이는 변수로 취급하기 쉽습니다. (즉, 매번 계산하지 않아도 됩니다.)

    디바이스 크기 관련 문제도 있습니다.

    분명히 같은 줌 레벨이지만, 디바이스의 크기나 해상도에 따라 지도가 보여지는 정도가 다릅니다.

    no offset

    위 사진은 구글에서 제공하는 zoom 레벨을 동일하게 맞춘 후, 여러 디바이스에서 호출한 것입니다.

    줌 레벨을 통해서 요청을 제한하다보면 여러 해상도를 제어하기 어렵습니다.

    no offset

    실제로 카페인 팀에서는 고해상도 모니터를 대응하기 위해 델타 값이 너무 크게 되면 요청의 제한을 하고 있습니다. 사진에서 보시다시피 고해상도 모니터의 경우, 너무 넓은 범위를 요청한다 싶으면 중심점으로 부터 일정 거리만 보여주도록 하고 있습니다.

    (참고로 줌 레벨에 따른 요청도 덤으로 제한하고 있어서 멀리서 호출하는 행위도 금지하고 있습니다.)

    delta의 유용한 점 3: 적당한 범위를 정해주기 편하다

    위 예제에서는 정확한 범위만큼 요청하는 것을 예제로 하지만, 프로젝트에 따라서 조금 더 넓은 영역을 호출하고 싶을 때가 있을 것입니다.

    no offset

    예를 들어 현재 사용자의 디바이스 크기보다 살짝 큰 범위의 데이터를 미리 로드해 놓으면 사용자가 좁은 움직임을 보일 때 불필요한 재 렌더링을 줄여서 더 빠른 렌더링이 가능하게 됩니다.

    사실 이 기법은 프로젝트마다 다르겠지만, 카페인 팀에서는 한번 불러온 마커를 매번 해제 하지 않고 이전 요청 데이터와 다음 요청 데이터를 비교하여 달라진 마커만을 정확하게 탈부착하는 작업을 진행하고 있습니다.

    이런 기법을 활용하면 사용자가 좁은 범위에서 움직임을 보였을 때, 기존에 불러온 마커를 메모리에서 탈락시키지 않으므로 사용자 경험을 개선할 수도 있을 것입니다.

    마커를 상태에 연동하여 정확하게 메모리에서 탈부착 시키는 전략에 대한 글은 이후에 작성할 예정입니다.

    긴 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git a/tags/google-maps-api/page/2.html b/tags/google-maps-api/page/2.html index 653a9ea..712cee9 100644 --- a/tags/google-maps-api/page/2.html +++ b/tags/google-maps-api/page/2.html @@ -5,13 +5,13 @@ "google maps api" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "google maps api" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 18분
    센트

    Untitled

    위 이미지는 현재까지 구현한 지도의 모습이다. 구현된 기능은 다음과 같다.

    • 충전소 정보를 서버에 요청해 받아온 충전소 정보를 바탕으로 화면에 마커를 표시하는 기능
    • 화면이 이동하거나 줌인, 줌 아웃을 할 시 화면의 마커 정보가 최신화 되는 기능
    • 마커 정보를 최신화 할 때 화면에서 사라진 마커를 dom에서 제거하는 기능
    • 마커 정보를 최신화 할 때 이전 화면에서도 있었던 마커를 재생성 하지 않는 기능
    • 마커를 클릭했을 시 해당 마커에 대한 간단 정보를 모달로 띄워주는 기능
    • 화면에 표시된 마커들에 대한 충전소 정보를 리스트로 보여주는 기능

    이번에 새로 추가하고자 한 기능은 다음과 같다.

    • 충전소 리스트에서 충전소를 선택하면 화면의 중심이 선택한 충전소 마커로 이동하고, 충전소의 간단 정보를 모달로 띄워주는 기능

    위 기능을 구현하기 위해선 google maps api의 InfoWindow객체를 이용해야 한다. 사용 방식은 다음과 같다.

    const infowindow = new google.maps.InfoWindow({
    content: contentString,
    ariaLabel: 'Uluru',
    });

    const marker = new google.maps.Marker({
    position: uluru,
    map,
    title: 'Uluru (Ayers Rock)',
    });

    infowindow.open({
    anchor: marker,
    map,
    });

    간단하게 요약하자면 다음과 같다.

    • InfoWindow 생성자 함수를 통해 infoWindow 인스턴스를 생성한다.
      • 생성시 dom 요소 혹은 string을 전달해 infoWindow가 생성될 dom위치를 지정해준다.
    • marker 인스턴스를 infoWindow 인스턴스의 open 메서드에 인자로 전달한다.
    • infoWindow 생성 시 전달했던 dom요소의 위치가 marker의 위치로 고정되면서 화면에 그려진다.

    Untitled

    충전소 정보를 보여주는 위 StationList 컴포넌트는 충전소 정보에 접근할 때 react-query를 통해 서버 상태를 직접 내려 받아 컴포넌트 내부 리스트를 렌더링 한다.

    또한, StationMarkersContainer에서도 충전소 정보를 react-query의 서버 상태에서 참조해 마커를 렌더링 하고 있다.

    따라서 StationList 컴포넌트와 StationMarkersContainer는 각각 따로 서버 상태에 접근해 렌더링을 수행하고 있으므로 둘 사이에는 어떠한 연결 고리가 없다.

    여기서 문제가 발생하게 되었다.


    현재까지의 코드에서는 infoWindow인스턴스를 StationMarkersContainer컴포넌트에서 생성한다. 이를 하위 컴포넌트인 StationMarker에 내려주고, 이 컴포넌트 내부에서 marker인스턴스를 생성한다.

    이번에 구현하기로 한 기능은 StationList의 항목 중 하나를 선택했을 시 선택된 충전소에 해당하는 마커에 간단 정보 모달이 뜨며 화면을 해당 마커가 중심으로 오도록 이동 시키는 것이었다.

    하지만 지금의 코드 구조상 StationListStationMarkersContainer사이에는 어떠한 연결 고리도 없으므로 infoWindowmarkerStationList는 접근할 수 없는 상태가 된다.

    이를 해결하기 위해서 다음과 같은 방법을 사용하기로 했다.

    • infoWindow인스턴스를 root 단에서 생성해 전역적으로 관리한다.
    • 생성될 marker 인스턴스들을 배열 형태의 전역 상태로 관리한다.

    위 내용을 말로만 본다면 별로 어려울 것 없어 보이지만 실제 구현을 진행해보니 내부적으로 큰 문제가 두 가지 존재했다.

    1. 따로 모듈을 분리해 infoWindow를 생성할 수 없다.
    2. marker인스턴스를 생성하는 주체가 StationMarkersContainer가 되어서는 안된다.

    각각의 문제점을 살펴보자.


    1. 따로 모듈을 분리해 infoWindow를 생성할 수 없다.

    infoWinodw를 전역 상태로 만들어 사용하기 위해 처음으로 했던 생각은 infoWindowStore.ts로 모듈을 분리하여 infoWindow를 생성해 store의 초기값으로 지정하는 것이었다.

    위 생각을 가지고 그대로 구현해보았더니 google을 참조할 수 없다는 에러가 발생했다. InfoWindow생성자 함수는 google.maps.InfoWindow를 통해 접근할 수 있기 때문에 해당 에러는 infoWindow인스턴스를 생성할 수 없다는 것을 의미했다.

    google을 참조할 수 없는지 이유를 분석해보니 이유는 다음과 같았다.

    우리 팀이 구글 지도 로드를 위해 선택한 라이브러리는 @googlemaps/react-wrapper이다. 이 라이브러리의 동작을 살펴보면 다음과 같다.

    • Wrapper컴포넌트가 @googlemaps/js-loader라이브러리의 Loader생성자 함수를 호출한다.
    • 생성된 loader인스턴스의 load메서드를 실행시켜 지도의 로딩 작업을 시작한다.
      • load 메서드는 최종적으로 Promise<typeof google>을 반환하는데, 지도 로드에 성공하면 resolve(window.google) 을 실행시켜 google을 전역적으로 사용 가능하도록 만들어준다.
    • 지도의 로딩이 완료되면 Wrapperrender props를 통해 받은 콜백 함수를 실행시킨다.
      • render콜백 함수는 로딩 상태를 나타내는 Status를 파라미터로 넘겨 받아 호출된다.

    최종적으로 render를 실행 시켰을 때 반환 되는 컴포넌트에서는 google 로딩 되어 전역적으로 접근이 가능함을 보장할 수 있으므로 이때부터 google에 접근이 가능해진다. → 따라서 Wrapper를 통해 반환되는 컴포넌트의 하위 컴포넌트에서 google.maps.Map생성자 함수를 사용해 지도를 생성할 수 있게 된다.

    infoWindow를 생성하기 위해 만든 새로운 모듈은 첫 import시기에 평가될 것이기 때문에 Wrapper의 하위 컴포넌트에서 import를 수행한다면 로드가 완료된 이후 시점일 것이므로 window.google이 등록되어 google에 접근이 가능할 것으로 예상했다.

    하지만 웹팩을 통한 번들링 과정에서 모듈이 뒤섞여 파일의 평가 시기를 보장할 수 없어져 새로 만든 모듈에서는 google에 대한 접근이 불가능해지게 되었다. 웹팩을 좀 더 공부해본다면 이 문제를 해결할 수 있을 것 같았지만, 너무 지엽적인 부분에서 많은 시간을 들이기 보단 기존에 개발하던 방식을 통해 문제를 해결해보기로 결정했다.

    최종적으로 문제를 해결한 방식은 다음과 같다.

    • InfoWindow생성자 함수를 호출할 CarFfeineInfoWindowInitializer컴포넌트를 만든다.
    • Wrapper로 감싸진 컴포넌트 하위에 CarFfeineInfoWindowInitializer 컴포넌트를 추가한다.
    • google에 접근이 가능한 상태를 보장받은 CarFfeineInfoWindowInitializer내부에서 infoWindow인스턴스를 생성한다.
    • storeinfoWindow인스턴스를 set해주어 전역적으로 infoWindow를 사용 가능하도록 한다.

    2. marker인스턴스를 생성하는 주체가 StationMarkersContainer가 되어서는 안된다.

    이번 팀 프로젝트에서 지도를 구현하기 위해 google maps api를 사용하게 되었다. 뜬금없이 이 이야기를 한 이유는 다음과 같다.

    • google maps api는 바닐라 자바스크립트를 기반으로 동작한다.
    • 이번 팀 프로젝트는 리액트를 기반으로 개발을 진행할 것이다.
    • 지도를 그리기 위해서 바닐라 자바스크립트와 리액트의 적절한 조화가 필요하다.
    • 다소 혼란스러울 수 있는 지도의 조작 방식을 리액트와 조화롭게 사용하기 위해서 컴포넌트 설계시 컴포넌트의 책임을 확실하게 구분해야겠다는 생각을 하게 되었다.

    이 컴포넌트의 책임에 대한 문제로 인해 marker 인스턴스를 생성하는 주체에 대해 많은 고민을 하게 되었다.

    일단 원래 코드 구조에서 마커를 그리기 위해 컴포넌트를 다음과 같이 추상화 했다.

    • StationMarkersContainer 컴포넌트
      • 리액트 쿼리를 통해 받아온 서버 상태(충전소 정보 배열)로 StationMarker를 호출한다.
    • StationMarker 컴포넌트
      • 상위에서 내려받은 충전소 정보 props를 통해 marker 인스턴스를 생성한다. (google maps api에서는 인스턴스 생성이 곧 렌더링을 의미한다)
      • 생성한 marker 인스턴스에 infoWindow 인스턴스의 open 메서드를 트리거 하는 클릭 이벤트 리스너를 추가해준다.
      • useEffect의 클린업 함수를 이용해 충전소 정보가 최신화 되었을 때 마커가 더이상 화면에 보이지 않는다면 marker 인스턴스의 setMap(null) 메서드를 호출해 google maps api에서 마커를 지우도록 한다. (마커 렌더링 최적화)

    간략히 설명하자면 StationMarkersContainer 컴포넌트는 충전소 정보를 서버에서 받아 StationMarker를 호출하는 역할만을 수행하고, 마커에 대한 모든 세부 로직은 StationMarker가 수행하도록 컴포넌트를 추상화 해보았다.

    이름에서도 드러나듯 StationMarker 컴포넌트가 marker 인스턴스를 생성하는 주체가 되어야 바닐라 자바스크립트와 리액트의 혼종인 이 프로젝트의 코드를 추후 유지보수 할 때 문제가 없으리라 판단했다.

    하지만 이렇게 추상화 된 컴포넌트들은 marker 인스턴스를 배열 형식의 전역 상태에 담아 관리하고자 할 때 문제가 되었다.


    일단 먼저 서버에서 내려 받은 충전소 정보를 station이라고 하자, 우리는 이 station을 통해 marker 인스턴스를 생성하고자 한다.

    이때 생각 할 수 있는 가장 간단한 방법은 station에서 map 메서드를 통해 marker 인스턴스를 생성하여 이 marker 인스턴스를 하위 컴포넌트인 StationMarker에 넘겨주는 방식일 것이다.

    하지만 이 방식은 인스턴스를 생성하는 것이 곧 화면에 렌더링을 발생시키는 것을 의미하는 google maps api의 특성상 우리가 처음 설계한 컴포넌트의 책임을 반하는 구조를 만들어내게 된다.

    자세히 설명해보자면 마커의 렌더링은 StationMarkersContainer가 수행하고 있는데 화면에 보이지 않는 마커를 지우는 역할은 StationMarker컴포넌트가 수행하고 있고, 이벤트 핸들러의 추가 역시 마커가 생성된 이후에 하위 컴포넌트에서 이를 수행하는 괴상한 코드가 만들어지게 된다.

    추후 코드의 유지보수성을 위해선 피해야 할 방식임이 명확했다.

    해결 방식을 고민해보다가 다음과 같은 해결 방안을 생각하게 되었다.

    StationMarker 컴포넌트의 역할

    • marker 인스턴스를 생성한다.
    • marker 인스턴스의 이벤트 핸들러를 추가한다.
    • 생성된 marker 인스턴스를 배열 형식의 전역 상태에 추가한다.
    • 충전소 정보가 최신화 되었을 때 마커가 화면에 보이지 않는 상태가 되었다면 marker 인스턴스를 전역 상태에서 삭제한다.

    위와 같이 StationMarker 의 역할을 잡게 되면 기존의 컴포넌트 설계 구조를 해치지 않으면서 전역 상태에 marker인스턴스를 잘 추가할 수 있게 된다. 하지만 이렇게 되면 StationMarker 컴포넌트는 다음의 큰 문제들을 가지게 된다.

    1. marker들을 가지는 전역 상태를 구독하고 있는 컴포넌트가 새로 생성되는 마커의 개수만큼 리렌더링 된다.
    2. 현재 사용하고 있는 전역 상태 관리 도구의 특성상 이전 상태를 참조해와야 marker를 추가할 수 있게 되는데, 이 때 이전 상태가 최신의 상태임을 보장하지 못할 수 있다.

    이 두 문제를 해결할 방식을 고민해보았을 때 다음과 같은 결론에 도달하게 되었다.

    • 현재 사용하고 있는 전역 상태 관리 도구는 React 18에 새로 추가된 useSyncExternalState 훅을 기반으로 recoil과 비슷하게 사용할 수 있도록 계층을 분리하여 만든 도구이다.
    • 기존에 사용하던 전역 상태 관리 도구의 메서드 useExternalState, useExternalValue, useSetExternalState 이외에 store 인스턴스에 직접 접근하여 최신의 상태를 참조하는 getStoreSnapShot 메서드를 추가한다.
    • store에 직접 접근해 받아온 최신의 상태는 바닐라 자바스크립트 객체 이므로 리액트의 리렌더링을 발생 시키지 않는다.
    • 리렌더링으로 인한 문제점들을 getStoreSnapShot 메서드를 추가함으로써 해결할 수 있다.

    새로운 기능 추가를 위해 마주했던 앞선 두 가지의 문제와 해결 방식을 살펴 보았다. 그래서 최종적으로 이전까지 계속해서 고민해왔던 문제를 해결한 과정을 간추려보자면 다음과 같다.

    • 충전소 정보를 서버에서 받아와 렌더링 하는 StationList 컴포넌트에서 marker 인스턴스 배열을 저장하고 있는 store인스턴스에 직접 접근해 최신의 marker인스턴스들을 가져온다.
    • 충전소 목록에서 사용자가 충전소를 클릭했을 때 전역으로 관리되는 infoWindow 인스턴스의 open메서드에 marker 인스턴스들 중 선택된 marker를 전달해 간단 정보 모달을 띄워준다.
    - + \ No newline at end of file diff --git a/tags/google-maps-api/page/3.html b/tags/google-maps-api/page/3.html index 969589e..48b52a8 100644 --- a/tags/google-maps-api/page/3.html +++ b/tags/google-maps-api/page/3.html @@ -5,13 +5,13 @@ "google maps api" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "google maps api" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    가브리엘

    지도 api 벤더 선택 이유

    국내 서비스 중인 지도 서비스로는 google, naver, kakao가 있습니다.

    이 중에서도 google maps api는 css로 지도의 테마를 직접 스타일링할 수 있는 기능이 있어서 선택하게 됐습니다.

    google maps api를 사용하기 위해서 별도의 라이브러리 사용이 필수는 아니지만

    저희 팀에서 대중적인 라이브러리들과 기본 환경 설정법을 모두 테스트 했을 때, 반드시 사용하고 싶은 라이브러리가 존재하여 비교를 기록으로 남기게 됐습니다.

    google maps api 관련 라이브러리

    (선택한 라이브러리들은 ✅으로 표시했습니다.)

    google maps API

    https://github.com/tomchentw/react-google-maps

    이 라이브러리는 구글에서 공식으로 제공하는 지도 api로, HTML DOM에 구글 지도를 부착하고, 사용(조작)할 수 있도록 도와줍니다. 이 라이브러리는 vanilla Javascript 기반으로 동작합니다.

    @types/google.maps

    https://www.npmjs.com/package/@types/google.maps

    TypeScript에서 구글 지도를 사용할 때 타입을 제공해주는 역할을 합니다.

    @googlemaps/js-api-loader

    https://www.npmjs.com/package/@googlemaps/js-api-loader

    이 라이브러리는 구글에서 공식으로 제공하는 지도 호출 api로, api key만 넘겨주더라도 구글 지도를 스크립트 형태로 불러와주는 역할을 하는 라이브러리입니다. 별도로 html 조작 없이 불러온 라이브러리에서 구글 지도를 꺼내서 동적으로 사용할 수 있습니다. vanilla Javascript 기반으로 동작하여 어디에서나 사용이 가능합니다.

    대중적인 라이브러리 비교

    react-google-maps@react-google-maps/api@googlemaps/react-wrapper
    링크https://www.npmjs.com/package/react-google-mapshttps://www.npmjs.com/package/@react-google-maps/apihttps://www.npmjs.com/package/@googlemaps/react-wrapper
    설명이 라이브러리는 개인이 만든 라이브러리로, google maps API를 react DOM 위에 올려서 사용하게 돕습니다.
    구글 지도와 마커를 react component 처럼 사용하여 react스럽게 렌더링 하는 것을 지원합니다.
    react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리였지만 2018년 이후로 업데이트가 끊겼습니다.
    이 라이브러리도 개인이 만든 라이브러리로 앞서 소개한 react-google-maps를 개량하여 만든 라이브러리입니다.
    이 라이브러리 역시 react에 지도나 마커 컴포넌트를 호출해서 사용이 가능합니다.
    현재 react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리 입니다.
    이 라이브러리는 구글에서 공식으로 제공하는 react용 라이브러리입니다.
    이 라이브러리는 앞서 소개한 js-api-loader를 활용하여 만든 Wrapper 컴포넌트를 제공하는데, 구글 지도를 호출하는 과정에서 수신중, 실패, 성공에 따라 지도를 보여줄 지, 로딩중 컴포넌트를 보여줄 지, 에러 컴포넌트를 보여줄 지 결정하는 기능이 있습니다.
    이외에는 기존의 js-api-loader의 기능과 완벽하게 동일합니다. (라이브러리를 열어서 직접 확인해봤습니다.)
    선택여부

    라이브러리 선택 이유

    저희 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 다보니 지도 위에 띄워줘야 할 마커를 최적화 하는 과정이 굉장히 중요합니다.

    1. 전국 6만여 개의 마커를 전부 보여줄 수 없다.
    2. 현재 디스플레이 영역의 마커만을 호출해야한다.
    3. 그 마커들의 렌더링 과정을 저수준에서 다룰 수 있어야 한다.

    이런 원칙을 가지고 있기에 대중적인 라이브러리들(react-google-maps, @react-google-maps/api)은 저희의 선택지에 없었습니다.

    따라서 구글 지도는 오로지 vanilla로 제공되는 상태에서 직접 제어하기로 결정하였고, 마커를 관리하는 주체 또한 구글 지도에서 직접 컨트롤을 하려고 합니다.

    따라서 구글 지도를 호출하는 작업은 @googlemaps/react-wrapper에 맡기고, 불러온 구글 지도는 vanilla로 통제하기로 했습니다.

    지도의 조작, 지도에 마커를 찍는 과정을 모두 공식 문서에 나와있는 방법대로 통제하려고 합니다.

    기존의 라이브러리들은 마커나 지도를 컴포넌트화 한 상태이기에 최적화 과정에서 저희가 제어할 수 없는 부분들이 있다고 생각합니다. 따라서 트러블슈팅 과정에서 마커의 호출 시점, 메모리에서 해제하는 시점, 렌더링하는 시점 등의 작업들을 훨씬 더 세밀하게 하려면 google maps api을 있는 그대로 사용할 수 있어야 합니다. 따라서 지도에 관련된 기능은 react DOM 위에서가 아닌 vanilla 환경에서 작업을 할 것입니다.

    구글 지도 제어 전략

    1. 구글 지도와 마커는 항상 바닐라 환경(react DOM 바깥)에서 동작하게 한다.
    2. 바닐라 환경에서만 동작하게 하여 리액트 컴포넌트에서의 재 렌더링을 일절 방지한다.
    3. 마커나 지도의 동작 이벤트에 의해 UI를 조작해야하는 경우에는 react DOM 조작을 하도록 한다.
    4. 바닐라 환경인 google maps api와 react DOM 사이의 제어 과정에는 useSyncExternalStore 훅을 이용하여 리액트 UI를 강제로 동기화 시킬 수 있도록 한다.

    구글 지도는 바닐라 환경에서, 각종 UI 통제는 리액트에서 통합하여 사용하는 환경을 구상하고 있습니다.

    시중에 나와있는 대부분의 라이브러리들을 활용하여 비교하고 테스트한 결과 @googlemaps/react-wrapper를 선택하는 것이 최적화와 생산성, 앱 안정성을 모두 확보할 수 있는 선택이라고 생각했습니다.

    현재 카페인 팀에서 사용중인 지도 제어에 관한 방법은 이후에 작성 될 글에서 상세하게 설명하겠습니다.

    - + \ No newline at end of file diff --git a/tags/google-maps.html b/tags/google-maps.html index 21ff86d..c06421e 100644 --- a/tags/google-maps.html +++ b/tags/google-maps.html @@ -5,13 +5,13 @@ "google maps" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "google maps" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    가브리엘

    지도 api 벤더 선택 이유

    국내 서비스 중인 지도 서비스로는 google, naver, kakao가 있습니다.

    이 중에서도 google maps api는 css로 지도의 테마를 직접 스타일링할 수 있는 기능이 있어서 선택하게 됐습니다.

    google maps api를 사용하기 위해서 별도의 라이브러리 사용이 필수는 아니지만

    저희 팀에서 대중적인 라이브러리들과 기본 환경 설정법을 모두 테스트 했을 때, 반드시 사용하고 싶은 라이브러리가 존재하여 비교를 기록으로 남기게 됐습니다.

    google maps api 관련 라이브러리

    (선택한 라이브러리들은 ✅으로 표시했습니다.)

    google maps API

    https://github.com/tomchentw/react-google-maps

    이 라이브러리는 구글에서 공식으로 제공하는 지도 api로, HTML DOM에 구글 지도를 부착하고, 사용(조작)할 수 있도록 도와줍니다. 이 라이브러리는 vanilla Javascript 기반으로 동작합니다.

    @types/google.maps

    https://www.npmjs.com/package/@types/google.maps

    TypeScript에서 구글 지도를 사용할 때 타입을 제공해주는 역할을 합니다.

    @googlemaps/js-api-loader

    https://www.npmjs.com/package/@googlemaps/js-api-loader

    이 라이브러리는 구글에서 공식으로 제공하는 지도 호출 api로, api key만 넘겨주더라도 구글 지도를 스크립트 형태로 불러와주는 역할을 하는 라이브러리입니다. 별도로 html 조작 없이 불러온 라이브러리에서 구글 지도를 꺼내서 동적으로 사용할 수 있습니다. vanilla Javascript 기반으로 동작하여 어디에서나 사용이 가능합니다.

    대중적인 라이브러리 비교

    react-google-maps@react-google-maps/api@googlemaps/react-wrapper
    링크https://www.npmjs.com/package/react-google-mapshttps://www.npmjs.com/package/@react-google-maps/apihttps://www.npmjs.com/package/@googlemaps/react-wrapper
    설명이 라이브러리는 개인이 만든 라이브러리로, google maps API를 react DOM 위에 올려서 사용하게 돕습니다.
    구글 지도와 마커를 react component 처럼 사용하여 react스럽게 렌더링 하는 것을 지원합니다.
    react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리였지만 2018년 이후로 업데이트가 끊겼습니다.
    이 라이브러리도 개인이 만든 라이브러리로 앞서 소개한 react-google-maps를 개량하여 만든 라이브러리입니다.
    이 라이브러리 역시 react에 지도나 마커 컴포넌트를 호출해서 사용이 가능합니다.
    현재 react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리 입니다.
    이 라이브러리는 구글에서 공식으로 제공하는 react용 라이브러리입니다.
    이 라이브러리는 앞서 소개한 js-api-loader를 활용하여 만든 Wrapper 컴포넌트를 제공하는데, 구글 지도를 호출하는 과정에서 수신중, 실패, 성공에 따라 지도를 보여줄 지, 로딩중 컴포넌트를 보여줄 지, 에러 컴포넌트를 보여줄 지 결정하는 기능이 있습니다.
    이외에는 기존의 js-api-loader의 기능과 완벽하게 동일합니다. (라이브러리를 열어서 직접 확인해봤습니다.)
    선택여부

    라이브러리 선택 이유

    저희 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 다보니 지도 위에 띄워줘야 할 마커를 최적화 하는 과정이 굉장히 중요합니다.

    1. 전국 6만여 개의 마커를 전부 보여줄 수 없다.
    2. 현재 디스플레이 영역의 마커만을 호출해야한다.
    3. 그 마커들의 렌더링 과정을 저수준에서 다룰 수 있어야 한다.

    이런 원칙을 가지고 있기에 대중적인 라이브러리들(react-google-maps, @react-google-maps/api)은 저희의 선택지에 없었습니다.

    따라서 구글 지도는 오로지 vanilla로 제공되는 상태에서 직접 제어하기로 결정하였고, 마커를 관리하는 주체 또한 구글 지도에서 직접 컨트롤을 하려고 합니다.

    따라서 구글 지도를 호출하는 작업은 @googlemaps/react-wrapper에 맡기고, 불러온 구글 지도는 vanilla로 통제하기로 했습니다.

    지도의 조작, 지도에 마커를 찍는 과정을 모두 공식 문서에 나와있는 방법대로 통제하려고 합니다.

    기존의 라이브러리들은 마커나 지도를 컴포넌트화 한 상태이기에 최적화 과정에서 저희가 제어할 수 없는 부분들이 있다고 생각합니다. 따라서 트러블슈팅 과정에서 마커의 호출 시점, 메모리에서 해제하는 시점, 렌더링하는 시점 등의 작업들을 훨씬 더 세밀하게 하려면 google maps api을 있는 그대로 사용할 수 있어야 합니다. 따라서 지도에 관련된 기능은 react DOM 위에서가 아닌 vanilla 환경에서 작업을 할 것입니다.

    구글 지도 제어 전략

    1. 구글 지도와 마커는 항상 바닐라 환경(react DOM 바깥)에서 동작하게 한다.
    2. 바닐라 환경에서만 동작하게 하여 리액트 컴포넌트에서의 재 렌더링을 일절 방지한다.
    3. 마커나 지도의 동작 이벤트에 의해 UI를 조작해야하는 경우에는 react DOM 조작을 하도록 한다.
    4. 바닐라 환경인 google maps api와 react DOM 사이의 제어 과정에는 useSyncExternalStore 훅을 이용하여 리액트 UI를 강제로 동기화 시킬 수 있도록 한다.

    구글 지도는 바닐라 환경에서, 각종 UI 통제는 리액트에서 통합하여 사용하는 환경을 구상하고 있습니다.

    시중에 나와있는 대부분의 라이브러리들을 활용하여 비교하고 테스트한 결과 @googlemaps/react-wrapper를 선택하는 것이 최적화와 생산성, 앱 안정성을 모두 확보할 수 있는 선택이라고 생각했습니다.

    현재 카페인 팀에서 사용중인 지도 제어에 관한 방법은 이후에 작성 될 글에서 상세하게 설명하겠습니다.

    - + \ No newline at end of file diff --git a/tags/googlemaps-react-wrapper.html b/tags/googlemaps-react-wrapper.html index 8f66daa..cd146d3 100644 --- a/tags/googlemaps-react-wrapper.html +++ b/tags/googlemaps-react-wrapper.html @@ -5,13 +5,13 @@ "@googlemaps/react-wrapper" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "@googlemaps/react-wrapper" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    가브리엘

    지도 api 벤더 선택 이유

    국내 서비스 중인 지도 서비스로는 google, naver, kakao가 있습니다.

    이 중에서도 google maps api는 css로 지도의 테마를 직접 스타일링할 수 있는 기능이 있어서 선택하게 됐습니다.

    google maps api를 사용하기 위해서 별도의 라이브러리 사용이 필수는 아니지만

    저희 팀에서 대중적인 라이브러리들과 기본 환경 설정법을 모두 테스트 했을 때, 반드시 사용하고 싶은 라이브러리가 존재하여 비교를 기록으로 남기게 됐습니다.

    google maps api 관련 라이브러리

    (선택한 라이브러리들은 ✅으로 표시했습니다.)

    google maps API

    https://github.com/tomchentw/react-google-maps

    이 라이브러리는 구글에서 공식으로 제공하는 지도 api로, HTML DOM에 구글 지도를 부착하고, 사용(조작)할 수 있도록 도와줍니다. 이 라이브러리는 vanilla Javascript 기반으로 동작합니다.

    @types/google.maps

    https://www.npmjs.com/package/@types/google.maps

    TypeScript에서 구글 지도를 사용할 때 타입을 제공해주는 역할을 합니다.

    @googlemaps/js-api-loader

    https://www.npmjs.com/package/@googlemaps/js-api-loader

    이 라이브러리는 구글에서 공식으로 제공하는 지도 호출 api로, api key만 넘겨주더라도 구글 지도를 스크립트 형태로 불러와주는 역할을 하는 라이브러리입니다. 별도로 html 조작 없이 불러온 라이브러리에서 구글 지도를 꺼내서 동적으로 사용할 수 있습니다. vanilla Javascript 기반으로 동작하여 어디에서나 사용이 가능합니다.

    대중적인 라이브러리 비교

    react-google-maps@react-google-maps/api@googlemaps/react-wrapper
    링크https://www.npmjs.com/package/react-google-mapshttps://www.npmjs.com/package/@react-google-maps/apihttps://www.npmjs.com/package/@googlemaps/react-wrapper
    설명이 라이브러리는 개인이 만든 라이브러리로, google maps API를 react DOM 위에 올려서 사용하게 돕습니다.
    구글 지도와 마커를 react component 처럼 사용하여 react스럽게 렌더링 하는 것을 지원합니다.
    react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리였지만 2018년 이후로 업데이트가 끊겼습니다.
    이 라이브러리도 개인이 만든 라이브러리로 앞서 소개한 react-google-maps를 개량하여 만든 라이브러리입니다.
    이 라이브러리 역시 react에 지도나 마커 컴포넌트를 호출해서 사용이 가능합니다.
    현재 react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리 입니다.
    이 라이브러리는 구글에서 공식으로 제공하는 react용 라이브러리입니다.
    이 라이브러리는 앞서 소개한 js-api-loader를 활용하여 만든 Wrapper 컴포넌트를 제공하는데, 구글 지도를 호출하는 과정에서 수신중, 실패, 성공에 따라 지도를 보여줄 지, 로딩중 컴포넌트를 보여줄 지, 에러 컴포넌트를 보여줄 지 결정하는 기능이 있습니다.
    이외에는 기존의 js-api-loader의 기능과 완벽하게 동일합니다. (라이브러리를 열어서 직접 확인해봤습니다.)
    선택여부

    라이브러리 선택 이유

    저희 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 다보니 지도 위에 띄워줘야 할 마커를 최적화 하는 과정이 굉장히 중요합니다.

    1. 전국 6만여 개의 마커를 전부 보여줄 수 없다.
    2. 현재 디스플레이 영역의 마커만을 호출해야한다.
    3. 그 마커들의 렌더링 과정을 저수준에서 다룰 수 있어야 한다.

    이런 원칙을 가지고 있기에 대중적인 라이브러리들(react-google-maps, @react-google-maps/api)은 저희의 선택지에 없었습니다.

    따라서 구글 지도는 오로지 vanilla로 제공되는 상태에서 직접 제어하기로 결정하였고, 마커를 관리하는 주체 또한 구글 지도에서 직접 컨트롤을 하려고 합니다.

    따라서 구글 지도를 호출하는 작업은 @googlemaps/react-wrapper에 맡기고, 불러온 구글 지도는 vanilla로 통제하기로 했습니다.

    지도의 조작, 지도에 마커를 찍는 과정을 모두 공식 문서에 나와있는 방법대로 통제하려고 합니다.

    기존의 라이브러리들은 마커나 지도를 컴포넌트화 한 상태이기에 최적화 과정에서 저희가 제어할 수 없는 부분들이 있다고 생각합니다. 따라서 트러블슈팅 과정에서 마커의 호출 시점, 메모리에서 해제하는 시점, 렌더링하는 시점 등의 작업들을 훨씬 더 세밀하게 하려면 google maps api을 있는 그대로 사용할 수 있어야 합니다. 따라서 지도에 관련된 기능은 react DOM 위에서가 아닌 vanilla 환경에서 작업을 할 것입니다.

    구글 지도 제어 전략

    1. 구글 지도와 마커는 항상 바닐라 환경(react DOM 바깥)에서 동작하게 한다.
    2. 바닐라 환경에서만 동작하게 하여 리액트 컴포넌트에서의 재 렌더링을 일절 방지한다.
    3. 마커나 지도의 동작 이벤트에 의해 UI를 조작해야하는 경우에는 react DOM 조작을 하도록 한다.
    4. 바닐라 환경인 google maps api와 react DOM 사이의 제어 과정에는 useSyncExternalStore 훅을 이용하여 리액트 UI를 강제로 동기화 시킬 수 있도록 한다.

    구글 지도는 바닐라 환경에서, 각종 UI 통제는 리액트에서 통합하여 사용하는 환경을 구상하고 있습니다.

    시중에 나와있는 대부분의 라이브러리들을 활용하여 비교하고 테스트한 결과 @googlemaps/react-wrapper를 선택하는 것이 최적화와 생산성, 앱 안정성을 모두 확보할 수 있는 선택이라고 생각했습니다.

    현재 카페인 팀에서 사용중인 지도 제어에 관한 방법은 이후에 작성 될 글에서 상세하게 설명하겠습니다.

    - + \ No newline at end of file diff --git a/tags/hello.html b/tags/hello.html index 488714c..8ad9175 100644 --- a/tags/hello.html +++ b/tags/hello.html @@ -5,7 +5,7 @@ "hello" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -21,7 +21,7 @@ 거기에 file을 만듭니다. 파일 이름이 중요한데요 V1__init.sql 이러한 방식으로 V{version 숫자}__{어떠한 파일인지에 대한 이름}.sql 언더스코어 2개는 필수로 작성해야합니다.

    create table member(
    id bigint auto_increment primary key,
    name varchar(255) null,
    );

    이렇게 V1__init.sql에 대한 파일을 작성했습니다. 이제는 email을 추가한다는 요구사항을 반영해보겠습니다.

    ALTER TABLE member
    ADD COLUMN email varchar(255);

    이렇게 새로운 파일을 만들어서 해당 스크립트를 작성했습니다. 파일명이 중요한데요, 이전 파일의 숫자보다 +1 이 되는 숫자를 V 뒤에 붙입니다.

    따라서 이번 파일은 V2__add_column_email.sql 이라고 만들었습니다.

    그럼 이제 또 시간이 지나 회원이 많아졌습니다. 하지만 email이 없는 사용자도 많습니다. 이 상황에서 email을 not null로 변경해야한다는 요구사항이 생겼습니다.

    그러면 아래와 같이 반영할 수 있습니다.

    ALTER TABLE member
    MODIFY email VARCHAR(20) NOT NULL default 'default'

    이렇게 V3__add_constraints.sql 파일을 만들었습니다. 그러면 null이 있던 row들은 email이 default가 되고 not null 제약조건이 활성화 된 것을 볼 수 있습니다.

    그러면 주어진 요구사항은 모두 만족할 수 있습니다. 거기에다 v1, v2, v3 가 나뉘어져있어서 어느 커밋부터 해당 sql이 추가되었는지도 확인할 수 있습니다.

    그리고 ddl-auto update를 사용하면 반영되지 않았던 제약조건의 추가도 확인할 수 있습니다. 그러면 ddl-auto의 속성을 validate로 변경하여, db schema와 entity의 필드가 다르면 어플리케이션이 실행되지 않도록 해서 좀 더 안전한 개발을 할 수 있습니다.

    결론

    flyway는 roll back을 하는 것이 유료라서, production 서버에서 혹은 롤백을 해야하는 일이 있는 서버에서는 사용하는 것이 좋지 않지만, 이와 같이 데이터를 drop 할 수 없는 상황이라면, 사용하지 않을 이유가 없어보이는 좋은 도구입니다.

    짧은 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git a/tags/hello/page/2.html b/tags/hello/page/2.html index 0f5923a..d1e2b99 100644 --- a/tags/hello/page/2.html +++ b/tags/hello/page/2.html @@ -5,13 +5,13 @@ "hello" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +
    - + \ No newline at end of file diff --git a/tags/hibernate.html b/tags/hibernate.html index bb4571d..a25600c 100644 --- a/tags/hibernate.html +++ b/tags/hibernate.html @@ -5,13 +5,13 @@ "Hibernate" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "Hibernate" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    누누
    박스터

    안녕하세요 카페인팀 누누입니다

    이번에는 대량의 데이터를 DB에 넣는 과정을 최적화하는 과정에서 알게 된 내용을 공유하려고 합니다

    이번 최적화의 목표

    전기차 충전소에 대한 공공 데이터를 가져오고, 그 데이터를 DB 에 넣는 과정을 최적화해보자

    대량의 데이터를 삽입하는 과정

    저희 팀의 요구사항을 간단하게 정리하면 다음과 같습니다

    1. 대량의 데이터를 공공 데이터에서 전기차 충전소와 전기차 충전기에 대한 데이터를 가져온다
      • 충전소는 6만 개, 충전기는 23만 개의 데이터가 존재한다.
      • 한 번에 가져올 수 있는 양은 9999개 까지다.
    2. 이 데이터를 DB에 넣는다
      • 충전소와 충전기는 1:N 관계이다

    최적화 전은 어떤 상황이었는데?

    before_optimize

    위 사진을 잘 보시면 아실 수 있으시겠지만, 2000개를 저장하는데, 231.762 초가 사용되었습니다.

    물론 출력을 위한 시간도 포함되었기에, 230초 정도라고 생각하셔도 좋습니다

    1만 개라면? 231.762초 * 5 = 1,158.81초

    23만 개라면? 1158.81 * 23 = 26,652.63초

    시간으로 바꿔보면 7.4 시간이 걸린다는 것을 볼 수 있습니다

    이 과정에서 볼 수 있는 문제점

    1. 데이터를 저장할 때마다, 새로운 Transaction 이 생성된다.

    어떻게 개선할 수 있을까?

    데이터를 저장할 때마다, 새로운 Transaction 이 생성되는 것을 방지하기 위해, 전체를 하나의 트랜잭션으로 묶는다

    전체를 한 트랜잭션으로 묶은 버전

    all_in_transaction

    이 과정에서 2000개를 저장하는데 65초 가 사용되었습니다.

    1만 개라면? 65초 * 5 = 325초

    23만 개라면? 325초 * 23 = 7,475초

    시간으로 바꿔보면 2시간이 걸린다는 것을 볼 수 있습니다

    전체적으로 3배 정도 빨라졌습니다

    이 과정에서 볼 수 있는 문제점

    1. 23만 개의 저장이 모두 한 트랜잭션이 되어서, 하나가 실패하면 23만개를 새로 저장해야 하는 상황에 처한다

    어떻게 개선할 수 있을까?

    23만개의 저장이 모두 한 트랜잭션이 되는 것을 방지하기 위해, 1만 개씩 영속화시킨다

    1만 개가 한 트랜잭션으로 묶인 버전

    separateTransaction

    성능상으로 개선한 부분은 그렇게 크지 않지만, 실패했을 때, 1만 개만 다시 저장하면 되기에, 훨씬 빠르게 복구가 가능합니다.

    여기서 PageNo라는 클래스는, i를 바로 참조했을 경우, effectively final을 보장할 수 없어서 만들었습니다.

    성능은 전체를 한 트랜잭션으로 묶은 버전과 큰 차이가 나지 않습니다.

    이 과정에서 볼 수 있는 문제점

    1. id 생성 전략이 GenerationType.IDENTITY 이기에, 데이터를 저장할 때마다, DB에서 id를 생성해야 한다.

    JPA에 있는 쓰기 지연을 전혀 활용할 수 없고, DB에서 id를 생성하기 위해, DB와 매번 통신을 해야 한다.

    어떻게 개선할 수 있을까?

    id를 미리 생성해서, DB 에서 id 를 생성하는 과정을 생략한다

    ID 생성 전략을 GenerationType.Table의 형태로 바꿔서, DB에서 id를 생성하는 과정을 줄여서, 성능을 개선한다

    1만 개가 한 트랜잭션으로 묶이고, id를 미리 생성한 버전

    이때 batch size를 1000 단위로 설정해서 1000개씩 id 가 늘어나도록 설정했다

    charger_generatorstation_generator

    spring.jdbc.template.fetch-size=10000

    10000batch_size

    1자리 숫자는 앞에서부터 n(만개)를 의미하고, 2번째 숫자는 1만 개를 저장하는 데 걸린 시간(ms)을 의미합니다.

    처음 1만 개는 142초가 걸리고, 2만 개는 285초가 걸렸습니다.

    23만 개라면? 142 * 26 = 3,266초

    처음과 비교하자면 7.4시간이 걸리는 것에서 54분 정도 걸리는 것으로 개선되었습니다.

    이 과정에서 볼 수 있는 문제점

    하나의 스레드에서만 동작하기에, 성능이 개선되었지만, 여전히 느립니다.

    하나의 스레드에서만 동작하기에, 하나의 커넥션을 사용하게 됩니다.

    어떻게 개선할 수 있을까?

    여러 스레드에서 동작하게 하고, 여러 커넥션을 사용하게 합니다.

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전

    multi_thread

    이 버전에서 89991 개를 저장하는데 총 157초가 걸렸습니다.

    23만 개라면? 157 * 3 = 471초

    시간으로 바꿔보면 5분도 채 걸리지 않는 시간이죠

    이 과정에서 볼 수 있는 문제점

    hikari connection pool 사이즈를 10으로 설정했는데, 10개의 커넥션을 사용하면서 저장을 하다 보니, 10개의 커넥션을 모두 사용하고 나서, 11번째부터는 커넥션을 가져오기 위해, 기다려야 하는 상황이 발생합니다.

    어떻게 개선할 수 있을까?

    hikari connection pool 사이즈를 25로 설정해서, 25개의 커넥션을 사용하도록 합니다.

    spring.datasource.hikari.maximum-pool-size=25

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전 2

    multi_thread2

    총 13만 개의 데이터를 저장하는데, 147초가 걸리고, db 인스턴스의 cpu 사용률이 100%에 가까워져서 ec2 가 다운되었습니다.

    이 과정에서 볼 수 있는 문제점

    db의 cpu 사용량을 고려하지 않고, 23만 개가 조금 넘는 데이터를 25개의 커넥션을 활용해 저장하려고 했습니다

    결론

    1. 데이터를 저장할 때마다, transaction을 사용하지 말자
    2. 데이터를 저장할 때마다, id를 생성하지 말자
    3. 여러 스레드에서 동작하고, 여러 커넥션을 사용하자
    4. db의 cpu 사용량을 고려하자

    긴 글 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/tags/index.html b/tags/index.html index f2e8676..279feb7 100644 --- a/tags/index.html +++ b/tags/index.html @@ -5,7 +5,7 @@ "index" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -18,7 +18,7 @@ 평균적으로 0.63초가 나왔습니다. 약 25 ~ 30%의 조회 속도가 개선되었습니다.

    아직 이 부분은 개선이 더 필요해보입니다.

    그래도 개선이 됐고, 삽입과 갱신에는 큰 지장이 없어서 일단 이정도로 마무리 하고, 추후에 개선을 해보도록 하겠습니다.

    이미지 추가적으로 충전기 조회는 굉장히 빨라졌습니다!

    배우는 단계이다보니 미숙하고 틀린 부분이 있을 수 있습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git a/tags/infra.html b/tags/infra.html index 5d0122c..ce0b442 100644 --- a/tags/infra.html +++ b/tags/infra.html @@ -5,7 +5,7 @@ "infra" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "infra" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    제이

    안녕하세요! 카페인팀의 제이입니다.

    저희 카페인 팀에서 무중단 배포를 진행했습니다. 어떤 과정으로 진행을 했는지 작성해보도록 하겠습니다!


    기존 배포 방식과 문제점

    먼저 카페인 팀의 기존 배포 방식은 다음과 같습니다.

    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근하여서 기존에 띄워진 서버를 다운 시킵니다.
    4. Docker Hub에 업로드한 Docker image를 pull해서 서버를 가동시킵니다.

    이런 과정으로 배포 스크립트가 작성되어 있습니다. 하지만 이 방법은 기존 서버를 다운 시키고 새로운 서버를 띄울 때 다운 타임이 존재한다는 문제점이 있습니다.

    사용자 입장에서는 잘 사용하고 있는데 갑자기 서비스가 작동되지 않는다면 서비스에 대한 신뢰성이 낮아질 수도 있고 이런 이유로 이탈할 수도 있습니다.

    기존 문제를 해결하기

    저희는 먼저 제한된 EC2 인스턴스로 인해 롤링 배포의 장점을 가져갈 수 없었고, 카나리 방식 또한 저희 서비스에서 필요로한 전략이 아니기 때문에 비교적 롤백도 빠른 Blue/Green 전략을 선택하였습니다.

    저희의 Blue/Green 무중단 배포 시나리오는 다음과 같습니다. 편의를 위해서 [기존 서버(기존 포트) / 새로운 서버(새로운 포트)] 라는 명칭을 사용하겠습니다.


    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub 에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근해서 Docker Hub에 업로드한 새로운 버전의 Image를 pull 해옵니다.
    4. 만약 8080 포트에 기존 서버가 띄워져 있으면 8081 포트를 새로운 서버가 띄워질 포트로 지정해주고, 반대로 8081 포트에 기존 서버가 띄워져 있으면 8080 포트에 새로운 서버가 띄워질 포트로 지정해줍니다.
    5. 미리 Docker Hub에 업로드한 Docker image를 [image+port]라는 네이밍으로 pull을 한 후 새로운 포트로 서버를 가동시킵니다.
    6. 새로운 서버가 제대로 가동 됐는지 확인하기 위해서 헬스 체크를 진행합니다. 20번 동안 서버가 정상 동작하는지 Spring Actuactor를 통해서 확인을 합니다.
    7. 정상 작동이 됐음을 확인하면 현재 인스턴스에는 2대의 서버가 띄워져있고 요청은 여전히 기존 서버로 들어가게 됩니다. 따라서 Nginx를 통해 포트포워딩을 새로운 서버의 포트로 지정해주고 기존 서버는 내려줍니다.

    여기까지가 카페인 팀의 시나리오입니다. 그렇다면 하나씩 스크립트를 확인해보겠습니다. 설명은 주석으로 달아두겠습니다 :)

    backend-deploy.yml

    (Github Actions에서 사용)

    name: deploy

    # 1. prod/backend branch에 push 작업이 일어나면 해당 작업을 수행한다
    on:
    push:
    branches:
    - prod/backend

    jobs:
    docker-build:
    runs-on: ubuntu-latest
    defaults:
    run:
    working-directory: ./backend

    steps:
    # 2. 도커 허브에 로그인
    - name: Log in to Docker Hub
    uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
    with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_PASSWORD }}
    - uses: actions/checkout@v3

    # 3. JDK 17 설치 및 빌드 (프로젝트 Java version)
    - name: Set up JDK 17
    uses: actions/setup-java@v3
    with:
    java-version: '17'
    distribution: 'adopt'

    - name: Gradle Caching
    uses: actions/cache@v3
    with:
    path: |
    ~/.gradle/caches
    ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
    ${{ runner.os }}-gradle-

    - name: Grant execute permission for gradlew
    run: chmod +x gradlew
    - name: Build for asciiDoc
    run: ./gradlew bootjar

    - name: Build with Gradle
    run: ./gradlew bootjar

    # 4. 산출물을 Image로 빌드 후 Docker Hub에 Image Push하기
    - name: Extract metadata (tags, labels) for Docker
    id: meta
    uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
    with:
    images: woowacarffeine/backend

    - name: Build and push Docker image
    uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
    with:
    context: .
    file: ./backend/Dockerfile
    push: true
    platforms: linux/arm64
    tags: woowacarffeine/backend:latest
    labels: ${{ steps.meta.outputs.labels }}


    deploy:
    # 5. Self-hosted 작동 -> infra 인스턴스에서 작동됨
    runs-on: self-hosted
    if: ${{ needs.docker-build.result == 'success' }}
    needs: [ docker-build ]
    steps:

    # 6. infra 인스턴스에서 prod 인스턴스로 접근 (아래부터는 prod 서버 내에서 작업)
    - name: Join EC2 prod server
    uses: appleboy/ssh-action@master
    env:
    JASYPT_KEY: ${{ secrets.JASYPT_KEY }}
    DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
    DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
    with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USERNAME }}
    key: ${{ secrets.SERVER_KEY }}
    port: ${{ secrets.SERVER_PORT }}
    envs: JASYPT_KEY, DATABASE_USERNAME, DATABASE_PASSWORD

    script: |

    # 7. Docker Hub에서 Image를 pull해온다
    sudo docker pull woowacarffeine/backend:latest

    # 8. 만약 8080 포트가 켜져 있으면 새로운 서버의 포트는 8081로 설정
    if sudo docker ps | grep ":8080"; then
    export BEFORE_PORT=8080
    export NEW_PORT=8081
    export NEW_ACTUATOR_PORT=8089

    # 9. 만약 8081 포트가 켜져 있으면 새로운 서버의 포트는 8080로 설정
    else
    export BEFORE_PORT=8081
    export NEW_PORT=8080
    export NEW_ACTUATOR_PORT=8088
    fi

    # 10. Docker로 새로운 서버를 띄운다.
    sudo docker run -d -p $NEW_PORT:8080 -p $NEW_ACTUATOR_PORT:8088 \
    -e "SPRING_PROFILE=prod" \
    -e "ENCRYPT_KEY=${{secrets.JASYPT_KEY}}" \
    -e "DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}}" \
    -e "DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}}" \
    -e "REPLICA_DATABASE_USERNAME=${{secrets.REPLICA_DB_USER_NAME}}" \
    -e "REPLICA_DATABASE_PASSWORD=${{secrets.REPLICA_DB_USER_PASSWORD}}" \
    -e "SLACK_WEBHOOK_URL=${{secrets.SLACK_WEBHOOK_URL}}" \
    --name backend$NEW_PORT \
    woowacarffeine/backend:latest

    # 11. prod 인스턴스에 있는 bluegreen.sh 를 작동한다. (이 때 port 값을 같이 넣어준다.)
    sudo sh /home/ubuntu/bluegreen.sh $BEFORE_PORT $NEW_PORT $NEW_ACTUATOR_PORT



    bluegreen.sh

    (prod 인스턴스 내부에 존재)

    #!/bin/bash

    # 1. Github Actions를 통해 넘겨 받은 환경변수 값
    BEFORE_PORT=$1
    NEW_PORT=$2
    NEW_ACTUATOR_PORT=$3

    echo "기존 포트 : $BEFORE_PORT"
    echo "새로운 포트: $NEW_PORT"
    echo "새로운 ACTUATOR_PORT: $NEW_ACTUATOR_PORT"


    # 2. 20번 동안 헬스 체크를 진행
    count=0
    for count in {0..20}
    do
    echo "서버 상태 확인(${count}/20)";

    # 3. 새로운 서버가 작동되는지 Actuator를 통해 값을 받아옴
    STATUS=$(curl -s http://127.0.0.1:${NEW_ACTUATOR_PORT}/actuator/health-check)

    # 4. Actuator를 통해 성공적으로 서버가 띄워지지 않은 경우
    if [ "${STATUS}" != '{"status":"up"}' ]
    then
    # 5. 10초를 기다린 후 다시 헬스 체크를 진행한다.
    sleep 10
    continue
    else
    # 6. 헬스 체크를 통해 새로운 서버가 성공적으로 작동된다면 멈춘다.
    break
    fi
    done


    # 7. 20번의 헬스 체크를 하는 동안 새로운 서버가 제대로 작동되지 않은 경우 종료
    if [ $count -eq 20 ]
    then
    echo "새로운 서버 배포를 실패했습니다."
    exit 1
    fi


    # 8. 새로운 서버가 성공적으로 작동한 경우
    # Nginx를 통해 포트포워딩을 기존 포트에서 새로운 포트로 변경해준다.
    # 이 부분은 .inc 파일을 통해 Nginx에서 주입 받아서 포트만 변경해도 됩니다!
    export BACKEND_PORT=$NEW_PORT
    envsubst '${BACKEND_PORT}' < backend.template > backend.conf
    sudo mv backend.conf /etc/nginx/conf.d/
    sudo nginx -s reload


    # 9. 기존 서버를 내려주고, 도커 리소스를 정리해준다
    docker stop backend$BEFORE_PORT
    sudo docker container prune -f

    이렇게 카페인 팀에서는 무중단 배포를 도입할 수 있었습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git a/tags/infra/page/2.html b/tags/infra/page/2.html index 1a57916..9178c1a 100644 --- a/tags/infra/page/2.html +++ b/tags/infra/page/2.html @@ -5,7 +5,7 @@ "infra" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -21,7 +21,7 @@ 물론 이는 계획이고 공부하지 않은 다른 내용이 있을 수 있기 때문에 언제든 바뀔 수 있습니다.

    무중단 배포 아키텍처 적용

    이 또한 아직은 먼 이야기지만, 고려해 볼 상황이라서 적어봤습니다.

    사용자가 이용하고 있는 서비스가 갑자기 중단된다면 어떨까요? 저는 화가 많이 날 것 같습니다.

    피치 못할 사정으로 서버가 터져도, 사용자가 서비스를 계속 이용할 방법이 없을까요?

    이런 고민을 해결하기 위해서 나온 개념이 무중단 배포입니다.

    카나리아 배포, Blue/Green 배포, 롤링등 무중단 배포를 위한 여러가지 전략은 이미 존재합니다. 이 부분은 아직은 서버의 명세가 정확하지 않아서 어떤 방식으로 어떻게 처리할 것인지에 대해서는 아직 정할 수는 없습니다.

    이는 명세가 확실하게 정해진 후 팀원과 장단점을 상의하며 결정할 일이기 때문에 현재까지는 "이 정도를 고려하고 있다." 정도만 알면 될 것 같습니다.

    - + \ No newline at end of file diff --git a/tags/ip.html b/tags/ip.html index 66f2833..f2d93a4 100644 --- a/tags/ip.html +++ b/tags/ip.html @@ -5,13 +5,13 @@ "ip" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "ip" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    어떤 문제가 있었나요?

    우아한테크코스에서 private 서브넷에 db 인스턴스를 두고, 보안을 위해 외부에서 접속을 차단하려고 했습니다.

    이 과정에서 총 2가지의 문제점이 있었습니다.

    1. private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었습니다.
    2. public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안되었습니다.

    이 부분을 어떻게 해결했는지 알아보도록 하겠습니다.

    아래의 모든 설명은 AWS 를 기준으로 합니다.

    private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었다.

    해결 방법

    public ip 자동할당을 해주지 않아서, 인터넷에 연결이 안 되었습니다.

    이를 해결하기 위해 public ip 자동할당을 해주었습니다.

    왜 public ip를 할당했더니 문제가 해결되었을까요?

    private 서브넷이란?

    정말 간단하게 설명했을 때

    private 서브넷은 인터넷에 연결되지 않은 서브넷입니다.

    조금 자세하게 들어가 보도록 하겠습니다

    private 서브넷은 인터넷 게이트웨이가 연결되지 않은 서브넷입니다.

    aws 공식문서에서 사진을 통해 보면 아래와 같이 되어있습니다

    private subnet

    public 서브넷에만 인터넷 게이트웨이가 연결되어 있고, private 서브넷에는 인터넷 게이트웨이가 연결되어있지 않습니다.

    private 서브넷에 인터넷 게이트웨이가 연결되어 있지 않다고 했을 때, 기본적으로 인터넷에 접속이 안됩니다.

    mysql을 설치할 때도, 인터넷에 접속을 해야하는데, 인터넷에 접속이 안되니 설치가 안되는 것입니다.

    어? 인터넷 자체가 접근이 안되면 어떻게 설치하나요?

    정말 원시적으로 해결하기 위해서는 public 서브넷에 인스턴스를 하나 더 만들어서, mysql 을 압축해서 scp를 통해 private 서브넷에 있는 인스턴스에 전송하고, 압축을 풀어서 설치하는 방법이 있습니다.

    하지만 이 방법은 너무 원시적이고, 비효율적입니다.

    그래서 인터넷으로 요청을 보낼 수 있도록 만드는 과정이 필요합니다.

    인터넷으로 요청을 보낼 수 있도록 만드는 과정

    인터넷으로 요청을 보낼 수 있도록 만드는 과정은 크게 2가지가 있습니다.

    private 서브넷을 public 서브넷으로 바꾸기

    보안을 위해서 private 서브넷에 두려고 했던 것을 public 서브넷으로 바꾼다는 부분은 매우 위험합니다.

    그래서 이 방법은 보통 사용하지 않습니다.

    NAT 인스턴스(Gateway) 만들기

    NAT 인스턴스는 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들어주는 인스턴스입니다.

    인터넷에 접속을 하기 위해서는 public ip 가 필요합니다.

    따라서 NAT 인스턴스, NAT 게이트웨이는 public 서브넷에 존재해야 합니다.

    어? NAT 인스턴스를 통해서 바로 통신이 가능하면 왜 private 서브넷이 필요한가요? 그냥 다 public 서브넷에 두면 되지 않나요?

    NAT 인스턴스, NAT Gateway는 내부에서 출발한 트래픽만 통과할 수 있도록 설정이 되어있습니다.

    예를 들면 private 서브넷에 인스턴스에 접속해서 직접 mysql download 요청을 했을 때만 허용이 됩니다.

    외부에서 바로 private 인스턴스로 접근할 수는 없습니다.

    NAT 인스턴스만 설정을 하면 바로 연결이 되나요?

    public ip도 자동 할당을 해줘야 합니다

    public ip 가 필요한 이유

    NAT 인스턴스를 통해서 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들었는데, 왜 public ip 가 필요할까요?

    외부 인터넷과 통신을 할 때 public ip 가 필요합니다.

    NAT 인스턴스 혹은 NAT 게이트웨이가 인터넷과 통신할 때, NAT 인스턴스의 public ip + private ip를 통해서 통신을 하지 않습니다.

    내부 인스턴스의 public ip 를 통해서 통신을 하게 되어있습니다.

    따라서 NAT 인스턴스와 내부 인스턴스 모두 public ip 가 필요합니다.

    이 과정을 통해서 1번 문제를 해결할 수 있었습니다.

    이제 2번째 문제를 해결해 보도록 하겠습니다.

    public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안 되는 문제

    public 서브넷에 있는 서버가 private 서브넷에 있는 서버에 접속을 하려고 했는데, 접속이 안 되는 문제가 있었습니다.

    해결 방법

    해결 방법에는 2가지 과정이 있습니다.

    public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해 주기

    기본적으로 public 서브넷에 있는 인스턴스의 보안 그룹에는 private 서브넷에 있는 인스턴스의 보안 그룹이 추가되어있지 않습니다.

    따라서 public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.

    private ip를 통해서 접속하기

    public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속할 때, public ip 를 통해서 접속을 하면 안 됩니다.

    public ip를 통해서 접속하는 과정을 자세하게 알아보겠습니다.

    1. public 서브넷에 있는 인스턴스가 public ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
    2. 라우팅 테이블에서 public ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
    3. 라우터를 통해서 외부 인터넷으로 나가게 됩니다.
    4. 트래픽이 NAT 인스턴스에 도착합니다.
    5. NAT 인스턴스는 내부에서 출발한 트래픽이 아니기 때문에, 트래픽을 거부합니다.

    이 과정이 일어나기에, public ip 를 통해서 접속을 하면 안 됩니다.

    private ip를 통해서 접근하면 어떻게 되는지 알아보겠습니다

    1. public 서브넷에 있는 인스턴스가 private ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
    2. 라우팅 테이블에서 private ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
    3. 라우터를 거쳐서 private 서브넷의 라우터로 이동합니다.
    4. private 서브넷의 라우터는 private 서브넷에 있는 인스턴스에게 트래픽을 전달합니다.
    5. private 서브넷에 있는 인스턴스는 트래픽을 받아서 처리합니다.

    이 과정을 통해서 2번 문제를 해결할 수 있었습니다.

    요약

    1. private 서브넷에 있는 인스턴스가 인터넷에 접속을 하려면 NAT 인스턴스 혹은 NAT 게이트웨이가 필요합니다.
    2. private 서브넷에 있는 인스턴스도 public ip 가 필요합니다.
    3. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 하려면 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.
    4. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 할 때, private ip 를 통해서 접속을 해야 합니다.
    - + \ No newline at end of file diff --git a/tags/issue.html b/tags/issue.html index 54fd534..5757d39 100644 --- a/tags/issue.html +++ b/tags/issue.html @@ -5,14 +5,14 @@ "issue" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "issue" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 3분
    야미

    프로젝트 브랜치명 컨벤션이 feat/이슈번호여서, 브랜치명에서 이슈번호만 가져온 다음 커밋할 때마다 커밋 메시지 아래단(footer)에 이슈 번호를 자동으로 입력해주고 싶었다. 자동으로 입력된다면 깜빡하고 이슈 번호를 안 적는 일도 없고, 시간도 단축할 수 있기 때문이다.

    아래 순서대로 진행한다면 이슈 번호 POSTFIX 자동화를 할 수 있다.

    1) 프로젝트 폴더에 .githooks 폴더 생성

    2) .githooks 폴더에 commit-msg 파일 생성

    #!/bin/bash

    COMMIT_MESSAGE_FILE_PATH=$1
    MESSAGE=$(cat "$COMMIT_MESSAGE_FILE_PATH")

    # 커밋 메시지가 없을 때, 커밋 방지
    if [[ $(head -1 "$COMMIT_MESSAGE_FILE_PATH") == '' ]]; then
    exit 0
    fi

    # 브랜치명에서 이슈 번호만 추출 ('/' 뒤에 있는 문자만 추출)
    POSTFIX=$(git branch | grep '\*' | sed 's/* //' | sed 's/^.*\///' | sed 's/^\([^-]*-[^-]*\).*/\1/')

    COMMIT_SOURCE=$2
    CURRENT_BRANCH=$(git branch --show-current)

    # [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] 👉🏻 현재 브랜치명과 POSTFIX가 똑같으면 POSTFIX 입력 방지
    # [ "$COMMIT_SOURCE" != "merge" ] 👉🏻 merge할 때, POSTFIX 입력 방지
    # [[ "$MESSAGE" != *"[#$POSTFIX]"* ]] 👉🏻 이미 POSTFIX가 존재할 때, POSTFIX 중복 입력 방지
    if [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] && [ "$COMMIT_SOURCE" != "merge" ] && [[ "$MESSAGE" != *"[#$POSTFIX]"* ]]; then
    printf "%s\n\n[#%s]" "$MESSAGE" "$POSTFIX" > "$COMMIT_MESSAGE_FILE_PATH"
    fi

    🧐 이슈 번호 추출에 사용된 명령어 설명

    • grep '*' 👉 * 표시된 브랜치(현재 위치의 브랜치)를 가져온다.
    • sed 's/_ //' 👉 * 제거
    • sed 's/(/)./\1/' 👉 / 이후의 문자만 추출
    • sed 's/^(---)._/\1/' 👉 하나의 이슈에 여러 브랜치를 만들면서 feat/10-1 이런 형태로 브랜치를 만들 경우, 첫 번째 '-' 앞 뒤만 추출 (ex. 10-1)

    3) 프로젝트 폴더에 Makefile 파일 생성

    init:
    git config core.hooksPath .githooks
    chmod +x .githooks/commit-msg
    git update-index --chmod=+x .githooks/commit-msg

    # chmod +x .githooks/commit-msg 👉🏻 macOS, 리눅스에서 스크립트 권한 부여
    # git update-index --chmod=+x .githooks/commit-msg
    # 👉 macOS, 리눅스에서 브랜치가 바뀔 때마다 스크립트 실행시켜줘야 하는 문제 해결

    4) 아래 코드 실행

    새로 git clone을 할 때마다 아래 코드를 실행시켜줘야 한다. 한 번만 실행시키면 계속 적용된다. (window 기준)

    git config core.hooksPath .githooks

    ❗macOS는 git clone 할 때마다 아래 코드를 실행시켜줘야 한다.

    make

    참고 블로그 https://blog.deering.co/commit-convention/

    - + \ No newline at end of file diff --git a/tags/issue/page/2.html b/tags/issue/page/2.html index a07a11c..0889a42 100644 --- a/tags/issue/page/2.html +++ b/tags/issue/page/2.html @@ -5,13 +5,13 @@ "issue" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "issue" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 4분
    누누

    안녕하세요 우테코 카페인팀 누누입니다

    빠르게 결과부터 보고 가시죠.

    어떤 결과가 나왔나요?

    pr의 본문 끝에, 연관된 이슈 번호를 달아주는 기능을 만들었습니다.

    밑에 사진을 보시면 쉽게 이해하실 수 있을 것 같습니다.

    imgimg

    github에서 issue 번호가 pr에 담겨있다면 2가지 장점이 생기는데요.

    1. issue를 클릭했을 때, 자동으로 그 issue로 넘어갈 수 있습니다. (호버만으로 이슈에 대한 간단한 정보를 볼 수 있습니다)
    2. pr 이 merge 되었을 때, 자동으로 issue 가 close 됩니다.

    이 과정을 손으로 진행하는 것보다, 자동으로 진행하게 되면 실수도 줄어들고, 개발 과정이 편해질 것 같아서 이 기능을 제작하게 되었는데요

    중요한 점

    이 과정을 진행하려면 밑에서 소개해드릴 브랜치 네이밍 규칙이 필요합니다.

    브랜치 이름 규칙

    • 브랜치 이름은 타입/이슈번호 으로 구성합니다. ex) feat/1
    • 타입은 feat, fix, docs, refactor, test 등 여러 가지가 있을 수 있습니다.

    이렇게 했을 때, 이슈 번호를 브랜치 명에서부터 가져올 수 있기에, 자동화를 할 수 있습니다.

    이런 규칙이 아닌, feat/action 같은 형태가 된다면 issue 번호를 찾기 어렵겠죠?

    사용 방법

    작성된 코드부터 보시고, 설명을 드리겠습니다.

    아래에 작성된 코드를. github/workflows/assign_issue_number_to_pr_body.yml로 저장하시면 끝입니다.

    name: assign_issue_number_to_pr_body

    on:
    pull_request:
    types: [ opened ]
    branches-ignore:
    - develop

    jobs:
    append_issue_number_to_pr_body:
    runs-on: ubuntu-latest
    steps:
    - name: append feature number to pr body pr branch = feat/(issueNumber)
    uses: actions/github-script@v4
    with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    script: |
    const pr = await github.pulls.get({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number
    });
    const body = pr.data.body;
    const issueNumber= pr.data.head.ref.split('/')[1];
    const newBody = body + "\n\n" + "close #" + issueNumber;
    await github.pulls.update({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number,
    body: newBody
    });

    진행 과정

    1. pr 이 생성되면, pr에 대한 정보를 가져옵니다.
    2. pr의 본문을 가져옵니다.
    3. pr의 브랜치 이름에서 이슈 번호를 가져옵니다.
    4. pr의 본문에 이슈 번호를 추가합니다.
    5. pr의 본문을 업데이트합니다.

    이 과정을 통해서, 직접 pr의 본문을 수정하지 않아도, 자동으로 이슈 번호가 추가되기에, 실수를 줄일 수 있으니, 한 번 시도해 보세요

    - + \ No newline at end of file diff --git a/tags/jasypt.html b/tags/jasypt.html index 87b4719..a01545e 100644 --- a/tags/jasypt.html +++ b/tags/jasypt.html @@ -5,13 +5,13 @@ "jasypt" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "jasypt" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 6분
    키아라

    서론

    안녕하세요 카페인팀 키아라입니다.

    이번 프로젝트를 시작하면서 프로퍼티를 암호화하는 방법으로 jasypt를 알게되어

    사용하는 방법을 익혀 저희 프로젝트에 적용해볼 계획입니다.

    프로퍼티 암호화는 왜 필요할까?

    spring:
    datasource:
    url: 데이터베이스 url
    username: 계정
    password: 비밀번호

    프로젝트를 진행하면서 yml 파일에 DB 연결 URL이나 계정, 비밀번호 같이 노출되어선 안 되는 민감한 정보들이 많습니다.

    git의 public repository와 CI/CD를 연동해 어플리케이션을 배포한다면 중요한 정보가 탈취될 가능성이 있죠.

    Jasypt 라이브러리를 사용하면 평문으로 된 데이터베이스 접속 정보를 암호화 하여 방어막을 한 겹 쌓을 수 있게 됩니다.

    간략하게 라이브러리를 소개하고 사용 방법을 알아볼까요?

    jasypt는 뭐지?

    Jasypt이란 쉽게 암호화 기능을 사용할 수 있도록 제공하는 Java 라이브러리입니다.

    민감한 평문 정보를 암호화하고, 아래처럼 설정 값을 지정하면 어플리케이션이 실행될 때 자동으로 이를 복호화하여 사용합니다.

    사용자가 편하게 암호화 기능을 사용할 수 있도록 제공하는 Java 라이브러리로

    공식 홈페이지는 http://www.jasypt.org/ 에 가면 더 자세한 정보를 확인할 수 있습니다.

    사용 방법

    정말 간단하게 라이브러리 추가, key값 넘겨주기, 암호화 세 가지 단계로 프로퍼티를 암호화하여 관리할 수 있습니다.

    1. 라이브러리 추가 (= 의존성 추가)

    implementation "com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.3"

    2. Jasypt 설정 및 Bean 등록

    key를 사용해서 Bean을 등록하는 기본 설정입니다. 여기서 Bean의 이름을 jasyptEncryptor라고 설정했다면 프로퍼티 등록해야 합니다.

    @Configuration
    public class JasyptConfig {

    private String ENCRYPT_KEY = "hello";

    @Bean(name = "jasyptEncryptor")
    public StringEncryptor stringEncryptor() {
    PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();

    SimpleStringPBEConfig config = new SimpleStringPBEConfig();

    config.setPassword(ENCRYPT_KEY);
    config.setAlgorithm("PBEWithMD5AndDES");
    config.setKeyObtentionIterations(1000);
    config.setPoolSize(1);
    config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
    config.setStringOutputType("base64");
    encryptor.setConfig(config);
    return encryptor;
    }
    }
    jasypt:
    encryptor:
    bean: jasyptEncryptor

    3. 암호화

    라이브러리를 사용할 준비는 거의 다 끝났습니다. 이제 암호화하여 프로퍼티에 작성합니다.

    이때 암호화 하는 방법은, 아래 사이트에 접속해 평문과 키를 입력한 후 나온 암호문을 프로퍼티 파일에 'ENC(암호문)' 로 작성합니다.

    암복호화 사이트

    평문

      datasource:
    url: 데이터베이스 url
    username: 계정
    password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)

    나머지도 마저 암호화해줍시다.

      datasource:
    url: ENC(j94r94hQbd1SfFHGCUeweg+GGDosfnxP8dL0FQxfXtE=)
    username: ENC(vp3Gw8kLpwDZhmMMqf88/Q==)
    password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)

    실행

    올바른 암호문을 입력했다면 정상적으로 실행이 됩니다.

    그러나 이때 임의로 암호문을 수정한다면 다음과 같이 빌드를 실패합니다.

    실행 실패

    그런데 뭔가 이상하지 않나요?

    프로퍼티는 분명 암호화 했는데 키가 코드에 그대로 노출되어 있습니다.

    Git의 public Repository에 배포하면 다른 사람들도 볼 수 있습니다.

    그럼 이 키를 어디에 숨길 수 있을까요?

    저는 처음에 일반 file에 키를 넣어놓고 파일을 읽어오는 식으로 키를 관리하려고 했습니다. 당연히 해당 파일은 .gitignore로 커밋 대상에서 제외해야겠죠.

    그런데 이것보다 더 쉽고 빠른 방법이 있습니다.

    바로 환경변수를 설정하는 것이죠.

    + 환경변수 설정

    private String ENCRYPT_KEY = "hello";

    기존의 키를 관리하는 방식이었습니다.

    우선 이 키를 프로퍼티에서 관리하도록 설정해볼까요?

    // JasyptConfig.class
    @Value("${jasypt.encryptor.password}")
    private String ENCRYPT_KEY;
    // application.yml
    jasypt:
    encryptor:
    password: hello

    이제 환경변수를 설정해봅시다.

    Run > Edit Configurations... 경로로 들어가면

    Run/Debug Configurations 창이 나오는데

    Environment variables: 부분에 ENCRYPT_KEY=hello

    라고 적어주세요.

    그 후 다시 yml 파일로 돌아와 기존 hello로 되어있는 부분을 ${ENCRYPT_KEY}로 변경하고 실행한다면 정상적으로 작동됩니다.

    jasypt:
    encryptor:
    password: ${ENCRYPT_KEY}

    긴 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git a/tags/java-11.html b/tags/java-11.html index 9833944..7637efb 100644 --- a/tags/java-11.html +++ b/tags/java-11.html @@ -5,13 +5,13 @@ "java11" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "java11" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 6분
    누누

    우아한테크코스에서 자바 11을 사용하는 것이 너무 익숙해진 상황이어서, java 11 대신 java 17을 쓰려면 쓰는 대신, 왜 java 17을 쓰면 좋은지에 대해서 설득을 하는 시간이 있어야 하는데요

    처음에는 단순히 record 클래스가 좋아요, collect(Collectors.toList()); 대신 toList() 만으로 해결할 수 있어서 좋아요

    까지밖에 설명할 수 없었습니다.

    이것만으로 동의를 해줘서 일단 java 17 을 사용하기로 했지만, 이번 기회에 조금 더 자세하게 알아보려고 합니다

    Java 17 과 Java 11의 중요한 차이들

    기능적인 부분과, 숨겨진 부분을 나누어볼 수 있을 것 같습니다.

    기능적인 차이점

    언제나 직접 차이를 보면 더 직관적이기 때문에, 직접 코드를 보면서 설명을 해보려고 합니다

    record 클래스

    간단한 dto 클래스를 만들었을 때 코드가 정말 간단해지는 것을 확인할 수 있습니다

    Java 11

    public class Dto {
    private final int data;

    public Dto(int data) {
    this.data = data;
    }

    public int getData() {
    return data;
    }
    }

    lombok 을 사용했을 때


    @Getter
    @AllArgsConstructor
    public class Dto {
    private final int data;
    }

    Java17

    public record Record(int data) {
    }

    이렇게 보면 훨씬 간단해진 것을 볼 수 있습니다

    예상되는 문제점

    objectMapper를 사용하면 어떻게 되나요? noArgsConstructor 가 필요하지 않나요?

    class RecordTest {

    @Test
    void objectMapper_로_변환() throws JsonProcessingException {
    // given
    ObjectMapper objectMapper = new ObjectMapper();
    Record record = new Record(1);

    // when
    String json = objectMapper.writeValueAsString(record);

    // then
    assertEquals("{\"data\":1}", json);
    }

    @Test
    void string_에서_객체로_변환() throws JsonProcessingException {
    // given
    String json = "{\"data\":1}";
    ObjectMapper objectMapper = new ObjectMapper();

    // when
    Record record = objectMapper.readValue(json, Record.class);

    // then
    assertEquals(1, record.data());
    }
    }

    이 테스트에서 볼 수 있는 것처럼 성공적으로 deserialize, serialize 가 가능합니다

    toList() method

    Java 11

    이 부분도 정말 편의성이 높다고 생각하는 부분 중 하나인데요

    Collectors.toList() 대신 toList() 만으로도 사용이 가능합니다

    public class ToListWith11 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .collect(Collectors.toList());
    System.out.println(result);
    }
    }

    Java 17

    public class ToListWith17 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .toList();
    System.out.println(result);
    }
    }

    switch expression

    Java 11

    우테코에서는 switch, case 를 싫어하기에 볼 수는 없겠지만

    switch 문에도 정말 편하게 바뀌었는데요

    public class SwitchWith11 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = 0;
    switch (day) {
    case "Monday":
    result = 1;
    break;
    case "Tuesday":
    result = 2;
    break;
    case "Wednesday":
    result = 3;
    break;
    case "Thursday":
    result = 4;
    break;
    case "Friday":
    result = 5;
    break;
    case "Saturday":
    result = 6;
    break;
    case "Sunday":
    result = 7;
    break;
    }
    System.out.println(result);
    }
    }

    Java 17

    public class SwitchWith17 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = switch (day) {
    case "Monday" -> 1;
    case "Tuesday" -> 2;
    case "Wednesday" -> 3;
    case "Thursday" -> 4;
    case "Friday" -> 5;
    case "Saturday" -> 6;
    case "Sunday" -> 7;
    default -> 0;
    };
    System.out.println(result);
    }
    }

    코드 량이 엄청 줄어든 것을 확인하실 수 있습니다

    instanceof pattern matching

    물론 instanceof 를 사용할 경우가 많은가? 하면 많지는 않겠지만

    아래와 같이 변경되었습니다

    Java 11

    public class InstanceOfWith11 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.toUpperCase());
    }
    }
    }

    Java 17

    public class InstanceOfWith17 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String str) {
    System.out.println(str.toUpperCase());
    }
    }
    }

    number format

    이 기능은 12에 나왔는데요

    언어별로 숫자를 표현하는 방식이 다르지만, 쉽게 표현할 수 있도록 도와주는 기능입니다

    Java 17

    public class NumberFormatterWith11 {
    public static void main(String[] args) {
    int number = 1_000_000;

    String result = NumberFormat.getCompactNumberInstance(Locale.KOREA, NumberFormat.Style.LONG).format(number);

    System.out.println(result.equals("100만"));
    }
    }

    나머지 부분은 사실 그렇게 큰 역할을 할 것 같지는 않아서 생략하겠습니다

    숨겨진 부분들

    gc throughput

    위의 사진은 gc 의 버전별 처리량입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 15% 정도 향상되었고, java 11과는 10% 정도 향상되었습니다.

    gc latency

    위의 사진은 gc의 버전별 지연시간입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 30% 정도 향상되었고, java 11과는 25% 정도 향상되었습니다.

    이와 같이, 단순하게 새로운 기능만 추가되는 것이 아니라 꾸준히 성능도 향상되고 있습니다.

    이런 부분을 고려했을 때, Java 17을 사용하는 것이 좋을 것 같습니다.

    참고

    - + \ No newline at end of file diff --git a/tags/java-17.html b/tags/java-17.html index 04b7a84..c040ea9 100644 --- a/tags/java-17.html +++ b/tags/java-17.html @@ -5,7 +5,7 @@ "java17" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -21,7 +21,7 @@ 물론 이는 계획이고 공부하지 않은 다른 내용이 있을 수 있기 때문에 언제든 바뀔 수 있습니다.

    무중단 배포 아키텍처 적용

    이 또한 아직은 먼 이야기지만, 고려해 볼 상황이라서 적어봤습니다.

    사용자가 이용하고 있는 서비스가 갑자기 중단된다면 어떨까요? 저는 화가 많이 날 것 같습니다.

    피치 못할 사정으로 서버가 터져도, 사용자가 서비스를 계속 이용할 방법이 없을까요?

    이런 고민을 해결하기 위해서 나온 개념이 무중단 배포입니다.

    카나리아 배포, Blue/Green 배포, 롤링등 무중단 배포를 위한 여러가지 전략은 이미 존재합니다. 이 부분은 아직은 서버의 명세가 정확하지 않아서 어떤 방식으로 어떻게 처리할 것인지에 대해서는 아직 정할 수는 없습니다.

    이는 명세가 확실하게 정해진 후 팀원과 장단점을 상의하며 결정할 일이기 때문에 현재까지는 "이 정도를 고려하고 있다." 정도만 알면 될 것 같습니다.

    - + \ No newline at end of file diff --git a/tags/java-17/page/2.html b/tags/java-17/page/2.html index 42e5cca..618d7a7 100644 --- a/tags/java-17/page/2.html +++ b/tags/java-17/page/2.html @@ -5,13 +5,13 @@ "java17" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "java17" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 6분
    누누

    우아한테크코스에서 자바 11을 사용하는 것이 너무 익숙해진 상황이어서, java 11 대신 java 17을 쓰려면 쓰는 대신, 왜 java 17을 쓰면 좋은지에 대해서 설득을 하는 시간이 있어야 하는데요

    처음에는 단순히 record 클래스가 좋아요, collect(Collectors.toList()); 대신 toList() 만으로 해결할 수 있어서 좋아요

    까지밖에 설명할 수 없었습니다.

    이것만으로 동의를 해줘서 일단 java 17 을 사용하기로 했지만, 이번 기회에 조금 더 자세하게 알아보려고 합니다

    Java 17 과 Java 11의 중요한 차이들

    기능적인 부분과, 숨겨진 부분을 나누어볼 수 있을 것 같습니다.

    기능적인 차이점

    언제나 직접 차이를 보면 더 직관적이기 때문에, 직접 코드를 보면서 설명을 해보려고 합니다

    record 클래스

    간단한 dto 클래스를 만들었을 때 코드가 정말 간단해지는 것을 확인할 수 있습니다

    Java 11

    public class Dto {
    private final int data;

    public Dto(int data) {
    this.data = data;
    }

    public int getData() {
    return data;
    }
    }

    lombok 을 사용했을 때


    @Getter
    @AllArgsConstructor
    public class Dto {
    private final int data;
    }

    Java17

    public record Record(int data) {
    }

    이렇게 보면 훨씬 간단해진 것을 볼 수 있습니다

    예상되는 문제점

    objectMapper를 사용하면 어떻게 되나요? noArgsConstructor 가 필요하지 않나요?

    class RecordTest {

    @Test
    void objectMapper_로_변환() throws JsonProcessingException {
    // given
    ObjectMapper objectMapper = new ObjectMapper();
    Record record = new Record(1);

    // when
    String json = objectMapper.writeValueAsString(record);

    // then
    assertEquals("{\"data\":1}", json);
    }

    @Test
    void string_에서_객체로_변환() throws JsonProcessingException {
    // given
    String json = "{\"data\":1}";
    ObjectMapper objectMapper = new ObjectMapper();

    // when
    Record record = objectMapper.readValue(json, Record.class);

    // then
    assertEquals(1, record.data());
    }
    }

    이 테스트에서 볼 수 있는 것처럼 성공적으로 deserialize, serialize 가 가능합니다

    toList() method

    Java 11

    이 부분도 정말 편의성이 높다고 생각하는 부분 중 하나인데요

    Collectors.toList() 대신 toList() 만으로도 사용이 가능합니다

    public class ToListWith11 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .collect(Collectors.toList());
    System.out.println(result);
    }
    }

    Java 17

    public class ToListWith17 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .toList();
    System.out.println(result);
    }
    }

    switch expression

    Java 11

    우테코에서는 switch, case 를 싫어하기에 볼 수는 없겠지만

    switch 문에도 정말 편하게 바뀌었는데요

    public class SwitchWith11 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = 0;
    switch (day) {
    case "Monday":
    result = 1;
    break;
    case "Tuesday":
    result = 2;
    break;
    case "Wednesday":
    result = 3;
    break;
    case "Thursday":
    result = 4;
    break;
    case "Friday":
    result = 5;
    break;
    case "Saturday":
    result = 6;
    break;
    case "Sunday":
    result = 7;
    break;
    }
    System.out.println(result);
    }
    }

    Java 17

    public class SwitchWith17 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = switch (day) {
    case "Monday" -> 1;
    case "Tuesday" -> 2;
    case "Wednesday" -> 3;
    case "Thursday" -> 4;
    case "Friday" -> 5;
    case "Saturday" -> 6;
    case "Sunday" -> 7;
    default -> 0;
    };
    System.out.println(result);
    }
    }

    코드 량이 엄청 줄어든 것을 확인하실 수 있습니다

    instanceof pattern matching

    물론 instanceof 를 사용할 경우가 많은가? 하면 많지는 않겠지만

    아래와 같이 변경되었습니다

    Java 11

    public class InstanceOfWith11 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.toUpperCase());
    }
    }
    }

    Java 17

    public class InstanceOfWith17 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String str) {
    System.out.println(str.toUpperCase());
    }
    }
    }

    number format

    이 기능은 12에 나왔는데요

    언어별로 숫자를 표현하는 방식이 다르지만, 쉽게 표현할 수 있도록 도와주는 기능입니다

    Java 17

    public class NumberFormatterWith11 {
    public static void main(String[] args) {
    int number = 1_000_000;

    String result = NumberFormat.getCompactNumberInstance(Locale.KOREA, NumberFormat.Style.LONG).format(number);

    System.out.println(result.equals("100만"));
    }
    }

    나머지 부분은 사실 그렇게 큰 역할을 할 것 같지는 않아서 생략하겠습니다

    숨겨진 부분들

    gc throughput

    위의 사진은 gc 의 버전별 처리량입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 15% 정도 향상되었고, java 11과는 10% 정도 향상되었습니다.

    gc latency

    위의 사진은 gc의 버전별 지연시간입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 30% 정도 향상되었고, java 11과는 25% 정도 향상되었습니다.

    이와 같이, 단순하게 새로운 기능만 추가되는 것이 아니라 꾸준히 성능도 향상되고 있습니다.

    이런 부분을 고려했을 때, Java 17을 사용하는 것이 좋을 것 같습니다.

    참고

    - + \ No newline at end of file diff --git a/tags/java.html b/tags/java.html index 5a42a4e..8d09c5b 100644 --- a/tags/java.html +++ b/tags/java.html @@ -5,7 +5,7 @@ "java" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -23,7 +23,7 @@ 따라서 Schedule Thread Pool Size를 늘리도록 하겠습니다.

    @Configuration
    public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
    taskScheduler.setPoolSize(10);
    taskScheduler.setThreadNamePrefix("schedule-task-");
    taskScheduler.initialize();
    taskRegistrar.setTaskScheduler(taskScheduler);
    }
    }

    SchedulingConfigurer 를 구현하여 Thread Pool size를 일단 10개로 정의했습니다.

    success 스레드 풀을 늘렸더니 위와 같이 2의 배수의 시간에 정확히 작동이 되는 것을 확인할 수 있습니다.

    하지만 이렇게 여러 작업을 동시에 실행된다면 데이터베이스에 병목현상이 발생되어 오히려 작업이 더 느리게 끝날 수도 있다고 생각했습니다.

    그래서 해당 부분의 실행을 관리하는 클래스를 생성하여 해당 클래스에서 Schedule의 작업을 관리하도록 구현했습니다.

    @Service
    public class BusinessLogic {

    private final ApplicationEventPublisher applicationEventPublisher;

    @Scheduled(cron = "0/2 * * * * *")
    public void complexJobSchedule() {
    applicationEventPublisher.publishEvent(new SchedulingEvent(this::complexJob, "complexJob", LocalDateTime.now()));
    }

    @Scheduled(cron = "0/4 * * * * *")
    public void moreComplexJobSchedule() {
    applicationEventPublisher.publishEvent(new SchedulingEvent(this::moreComplexJob, "moreComplexJob", LocalDateTime.now()));
    }
    }

    로직이 있는 BusinessLogic 서비스에서 스케줄의 시간마다 실행해야할 메서드를 Event로 발행합니다.

    @Component
    public class ScheduleService {

    private final ExecutorService executorService = Executors.newFixedThreadPool(1);
    private final Queue<SchedulingEvent> scheduleTasks = new ConcurrentLinkedQueue<>();
    private final AtomicBoolean isRunning = new AtomicBoolean(false);

    @EventListener
    public void addTask(SchedulingEvent schedulingEvent) {
    scheduleTasks.add(schedulingEvent);
    }

    @Scheduled(cron = "0/1 * * * * *")
    public void polling() {
    if (!scheduleTasks.isEmpty() || isRunning.compareAndSet(false, true)) {
    SchedulingEvent schedulingEvent = scheduleTasks.poll();
    executorService.execute(() -> execute(schedulingEvent));
    }
    }
    }

    그리고 위와 같은 스케줄을 관리하는 서비스에서는 Schedule Event를 받아 실행하도록 만들었습니다. 해당 클래스에서는 ThreadPool을 새로 생성하여, schedule의 스레드에 영향을 받지 않도록 구현했습니다.

    그리고 1초마다 실행되는 스케줄을 만들어 queue에 작업이 있는지, 현재 작업 중인지 확인하여 그렇지 않다면 queue에서 작업을 꺼내 실행하도록 만들었습니다.

    거의 구현이 끝나갑니다. 이제는 해당 Schedule의 데이터를 저장하고, 작업이 실패했을 시에 다시 작업을 하기 위한 기능만 구현하면 될 것 같습니다.

    @Component
    public class ScheduleService {

    ...

    private void execute(SchedulingEvent schedulingEvent) {
    String jobId = schedulingEvent.jobId();
    LocalDateTime executionTime = schedulingEvent.executionTime();

    if (isJobInProgressOrDone(jobId)) {
    log.info("작업이 실행중입니다. {} {}", executionTime, jobId);
    return;
    }
    ScheduleTask entity = new ScheduleTask(jobId, executionTime, JobStatus.RUNNING);
    scheduleTaskJdbcRepository.save(entity);

    try {
    schedulingEvent.runnable().run();
    scheduleTaskJdbcRepository.updateById(entity.getId(), JobStatus.DONE);
    } catch (Exception e) {
    log.error("{} 작업 실행 중 에러가 발생했습니다.", jobId);
    scheduleTaskJdbcRepository.updateById(entity.getId(), JobStatus.ERROR);
    tasks.add(schedulingEvent);
    }
    }

    private boolean isJobInProgressOrDone(String jobId) {
    Optional<ScheduleTask> taskOptional = scheduleTaskRepository.findById(jobId);
    if (taskOptional.isPresent()) {
    ScheduleTask scheduleTask = taskOptional.get();
    return scheduleTask.getStatus() == JobStatus.RUNNING || scheduleTask.getStatus() == JobStatus.DONE;
    }
    return false;
    }
    }

    이 부분은 간단하게 구현할 수 있습니다. 위와 같이 작업의 실행 시간과, job의 이름으로 데이터베이스에서 조회하고, 없다면 작업을 실행하고 있다면 작업이 ERROR 인지 확인하여 작업을 실행해주면 될 것 같습니다.

    complete

    위와 같이 두 개의 서버를 동시에 띄웠을 때에도 스케줄이 잘 작동하는 것을 확인할 수 있습니다.

    결론

    스케줄을 이렇게 구현할 수도 있지만 환경이 된다면 Message Queue를 사용하는 것이 어떨까요?

    혹시 틀린 부분이 있다면 지적 부탁드립니다.

    - + \ No newline at end of file diff --git a/tags/java/page/2.html b/tags/java/page/2.html index 5f72c57..36f4c3b 100644 --- a/tags/java/page/2.html +++ b/tags/java/page/2.html @@ -5,7 +5,7 @@ "java" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -16,7 +16,7 @@ 아래와 같이 간단히 조건을 stream()의 filter()를 사용해서 구현했습니다.

    public class StationCacheRepository {

    private final List<StationInfo> cachedStations;

    public List<StationInfo> findByCoordinate(
    BigDecimal minLatitude,
    BigDecimal maxLatitude,
    BigDecimal minLongitude,
    BigDecimal maxLongitude
    ) {
    return cachedStations.stream()
    .filter(it -> it.latitude().compareTo(minLatitude) >= 0 && it.latitude().compareTo(maxLatitude) <= 0)
    .filter(it -> it.longitude().compareTo(minLongitude) >= 0 && it.longitude().compareTo(maxLongitude) <= 0)
    .toList();
    }
    }

    하지만 해당 방법으로 로컬에서 조회를 테스트 했을 때 캐시를 적용한 것보다 더 느려진 결과가 나왔습니다. 캐싱을 해서 데이터베이스까지 요청을 보내지 않는데 왜 더 느려진 것일까요?

    답은 인덱스 였습니다. Mysql 에서 인덱스는 B Tree로 구성되어 있습니다. 데이터베이스에서는 위도, 경도로 복합 인덱스가 설정되어 있었지만, 현재 어플리케이션 로직에는 해당 부분이 없습니다.

    그래서 filter로 순회하는 시간복잡도가 O(n)이고, 데이터베이스에서는 O(log n)이기 때문에 더 느려진 것입니다. 그렇다고 제가 직접 B tree 자료구조를 직접 구현해야할까요?

    현재 해당 조회 API는 위도 경도로 범위 탐색을 하고 있습니다. 결국엔 station의 정보들이 위도, 경도로 정렬만 되어 있다면 B tree를 직접 구현하지 않더라도 같은 시간복잡도 O(log n)으로 탐색할 수 있습니다. 물론 B tree와 다른 부분은 해당 충전소의 정확한 위도, 경도로 단일 칼럼을 조회할 때는 O(n)이기 때문에 이런 방법이 문제가 될 수 있지만, 해당 캐시 데이터로는 무조건 범위 탐색을 하기 때문에, B tree를 구현하지 않고 이분 탐색으로 조회하는 방식으로 변경해보겠습니다.

        public void initialize(List<StationInfo> stations) {
    cachedStations.addAll(stations);
    cachedStations.sort((o1, o2) -> {
    int latitudeCompare = o1.latitude().compareTo(o2.latitude());
    if (latitudeCompare == 0) {
    return o1.longitude().compareTo(o2.longitude());
    }
    return latitudeCompare;
    });
    }

    private List<StationInfo> findStations(BigDecimal minLatitude, BigDecimal maxLatitude, BigDecimal minLongitude, BigDecimal maxLongitude) {
    int lowerBound = binarySearch(minLatitude, START_INDEX);
    int upperBound = binarySearch(maxLatitude, lowerBound);
    if (lowerBound == -1 || upperBound == -1) {
    return Collections.emptyList();
    }
    return cachedStations.stream()
    .skip(lowerBound)
    .limit(upperBound - lowerBound)
    .filter(station -> station.longitude().compareTo(minLongitude) >= 0 && station.longitude().compareTo(maxLongitude) <= 0)
    .toList();
    }

    private int binarySearch(BigDecimal latitude, int startIndex) {
    int left = startIndex;
    int right = cachedStations.size() - 1;
    int result = -1;
    while (left <= right) {
    int middle = left + (right - left) / 2;
    StationInfo middleStation = cachedStations.get(middle);
    if (middleStation.latitude().compareTo(latitude) >= 0) {
    result = middle;
    right = middle - 1;
    } else {
    left = middle + 1;
    }
    }
    return result;
    }

    먼저 어플리케이션이 실행될 때 cache 데이터를 찾아 저장하는 것 뿐만 아니라, 위도(Latitude)를 기준으로 정렬하도록 만들었습니다. 그리고 위도의 최소, 최대값의 인덱스를 가장 효율적으로 찾아올 수 있도록 binary search를 하는 메서드를 만들었습니다. 이렇게 한다면 O(log n) 으로 위도의 최대 최소 조건에 포함되는 모든 station의 값을 조회할 수 있습니다. 그리고 조회한 데이터들의 개수만큼 filter를 통해 경도(longitude) 가 포함되는지 확인합니다. 해당 방식의 구현은 B tree가 작동하는 방식과 유사할 것입니다.

    이분 탐색을 적용한 결과 로컬에서 응답 속도가 120 ms -> 50 ~ 70 ms로 약 2배 빨라진 것을 확인할 수 있습니다.

    실시간이 중요한 데이터는?

    앞서 말씀드렸다시피 지도로 충전소를 조회할 때, 충전소의 정보들에는 바뀌지 않는 정보뿐만 아니라, 최신화해야하는 충전기의 현재 상태 정보가 있습니다. 이러한 정보들은 캐싱해둘 수 없습니다. 하더라도, 관리 포인트가 늘어나기 때문에 데이터베이스에서 캐싱해둔 충전기 id로 충전기의 상태를 찾아와서 정보를 합쳐 반환하는 식으로 만들 수 있습니다.

        select cs.station_id,
    sum(case
    when cs.charger_condition = 'STANDBY' then 1
    else 0
    end)
    from charger_status cs
    where cs.station_id in (?, ?, ?, ?, ?, ?, ?)
    group by cs.station_id

    위와 같은 쿼리로 해당 충전소의 최신화된 충전기 상태를 가져올 수 있습니다.

    캐싱을 하기전에 데이터베이스를 이용해 데이터를 가져올 때의 쿼리는 아래와 같습니다.

     select
    distinct s.station_id
    from
    charge_station s
    inner join
    charger c
    on (
    c.station_id=s.station_id
    )
    where
    s.latitude>=?
    and s.latitude<=?
    and s.longitude>=?
    and s.longitude<=?
    -------------------------------------------------
    select
    s.station_id,
    s.station_name,
    s.latitude,
    s.longitude,
    s.is_parking_free,
    s.is_private,
    sum(case
    when cs.charger_condition='STANDBY' then 1
    else 0
    end),
    sum(case
    when c.capacity>=50 then 1
    else 0
    end)
    from
    charge_station s
    inner join
    charger c
    on (
    c.station_id=s.station_id
    )
    inner join
    charger_status cs
    on (
    c.station_id=cs.station_id
    and c.charger_id=cs.charger_id
    )
    where
    s.station_id in (
    ?,?,?,?
    )
    group by
    s.station_id

    원래는 위와 같이 여러번의 Join을 하고, 2번의 쿼리가 나갔던 반면 지금은 join을 하지않는 한번의 깔끔한 쿼리로 개선되었습니다.

    그리고 station 테이블의 위도, 경도로 범위 탐색을 위해 생성했던 index도 제거할 수 있게 되었습니다!

    결론

    1. 캐싱할 수 있는 부분은 하는 것도 좋을 것 같습니다
    2. 시간 복잡도를 계산해봅시다.
    3. 성능 개선 재밌습니다.
    - + \ No newline at end of file diff --git a/tags/java/page/3.html b/tags/java/page/3.html index d446ef2..2e9f32c 100644 --- a/tags/java/page/3.html +++ b/tags/java/page/3.html @@ -5,7 +5,7 @@ "java" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -31,7 +31,7 @@ 하지만 직접 확인해보기 전까지는 확신할 수 없으니 간단히 Runtime 클래스에서 제공해주는 totalMemory(), freeMemory() 메서드를 통해 알아보겠습니다.

        @Test
    void 페이징을_사용한_조회() {
    List<Station> stations = stationRepository.findAllByOrder(Pageable.ofSize(1000));

    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();
    System.out.println("paging 사용 중인 메모리: " + ((total - free) / 1024 / 1024) + "MB");
    }

    @Test
    void 페이징을_사용하지_않고_조회() {
    List<Station> stations = stationRepository.findAllFetch();

    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();

    System.out.println("findAll() 사용 중인 메모리: " + ((total - free) / 1024 / 1024) + "MB");
    }

    findAll paging 확연히 차이가 나는 것을 확인할 수 있습니다.

    물론 테스트코드에서는 23만건의 API 요청은 같은 조건이니 배제하고 확인했습니다.

    이로써 하나의 문제가 또 해결된 것 같습니다.

    아직 배우는 단계라 혹시 틀린 점이 있다면 지적 부탁드리겠습니다.

    Reference

    - + \ No newline at end of file diff --git a/tags/jpa.html b/tags/jpa.html index c1fe74d..35f881d 100644 --- a/tags/jpa.html +++ b/tags/jpa.html @@ -5,7 +5,7 @@ "jpa" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -22,7 +22,7 @@ 아주 간단하게 entity의 isNew()를 호출한다고 적혀있습니다. 하지만 Persistable 인터페이스를 구현한 Entity의 isNew() 를 호출하는 것 입니다.

    그럼 남은 하나의 클래스를 확인하겠습니다.

    info-support

    위 사진처럼 이 클래스가 Entity 마다 Persistable 구현 유무에 따라 동적으로 구현체를 변경해주고 있었습니다.

    그럼 답이 나온 것 같습니다. ID를 직접 할당하는 Entity에 Persistable을 구현해주면 됩니다.

    Persistable 구현하기

    @Entity
    public class ChargeStation implements Pesistable{

    @Id
    private String stationId;

    private String stationName;

    @CreatedDate
    private LocalDateTime createdTime;

    ...

    @Override
    public Object getId() {
    return getStationId();
    }

    @Override
    public boolean isNew() {
    return createdTime == null;
    }
    }

    간단히 만들어봤습니다. @CreatedDate는 Entity가 처음 영속화될 때 동작하기 때문에 이 Entity의 CreateTime 필드가 null 이면 새로운 Entity라고 확신할 수 있습니다. 그럼 이렇게 인터페이스를 구현하고 아까 실행했던 테스트를 다시 실행해보겠습니다.

    solved

    깔끔하게 구현된 것을 확인할 수 있었습니다. 원하던대로 쿼리가 2번 발생합니다. 이런 Persistable@MappedSuperClass를 통해 더 깔끔하게 구현할 수 있습니다. 하지만 따로 설명드리지는 않겠습니다.

    결론

    JPA는 많은 편의 기능을 제공해주는 것 같아보입니다. 쫄지맙시다.

    - + \ No newline at end of file diff --git a/tags/jpa/page/2.html b/tags/jpa/page/2.html index 3de7ed1..6c638b4 100644 --- a/tags/jpa/page/2.html +++ b/tags/jpa/page/2.html @@ -5,13 +5,13 @@ "jpa" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "jpa" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    누누
    박스터

    안녕하세요 카페인팀 누누입니다

    이번에는 대량의 데이터를 DB에 넣는 과정을 최적화하는 과정에서 알게 된 내용을 공유하려고 합니다

    이번 최적화의 목표

    전기차 충전소에 대한 공공 데이터를 가져오고, 그 데이터를 DB 에 넣는 과정을 최적화해보자

    대량의 데이터를 삽입하는 과정

    저희 팀의 요구사항을 간단하게 정리하면 다음과 같습니다

    1. 대량의 데이터를 공공 데이터에서 전기차 충전소와 전기차 충전기에 대한 데이터를 가져온다
      • 충전소는 6만 개, 충전기는 23만 개의 데이터가 존재한다.
      • 한 번에 가져올 수 있는 양은 9999개 까지다.
    2. 이 데이터를 DB에 넣는다
      • 충전소와 충전기는 1:N 관계이다

    최적화 전은 어떤 상황이었는데?

    before_optimize

    위 사진을 잘 보시면 아실 수 있으시겠지만, 2000개를 저장하는데, 231.762 초가 사용되었습니다.

    물론 출력을 위한 시간도 포함되었기에, 230초 정도라고 생각하셔도 좋습니다

    1만 개라면? 231.762초 * 5 = 1,158.81초

    23만 개라면? 1158.81 * 23 = 26,652.63초

    시간으로 바꿔보면 7.4 시간이 걸린다는 것을 볼 수 있습니다

    이 과정에서 볼 수 있는 문제점

    1. 데이터를 저장할 때마다, 새로운 Transaction 이 생성된다.

    어떻게 개선할 수 있을까?

    데이터를 저장할 때마다, 새로운 Transaction 이 생성되는 것을 방지하기 위해, 전체를 하나의 트랜잭션으로 묶는다

    전체를 한 트랜잭션으로 묶은 버전

    all_in_transaction

    이 과정에서 2000개를 저장하는데 65초 가 사용되었습니다.

    1만 개라면? 65초 * 5 = 325초

    23만 개라면? 325초 * 23 = 7,475초

    시간으로 바꿔보면 2시간이 걸린다는 것을 볼 수 있습니다

    전체적으로 3배 정도 빨라졌습니다

    이 과정에서 볼 수 있는 문제점

    1. 23만 개의 저장이 모두 한 트랜잭션이 되어서, 하나가 실패하면 23만개를 새로 저장해야 하는 상황에 처한다

    어떻게 개선할 수 있을까?

    23만개의 저장이 모두 한 트랜잭션이 되는 것을 방지하기 위해, 1만 개씩 영속화시킨다

    1만 개가 한 트랜잭션으로 묶인 버전

    separateTransaction

    성능상으로 개선한 부분은 그렇게 크지 않지만, 실패했을 때, 1만 개만 다시 저장하면 되기에, 훨씬 빠르게 복구가 가능합니다.

    여기서 PageNo라는 클래스는, i를 바로 참조했을 경우, effectively final을 보장할 수 없어서 만들었습니다.

    성능은 전체를 한 트랜잭션으로 묶은 버전과 큰 차이가 나지 않습니다.

    이 과정에서 볼 수 있는 문제점

    1. id 생성 전략이 GenerationType.IDENTITY 이기에, 데이터를 저장할 때마다, DB에서 id를 생성해야 한다.

    JPA에 있는 쓰기 지연을 전혀 활용할 수 없고, DB에서 id를 생성하기 위해, DB와 매번 통신을 해야 한다.

    어떻게 개선할 수 있을까?

    id를 미리 생성해서, DB 에서 id 를 생성하는 과정을 생략한다

    ID 생성 전략을 GenerationType.Table의 형태로 바꿔서, DB에서 id를 생성하는 과정을 줄여서, 성능을 개선한다

    1만 개가 한 트랜잭션으로 묶이고, id를 미리 생성한 버전

    이때 batch size를 1000 단위로 설정해서 1000개씩 id 가 늘어나도록 설정했다

    charger_generatorstation_generator

    spring.jdbc.template.fetch-size=10000

    10000batch_size

    1자리 숫자는 앞에서부터 n(만개)를 의미하고, 2번째 숫자는 1만 개를 저장하는 데 걸린 시간(ms)을 의미합니다.

    처음 1만 개는 142초가 걸리고, 2만 개는 285초가 걸렸습니다.

    23만 개라면? 142 * 26 = 3,266초

    처음과 비교하자면 7.4시간이 걸리는 것에서 54분 정도 걸리는 것으로 개선되었습니다.

    이 과정에서 볼 수 있는 문제점

    하나의 스레드에서만 동작하기에, 성능이 개선되었지만, 여전히 느립니다.

    하나의 스레드에서만 동작하기에, 하나의 커넥션을 사용하게 됩니다.

    어떻게 개선할 수 있을까?

    여러 스레드에서 동작하게 하고, 여러 커넥션을 사용하게 합니다.

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전

    multi_thread

    이 버전에서 89991 개를 저장하는데 총 157초가 걸렸습니다.

    23만 개라면? 157 * 3 = 471초

    시간으로 바꿔보면 5분도 채 걸리지 않는 시간이죠

    이 과정에서 볼 수 있는 문제점

    hikari connection pool 사이즈를 10으로 설정했는데, 10개의 커넥션을 사용하면서 저장을 하다 보니, 10개의 커넥션을 모두 사용하고 나서, 11번째부터는 커넥션을 가져오기 위해, 기다려야 하는 상황이 발생합니다.

    어떻게 개선할 수 있을까?

    hikari connection pool 사이즈를 25로 설정해서, 25개의 커넥션을 사용하도록 합니다.

    spring.datasource.hikari.maximum-pool-size=25

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전 2

    multi_thread2

    총 13만 개의 데이터를 저장하는데, 147초가 걸리고, db 인스턴스의 cpu 사용률이 100%에 가까워져서 ec2 가 다운되었습니다.

    이 과정에서 볼 수 있는 문제점

    db의 cpu 사용량을 고려하지 않고, 23만 개가 조금 넘는 데이터를 25개의 커넥션을 활용해 저장하려고 했습니다

    결론

    1. 데이터를 저장할 때마다, transaction을 사용하지 말자
    2. 데이터를 저장할 때마다, id를 생성하지 말자
    3. 여러 스레드에서 동작하고, 여러 커넥션을 사용하자
    4. db의 cpu 사용량을 고려하자

    긴 글 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/tags/login.html b/tags/login.html index 6812dd7..bcc1ea6 100644 --- a/tags/login.html +++ b/tags/login.html @@ -5,7 +5,7 @@ "login" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -24,7 +24,7 @@ Map<String, Object>로 지정해준 이유는, 플랫폼마다 반환되는 JSON 타입이 다르기 때문에 그런 부분에 대해 중복을 제거하기 위해 이러한 형태로 만들었습니다.

    그리고 아까 yml에 작성했던 정보들을 가져와야합니다. @Value 어노테이션으로도 가져올 수 있습니다.

            @Value("oauth.provider.google.id")
    private String id;
    @Value("oauth.provider.google.secret")
    private String secret;

    ...

    하지만 이렇게 계속 binding을 해줘야한다는 점이 아주 귀찮고 보기도 안좋습니다.

    build.gradle
    annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"

    하지만 위의 의존성을 추가해준다면 아주 편하게 property를 가져올 수 있습니다.

    OAuthProviderProperties.java
    @Component
    @ConfigurationProperties(prefix = "oauth2")
    public class OAuthProviderProperties {
    // prefix oauth2 기준으로 알아서 google이 이름인 Provider Enum을 찾아서 Key로 바인딩
    private final Map<Provider, OAuthProviderProperty> provider = new EnumMap<>(Provider.class);

    public OAuthProviderProperty getProviderProperties(Provider provider) {
    return this.provider.get(provider);
    }

    @Getter
    @Setter
    public static class OAuthProviderProperty {
    // 그리고 provider 하위 정보들은 아래의 필드에 바인딩
    private String id;
    private String secret;
    private String redirectUrl;
    private String tokenUrl;
    private String infoUrl;
    }
    }

    이렇게 되면 구조적인 준비는 끝났습니다.

    이제는 해당 플랫폼에 정보를 요청하는 작업만 하면 됩니다. 그럼 아까 말씀드렸던 순서로 요청을 해보겠습니다.

    RestTemplateOAuthRequester.java
    public class RestTemplateOAuthRequester implements OAuthRequester {

    @Override
    public OAuthMember login(OAuthLoginRequest request) {
    // frontend에서 받아온 로그인 platform
    Provider provider = Provider.from(request.provider());
    // 해당 Platform에 맞는 정보 찾음
    OAuthProviderProperty property = oAuthProviderProperties.getProviderProperties(provider);
    // frontend에서 받아온 code와 등록해놓은 property로 Access Token 요청
    OAuthTokenResponse token = requestAccessToken(property, requet.getCode());
    // 받아온 Token으로 해당 Resource Owner의 정보 요청
    Map<String, Object> userAttributes = getUserAttributes(property, token);
    return provider.getOAuthProvider(userAttributes);
    }

    private OAuthTokenResponse requestAccessToken(OAuthProviderProperty property, String code) {
    HttpHeaders headers = new HttpHeaders();
    headers.setBasicAuth(property.getId(), property.getSecret());
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers);
    URI tokenUri = getTokenUri(property, code);
    return restTemplate.postForEntity(tokenUri, request, OAuthTokenResponse.class).getBody();
    }

    private URI getTokenUri(OAuthProviderProperty property, String code) {
    return UriComponentsBuilder.fromUriString(property.getTokenUrl())
    .queryParam(CODE, URLDecoder.decode(code, StandardCharsets.UTF_8))
    .queryParam(GRANT_TYPE, AUTHORIZATION_CODE)
    .queryParam(REDIRECT_URI, property.getRedirectUrl())
    .build()
    .toUri();
    }

    private Map<String, Object> getUserAttributes(OAuthProviderProperty property, OAuthTokenResponse tokenResponse) {
    HttpHeaders headers = new HttpHeaders();
    headers.setBearerAuth(tokenResponse.accessToken());
    headers.setContentType(MediaType.APPLICATION_JSON);
    URI uri = URI.create(property.getInfoUrl());
    RequestEntity<?> requestEntity = new RequestEntity<>(headers, HttpMethod.GET, uri);
    ResponseEntity<Map<String, Object>> responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<>() {
    });
    return responseEntity.getBody();
    }
    }

    이렇게만 한다면 그 어려워 보이던 OAuth 인증도 간단하게 해결할 수 있습니다. (물론 제 코드가 정답이 아닙니다)

    Reference

    https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-16

    https://developers.google.com/identity/protocols/oauth2?hl=ko

    - + \ No newline at end of file diff --git a/tags/message.html b/tags/message.html index 5de8509..4a6ec3c 100644 --- a/tags/message.html +++ b/tags/message.html @@ -5,14 +5,14 @@ "message" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "message" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 3분
    야미

    프로젝트 브랜치명 컨벤션이 feat/이슈번호여서, 브랜치명에서 이슈번호만 가져온 다음 커밋할 때마다 커밋 메시지 아래단(footer)에 이슈 번호를 자동으로 입력해주고 싶었다. 자동으로 입력된다면 깜빡하고 이슈 번호를 안 적는 일도 없고, 시간도 단축할 수 있기 때문이다.

    아래 순서대로 진행한다면 이슈 번호 POSTFIX 자동화를 할 수 있다.

    1) 프로젝트 폴더에 .githooks 폴더 생성

    2) .githooks 폴더에 commit-msg 파일 생성

    #!/bin/bash

    COMMIT_MESSAGE_FILE_PATH=$1
    MESSAGE=$(cat "$COMMIT_MESSAGE_FILE_PATH")

    # 커밋 메시지가 없을 때, 커밋 방지
    if [[ $(head -1 "$COMMIT_MESSAGE_FILE_PATH") == '' ]]; then
    exit 0
    fi

    # 브랜치명에서 이슈 번호만 추출 ('/' 뒤에 있는 문자만 추출)
    POSTFIX=$(git branch | grep '\*' | sed 's/* //' | sed 's/^.*\///' | sed 's/^\([^-]*-[^-]*\).*/\1/')

    COMMIT_SOURCE=$2
    CURRENT_BRANCH=$(git branch --show-current)

    # [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] 👉🏻 현재 브랜치명과 POSTFIX가 똑같으면 POSTFIX 입력 방지
    # [ "$COMMIT_SOURCE" != "merge" ] 👉🏻 merge할 때, POSTFIX 입력 방지
    # [[ "$MESSAGE" != *"[#$POSTFIX]"* ]] 👉🏻 이미 POSTFIX가 존재할 때, POSTFIX 중복 입력 방지
    if [[ "$CURRENT_BRANCH" != "$POSTFIX" ]] && [ "$COMMIT_SOURCE" != "merge" ] && [[ "$MESSAGE" != *"[#$POSTFIX]"* ]]; then
    printf "%s\n\n[#%s]" "$MESSAGE" "$POSTFIX" > "$COMMIT_MESSAGE_FILE_PATH"
    fi

    🧐 이슈 번호 추출에 사용된 명령어 설명

    • grep '*' 👉 * 표시된 브랜치(현재 위치의 브랜치)를 가져온다.
    • sed 's/_ //' 👉 * 제거
    • sed 's/(/)./\1/' 👉 / 이후의 문자만 추출
    • sed 's/^(---)._/\1/' 👉 하나의 이슈에 여러 브랜치를 만들면서 feat/10-1 이런 형태로 브랜치를 만들 경우, 첫 번째 '-' 앞 뒤만 추출 (ex. 10-1)

    3) 프로젝트 폴더에 Makefile 파일 생성

    init:
    git config core.hooksPath .githooks
    chmod +x .githooks/commit-msg
    git update-index --chmod=+x .githooks/commit-msg

    # chmod +x .githooks/commit-msg 👉🏻 macOS, 리눅스에서 스크립트 권한 부여
    # git update-index --chmod=+x .githooks/commit-msg
    # 👉 macOS, 리눅스에서 브랜치가 바뀔 때마다 스크립트 실행시켜줘야 하는 문제 해결

    4) 아래 코드 실행

    새로 git clone을 할 때마다 아래 코드를 실행시켜줘야 한다. 한 번만 실행시키면 계속 적용된다. (window 기준)

    git config core.hooksPath .githooks

    ❗macOS는 git clone 할 때마다 아래 코드를 실행시켜줘야 한다.

    make

    참고 블로그 https://blog.deering.co/commit-convention/

    - + \ No newline at end of file diff --git a/tags/msw.html b/tags/msw.html index a5c6b98..12675dc 100644 --- a/tags/msw.html +++ b/tags/msw.html @@ -5,13 +5,13 @@ "msw" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "msw" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 5분
    센트

    웹팩에서 msw 설정

    이번 팀 프로젝트는 CRA와 같은 보일러 플레이트 코드를 사용하지 못하게 제한이 있다. 또한 요즘 많이 사용된다는 Vite의 사용도 제한이 있고, 웹팩으로 프로젝트를 시작하도록 강제하고 있다.

    팀원 모두 한 번도 웹팩을 통해 프로젝트를 시작해본 경험이 없어 프론트엔드 팀원 각자 개인 레포에서 웹팩 공부를 진행한 후 어느정도 진척이 있을 때 팀 레포에 프로젝트를 시작하기로 했다.

    다행히 웹팩으로 시작하는 프로젝트에 대한 많은 참고 자료들이 있어 첫 리액트 프로젝트 화면을 띄우는데 까지는 그리 오랜 시간이 걸리지 않았다. 그렇게 모든 팀원이 첫 웹팩 프로젝트를 성공시킨 후 모여 팀 프로젝트 초기 설정을 시작해보았다.

    eslint, prettier, 웹팩 등등 여러 설정들을 하고 필요한 패키지를 설치하는데 문제가 발생했다. 큰 데이터를 다루는 백엔드의 개발 속도를 고려해 프론트엔드 개발을 진행하기 위해서 미션중에 배웠던 MSW 라이브러리를 사용하기로 결정했는데, 이 라이브러리가 우리 팀의 개발 환경에서 동작하지 않았다.

    왜 동작하지 않는지 원인을 찾아보니 MSW service worker 파일을 찾을 수 없다는 오류 메세지가 나오는 것을 확인할 수 있었다. 원인을 더 자세히 알아보니 public 폴더에 있는 파일들은 웹팩이 번들링을 진행할 때 포함이 되지 않는다는 것을 알 수 있었고, 이를 어떻게 해결할 지 팀원들과 방법을 찾아보았다.

    약 한시간쯤 지났을 무렵 copy-webpack-plugin 패키지를 통해 public 경로에 있는 파일들도 빌드 폴더에 포함시킬 수 있다는 것을 알게 되었다. 하지만 이 copy-webpack-plugin에 대한 사용법이 미숙해 public 폴더에 있는 mockServiceWorker.js 파일만 빌드 폴더로 옮겼어야 했는데 index.html과 같은 다른 파일들 까지 한꺼번에 빌드 폴더로 옮겨지게 되었다.

    이런 저런 방법들을 시도해보다 webpack.config.js 파일의 plugins에 아래와 같은 설정을 추가 해주어 MSW를 프로젝트에 적용할 수 있게 되었다.

    new CopyWebpackPlugin({
    patterns: [
    { from: 'public/mockServiceWorker.js', to: '.' }, // msw service worker
    ],
    }),

    설정을 간단히 보면 public 경로에 있는 mockServiceWorker.js 파일을 빌드 후 폴더의 루트 디렉토리에 추가해준다는 설정이다.

    문제 상황과 해결 방법을 간단하게 다시 정리해보면 다음과 같다.

    1. MSW를 적용해보려고 함.
    2. 웹팩에서 개발 서버를 열었을 때 MSW 실행을 위해 필요한 mockServiceWorker.js 파일을 찾을 수 없다는 오류가 발생함.
    3. 문제의 원인은 웹팩에서 번들링을 진행할 때 public 폴더 하위 경로에 있는 파일들을 무시하기 때문이었음.
    4. 문제를 해결하기 위해 public 경로에 있는 mockServiceWorker.js 파일을 번들링 후 폴더의 루트 디렉토리에 저장하도록 하는 설정을 추가해줌.
    - + \ No newline at end of file diff --git a/tags/mysql.html b/tags/mysql.html index b22d0c9..ebb7bd1 100644 --- a/tags/mysql.html +++ b/tags/mysql.html @@ -5,7 +5,7 @@ "mysql" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -30,7 +30,7 @@ 위와 같은 조회 쿼리가 나왔으므로 인덱스를 아래와 같이 station_id, day_of_week에 걸어주었습니다.

    img 위 실행 속도에서 execution time을 확인해보면 인덱스를 걸고 134ms -> 5ms로 성능이 많이 개선 되었음을 확인할 수 있습니다.

    img 실행 계획도 의도한대로 잘 나오는 것을 보실 수 있습니다.


    정리

    1. DB Partitioning - (day_of_week : 요일)을 기준으로 파티셔닝
    2. 조회 쿼리에 맞게 인덱스 설정
    3. API 수정 (모든 요일의 혼잡도 조회 -> 해당 요일의 혼잡도 조회)

    결과적으로 기존 혼잡도 조회시 511ms가 나왔으나, 요일 별 조회 및 파티셔닝 & 인덱스를 적용하고 execution time = 5ms로 개선

    - + \ No newline at end of file diff --git a/tags/mysql/page/2.html b/tags/mysql/page/2.html index 952f3db..e3abcce 100644 --- a/tags/mysql/page/2.html +++ b/tags/mysql/page/2.html @@ -5,7 +5,7 @@ "mysql" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -22,7 +22,7 @@ 소스 서버에서 커밋된 트랜잭션은 바이너리 로그에 기록되고, 레플리카 서버에서는 주기적으로 새로운 트랜잭션에 대한 바이너리 로그를 요청합니다. 이러한 방식은 소스 서버는 레플리카 서버가 제대로 변경 되었는지 알 수 없습니다. 즉 데이터 정합성에 문제가 생긴다는 단점이 있습니다. 하지만 이러한 방식은 소스 서버가 각 트랜잭션에 대해 레플리카 서버로 전송되는 부분을 고려하지 않는다는 점이 속도 측면에서 빠르고, 또 여러 대의 레플리카 서버를 구성하더라도 큰 성능 저하가 없다는 점이서 장점이 있습니다.

    반동기 복제

    반동기 복제는 비동기 복제보다 좀 더 데이터 정합성이 올라갑니다. 소스 서버는 변경된 트랜잭션이 있을 때 레플리카 서버가 다 전송이 되었다는 ACK 신호를 받기 때문에 확실히 알 수 있습니다. 하지만 전송여부만 확인하기 때문에 트랜잭션이 반영이 되었다는 보장은 없습니다. 반동기 복제 방식은 2가지가 있습니다.

    1. After sync: After Sync 방식은 소스 서버에서 트랜잭션을 바이너리 로그에 기록 후 Storage Engine에 바로 커밋하지 않습니다. 먼저 바이너리 로그에 기록 후 레플리카 서버의 ACK 응답을 기다립니다. 그리고 ACK 응답이 도착하면 그제서야 스토리지 엔진을 커밋하여 트랜잭션을 처리하고 결과를 반환합니다.
    2. After commit: After commit은 이름 그대로 커밋을 먼저 하는 것입니다. 트랜잭션이 생기면 먼저 바이너리 로그에 기록 후 소스 서버 스토리지 엔진에 커밋합니다. 그리고 레플리카 서버의 ACK 응답이 내려오면 클라이언트는 처리 결과를 얻고 다음 쿼리를 수행할 수 있습니다.

    먼저 after commit 방식은 소스 서버에 장애가 발생했을 때 팬텀 리드가 발생하게 됩니다. 트랜잭션이 스토리지 엔진 커밋까지된 후 레플리카 서버의 응답을 기다립니다. 이처럼 스토리지 엔진 커밋까지 완료된 데이터는 다른 세션에서도 조회가 가능합니다. 트랜잭션이 커밋되었고, 레플리카 서버로 아직 응답을 기다릴 때, 소스 서버에 장애가 발생한다면 새로운 소스 서버로 승격된 레플리카 서버에서 데이터를 조회할 때 자신이 이전 소스 서버에서 조회했던 데이터를 보지 못할 수도 있습니다.

    그리고 이처럼 레플리카 서버가 승격된 상황에 소스 서버의 장애가 복구되어 재사용할 경우 이미 커밋된 그 트랜잭션을 수동으로 롤백 시켜야만 데이터가 맞는 상황이 생깁니다.

    저희 팀의 복제 동기화 방식

    이러한 장단점으로 저희 팀은 데이터 무결성이 중요하다 판단되어 반동기 복제 방식을 사용하고, After Sync 방식을 적용하였습니다.

    복제 토폴리지

    복제 토폴리지는 여러가지 방식 중 자신의 상황과 가장 맞는 방식을 사용하면 될 것 같습니다. 저희 팀이 고려해야할 문제는 먼저 성능을 올려야 했고, 단일 장애포인트를 개선해야했습니다. 하지만 사용할 수 있는 서버는 2대 뿐이였습니다. 이러한 상황에서 어떤 방식을 택할 수 있을까요?

    싱글 레플리카

    가장 기본적이며 가장 많이 쓰이는 형태입니다. 어플리케이션에서 레플리카 서버에 읽기 요청을 전달하면, 레플리카 서버에 문제가 생겼을 때, 서비스 장애 상황이 발생할 수 있습니다. 그러므로 소스 서버에서 Read, Write를 둘 다 하고, 레플리카 서버는 failover를 위해 대기하는 예비용 서버로 구성합니다. 소스 서버에 장애가 발생했을 때 소스 서버를 대체하거나 데이터를 백업하는 용도로 사용합니다.

    멀티 레플리카

    싱글 레플리카와 비슷한 구성이지만 레플리카 서버가 한 대 더 추가된 구성입니다. 해당 방식은 SPOF 문제가 없기 때문에 레플리카 서버 하나를 읽기 전용 서버로 둘 수 있습니다. 읽기 작업을 분산함으로 어플리케이션의 성능을 향상 시킬 수 있습니다. 아까 말했던 장애 상황이 발생하면 예비용 서버인 Replica2 서버를 Source 서버 혹은 Replica1(읽기 전용) 서버로 대체할 수 있습니다.

    체인 복제

    레플리카 서버가 많아져 소스 서버의 바이너리 로그를 읽는 부하가 많아질 때 할 수 있는 구성입니다. 좀 전에 설명드렸던 멀티 레플리카 방식에서 똑같은 구성을 추가한 방식으로 볼 수 있습니다. Source 1 의 정보를 복제한 Replica 1-1, 1-2 서버는 빠르게 데이터가 반영되지만, Source1의 이벤트를 복제한 Source2를 복제한 Replica 2-1, 2-2 서버는 당연히 늦게 반영되기 때문에 해당 그룹은 예비용으로 사용합니다.

    듀얼 소스 복제

    데이터베이스 둘 다 소스 서버이면서 레플리카 서버인 경우입니다. 이 경우는 Active-Active구성과 Active-Passive 구성으로 나뉩니다

    Active-Active는 서버 둘 다 읽기와 쓰기가 가능한 형태입니다. 즉 부하를 분산시키기 위해 서버 모두 읽고 쓰는 작업을 하는 것입니다. 하지만 이러한 방식은 뻔한 단점이 있습니다. 서로의 이벤트가 동기화 되기 전에는 정합성이 깨질 수 있습니다. 또 동시에 같은 데이터에 대해 쓰기 작업을 수행할 때, 하나의 서버에서 쓰기가 완료되었더라도, 다른 하나의 서버에 늦게 끝난 쓰기가 있다면 마지막 트랜잭션인 늦게 끝난 쓰기 작업이 반영되어 예상하지 못한 결과가 나올 수 있습니다.

    또 다른 문제로는 Auto Increment를 사용할 때입니다. 새로운 데이터가 동시에 생성될 때 Auto Increment가 중복되는 에러가 발생할 수 있기 때문에 해당 토폴로지에서는 ID를 DB에 의존하지 않는 것이 좋습니다.

    Active-Passive 방식은 하나의 서버만 읽기와 쓰기 요청이 되지만, 나머지 서버는 대기하고 있습니다. 두 서버 모두 언제나 쓰기 작업이 가능한 형태이기 때문에 장애 발생 시 빠르게 Faliover할 수 있다는 점이 있습니다.

    멀티 소스 복제

    하나의 레플리카 서버가 다수의 소스 서버를 갖는 구성입니다. 데이터베이스 샤딩을 해뒀는데, 다시 하나의 서버로 통합하고 싶을 때 사용할 수 있습니다. 혹은 서로 다른 데이터를 한 곳에 백업을 할 때도 사용할 수 있습니다.

    저희 팀의 토폴로지 방식

    그럼 이렇게나 많은 구성 중에 저희 팀에서 택할 수 있는 토폴로지 방식은 싱글 레플리카 방식과 듀얼 소스 복제 방식 밖에 없습니다. 왜냐하면 주어진 서버가 2대뿐이기 때문입니다. 하지만 듀얼 소스 방식은 적용하는데 무리가 있는 부분이 있습니다. 일단 저희가 레플리케이션을 적용하려는 가장 큰 이유는 성능 이기 때문에 성능이 변하지 않는 듀얼 소스의 Active-Passive 방식은 제외하겠습니다. 그리고 Active-Active 방식은 부하를 분산시킬 수 있다는 장점이 있지만, 단점으로는 Auto Increment를 사용하는데에 위험이 있다는 점과, 데이터의 정합성 문제가 생길 수 있다는 점에서 듀얼 소스 방식은 제외하도록 했습니다.

    그럼 싱글 레플리카 방식을 적용할 수 밖에 없는데요. 싱글 레플리카의 방식은 가용성 문제를 해결하기 위해 만들어진 방식입니다. 하지만 저희 서비스는 현재 가용성보다 성능을 더 신경써야하는 상황이기때문에 싱글 레플리카 토폴로지를 구성하지만 레플리카 서버를 예비용이 아닌 읽기 전용 방식으로 사용하도록 하고, 가용성 부분을 포기하기로 정했습니다.

    코드에 적용하기

    replication-datasource Github 소스 코드를 참고하시거나, DB 복제, @Transactional에 따라 요청 분리해보기 글을 참고하여 따라하면 금방하실 수 있습니다!

    결론

    데이터베이스 레플리케이션 생각보다 어렵지 않습니다.

    데이터베이스 재밌습니다. 인프라도 재밌습니다.

    참고

    Real Mysql 8.0

    - + \ No newline at end of file diff --git a/tags/mysql/page/3.html b/tags/mysql/page/3.html index 1eb0fdc..def5b99 100644 --- a/tags/mysql/page/3.html +++ b/tags/mysql/page/3.html @@ -5,7 +5,7 @@ "mysql" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -14,7 +14,7 @@ cpu

    조회 성능 개선하기

    먼저 제가 개선하기 위해 사용했던 방법들에 대해 적어보겠습니다.

    DTO 이용하기

    현재 구조는 아래의 JPA를 이용해 아래와 같은 쿼리로 entity로 데이터를 조회합니다.

     select distinct station.station_id,
    charger.charger_id,
    charger.station_id,
    chargerStatus.charger_id,
    chargerStatus.station_id,
    station.created_at,
    station.updated_at,
    station.address,
    station.company_name,
    station.contact,
    station.detail_location,
    station.is_parking_free,
    station.is_private,
    station.latitude,
    station.longitude,
    station.operating_time,
    station.private_reason,
    station.station_name,
    station.station_state,
    charger.created_at,
    charger.updated_at,
    charger.capacity,
    charger.method,
    charger.price,
    charger.type,
    charger.station_id,
    charger.charger_id,
    chargerStatus.created_at,
    chargerStatus.updated_at,
    chargerStatus.charger_condition,
    chargerStatus.latest_update_time
    from charge_station station
    inner join
    charger charger on station.station_id = charger.station_id
    inner join
    charger_status chargerStatus on charger.charger_id = chargerStatus.charger_id
    and charger.station_id = chargerStatus.station_id
    where station.latitude >= 37.5019194727953082567
    and station.latitude <= 37.5092305272047217433
    and station.longitude >= 127.044542269049714936
    and station.longitude <= 127.058071330950285064

    JPA를 통해 이러한 방식으로 조회한다면 아주 편하게 값을 가져오고, fetch join을 통해 하위의 entity들의 정보도 깔끔하게 가져옵니다.

    가져온 값으로 필요한 정보들을 매핑하고 가공하여 응답을 내려줬습니다.

    하지만 조회만을 위해 JPA의 entity를 조회한다는 것은 여러 단점이 존재합니다.

    제일 먼저 응답을 내려줄 때 불필요한 데이터까지 모두 조회를 한다는 부분입니다. 이렇게 많은 필드들이 있습니다. 하지만 응답에서는 대부분의 경우 모든 정보가 필요하지 않습니다. 그리고 모든 정보를 다 보내주는 것도 좋지 않습니다. 대량의 데이터를 조회할 때의 성능이 아주 나빠집니다.

    그래서 필요한 칼럼만 조회하는 것이 좋습니다.

    그리고 또 다른 단점으로는 JPA로 entity를 조회할 때 Hibernate 캐시에 저장한다던가, One To One 에서 N+1 쿼리가 발생하기 때문에 성능적인 이슈가 여러가지 있습니다.

    그래서 조회만 하는 api라면 DTO Projection으로 하는 것이 좋을 것 같습니다. 그리고 아래와 같이 변경하였습니다.

    SELECT s.station_id,
    s.station_name,
    s.latitude,
    s.longitude,
    s.is_parking_free,
    s.is_private,
    sum(case
    when cs.charger_condition = 'STANDBY' then 1
    else 0
    end),
    sum(case
    when c.capacity >= 50 then 1
    else 0
    end)
    FROM charge_station s
    inner join charger c on (c.station_id = s.station_id)
    inner join charger_status cs on (c.charger_id = cs.charger_id and c.station_id = cs.station_id)
    where s.station_id in (?, ?)
    group by s.station_id;

    이렇게 필요한 칼럼만 조회하는 방식으로 변경하여, 선릉역 근처를 조회하는 기준으로 약 450ms -> 350ms로 개선되었습니다.

    하지만 아직도 너무 느린 것을 확인할 수 있습니다. 그래서 실행 계획을 확인했습니다.

    실행 계획 확인하기

    sql의 실행 계획은 아주 중요하고 성능을 개선할 때 아주 유용합니다.

    실행 계획에는 여러가지 정보들이 있습니다.

    1. ID: 실행 계획 내에서 각 작업 또는 단계를 식별하는 일련번호입니다. 실행 계획은 여러 단계로 나뉘며, ID를 통해 이러한 단계를 식별할 수 있습니다.

    2. Select Type: 쿼리의 각 단계(예: SIMPLE, PRIMARY, SUBQUERY)에 대한 실행 유형을 나타냅니다. 이는 MySQL이 데이터를 선택하고 처리하는 방식을 나타냅니다.

    3. Table: 실행 계획에 포함된 테이블의 이름 또는 별칭입니다. 어떤 테이블이 사용되는지를 확인할 수 있습니다.

    4. Type: 테이블 접근 방식을 나타냅니다. 이 값은 인덱스 스캔, 풀 테이블 스캔 등과 같은 값일 수 있으며, 성능에 큰 영향을 미칩니다.

    5. Possible Keys: 사용 가능한 인덱스를 나타냅니다. MySQL이 어떤 인덱스를 사용할 수 있는지 알려줍니다.

    6. Key: 실제로 선택된 인덱스입니다. 이 값은 가능한 인덱스 중에서 실제로 사용되는 인덱스를 나타냅니다.

    7. Key Len: 사용된 인덱스의 길이를 나타냅니다.

    8. Ref: 인덱스를 사용하여 테이블 간의 연결을 나타내는 열입니다.

    9. Rows: 각 단계에서 예상되는 행의 수입니다. 이 값은 성능 평가에 중요한 역할을 합니다.

    10. Extra: 기타 정보를 제공합니다. 이 칼럼에는 추가 정보 및 힌트가 포함될 수 있습니다.

    이렇게 여러 칼럼이 있습니다. 그 중 성능에 큰 영향을 미치는 칼럼 두 가지만 자세히 알아보겠습니다.

    Type

    1. const : 쿼리에 Primary key 혹은 unique key 칼럼을 이용하는 where 조건절을 가지고 있고, 반드시 하나의 데이터를 반환하는 방식이다. (옵티마이저가 해당 부분은 상수로 처리하기 때문에 const라고 한다.)
    2. eq_ref : 조인에서 Primary key 혹은 unique key 칼럼을 이용하는 where 조건절을 가지고 있고, 반드시 하나의 데이터를 반환하는 방식이다. (const와 다른 점은 eq_ref는 조인에서 사용된다는 점이다.)
    3. ref : eq_ref와 다르게 join의 순서와 관계없이 사용된다. 그리고 primary key, unique key도 관계없다. 그냥 인덱스의 종류와 관계없이 = 조건으로 검색할 때 사용된다
    4. fulltext: mysql 전문 검색 인덱스를 사용해서 레코드에 접근하는 방법, 전문 검색할 컬럼에 인덱스가 있어야 한다. "MATCH ... AGAINST ..." 구문을 사용해서 실행된다
    5. range: 인덱스를 이용해서 검색하는데, 검색 조건이 >, >=, <, <=, BETWEEN, IN() 등의 연산자를 사용하는 경우이다. 보통의 인덱스 스캔이라고 하면 range, const, ref를 칭한다
    6. index: 인덱스 풀 스캔이다. 인덱스를 이용해서 테이블의 모든 레코드를 읽는다. 인덱스를 이용해서 테이블을 읽는 것이기 때문에 all보다는 빠르다.
    7. all: 테이블 풀 스캔이다. 테이블의 모든 레코드를 읽는다. 가장 느린 방법이다.

    실행 계획에서 자주 보이는 type들만 성능이 좋은 순으로 정리해봤습니다.

    Extra

    1. using filesort: 정렬을 위해 별도의 파일 정렬을 수행한다. 이는 인덱스를 사용하지 않고 정렬을 수행한다는 의미이다. 이는 성능에 좋지 않다.
    2. using index: 인덱스만으로 쿼리를 처리한다. 이는 인덱스만으로 쿼리를 처리하기 때문에 성능이 좋다.
    3. using join buffer: join이 되는 칼럼은 인덱스를 생성한다. 하지만 driven table에 적절한 인덱스가 없다면 driving table에 있는 모든 레코드를 읽어서 join을 수행한다. 그래서 이걸 보완하기 위해 driving table에 읽은 레코드를 임시 공간에 저장하는데 그 곳이 join buffer이다.
    4. using temporary: 쿼리를 처리하기 위해 임시 테이블을 생성한다. 인덱스를 사용하지 못하는 group by 쿼리가 대표적인 예이다.
    5. using where: mysql 엔진이 별도의 가공, 필터링 작업을 처리한 경우일 때만 나타난다. 범위 조건은 스토리지 엔진에서 처리되어 레코드를 리턴해주지만, 체크 조건은 mysql 엔진에서 처리된다.

    type뿐만 아니라 extra도 쿼리의 문제를 파악하는데 아주 큰 도움을 줍니다. 그 중 자주 보이는 것들에 대해서만 정리해봤습니다.

    그럼 아까 생성한 쿼리의 실행 계획을 확인해봅시다.

    +----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+
    | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    +----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+
    | 1 | SIMPLE | station | NULL | range | PRIMARY,idx_station_coordination | PRIMARY | 1022 | NULL | 2 | 100.00 | Using where; Using temporary |
    | 1 | SIMPLE | charger | NULL | ALL | PRIMARY | NULL | NULL | NULL | 240340 | 10.00 | Using where; Using join buffer (hash join) |
    | 1 | SIMPLE | chargersta | NULL | eq_ref | PRIMARY | PRIMARY | 2044 | charge.charger1_.charger_id,charge.station0_.station_id | 1 | 100.00 | NULL |
    +----+-------------+--------------+------------+--------+----------------------------------+---------+---------+---------------------------------------------------------+--------+----------+--------------------------------------------+

    station 테이블에 대해서는 range 스캔, 임시 테이블을 생성하고 있습니다, 그리고 charger에서는 테이블 풀 스캔, join buffer까지 생성하고 있습니다. 다행히도 chargersta 테이블에서는 적당한 조건을 생성한 것 같습니다.

    다시 한번 쿼리를 보고 실행 계획이 이렇게 나온 이유를 알아보겠습니다.

    SELECT
    ...
    FROM charge_station s
    inner join charger c on (c.station_id = s.station_id)
    inner join charger_status cs on (c.charger_id = cs.charger_id and c.station_id = cs.station_id)
    where s.station_id in (?, ?)
    group by s.station_id;

    아까 얘기했던, using temporary와 using join buffer가 발생하는 이유의 공통점을 찾아보면, 인덱스가 문제인 것을 유추할 수 있습니다.

    station과 charger를 join할 때, driven table 즉, charger 테이블에 적절한 인덱스가 없어 성능이 나빠진 것이라 의심하여, 인덱스를 생성하고 다시 한번 실행 계획을 확인했습니다.

    +----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+
    | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    +----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+
    | 1 | SIMPLE | station | NULL | range | PRIMARY,idx_station_coordination | PRIMARY | 1022 | NULL | 2 | 100.00 | Using where |
    | 1 | SIMPLE | charger | NULL | ref | PRIMARY,idx_station_id | idx_station_id | 1022 | charge.s.station_id | 3 | 100.00 | NULL |
    | 1 | SIMPLE | chargersta | NULL | eq_ref | PRIMARY | PRIMARY | 2044 | charge.charger1_.charger_id,charge.station0_.station_id | 1 | 100.00 | NULL |
    +----+-------------+--------------+------------+--------+----------------------------------+-------------------+---------+---------------------------------------------------------+--------+----------+---------------+

    이렇게 charger 테이블에 인덱스를 생성한 것만으로도 실행 계획을 깔끔하게 개선했습니다.

    결과

    아래는 인덱스를 생성하기 전 실행 속도입니다.

    개선_전

    아래는 인덱스를 생성한 후 실행 속도입니다.

    개선_후

    315ms -> 24ms 로 약 13배 빨라진 것을 확인할 수 있습니다.

    결론

    실행 계획 확인은 필수입니다!

    참고

    real mysql 책

    - + \ No newline at end of file diff --git a/tags/oauth.html b/tags/oauth.html index 5e17272..f5e4d3e 100644 --- a/tags/oauth.html +++ b/tags/oauth.html @@ -5,7 +5,7 @@ "oauth" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -24,7 +24,7 @@ Map<String, Object>로 지정해준 이유는, 플랫폼마다 반환되는 JSON 타입이 다르기 때문에 그런 부분에 대해 중복을 제거하기 위해 이러한 형태로 만들었습니다.

    그리고 아까 yml에 작성했던 정보들을 가져와야합니다. @Value 어노테이션으로도 가져올 수 있습니다.

            @Value("oauth.provider.google.id")
    private String id;
    @Value("oauth.provider.google.secret")
    private String secret;

    ...

    하지만 이렇게 계속 binding을 해줘야한다는 점이 아주 귀찮고 보기도 안좋습니다.

    build.gradle
    annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"

    하지만 위의 의존성을 추가해준다면 아주 편하게 property를 가져올 수 있습니다.

    OAuthProviderProperties.java
    @Component
    @ConfigurationProperties(prefix = "oauth2")
    public class OAuthProviderProperties {
    // prefix oauth2 기준으로 알아서 google이 이름인 Provider Enum을 찾아서 Key로 바인딩
    private final Map<Provider, OAuthProviderProperty> provider = new EnumMap<>(Provider.class);

    public OAuthProviderProperty getProviderProperties(Provider provider) {
    return this.provider.get(provider);
    }

    @Getter
    @Setter
    public static class OAuthProviderProperty {
    // 그리고 provider 하위 정보들은 아래의 필드에 바인딩
    private String id;
    private String secret;
    private String redirectUrl;
    private String tokenUrl;
    private String infoUrl;
    }
    }

    이렇게 되면 구조적인 준비는 끝났습니다.

    이제는 해당 플랫폼에 정보를 요청하는 작업만 하면 됩니다. 그럼 아까 말씀드렸던 순서로 요청을 해보겠습니다.

    RestTemplateOAuthRequester.java
    public class RestTemplateOAuthRequester implements OAuthRequester {

    @Override
    public OAuthMember login(OAuthLoginRequest request) {
    // frontend에서 받아온 로그인 platform
    Provider provider = Provider.from(request.provider());
    // 해당 Platform에 맞는 정보 찾음
    OAuthProviderProperty property = oAuthProviderProperties.getProviderProperties(provider);
    // frontend에서 받아온 code와 등록해놓은 property로 Access Token 요청
    OAuthTokenResponse token = requestAccessToken(property, requet.getCode());
    // 받아온 Token으로 해당 Resource Owner의 정보 요청
    Map<String, Object> userAttributes = getUserAttributes(property, token);
    return provider.getOAuthProvider(userAttributes);
    }

    private OAuthTokenResponse requestAccessToken(OAuthProviderProperty property, String code) {
    HttpHeaders headers = new HttpHeaders();
    headers.setBasicAuth(property.getId(), property.getSecret());
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers);
    URI tokenUri = getTokenUri(property, code);
    return restTemplate.postForEntity(tokenUri, request, OAuthTokenResponse.class).getBody();
    }

    private URI getTokenUri(OAuthProviderProperty property, String code) {
    return UriComponentsBuilder.fromUriString(property.getTokenUrl())
    .queryParam(CODE, URLDecoder.decode(code, StandardCharsets.UTF_8))
    .queryParam(GRANT_TYPE, AUTHORIZATION_CODE)
    .queryParam(REDIRECT_URI, property.getRedirectUrl())
    .build()
    .toUri();
    }

    private Map<String, Object> getUserAttributes(OAuthProviderProperty property, OAuthTokenResponse tokenResponse) {
    HttpHeaders headers = new HttpHeaders();
    headers.setBearerAuth(tokenResponse.accessToken());
    headers.setContentType(MediaType.APPLICATION_JSON);
    URI uri = URI.create(property.getInfoUrl());
    RequestEntity<?> requestEntity = new RequestEntity<>(headers, HttpMethod.GET, uri);
    ResponseEntity<Map<String, Object>> responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<>() {
    });
    return responseEntity.getBody();
    }
    }

    이렇게만 한다면 그 어려워 보이던 OAuth 인증도 간단하게 해결할 수 있습니다. (물론 제 코드가 정답이 아닙니다)

    Reference

    https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-16

    https://developers.google.com/identity/protocols/oauth2?hl=ko

    - + \ No newline at end of file diff --git a/tags/oom.html b/tags/oom.html index 861da6b..c9695c1 100644 --- a/tags/oom.html +++ b/tags/oom.html @@ -5,7 +5,7 @@ "OOM" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -31,7 +31,7 @@ 하지만 직접 확인해보기 전까지는 확신할 수 없으니 간단히 Runtime 클래스에서 제공해주는 totalMemory(), freeMemory() 메서드를 통해 알아보겠습니다.

        @Test
    void 페이징을_사용한_조회() {
    List<Station> stations = stationRepository.findAllByOrder(Pageable.ofSize(1000));

    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();
    System.out.println("paging 사용 중인 메모리: " + ((total - free) / 1024 / 1024) + "MB");
    }

    @Test
    void 페이징을_사용하지_않고_조회() {
    List<Station> stations = stationRepository.findAllFetch();

    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();

    System.out.println("findAll() 사용 중인 메모리: " + ((total - free) / 1024 / 1024) + "MB");
    }

    findAll paging 확연히 차이가 나는 것을 확인할 수 있습니다.

    물론 테스트코드에서는 23만건의 API 요청은 같은 조건이니 배제하고 확인했습니다.

    이로써 하나의 문제가 또 해결된 것 같습니다.

    아직 배우는 단계라 혹시 틀린 점이 있다면 지적 부탁드리겠습니다.

    Reference

    - + \ No newline at end of file diff --git a/tags/pr.html b/tags/pr.html index 5e0dc38..3db7f43 100644 --- a/tags/pr.html +++ b/tags/pr.html @@ -5,7 +5,7 @@ "pr" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -17,7 +17,7 @@ 아까 environment 속성을 보면 test라고 설정해놓은 것을 볼 수 있습니다. 해당 환경이 여기에 적용됩니다.

    Branch rule 정의하기

    이번에는 해당 Repository의 Settings -> Branches 탭으로 들어갑니다. 그리고 원하는 branch에 들어가 edit 버튼을 누릅니다.

    그리고 사진과 같이 Require deployments to succeed before merging 속성을 클릭합니다. 그리고 아래와 같이 어떤 환경을 적용할 것인지 선택할 수 있습니다.

    이 속성은 해당 배포가 성공해야 merge 할 수 있도록 브랜치를 보호하는 기능입니다.

    그리고 저희는 frontend와 backend Job의 환경을 둘 다 test라는 이름으로 정의했기 때문에 하나의 environment만 선택해도 둘 다 적용되는 효과를 볼 수 있습니다. branch rule

    적용 후

    아래와 같이 merge가 안된다는 글과 빨간색으로 경고 표시를 해주고 있습니다. blocked

    결론

    간단한 github action을 통해서 생산성을 많이 올릴 수 있는 좋은 기능인 것 같습니다. 다른 팀들도 이 기능을 도입하여 사용하는 것을 추천드립니다.

    - + \ No newline at end of file diff --git a/tags/pr/page/2.html b/tags/pr/page/2.html index 362dc9b..6accf4f 100644 --- a/tags/pr/page/2.html +++ b/tags/pr/page/2.html @@ -5,13 +5,13 @@ "pr" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "pr" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 4분
    누누

    안녕하세요 우테코 카페인팀 누누입니다

    빠르게 결과부터 보고 가시죠.

    어떤 결과가 나왔나요?

    pr의 본문 끝에, 연관된 이슈 번호를 달아주는 기능을 만들었습니다.

    밑에 사진을 보시면 쉽게 이해하실 수 있을 것 같습니다.

    imgimg

    github에서 issue 번호가 pr에 담겨있다면 2가지 장점이 생기는데요.

    1. issue를 클릭했을 때, 자동으로 그 issue로 넘어갈 수 있습니다. (호버만으로 이슈에 대한 간단한 정보를 볼 수 있습니다)
    2. pr 이 merge 되었을 때, 자동으로 issue 가 close 됩니다.

    이 과정을 손으로 진행하는 것보다, 자동으로 진행하게 되면 실수도 줄어들고, 개발 과정이 편해질 것 같아서 이 기능을 제작하게 되었는데요

    중요한 점

    이 과정을 진행하려면 밑에서 소개해드릴 브랜치 네이밍 규칙이 필요합니다.

    브랜치 이름 규칙

    • 브랜치 이름은 타입/이슈번호 으로 구성합니다. ex) feat/1
    • 타입은 feat, fix, docs, refactor, test 등 여러 가지가 있을 수 있습니다.

    이렇게 했을 때, 이슈 번호를 브랜치 명에서부터 가져올 수 있기에, 자동화를 할 수 있습니다.

    이런 규칙이 아닌, feat/action 같은 형태가 된다면 issue 번호를 찾기 어렵겠죠?

    사용 방법

    작성된 코드부터 보시고, 설명을 드리겠습니다.

    아래에 작성된 코드를. github/workflows/assign_issue_number_to_pr_body.yml로 저장하시면 끝입니다.

    name: assign_issue_number_to_pr_body

    on:
    pull_request:
    types: [ opened ]
    branches-ignore:
    - develop

    jobs:
    append_issue_number_to_pr_body:
    runs-on: ubuntu-latest
    steps:
    - name: append feature number to pr body pr branch = feat/(issueNumber)
    uses: actions/github-script@v4
    with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    script: |
    const pr = await github.pulls.get({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number
    });
    const body = pr.data.body;
    const issueNumber= pr.data.head.ref.split('/')[1];
    const newBody = body + "\n\n" + "close #" + issueNumber;
    await github.pulls.update({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: context.issue.number,
    body: newBody
    });

    진행 과정

    1. pr 이 생성되면, pr에 대한 정보를 가져옵니다.
    2. pr의 본문을 가져옵니다.
    3. pr의 브랜치 이름에서 이슈 번호를 가져옵니다.
    4. pr의 본문에 이슈 번호를 추가합니다.
    5. pr의 본문을 업데이트합니다.

    이 과정을 통해서, 직접 pr의 본문을 수정하지 않아도, 자동으로 이슈 번호가 추가되기에, 실수를 줄일 수 있으니, 한 번 시도해 보세요

    - + \ No newline at end of file diff --git a/tags/prod.html b/tags/prod.html index 7f8ab35..5897a3a 100644 --- a/tags/prod.html +++ b/tags/prod.html @@ -5,7 +5,7 @@ "prod" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -17,7 +17,7 @@ 기존 dev 서버는 총 4개의 서버를 배포하고 있었고 배포하는 서버는 다음과 같습니다. prod-BE, prod-FE, dev-BE, dev-FE

    그리고, 기존 dev 서버에서는 환경을 분리해주기 위해서 Nginx를 통해서 포트 포워딩은 다음과 같이 해주었습니다.

    • prod-BE = 8080
    • prod-FE = 3031
    • dev-BE = 8081
    • dev-FE = 3031

    카페인 팀에서는 dev, prod 환경이 분리되지 않아서 인스턴스의 사용량이 높았고, 이에 따라 추가적인 EC2 인스턴스가 필요했습니다.


    문제 해결

    다행히도 카페인 팀에서 추가적인 EC2 인스턴스를 받았고, 저희는 배포 환경을 분리할 수 있었습니다.

    dev-prod-server

    이와 같이 기존 dev 서버 한 개가 infra 서버와 연결되어 있었는데, 두 갈래로 나뉜 것을 확인하실 수 있습니다.

    먼저 배포는 다음과 같이 진행됩니다.

    release branch에 push가 일어나면 dev서버에 배포 작업이 이뤄집니다. prod branch에 push가 일어나면 prod서버에 배포 작업이 이뤄집니다.

    또한 기존 dev 서버에서 4개의 포트포워딩 또한 굳이 그럴 필요가 없어졌습니다. 새로운 서버가 추가됨에 따라 dev, prod 서버 각각 Nginx에서 포트포워딩을 동일하게 FE:3000, BE:8080 으로 변경하였습니다.

    이렇게 카페인 팀에서는 dev, prod 환경을 분리했습니다.

    감사합니다!

    - + \ No newline at end of file diff --git a/tags/react-state-management.html b/tags/react-state-management.html index ee22f72..ac5acaf 100644 --- a/tags/react-state-management.html +++ b/tags/react-state-management.html @@ -5,13 +5,13 @@ "react state management" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "react state management" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    가브리엘

    안녕하세요? 카페인 팀 FE에서 상태관리 라이브러리를 어떻게 해야할 지 고민 끝에 서드파티 라이브러리가 필요하게 되어 글을 작성하게됐습니다.

    서버 상태와 클라이언트 상태의 구분

    서버상태와 UI상태를 이해하는 것은 굉장히 중요했습니다. 데이터를 송수신하는 작업과 상태를 관리하는 작업은 유기적으로 동작해야했습니다. 기존에는 상태와 데이터 송수신 과정을 분리해서 생각했다면, 현대의 react 프로젝트들은 서버와 동기화를 해야할 상태그렇지 않은 상태로 분리해서 생각해야 합니다.

    React에서 어떤 데이터를 상태로 다뤄야 하는가에 대해서는 여러 의견이 나올 수 있다고 생각하지만 상태가 특성을 가지고 있는가에 대해서는 대부분 특성이 있다고 동의할 것입니다. 이 글에서는 React의 상태란 무엇인가?에 대해서 다루지 않고 React의 상태의 특성에 대해서만 언급을 하려고 합니다.

    상태의 특성으로는 크게 두 가지가 있습니다.

    클라이언트 상태

    클라이언트 상태는 컴포넌트들 간에 어떤 값을 공유해야하면서 오로지 React DOM 내부에서만 CRUD가 일어나는 상태를 의미합니다. 이 상태들은 React DOM 외부 세계와 크게 관련이 없으며 동기적으로 반영됩니다. 대표적으로는 UI를 조작하는 상태들이 될 것입니다. 클라이언트 상태들은 대부분 장기적으로 유지될 필요가 없기에 화면을 벗어나거나 세션이 끊기는 경우 사라져도 괜찮은 경우가 많습니다.

    서버 상태

    서버 상태는 React의 바깥 세상(서버)에 존재하는 데이터가 React의 상태 관리와 비동기적으로 동기화 된 것을 의미합니다. 어떤 상태가 외부에서 관리되는 데이터와 반드시 연동되어야 한다면 이는 곧 서버 상태임을 의미합니다. React의 상태를 CRUD 하는 것 뿐만 아닌, 서버에서도 항상 같은 일이 일어나야 합니다. 서버 상태는 장기적으로 유지되어야 하며, 세션에서 벗어나더라도 서버로 부터 복구를 해야 합니다.

    기존의 상태 관리 라이브러리들은 리액트의 전역에서 상태를 조작하는 것에 특화되어있고, 비동기적인 상태 관리도 지원하여 서버와의 통신이 가능합니다. 하지만 대부분의 라이브러리들은 클라이언트 상태를 조작하는 것에 초점이 맞춰져있습니다.

    더군다나 클라이언트 상태와 서버 상태가 하는 일이 명확하게 다른 상황에서 이 둘을 한 곳에서 관리하는 것 보다는 완벽하게 분리하는 것이 더 나을 것입니다. 따라서 서버 상태를 관리하는 것에 중점을 둔 라이브러리들이 등장하였습니다. 대표적인 라이브러리로는 RTK Query, Tanstack Query, SWR 등이 있습니다.

    왜 Tanstack Query였나?

    vs RTK Query

    RTK Query는 RTK를 반드시 사용해야 하는 것은 아니지만 RTK를 타겟으로 나온 서버 상태 관리 라이브러리입니다. 카페인 팀에서는 클라이언트 상태를 관리하기 위해 라이브러리를 사용하지 않습니다. 더욱이 Redux의 복잡한 코드 구성과 방대한 보일러 플레이트는 매력적이지 않았습니다. tanstack query에서는 무한 데이터 페칭을 지원하기 위해 Infinite Queries가 있지만 RTK Query는 그렇지 않았습니다.

    vs SWR

    SWR도 하나의 좋은 선택지였지만, 전역 상태 관리 라이브러리들이 범용적으로 지원하는 셀렉터 기능을 지원하지 않았습니다. 또, 가비지 컬렉터의 부재도 아쉬웠습니다. 재요청을 하기 위한 stale time 설정이나 쿼리 취소 기능이 없는 점도 매력적이지 않았습니다.

    카페인 팀에서 하려는 일은요…

    저희 카페인 팀의 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 로 지도 기반의 프로젝트입니다. 서버 상태를 적극적으로 다뤄야 하는 상황에서 Tanstack Query를 서버 상태 관리 라이브러리로 선정하게 됐습니다.

    메인 기능 중 Tanstack Query가 핵심으로 사용될 것 같은 기능은 다음과 같습니다.

    • 지도에서 충전소 조회
      • 현재 접속한 클라이언트에 렌더링 된 지도 화면(디스플레이)의 크기에 따른 GPS좌표를 알아내어 서버로 부터 충전소 정보를 수신 받습니다. 즉, 화면이 이동하게 되면 사용자가 바라보고 있는 영역이 변하므로 새로운 요청을 보내게 됩니다.
      • 서버에서 수신한 충전소 정보는 실시간 사용 현황도 반영되어있으므로 주기적인 업데이트도 필요합니다.
      • 빈번한 데이터의 변화가 필요하며 그만큼 통신 실패 등 에러가 발생할 가능성도 많아지게 됩니다.
      • 사용자의 빠른 지도 이동이 발생하는 경우를 대응할 수 있어야 합니다.
    • 전국 충전소 검색기
      • 원하는 충전소 검색을 하는 기능을 지원합니다. 전국 단위로 검색 결과를 수신하는 기능입니다.
      • 네이버와 구글 검색창 처럼 사용자가 input 창에 검색어를 입력할 때 마다 검색 결과가 동적으로 표시되어야 합니다.
      • 빈번한 데이터의 변화가 필요하고, 사용자의 빠른 타이핑으로 인해 잦은 검색이 발생하는 경우를 대응할 수 있어야 합니다.
      • 이를 위해 데이터를 캐싱할 필요도 있다고 생각합니다.

    프로젝트에서 클라이언트와 서버와의 통신이 어쩌다 한번 일어난다면 굳이 라이브러리가 필요가 없겠지만, 서버의 데이터 전적으로 의존해야 하는 저희 프로젝트 특성상 Tanstack Query의 여러 기능이 생산성에 많은 도움이 될 것으로 기대합니다.

    - + \ No newline at end of file diff --git a/tags/react-wrapper.html b/tags/react-wrapper.html index f44c31c..f9ef1c1 100644 --- a/tags/react-wrapper.html +++ b/tags/react-wrapper.html @@ -5,13 +5,13 @@ "react-wrapper" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "react-wrapper" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    가브리엘

    지도 api 벤더 선택 이유

    국내 서비스 중인 지도 서비스로는 google, naver, kakao가 있습니다.

    이 중에서도 google maps api는 css로 지도의 테마를 직접 스타일링할 수 있는 기능이 있어서 선택하게 됐습니다.

    google maps api를 사용하기 위해서 별도의 라이브러리 사용이 필수는 아니지만

    저희 팀에서 대중적인 라이브러리들과 기본 환경 설정법을 모두 테스트 했을 때, 반드시 사용하고 싶은 라이브러리가 존재하여 비교를 기록으로 남기게 됐습니다.

    google maps api 관련 라이브러리

    (선택한 라이브러리들은 ✅으로 표시했습니다.)

    google maps API

    https://github.com/tomchentw/react-google-maps

    이 라이브러리는 구글에서 공식으로 제공하는 지도 api로, HTML DOM에 구글 지도를 부착하고, 사용(조작)할 수 있도록 도와줍니다. 이 라이브러리는 vanilla Javascript 기반으로 동작합니다.

    @types/google.maps

    https://www.npmjs.com/package/@types/google.maps

    TypeScript에서 구글 지도를 사용할 때 타입을 제공해주는 역할을 합니다.

    @googlemaps/js-api-loader

    https://www.npmjs.com/package/@googlemaps/js-api-loader

    이 라이브러리는 구글에서 공식으로 제공하는 지도 호출 api로, api key만 넘겨주더라도 구글 지도를 스크립트 형태로 불러와주는 역할을 하는 라이브러리입니다. 별도로 html 조작 없이 불러온 라이브러리에서 구글 지도를 꺼내서 동적으로 사용할 수 있습니다. vanilla Javascript 기반으로 동작하여 어디에서나 사용이 가능합니다.

    대중적인 라이브러리 비교

    react-google-maps@react-google-maps/api@googlemaps/react-wrapper
    링크https://www.npmjs.com/package/react-google-mapshttps://www.npmjs.com/package/@react-google-maps/apihttps://www.npmjs.com/package/@googlemaps/react-wrapper
    설명이 라이브러리는 개인이 만든 라이브러리로, google maps API를 react DOM 위에 올려서 사용하게 돕습니다.
    구글 지도와 마커를 react component 처럼 사용하여 react스럽게 렌더링 하는 것을 지원합니다.
    react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리였지만 2018년 이후로 업데이트가 끊겼습니다.
    이 라이브러리도 개인이 만든 라이브러리로 앞서 소개한 react-google-maps를 개량하여 만든 라이브러리입니다.
    이 라이브러리 역시 react에 지도나 마커 컴포넌트를 호출해서 사용이 가능합니다.
    현재 react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리 입니다.
    이 라이브러리는 구글에서 공식으로 제공하는 react용 라이브러리입니다.
    이 라이브러리는 앞서 소개한 js-api-loader를 활용하여 만든 Wrapper 컴포넌트를 제공하는데, 구글 지도를 호출하는 과정에서 수신중, 실패, 성공에 따라 지도를 보여줄 지, 로딩중 컴포넌트를 보여줄 지, 에러 컴포넌트를 보여줄 지 결정하는 기능이 있습니다.
    이외에는 기존의 js-api-loader의 기능과 완벽하게 동일합니다. (라이브러리를 열어서 직접 확인해봤습니다.)
    선택여부

    라이브러리 선택 이유

    저희 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 다보니 지도 위에 띄워줘야 할 마커를 최적화 하는 과정이 굉장히 중요합니다.

    1. 전국 6만여 개의 마커를 전부 보여줄 수 없다.
    2. 현재 디스플레이 영역의 마커만을 호출해야한다.
    3. 그 마커들의 렌더링 과정을 저수준에서 다룰 수 있어야 한다.

    이런 원칙을 가지고 있기에 대중적인 라이브러리들(react-google-maps, @react-google-maps/api)은 저희의 선택지에 없었습니다.

    따라서 구글 지도는 오로지 vanilla로 제공되는 상태에서 직접 제어하기로 결정하였고, 마커를 관리하는 주체 또한 구글 지도에서 직접 컨트롤을 하려고 합니다.

    따라서 구글 지도를 호출하는 작업은 @googlemaps/react-wrapper에 맡기고, 불러온 구글 지도는 vanilla로 통제하기로 했습니다.

    지도의 조작, 지도에 마커를 찍는 과정을 모두 공식 문서에 나와있는 방법대로 통제하려고 합니다.

    기존의 라이브러리들은 마커나 지도를 컴포넌트화 한 상태이기에 최적화 과정에서 저희가 제어할 수 없는 부분들이 있다고 생각합니다. 따라서 트러블슈팅 과정에서 마커의 호출 시점, 메모리에서 해제하는 시점, 렌더링하는 시점 등의 작업들을 훨씬 더 세밀하게 하려면 google maps api을 있는 그대로 사용할 수 있어야 합니다. 따라서 지도에 관련된 기능은 react DOM 위에서가 아닌 vanilla 환경에서 작업을 할 것입니다.

    구글 지도 제어 전략

    1. 구글 지도와 마커는 항상 바닐라 환경(react DOM 바깥)에서 동작하게 한다.
    2. 바닐라 환경에서만 동작하게 하여 리액트 컴포넌트에서의 재 렌더링을 일절 방지한다.
    3. 마커나 지도의 동작 이벤트에 의해 UI를 조작해야하는 경우에는 react DOM 조작을 하도록 한다.
    4. 바닐라 환경인 google maps api와 react DOM 사이의 제어 과정에는 useSyncExternalStore 훅을 이용하여 리액트 UI를 강제로 동기화 시킬 수 있도록 한다.

    구글 지도는 바닐라 환경에서, 각종 UI 통제는 리액트에서 통합하여 사용하는 환경을 구상하고 있습니다.

    시중에 나와있는 대부분의 라이브러리들을 활용하여 비교하고 테스트한 결과 @googlemaps/react-wrapper를 선택하는 것이 최적화와 생산성, 앱 안정성을 모두 확보할 수 있는 선택이라고 생각했습니다.

    현재 카페인 팀에서 사용중인 지도 제어에 관한 방법은 이후에 작성 될 글에서 상세하게 설명하겠습니다.

    - + \ No newline at end of file diff --git a/tags/react.html b/tags/react.html index 09282c3..420a990 100644 --- a/tags/react.html +++ b/tags/react.html @@ -5,13 +5,13 @@ "react" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "react" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 13분
    센트

    1. 개요

    기존의 구조에서는 마커 하나를 렌더링하기 위해 다음과 같은 과정을 거쳤다.

    1. StationMarkersContainer 컴포넌트에서 충전소 정보 요청
    2. 충전소 정보를 props로 넘겨 Marker 컴포넌트 호출
    3. 지도에 부착될 DOM요소 생성
    4. createRoot를 통해 리액트 root 생성
    5. 2번에서 생성한 DOM 요소를 전달해 구글 지도 api의 Marker 생성자 함수 호출
    6. 3번에서 생성했던 root의 render 메서드 호출
    7. 마커 인스턴스 전역 상태에 새로 생성한 마커 추가

    위 과정을 거쳤을 때의 마커 렌더링 모습을 보면 다음과 같다.

    before

    마커들이 한번에 렌더링 되는 것이 아니라 산발적으로 렌더링 되는 모습을 확인할 수 있다.

    2. 문제 원인 분석

    마커를 렌더링 하기 위해 거치는 과정을 분석해 보았다.

    1 ~ 3 과정에서는 성능에 크게 영향을 끼칠 요소가 없지만 4번 과정은 일반적인 리액트 프로젝트를 개발할 때 겪는 과정이 아니다. 따라서 createRoot를 통해 많은 개수의 루트를 생성했을 때의 영향에 대해 알아보았다.

    image

    리액트 공식 문서를 보니 페이지의 일부에 리액트를 뿌려서 사용하는 경우에는 루트를 필요한 만큼 생성해도 된다는 이야기가 포함되어 있었다. 따라서 4번 과정 또한 문제의 원인이라고 볼 수 없었다.

    5번 과정은 구글 지도에 마커를 특정 위도 경도에 위치시키기 위해서 어쩔 수 없이 거쳐야 하는 과정이므로 이 과정은 문제가 있더라도 개선이 불가능해 일단 고려하지 않았다.

    6번 과정은 4번 과정에서 생성했던 리액트 루트의 render 메서드를 호출해 실제로 화면에 리액트 컴포넌트를 그리도록 하는 과정이다. 이 과정 또한 리액트 컴포넌트를 화면에 렌더링하기 위해선 어쩔 수 없이 거쳐야 하는 과정이므로 고려하지 않았다.

    하지만 6번 과정에서 리액트 컴포넌트를 직접 그리는 것이 아니라 구글 지도 api의 기본 마커를 사용하면 성능을 향상시킬 수 있지 않냐고 반문할 수도 있을 것이다. 이전에는 이러한 방식을 사용해 마커를 렌더링 했었다. 우리의 서비스는 현재 사용 가능한 충전소 개수를 마커를 통해서도 전달하기 때문에 이를 고려해 기본 마커를 사용할 때 다음의 두 가지 문제가 생긴다.

    1. 사용 가능한 충전소 개수를 기본 마커에 렌더링 할 때 성능이 매우 좋지 않다.
    2. 마커의 디자인을 바꾸고자 할 때 변경에 대응하기 어렵다.

    따라서 마커는 리액트 루트의 render 메서드를 호출해 리액트 컴포넌트를 렌더링하는 것으로 결정했다.

    마지막으로 남은 7번 과정에서는 useSyncExternalState 훅을 사용해 전역적으로 관리하고 있던 상태에 수정을 가하는 연산을 수행한다. 이 과정은 이전에도 성능 저하를 유발할 것으로 예상되던 부분이었다. (하단 링크 참고)

    useSyncExternalStore 훅을 통해 구독한 state가 한번에 업데이트 되는 이유

    요청의 결과로 받아온 마커 정보의 개수가 100개라고 가정해보자. 우리는 이제 마커를 렌더링 할 것이다. 첫 번째 마커의 렌더링을 위해 1번 ~ 6번의 과정을 거친 후 7번 과정을 수행한다. 그러면 리액트 입장에서는 리액트 루트의 render 메서드 호출에 대한 동작을 수행해야 하고, 새로운 마커 인스턴스에 대한 전역 상태를 변경시키는 동작을 수행해야 한다. 리액트가 이 과정을 100번 반복하고 나면 우리는 비로소 모든 마커가 화면에 렌더링 된 모습을 볼 수 있을 것이다.

    나는 이 부분에서 성능 저하의 요소가 있다고 생각했다. 리액트에서의 상태 변화는 곧 리액트 내부의 렌더링을 위한 로직이 수행되게 함을 의미하고, 이 과정을 개선 이전에는 마커의 개수만큼 반복하고 있었던 것이다. 여기까지 생각해보니 전역 상태 변화에 대해 리액트가 렌더링을 위한 연산을 진행할 동안에는 마커의 렌더링(render 메서드 호출)이 멈추는 것이 아닐까 하는 생각이 들었다.

    그래서 크롬 개발자 도구의 퍼포먼스 탭을 들어가 보니 산발적으로 발생하던 마커 렌더링의 문제 원인이 짐작했던 그 원인임을 확인할 수 있었다.

    image

    프레임 이미지 하단을 보면 산발적인 마커 렌더링이 수행될 때마다 수반되는 어떤 함수 호출이 있음을 확인할 수 있다.

    image

    이 부분이 문제의 함수 호출 부분이다. 자세히 살펴보면 상단에 performWorkUntilDeadline이란 함수가 호출됨을 볼 수 있다.

    image

    performWorkUntilDeadline 라는 함수를 조금 알아보니 해당 함수는 간단히 말해 리액트에서 state의 변경이 한번에 많이 발생할 때 5ms의 데드라인 시간을 줄 때 사용하는 함수라는 것을 알게 되었다. 문제의 원인이라고 생각했던 마커 개수 만큼의 전역 상태 변화가 실제로 마커 렌더링을 잠시 중단하게 만들고 있음을 알게 되었다.

    3. 문제 해결

    앞서 분석한 문제를 개선해보고자 마커 렌더링에 필요한 충전소 정보 배열을 부모 컴포넌트에서 받아와 각 충전소 정보를 자식 컴포넌트에 넘겨주고, 자식 컴포넌트에서 마커 생성과 렌더링 로직을 수행하던 기존의 방식을 부수고 부모 컴포넌트에서 모든 것을 일괄 처리하는 방식으로 고쳐보았다.

    고치는 과정에서 기존 방식에서는 리액트 생명 주기에 의존하여 화면에 보여지지 않는 마커를 지워주던 로직을 이제는 모두 직접 구현해야 했다.

    이전의 영역과 겹치는 부분에 있는 충전소는 다시 그리지 않고, 영역 밖의 충전소를 나타내는 마커는 지워주고, 이전의 영역과 겹치지 않는 새로 받아온 충전소는 그리도록 다음과 같이 메서드를 분리해보았다.

    • 기존과 겹치지 않는 새로운 영역에 대한 마커를 생성하는 메서드
    • 기존과 겹쳐지는 영역에 대한 마커들을 반환하는 메서드
    • 새로운 영역 밖에 있는 마커들을 지워주는 메서드
    • 새롭게 생성된 마커를 화면에 렌더링하는 메서드

    이 메서드들을 커스텀 훅으로 분리해 부모 컴포넌트에서 이를 활용하도록 하여 다소 복잡할 수 있는 마커 렌더링 로직을 선언적으로 구현할 수 있도록 했다.

    결과적으로 기존에 사용되던 기능들을 그대로 사용할 수 있으면서 화면에 마커가 산발적으로 렌더링 되던 문제가 해결 되었고, 부가적인 효과로 전체 마커의 렌더링 시점도 앞당길 수 있게 되었다. + 기존에는 구조적인 문제로 연산량이 너무 많아 클러스터링이 늦어져 이를 도입할 수 없었던 문제를 구조 수정으로 인해 적용할 수 있게 되었다.

    작업한 PR

    https://github.com/woowacourse-teams/2023-car-ffeine/pull/737

    결과 분석 (performance 탭 활용)

    before

    마커 조회 요청이 종료된 시점: 약 2499ms

    image

    첫 마커 렌더링 시점: 3093ms

    image

    모든 마커 렌더링 종료 시점: 약 3611ms

    image

    처음으로 마커가 렌더링 될 때까지 소요된 시간: 594ms

    모든 마커 렌더링에 소요된 시간: 1112ms

    after

    마커 조회 요청의 시작점: 약 1875ms

    image

    모든 마커 렌더링 종료 시점: 2395ms

    image

    처음으로 마커가 렌더링 될 때까지 소요된 시간: 519ms

    모든 마커 렌더링에 소요된 시간: 519ms

    개선 결과

    처음으로 마커가 렌더링 되는 시점은 두 방식 모두 비슷한 결과를 보인다. 하지만 개선 후 방식은 한번에 모든 마커가 렌더링 되는 방식이고, 개선 이전의 방식은 산발적으로 마커가 렌더링 되는 방식이므로 개선 후의 방식에서 전체 마커를 렌더링 하는 시점이 훨씬 빨라지게 되었다.

    결과적으로 전체 마커가 렌더링 되는 속도 약 55.6% 단축하게 되었다. 이 결과는 마커가 늘어날 수록 더욱 차이가 극적으로 벌어질 것으로 예상된다.

    before

    before

    after

    after

    - + \ No newline at end of file diff --git a/tags/react/page/2.html b/tags/react/page/2.html index 3ab7748..758f2ca 100644 --- a/tags/react/page/2.html +++ b/tags/react/page/2.html @@ -5,13 +5,13 @@ "react" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "react" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 18분
    센트

    Untitled

    위 이미지는 현재까지 구현한 지도의 모습이다. 구현된 기능은 다음과 같다.

    • 충전소 정보를 서버에 요청해 받아온 충전소 정보를 바탕으로 화면에 마커를 표시하는 기능
    • 화면이 이동하거나 줌인, 줌 아웃을 할 시 화면의 마커 정보가 최신화 되는 기능
    • 마커 정보를 최신화 할 때 화면에서 사라진 마커를 dom에서 제거하는 기능
    • 마커 정보를 최신화 할 때 이전 화면에서도 있었던 마커를 재생성 하지 않는 기능
    • 마커를 클릭했을 시 해당 마커에 대한 간단 정보를 모달로 띄워주는 기능
    • 화면에 표시된 마커들에 대한 충전소 정보를 리스트로 보여주는 기능

    이번에 새로 추가하고자 한 기능은 다음과 같다.

    • 충전소 리스트에서 충전소를 선택하면 화면의 중심이 선택한 충전소 마커로 이동하고, 충전소의 간단 정보를 모달로 띄워주는 기능

    위 기능을 구현하기 위해선 google maps api의 InfoWindow객체를 이용해야 한다. 사용 방식은 다음과 같다.

    const infowindow = new google.maps.InfoWindow({
    content: contentString,
    ariaLabel: 'Uluru',
    });

    const marker = new google.maps.Marker({
    position: uluru,
    map,
    title: 'Uluru (Ayers Rock)',
    });

    infowindow.open({
    anchor: marker,
    map,
    });

    간단하게 요약하자면 다음과 같다.

    • InfoWindow 생성자 함수를 통해 infoWindow 인스턴스를 생성한다.
      • 생성시 dom 요소 혹은 string을 전달해 infoWindow가 생성될 dom위치를 지정해준다.
    • marker 인스턴스를 infoWindow 인스턴스의 open 메서드에 인자로 전달한다.
    • infoWindow 생성 시 전달했던 dom요소의 위치가 marker의 위치로 고정되면서 화면에 그려진다.

    Untitled

    충전소 정보를 보여주는 위 StationList 컴포넌트는 충전소 정보에 접근할 때 react-query를 통해 서버 상태를 직접 내려 받아 컴포넌트 내부 리스트를 렌더링 한다.

    또한, StationMarkersContainer에서도 충전소 정보를 react-query의 서버 상태에서 참조해 마커를 렌더링 하고 있다.

    따라서 StationList 컴포넌트와 StationMarkersContainer는 각각 따로 서버 상태에 접근해 렌더링을 수행하고 있으므로 둘 사이에는 어떠한 연결 고리가 없다.

    여기서 문제가 발생하게 되었다.


    현재까지의 코드에서는 infoWindow인스턴스를 StationMarkersContainer컴포넌트에서 생성한다. 이를 하위 컴포넌트인 StationMarker에 내려주고, 이 컴포넌트 내부에서 marker인스턴스를 생성한다.

    이번에 구현하기로 한 기능은 StationList의 항목 중 하나를 선택했을 시 선택된 충전소에 해당하는 마커에 간단 정보 모달이 뜨며 화면을 해당 마커가 중심으로 오도록 이동 시키는 것이었다.

    하지만 지금의 코드 구조상 StationListStationMarkersContainer사이에는 어떠한 연결 고리도 없으므로 infoWindowmarkerStationList는 접근할 수 없는 상태가 된다.

    이를 해결하기 위해서 다음과 같은 방법을 사용하기로 했다.

    • infoWindow인스턴스를 root 단에서 생성해 전역적으로 관리한다.
    • 생성될 marker 인스턴스들을 배열 형태의 전역 상태로 관리한다.

    위 내용을 말로만 본다면 별로 어려울 것 없어 보이지만 실제 구현을 진행해보니 내부적으로 큰 문제가 두 가지 존재했다.

    1. 따로 모듈을 분리해 infoWindow를 생성할 수 없다.
    2. marker인스턴스를 생성하는 주체가 StationMarkersContainer가 되어서는 안된다.

    각각의 문제점을 살펴보자.


    1. 따로 모듈을 분리해 infoWindow를 생성할 수 없다.

    infoWinodw를 전역 상태로 만들어 사용하기 위해 처음으로 했던 생각은 infoWindowStore.ts로 모듈을 분리하여 infoWindow를 생성해 store의 초기값으로 지정하는 것이었다.

    위 생각을 가지고 그대로 구현해보았더니 google을 참조할 수 없다는 에러가 발생했다. InfoWindow생성자 함수는 google.maps.InfoWindow를 통해 접근할 수 있기 때문에 해당 에러는 infoWindow인스턴스를 생성할 수 없다는 것을 의미했다.

    google을 참조할 수 없는지 이유를 분석해보니 이유는 다음과 같았다.

    우리 팀이 구글 지도 로드를 위해 선택한 라이브러리는 @googlemaps/react-wrapper이다. 이 라이브러리의 동작을 살펴보면 다음과 같다.

    • Wrapper컴포넌트가 @googlemaps/js-loader라이브러리의 Loader생성자 함수를 호출한다.
    • 생성된 loader인스턴스의 load메서드를 실행시켜 지도의 로딩 작업을 시작한다.
      • load 메서드는 최종적으로 Promise<typeof google>을 반환하는데, 지도 로드에 성공하면 resolve(window.google) 을 실행시켜 google을 전역적으로 사용 가능하도록 만들어준다.
    • 지도의 로딩이 완료되면 Wrapperrender props를 통해 받은 콜백 함수를 실행시킨다.
      • render콜백 함수는 로딩 상태를 나타내는 Status를 파라미터로 넘겨 받아 호출된다.

    최종적으로 render를 실행 시켰을 때 반환 되는 컴포넌트에서는 google 로딩 되어 전역적으로 접근이 가능함을 보장할 수 있으므로 이때부터 google에 접근이 가능해진다. → 따라서 Wrapper를 통해 반환되는 컴포넌트의 하위 컴포넌트에서 google.maps.Map생성자 함수를 사용해 지도를 생성할 수 있게 된다.

    infoWindow를 생성하기 위해 만든 새로운 모듈은 첫 import시기에 평가될 것이기 때문에 Wrapper의 하위 컴포넌트에서 import를 수행한다면 로드가 완료된 이후 시점일 것이므로 window.google이 등록되어 google에 접근이 가능할 것으로 예상했다.

    하지만 웹팩을 통한 번들링 과정에서 모듈이 뒤섞여 파일의 평가 시기를 보장할 수 없어져 새로 만든 모듈에서는 google에 대한 접근이 불가능해지게 되었다. 웹팩을 좀 더 공부해본다면 이 문제를 해결할 수 있을 것 같았지만, 너무 지엽적인 부분에서 많은 시간을 들이기 보단 기존에 개발하던 방식을 통해 문제를 해결해보기로 결정했다.

    최종적으로 문제를 해결한 방식은 다음과 같다.

    • InfoWindow생성자 함수를 호출할 CarFfeineInfoWindowInitializer컴포넌트를 만든다.
    • Wrapper로 감싸진 컴포넌트 하위에 CarFfeineInfoWindowInitializer 컴포넌트를 추가한다.
    • google에 접근이 가능한 상태를 보장받은 CarFfeineInfoWindowInitializer내부에서 infoWindow인스턴스를 생성한다.
    • storeinfoWindow인스턴스를 set해주어 전역적으로 infoWindow를 사용 가능하도록 한다.

    2. marker인스턴스를 생성하는 주체가 StationMarkersContainer가 되어서는 안된다.

    이번 팀 프로젝트에서 지도를 구현하기 위해 google maps api를 사용하게 되었다. 뜬금없이 이 이야기를 한 이유는 다음과 같다.

    • google maps api는 바닐라 자바스크립트를 기반으로 동작한다.
    • 이번 팀 프로젝트는 리액트를 기반으로 개발을 진행할 것이다.
    • 지도를 그리기 위해서 바닐라 자바스크립트와 리액트의 적절한 조화가 필요하다.
    • 다소 혼란스러울 수 있는 지도의 조작 방식을 리액트와 조화롭게 사용하기 위해서 컴포넌트 설계시 컴포넌트의 책임을 확실하게 구분해야겠다는 생각을 하게 되었다.

    이 컴포넌트의 책임에 대한 문제로 인해 marker 인스턴스를 생성하는 주체에 대해 많은 고민을 하게 되었다.

    일단 원래 코드 구조에서 마커를 그리기 위해 컴포넌트를 다음과 같이 추상화 했다.

    • StationMarkersContainer 컴포넌트
      • 리액트 쿼리를 통해 받아온 서버 상태(충전소 정보 배열)로 StationMarker를 호출한다.
    • StationMarker 컴포넌트
      • 상위에서 내려받은 충전소 정보 props를 통해 marker 인스턴스를 생성한다. (google maps api에서는 인스턴스 생성이 곧 렌더링을 의미한다)
      • 생성한 marker 인스턴스에 infoWindow 인스턴스의 open 메서드를 트리거 하는 클릭 이벤트 리스너를 추가해준다.
      • useEffect의 클린업 함수를 이용해 충전소 정보가 최신화 되었을 때 마커가 더이상 화면에 보이지 않는다면 marker 인스턴스의 setMap(null) 메서드를 호출해 google maps api에서 마커를 지우도록 한다. (마커 렌더링 최적화)

    간략히 설명하자면 StationMarkersContainer 컴포넌트는 충전소 정보를 서버에서 받아 StationMarker를 호출하는 역할만을 수행하고, 마커에 대한 모든 세부 로직은 StationMarker가 수행하도록 컴포넌트를 추상화 해보았다.

    이름에서도 드러나듯 StationMarker 컴포넌트가 marker 인스턴스를 생성하는 주체가 되어야 바닐라 자바스크립트와 리액트의 혼종인 이 프로젝트의 코드를 추후 유지보수 할 때 문제가 없으리라 판단했다.

    하지만 이렇게 추상화 된 컴포넌트들은 marker 인스턴스를 배열 형식의 전역 상태에 담아 관리하고자 할 때 문제가 되었다.


    일단 먼저 서버에서 내려 받은 충전소 정보를 station이라고 하자, 우리는 이 station을 통해 marker 인스턴스를 생성하고자 한다.

    이때 생각 할 수 있는 가장 간단한 방법은 station에서 map 메서드를 통해 marker 인스턴스를 생성하여 이 marker 인스턴스를 하위 컴포넌트인 StationMarker에 넘겨주는 방식일 것이다.

    하지만 이 방식은 인스턴스를 생성하는 것이 곧 화면에 렌더링을 발생시키는 것을 의미하는 google maps api의 특성상 우리가 처음 설계한 컴포넌트의 책임을 반하는 구조를 만들어내게 된다.

    자세히 설명해보자면 마커의 렌더링은 StationMarkersContainer가 수행하고 있는데 화면에 보이지 않는 마커를 지우는 역할은 StationMarker컴포넌트가 수행하고 있고, 이벤트 핸들러의 추가 역시 마커가 생성된 이후에 하위 컴포넌트에서 이를 수행하는 괴상한 코드가 만들어지게 된다.

    추후 코드의 유지보수성을 위해선 피해야 할 방식임이 명확했다.

    해결 방식을 고민해보다가 다음과 같은 해결 방안을 생각하게 되었다.

    StationMarker 컴포넌트의 역할

    • marker 인스턴스를 생성한다.
    • marker 인스턴스의 이벤트 핸들러를 추가한다.
    • 생성된 marker 인스턴스를 배열 형식의 전역 상태에 추가한다.
    • 충전소 정보가 최신화 되었을 때 마커가 화면에 보이지 않는 상태가 되었다면 marker 인스턴스를 전역 상태에서 삭제한다.

    위와 같이 StationMarker 의 역할을 잡게 되면 기존의 컴포넌트 설계 구조를 해치지 않으면서 전역 상태에 marker인스턴스를 잘 추가할 수 있게 된다. 하지만 이렇게 되면 StationMarker 컴포넌트는 다음의 큰 문제들을 가지게 된다.

    1. marker들을 가지는 전역 상태를 구독하고 있는 컴포넌트가 새로 생성되는 마커의 개수만큼 리렌더링 된다.
    2. 현재 사용하고 있는 전역 상태 관리 도구의 특성상 이전 상태를 참조해와야 marker를 추가할 수 있게 되는데, 이 때 이전 상태가 최신의 상태임을 보장하지 못할 수 있다.

    이 두 문제를 해결할 방식을 고민해보았을 때 다음과 같은 결론에 도달하게 되었다.

    • 현재 사용하고 있는 전역 상태 관리 도구는 React 18에 새로 추가된 useSyncExternalState 훅을 기반으로 recoil과 비슷하게 사용할 수 있도록 계층을 분리하여 만든 도구이다.
    • 기존에 사용하던 전역 상태 관리 도구의 메서드 useExternalState, useExternalValue, useSetExternalState 이외에 store 인스턴스에 직접 접근하여 최신의 상태를 참조하는 getStoreSnapShot 메서드를 추가한다.
    • store에 직접 접근해 받아온 최신의 상태는 바닐라 자바스크립트 객체 이므로 리액트의 리렌더링을 발생 시키지 않는다.
    • 리렌더링으로 인한 문제점들을 getStoreSnapShot 메서드를 추가함으로써 해결할 수 있다.

    새로운 기능 추가를 위해 마주했던 앞선 두 가지의 문제와 해결 방식을 살펴 보았다. 그래서 최종적으로 이전까지 계속해서 고민해왔던 문제를 해결한 과정을 간추려보자면 다음과 같다.

    • 충전소 정보를 서버에서 받아와 렌더링 하는 StationList 컴포넌트에서 marker 인스턴스 배열을 저장하고 있는 store인스턴스에 직접 접근해 최신의 marker인스턴스들을 가져온다.
    • 충전소 목록에서 사용자가 충전소를 클릭했을 때 전역으로 관리되는 infoWindow 인스턴스의 open메서드에 marker 인스턴스들 중 선택된 marker를 전달해 간단 정보 모달을 띄워준다.
    - + \ No newline at end of file diff --git a/tags/react/page/3.html b/tags/react/page/3.html index 22d4790..7ee8525 100644 --- a/tags/react/page/3.html +++ b/tags/react/page/3.html @@ -5,13 +5,13 @@ "react" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "react" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    가브리엘

    지도 api 벤더 선택 이유

    국내 서비스 중인 지도 서비스로는 google, naver, kakao가 있습니다.

    이 중에서도 google maps api는 css로 지도의 테마를 직접 스타일링할 수 있는 기능이 있어서 선택하게 됐습니다.

    google maps api를 사용하기 위해서 별도의 라이브러리 사용이 필수는 아니지만

    저희 팀에서 대중적인 라이브러리들과 기본 환경 설정법을 모두 테스트 했을 때, 반드시 사용하고 싶은 라이브러리가 존재하여 비교를 기록으로 남기게 됐습니다.

    google maps api 관련 라이브러리

    (선택한 라이브러리들은 ✅으로 표시했습니다.)

    google maps API

    https://github.com/tomchentw/react-google-maps

    이 라이브러리는 구글에서 공식으로 제공하는 지도 api로, HTML DOM에 구글 지도를 부착하고, 사용(조작)할 수 있도록 도와줍니다. 이 라이브러리는 vanilla Javascript 기반으로 동작합니다.

    @types/google.maps

    https://www.npmjs.com/package/@types/google.maps

    TypeScript에서 구글 지도를 사용할 때 타입을 제공해주는 역할을 합니다.

    @googlemaps/js-api-loader

    https://www.npmjs.com/package/@googlemaps/js-api-loader

    이 라이브러리는 구글에서 공식으로 제공하는 지도 호출 api로, api key만 넘겨주더라도 구글 지도를 스크립트 형태로 불러와주는 역할을 하는 라이브러리입니다. 별도로 html 조작 없이 불러온 라이브러리에서 구글 지도를 꺼내서 동적으로 사용할 수 있습니다. vanilla Javascript 기반으로 동작하여 어디에서나 사용이 가능합니다.

    대중적인 라이브러리 비교

    react-google-maps@react-google-maps/api@googlemaps/react-wrapper
    링크https://www.npmjs.com/package/react-google-mapshttps://www.npmjs.com/package/@react-google-maps/apihttps://www.npmjs.com/package/@googlemaps/react-wrapper
    설명이 라이브러리는 개인이 만든 라이브러리로, google maps API를 react DOM 위에 올려서 사용하게 돕습니다.
    구글 지도와 마커를 react component 처럼 사용하여 react스럽게 렌더링 하는 것을 지원합니다.
    react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리였지만 2018년 이후로 업데이트가 끊겼습니다.
    이 라이브러리도 개인이 만든 라이브러리로 앞서 소개한 react-google-maps를 개량하여 만든 라이브러리입니다.
    이 라이브러리 역시 react에 지도나 마커 컴포넌트를 호출해서 사용이 가능합니다.
    현재 react 진영에서 가장 대중적으로 사용되는 구글 지도 라이브러리 입니다.
    이 라이브러리는 구글에서 공식으로 제공하는 react용 라이브러리입니다.
    이 라이브러리는 앞서 소개한 js-api-loader를 활용하여 만든 Wrapper 컴포넌트를 제공하는데, 구글 지도를 호출하는 과정에서 수신중, 실패, 성공에 따라 지도를 보여줄 지, 로딩중 컴포넌트를 보여줄 지, 에러 컴포넌트를 보여줄 지 결정하는 기능이 있습니다.
    이외에는 기존의 js-api-loader의 기능과 완벽하게 동일합니다. (라이브러리를 열어서 직접 확인해봤습니다.)
    선택여부

    라이브러리 선택 이유

    저희 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 다보니 지도 위에 띄워줘야 할 마커를 최적화 하는 과정이 굉장히 중요합니다.

    1. 전국 6만여 개의 마커를 전부 보여줄 수 없다.
    2. 현재 디스플레이 영역의 마커만을 호출해야한다.
    3. 그 마커들의 렌더링 과정을 저수준에서 다룰 수 있어야 한다.

    이런 원칙을 가지고 있기에 대중적인 라이브러리들(react-google-maps, @react-google-maps/api)은 저희의 선택지에 없었습니다.

    따라서 구글 지도는 오로지 vanilla로 제공되는 상태에서 직접 제어하기로 결정하였고, 마커를 관리하는 주체 또한 구글 지도에서 직접 컨트롤을 하려고 합니다.

    따라서 구글 지도를 호출하는 작업은 @googlemaps/react-wrapper에 맡기고, 불러온 구글 지도는 vanilla로 통제하기로 했습니다.

    지도의 조작, 지도에 마커를 찍는 과정을 모두 공식 문서에 나와있는 방법대로 통제하려고 합니다.

    기존의 라이브러리들은 마커나 지도를 컴포넌트화 한 상태이기에 최적화 과정에서 저희가 제어할 수 없는 부분들이 있다고 생각합니다. 따라서 트러블슈팅 과정에서 마커의 호출 시점, 메모리에서 해제하는 시점, 렌더링하는 시점 등의 작업들을 훨씬 더 세밀하게 하려면 google maps api을 있는 그대로 사용할 수 있어야 합니다. 따라서 지도에 관련된 기능은 react DOM 위에서가 아닌 vanilla 환경에서 작업을 할 것입니다.

    구글 지도 제어 전략

    1. 구글 지도와 마커는 항상 바닐라 환경(react DOM 바깥)에서 동작하게 한다.
    2. 바닐라 환경에서만 동작하게 하여 리액트 컴포넌트에서의 재 렌더링을 일절 방지한다.
    3. 마커나 지도의 동작 이벤트에 의해 UI를 조작해야하는 경우에는 react DOM 조작을 하도록 한다.
    4. 바닐라 환경인 google maps api와 react DOM 사이의 제어 과정에는 useSyncExternalStore 훅을 이용하여 리액트 UI를 강제로 동기화 시킬 수 있도록 한다.

    구글 지도는 바닐라 환경에서, 각종 UI 통제는 리액트에서 통합하여 사용하는 환경을 구상하고 있습니다.

    시중에 나와있는 대부분의 라이브러리들을 활용하여 비교하고 테스트한 결과 @googlemaps/react-wrapper를 선택하는 것이 최적화와 생산성, 앱 안정성을 모두 확보할 수 있는 선택이라고 생각했습니다.

    현재 카페인 팀에서 사용중인 지도 제어에 관한 방법은 이후에 작성 될 글에서 상세하게 설명하겠습니다.

    - + \ No newline at end of file diff --git a/tags/record.html b/tags/record.html index 018e71d..9021026 100644 --- a/tags/record.html +++ b/tags/record.html @@ -5,13 +5,13 @@ "record" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "record" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 6분
    누누

    우아한테크코스에서 자바 11을 사용하는 것이 너무 익숙해진 상황이어서, java 11 대신 java 17을 쓰려면 쓰는 대신, 왜 java 17을 쓰면 좋은지에 대해서 설득을 하는 시간이 있어야 하는데요

    처음에는 단순히 record 클래스가 좋아요, collect(Collectors.toList()); 대신 toList() 만으로 해결할 수 있어서 좋아요

    까지밖에 설명할 수 없었습니다.

    이것만으로 동의를 해줘서 일단 java 17 을 사용하기로 했지만, 이번 기회에 조금 더 자세하게 알아보려고 합니다

    Java 17 과 Java 11의 중요한 차이들

    기능적인 부분과, 숨겨진 부분을 나누어볼 수 있을 것 같습니다.

    기능적인 차이점

    언제나 직접 차이를 보면 더 직관적이기 때문에, 직접 코드를 보면서 설명을 해보려고 합니다

    record 클래스

    간단한 dto 클래스를 만들었을 때 코드가 정말 간단해지는 것을 확인할 수 있습니다

    Java 11

    public class Dto {
    private final int data;

    public Dto(int data) {
    this.data = data;
    }

    public int getData() {
    return data;
    }
    }

    lombok 을 사용했을 때


    @Getter
    @AllArgsConstructor
    public class Dto {
    private final int data;
    }

    Java17

    public record Record(int data) {
    }

    이렇게 보면 훨씬 간단해진 것을 볼 수 있습니다

    예상되는 문제점

    objectMapper를 사용하면 어떻게 되나요? noArgsConstructor 가 필요하지 않나요?

    class RecordTest {

    @Test
    void objectMapper_로_변환() throws JsonProcessingException {
    // given
    ObjectMapper objectMapper = new ObjectMapper();
    Record record = new Record(1);

    // when
    String json = objectMapper.writeValueAsString(record);

    // then
    assertEquals("{\"data\":1}", json);
    }

    @Test
    void string_에서_객체로_변환() throws JsonProcessingException {
    // given
    String json = "{\"data\":1}";
    ObjectMapper objectMapper = new ObjectMapper();

    // when
    Record record = objectMapper.readValue(json, Record.class);

    // then
    assertEquals(1, record.data());
    }
    }

    이 테스트에서 볼 수 있는 것처럼 성공적으로 deserialize, serialize 가 가능합니다

    toList() method

    Java 11

    이 부분도 정말 편의성이 높다고 생각하는 부분 중 하나인데요

    Collectors.toList() 대신 toList() 만으로도 사용이 가능합니다

    public class ToListWith11 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .collect(Collectors.toList());
    System.out.println(result);
    }
    }

    Java 17

    public class ToListWith17 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .toList();
    System.out.println(result);
    }
    }

    switch expression

    Java 11

    우테코에서는 switch, case 를 싫어하기에 볼 수는 없겠지만

    switch 문에도 정말 편하게 바뀌었는데요

    public class SwitchWith11 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = 0;
    switch (day) {
    case "Monday":
    result = 1;
    break;
    case "Tuesday":
    result = 2;
    break;
    case "Wednesday":
    result = 3;
    break;
    case "Thursday":
    result = 4;
    break;
    case "Friday":
    result = 5;
    break;
    case "Saturday":
    result = 6;
    break;
    case "Sunday":
    result = 7;
    break;
    }
    System.out.println(result);
    }
    }

    Java 17

    public class SwitchWith17 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = switch (day) {
    case "Monday" -> 1;
    case "Tuesday" -> 2;
    case "Wednesday" -> 3;
    case "Thursday" -> 4;
    case "Friday" -> 5;
    case "Saturday" -> 6;
    case "Sunday" -> 7;
    default -> 0;
    };
    System.out.println(result);
    }
    }

    코드 량이 엄청 줄어든 것을 확인하실 수 있습니다

    instanceof pattern matching

    물론 instanceof 를 사용할 경우가 많은가? 하면 많지는 않겠지만

    아래와 같이 변경되었습니다

    Java 11

    public class InstanceOfWith11 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.toUpperCase());
    }
    }
    }

    Java 17

    public class InstanceOfWith17 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String str) {
    System.out.println(str.toUpperCase());
    }
    }
    }

    number format

    이 기능은 12에 나왔는데요

    언어별로 숫자를 표현하는 방식이 다르지만, 쉽게 표현할 수 있도록 도와주는 기능입니다

    Java 17

    public class NumberFormatterWith11 {
    public static void main(String[] args) {
    int number = 1_000_000;

    String result = NumberFormat.getCompactNumberInstance(Locale.KOREA, NumberFormat.Style.LONG).format(number);

    System.out.println(result.equals("100만"));
    }
    }

    나머지 부분은 사실 그렇게 큰 역할을 할 것 같지는 않아서 생략하겠습니다

    숨겨진 부분들

    gc throughput

    위의 사진은 gc 의 버전별 처리량입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 15% 정도 향상되었고, java 11과는 10% 정도 향상되었습니다.

    gc latency

    위의 사진은 gc의 버전별 지연시간입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 30% 정도 향상되었고, java 11과는 25% 정도 향상되었습니다.

    이와 같이, 단순하게 새로운 기능만 추가되는 것이 아니라 꾸준히 성능도 향상되고 있습니다.

    이런 부분을 고려했을 때, Java 17을 사용하는 것이 좋을 것 같습니다.

    참고

    - + \ No newline at end of file diff --git a/tags/slack.html b/tags/slack.html index c85a714..9961e19 100644 --- a/tags/slack.html +++ b/tags/slack.html @@ -5,13 +5,13 @@ "slack" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "slack" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 12분
    누누

    안녕하세요 카페인팀 nunu입니다.

    오늘은 스프링에서 발생한 에러 로그를 슬랙으로 모니터링하는 방법에 대해서 알아보려고 합니다.

    목차는 다음과 같습니다.

    1. 스프링에서 로그를 남기는 방법
    2. Slf4 j의 동작원리
    3. Logback의 동작원리
    4. Logback을 사용해서 슬랙으로 에러 로그를 모니터링하는 방법

    스프링에서 로그는 어떻게 찍을까?

    스프링에서 로그를 찍는 방법은 여러 가지가 있지만, 가장 간단한 방법은 System.out.println()을 사용하는 것입니다.

    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test() {
    System.out.println("test");
    return "test";
    }
    }

    당연하지만, 성능이 안 좋아서 실제 서비스에서는 사용하지 않습니다.

    스프링에서는 Slf4 j를 통해서 로그를 남길 수 있습니다.

    @Slf4j // private final Logger log = LoggerFactory.getLogger(this.getClass()); 와 같다.
    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test() {
    log.info("test");
    return "test";
    }
    }

    이 코드를 통해서 로그를 남길 수 있는데, 자동으로 콘솔에 출력이 됩니다.

    스프링에서 로깅은 어떻게 작동하는 거지?

    스프링 4까지는 Commons Logging을 사용했었습니다.

    Commons LoggingJCL이라고도 불리며, JDK Logging, Log4 j, Logback 등 다양한 로깅 프레임워크를 지원합니다.

    JCL 은 런타임에 어떤 로깅 프레임워크를 사용할지 결정할 수 있습니다.

    런타임에 어떤 로깅 프레임워크를 사용할지 결정하는 방식으로 클래스 로더에게 질의를 하는 방식으로 작동하게 되는데

    클래스 로더에게 질의를 했을 경우에 몇 가지 문제점이 생깁니다

    1. 클래스 로더에 명확한 표준이 없고, 부모 자식 모델이 있어서, 클래스 로더에 따라서 다른 결과가 나올 수 있습니다. 참고
    2. 클래스로더는 gc의 동작에 방해를 일으켜서 메모리 누수를 발생시킬 수 있습니다. 참고

    @Slf4j 어노테이션을 붙이면, 컴파일 시점에 private final Logger log = LoggerFactory.getLogger(this.getClass()); 와 같은 코드로 변환됩니다.

    스프링 5에서는 Slf4j 가 사용하는 것처럼, 컴파일 타임에 어떤 로깅 프레임워크를 사용할지 결정하는 기능을 작성했고, Commons Logging을 사용하지 않게 되었습니다.

    spring 5에서 변경되었다는 링크

    Slf4 j에 대해서 알아보자

    Slf4 j는 로깅을 위한 인터페이스를 제공하는 프레임워크입니다.(Simple Logging Facade for Java)

    컴파일 타임에, 어떤 로그 라이브러리를 사용할지 결정하는 기능을 제공합니다.

    로그 라이브러리를 바꾸려고 했을 때, 기존 코드는 하나도 건드리지 않고, 로그 라이브러리만 바꿔주면 되도록 해줍니다.

    조금 더 자세한 동작 원리를 알아보자

    only slf4j

    Slf4 j 만을 사용했을 경우 위 사진 같은 형태로 요청이 처리가 됩니다.

    Slf4 j 라는 인터페이스를 통해서 로그를 남기고, 어떤 로그 라이브러리를 사용할지는 Slf4j binding이라는 것을 통해서 결정합니다.

    Slf4j bindingSlf4j의 인터페이스를 구현하고 있지 않은 라이브러리의 구현체를 연결해 주는 역할을 합니다.

    그 구현체로 Slf4j-log4 j12-{version}. jar 같은 것이 있다.

    이와는 다르게 Logback 은 Slf4 j 를 구현하고 있기에, Slf4j binding 을 사용하지 않아도 됩니다.

    logback example

    위 사진처럼 Slf4j binding 을 사용하지 않고, Logback 바로 사용하는 것도 가능합니다.

    그렇다면 Slf4 j를 바로 사용하지 않은 코드에서 Slf4j 를 사용하려면 어떻게 해야 할까요?

    slf4j working principle

    위 사진처럼 Slf4j bridge 를 통해서 외부 라이브러리를 사용하는 것처럼 갈아 끼울 수 있습니다.

    Log4j2 를 사용하는 코드를 전혀 바꾸지 않아도, BridgeSlf4j 를 통해 Logback으로 자연스럽게 로그를 남길 수 있도록 해줍니다.

    Logback에 대해서 알아보자

    Logback 은 스프링에서 기본으로 사용될 만큼 인기 있는 로그 라이브러리입니다.

    logback 동작 과정

    공식문서에서 아주 핵심적인 동작원리를 설명해주고 있는 사진이라서 가져왔습니다.

    너무 어려워 보여서, 조금 자세하게 각각의 구성요소에 대해서 알아보도록 하겠습니다

    이에 대해 알아보도록 하겠습니다

    로그백의 구성요소

    Appender

    Appender는 로그를 어디에 출력할지를 결정하는 역할을 합니다.

    외부로부터 어떤 데이터를 받아서, 어떤 방식으로 처리할지에 대해서 전체적으로 설정할 수 있습니다.

    기본적으로 수많은 Appender 가 제공되고 있습니다.

    • ConsoleAppender
    • FileAppender
    • RollingFileAppender
    • AsyncAppender
    • DBAppender
    • SMTPAppender
    • SocketAppender
    • SyslogAppender

    저희는 Slack에 알림을 주는 것이 목적이기 때문에, SlackAppender를 사용하면 될 것 같습니다.

    하지만 SlackAppender는 제공되고 있지 않기에 직접 구현을 해야 하는데요

    이를 구현했을 때, Slack API 가 끝날 때까지, 계속 기다리고 있을 필요가 없기에, AsyncAppender를 사용하는 것이 좋을 것 같습니다.

    사용 방법은 다음과 같습니다. xml 기반으로 가능한데요

    <configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>myapp.log</file>
    <encoder>
    <pattern>%logger{35} -%kvp -%msg%n</pattern>
    </encoder>
    </appender>

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE" />
    </appender>

    <root level="DEBUG">
    <appender-ref ref="ASYNC" />
    </root>
    </configuration>

    만약 여기에 있는 기능들로 부족하다면, 직접 Appender 를 구현해서 사용할 수도 있습니다.

    직접 구현하려면 AppenderBase를 상속받아서 구현하면 됩니다.

    이 클래스는 필요한 부분이 대부분 구현되어 있고, appender 만 구현하면 바로 사용할 수 있습니다. 당연하지만 필요하다면 override 도 가능하죠

    Layout

    Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 합니다.

    Appender는 로그를 어디에 출력할지를 결정하는 역할을 하고, Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 하도록 하는 것이 이상적이지만

    Logback 은 Appender에서 Layout 을 직접 지정할 수 있도록 해주고 있습니다.

    따라서, 직접 Layout 을 만들지 않고, Appender 에서 기존에 이미 있는 패턴만 사용하려고 합니다

    Encoder

    Encoder는 Layout 과 비슷한 역할을 합니다.

    Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 하고, Encoder 는 실제 byte 형태로 변환하는 역할을 합니다.

    Slack의 webhook을 사용할 것이지만, AppenderBase를 사용하기에, 이번에는 사용할 수 없습니다.

    Filter

    Filter는 로그를 어떤 조건에 따라서 출력할지를 결정하는 역할을 합니다.

    Filter 는 Appender를 등록하며 같이 등록할 수 있는데요

    이번 프로젝트에서는 Level 이 ERROR 이상인 것만 출력하도록 하고 싶기에, LevelFilter를 사용하면 좋을 것 같습니다.

    <configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>INFO</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>
    <encoder>
    <pattern>
    %-4relative [%thread] %-5level %logger{30} -%kvp -%msg%n
    </pattern>
    </encoder>
    </appender>
    <root level="DEBUG">
    <appender-ref ref="CONSOLE" />
    </root>
    </configuration>

    와 비슷하게 사용할 수 있어 보입니다.

    그러면 실제로 프로젝트에서 error 발생 시 slack으로 알림을 주는 것을 구현해 보도록 하겠습니다.

    슬랙에 추가하는 방법

    이 블로그를 보고서 작성했습니다

    실제 구현

    구현된 결과물은 아래와 같습니다

    slack appender

    SlackAppender 구현하기

    public class SlackAppender extends AppenderBase<ILoggingEvent> {

    @Override
    protected void append(final ILoggingEvent eventObject) {
    final var restTemplate = new RestTemplate();
    final var url = "https://hooks.slack.com/services/";
    final Map<String, Object> body = createSlackErrorBody(eventObject);
    restTemplate.postForEntity(url, body, String.class);
    }

    private Map<String, Object> createSlackErrorBody(final ILoggingEvent eventObject) {
    final String message = createMessage(eventObject);
    return Map.of(
    "attachments", List.of(
    Map.of(
    "fallback", "요청을 실패했어요 :cry:",
    "color", "#2eb886",
    "pretext", "에러가 발생했어요 확인해주세요 :cry:",
    "author_name", "car-ffeine",
    "text", message,
    "fields", List.of(
    Map.of(
    "title", "우선순위",
    "value", "High",
    "short", false
    ),
    Map.of(
    "title", "서버 환경",
    "value", "local",
    "short", false
    )
    ),
    "ts", eventObject.getTimeStamp()
    )
    )
    );
    }

    private String createMessage(final ILoggingEvent eventObject) {
    final String baseMessage = "에러가 발생했습니다.\n";
    final String pattern = baseMessage + "```%s %s %s [%s] - %s```";
    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    return String.format(pattern,
    simpleDateFormat.format(eventObject.getTimeStamp()),
    eventObject.getLevel(),
    eventObject.getThreadName(),
    eventObject.getLoggerName(),
    eventObject.getFormattedMessage());
    }
    }

    이 과정에서 url을 직접 입력하시면 됩니다.

    그리고, 이렇게 만든 SlackAppender를 logback-spring.xml 에 등록하면 됩니다.

    <?xml version="1.0" encoding="UTF-8"?>

    <configuration scan="true" scanPeriod="60 seconds">
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <include resource="org/springframework/boot/logging/logback/file-appender.xml"/>
    <root level="INFO">
    <appender-ref ref="FILE"/>
    <appender-ref ref="CONSOLE"/>
    </root>
    <appender name="SLACK_APPENDER" class="racingcar.SlackAppender">
    </appender>
    <appender name="ASYNC_SLACK_APPENDER" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="SLACK_APPENDER"/>
    </appender>
    <logger name="racingcar" level="ERROR" additivity="true">
    <appender-ref ref="ASYNC_SLACK_APPENDER"/>

    </logger>

    </configuration>

    이렇게 하면, racingcar 패키지에서 에러가 발생할 때만 slack으로 알림을 받을 수 있습니다.

    결론

    slack appender

    이번 글에서는 log 레벨에 따라 slack 으로 알림을 받는 방법을 알아보았습니다.

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/tags/spring.html b/tags/spring.html index 87fdf7d..905f27a 100644 --- a/tags/spring.html +++ b/tags/spring.html @@ -5,13 +5,13 @@ "Spring" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "Spring" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 6분
    키아라

    서론

    안녕하세요 카페인팀 키아라입니다.

    이번 프로젝트를 시작하면서 프로퍼티를 암호화하는 방법으로 jasypt를 알게되어

    사용하는 방법을 익혀 저희 프로젝트에 적용해볼 계획입니다.

    프로퍼티 암호화는 왜 필요할까?

    spring:
    datasource:
    url: 데이터베이스 url
    username: 계정
    password: 비밀번호

    프로젝트를 진행하면서 yml 파일에 DB 연결 URL이나 계정, 비밀번호 같이 노출되어선 안 되는 민감한 정보들이 많습니다.

    git의 public repository와 CI/CD를 연동해 어플리케이션을 배포한다면 중요한 정보가 탈취될 가능성이 있죠.

    Jasypt 라이브러리를 사용하면 평문으로 된 데이터베이스 접속 정보를 암호화 하여 방어막을 한 겹 쌓을 수 있게 됩니다.

    간략하게 라이브러리를 소개하고 사용 방법을 알아볼까요?

    jasypt는 뭐지?

    Jasypt이란 쉽게 암호화 기능을 사용할 수 있도록 제공하는 Java 라이브러리입니다.

    민감한 평문 정보를 암호화하고, 아래처럼 설정 값을 지정하면 어플리케이션이 실행될 때 자동으로 이를 복호화하여 사용합니다.

    사용자가 편하게 암호화 기능을 사용할 수 있도록 제공하는 Java 라이브러리로

    공식 홈페이지는 http://www.jasypt.org/ 에 가면 더 자세한 정보를 확인할 수 있습니다.

    사용 방법

    정말 간단하게 라이브러리 추가, key값 넘겨주기, 암호화 세 가지 단계로 프로퍼티를 암호화하여 관리할 수 있습니다.

    1. 라이브러리 추가 (= 의존성 추가)

    implementation "com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.3"

    2. Jasypt 설정 및 Bean 등록

    key를 사용해서 Bean을 등록하는 기본 설정입니다. 여기서 Bean의 이름을 jasyptEncryptor라고 설정했다면 프로퍼티 등록해야 합니다.

    @Configuration
    public class JasyptConfig {

    private String ENCRYPT_KEY = "hello";

    @Bean(name = "jasyptEncryptor")
    public StringEncryptor stringEncryptor() {
    PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();

    SimpleStringPBEConfig config = new SimpleStringPBEConfig();

    config.setPassword(ENCRYPT_KEY);
    config.setAlgorithm("PBEWithMD5AndDES");
    config.setKeyObtentionIterations(1000);
    config.setPoolSize(1);
    config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
    config.setStringOutputType("base64");
    encryptor.setConfig(config);
    return encryptor;
    }
    }
    jasypt:
    encryptor:
    bean: jasyptEncryptor

    3. 암호화

    라이브러리를 사용할 준비는 거의 다 끝났습니다. 이제 암호화하여 프로퍼티에 작성합니다.

    이때 암호화 하는 방법은, 아래 사이트에 접속해 평문과 키를 입력한 후 나온 암호문을 프로퍼티 파일에 'ENC(암호문)' 로 작성합니다.

    암복호화 사이트

    평문

      datasource:
    url: 데이터베이스 url
    username: 계정
    password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)

    나머지도 마저 암호화해줍시다.

      datasource:
    url: ENC(j94r94hQbd1SfFHGCUeweg+GGDosfnxP8dL0FQxfXtE=)
    username: ENC(vp3Gw8kLpwDZhmMMqf88/Q==)
    password: ENC(piAhHYGHR3dWDkdco6C3n8TpJdyq8FnO)

    실행

    올바른 암호문을 입력했다면 정상적으로 실행이 됩니다.

    그러나 이때 임의로 암호문을 수정한다면 다음과 같이 빌드를 실패합니다.

    실행 실패

    그런데 뭔가 이상하지 않나요?

    프로퍼티는 분명 암호화 했는데 키가 코드에 그대로 노출되어 있습니다.

    Git의 public Repository에 배포하면 다른 사람들도 볼 수 있습니다.

    그럼 이 키를 어디에 숨길 수 있을까요?

    저는 처음에 일반 file에 키를 넣어놓고 파일을 읽어오는 식으로 키를 관리하려고 했습니다. 당연히 해당 파일은 .gitignore로 커밋 대상에서 제외해야겠죠.

    그런데 이것보다 더 쉽고 빠른 방법이 있습니다.

    바로 환경변수를 설정하는 것이죠.

    + 환경변수 설정

    private String ENCRYPT_KEY = "hello";

    기존의 키를 관리하는 방식이었습니다.

    우선 이 키를 프로퍼티에서 관리하도록 설정해볼까요?

    // JasyptConfig.class
    @Value("${jasypt.encryptor.password}")
    private String ENCRYPT_KEY;
    // application.yml
    jasypt:
    encryptor:
    password: hello

    이제 환경변수를 설정해봅시다.

    Run > Edit Configurations... 경로로 들어가면

    Run/Debug Configurations 창이 나오는데

    Environment variables: 부분에 ENCRYPT_KEY=hello

    라고 적어주세요.

    그 후 다시 yml 파일로 돌아와 기존 hello로 되어있는 부분을 ${ENCRYPT_KEY}로 변경하고 실행한다면 정상적으로 작동됩니다.

    jasypt:
    encryptor:
    password: ${ENCRYPT_KEY}

    긴 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git a/tags/spring/page/2.html b/tags/spring/page/2.html index cc90109..256a5e3 100644 --- a/tags/spring/page/2.html +++ b/tags/spring/page/2.html @@ -5,13 +5,13 @@ "Spring" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "Spring" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 12분
    누누

    안녕하세요 카페인팀 nunu입니다.

    오늘은 스프링에서 발생한 에러 로그를 슬랙으로 모니터링하는 방법에 대해서 알아보려고 합니다.

    목차는 다음과 같습니다.

    1. 스프링에서 로그를 남기는 방법
    2. Slf4 j의 동작원리
    3. Logback의 동작원리
    4. Logback을 사용해서 슬랙으로 에러 로그를 모니터링하는 방법

    스프링에서 로그는 어떻게 찍을까?

    스프링에서 로그를 찍는 방법은 여러 가지가 있지만, 가장 간단한 방법은 System.out.println()을 사용하는 것입니다.

    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test() {
    System.out.println("test");
    return "test";
    }
    }

    당연하지만, 성능이 안 좋아서 실제 서비스에서는 사용하지 않습니다.

    스프링에서는 Slf4 j를 통해서 로그를 남길 수 있습니다.

    @Slf4j // private final Logger log = LoggerFactory.getLogger(this.getClass()); 와 같다.
    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test() {
    log.info("test");
    return "test";
    }
    }

    이 코드를 통해서 로그를 남길 수 있는데, 자동으로 콘솔에 출력이 됩니다.

    스프링에서 로깅은 어떻게 작동하는 거지?

    스프링 4까지는 Commons Logging을 사용했었습니다.

    Commons LoggingJCL이라고도 불리며, JDK Logging, Log4 j, Logback 등 다양한 로깅 프레임워크를 지원합니다.

    JCL 은 런타임에 어떤 로깅 프레임워크를 사용할지 결정할 수 있습니다.

    런타임에 어떤 로깅 프레임워크를 사용할지 결정하는 방식으로 클래스 로더에게 질의를 하는 방식으로 작동하게 되는데

    클래스 로더에게 질의를 했을 경우에 몇 가지 문제점이 생깁니다

    1. 클래스 로더에 명확한 표준이 없고, 부모 자식 모델이 있어서, 클래스 로더에 따라서 다른 결과가 나올 수 있습니다. 참고
    2. 클래스로더는 gc의 동작에 방해를 일으켜서 메모리 누수를 발생시킬 수 있습니다. 참고

    @Slf4j 어노테이션을 붙이면, 컴파일 시점에 private final Logger log = LoggerFactory.getLogger(this.getClass()); 와 같은 코드로 변환됩니다.

    스프링 5에서는 Slf4j 가 사용하는 것처럼, 컴파일 타임에 어떤 로깅 프레임워크를 사용할지 결정하는 기능을 작성했고, Commons Logging을 사용하지 않게 되었습니다.

    spring 5에서 변경되었다는 링크

    Slf4 j에 대해서 알아보자

    Slf4 j는 로깅을 위한 인터페이스를 제공하는 프레임워크입니다.(Simple Logging Facade for Java)

    컴파일 타임에, 어떤 로그 라이브러리를 사용할지 결정하는 기능을 제공합니다.

    로그 라이브러리를 바꾸려고 했을 때, 기존 코드는 하나도 건드리지 않고, 로그 라이브러리만 바꿔주면 되도록 해줍니다.

    조금 더 자세한 동작 원리를 알아보자

    only slf4j

    Slf4 j 만을 사용했을 경우 위 사진 같은 형태로 요청이 처리가 됩니다.

    Slf4 j 라는 인터페이스를 통해서 로그를 남기고, 어떤 로그 라이브러리를 사용할지는 Slf4j binding이라는 것을 통해서 결정합니다.

    Slf4j bindingSlf4j의 인터페이스를 구현하고 있지 않은 라이브러리의 구현체를 연결해 주는 역할을 합니다.

    그 구현체로 Slf4j-log4 j12-{version}. jar 같은 것이 있다.

    이와는 다르게 Logback 은 Slf4 j 를 구현하고 있기에, Slf4j binding 을 사용하지 않아도 됩니다.

    logback example

    위 사진처럼 Slf4j binding 을 사용하지 않고, Logback 바로 사용하는 것도 가능합니다.

    그렇다면 Slf4 j를 바로 사용하지 않은 코드에서 Slf4j 를 사용하려면 어떻게 해야 할까요?

    slf4j working principle

    위 사진처럼 Slf4j bridge 를 통해서 외부 라이브러리를 사용하는 것처럼 갈아 끼울 수 있습니다.

    Log4j2 를 사용하는 코드를 전혀 바꾸지 않아도, BridgeSlf4j 를 통해 Logback으로 자연스럽게 로그를 남길 수 있도록 해줍니다.

    Logback에 대해서 알아보자

    Logback 은 스프링에서 기본으로 사용될 만큼 인기 있는 로그 라이브러리입니다.

    logback 동작 과정

    공식문서에서 아주 핵심적인 동작원리를 설명해주고 있는 사진이라서 가져왔습니다.

    너무 어려워 보여서, 조금 자세하게 각각의 구성요소에 대해서 알아보도록 하겠습니다

    이에 대해 알아보도록 하겠습니다

    로그백의 구성요소

    Appender

    Appender는 로그를 어디에 출력할지를 결정하는 역할을 합니다.

    외부로부터 어떤 데이터를 받아서, 어떤 방식으로 처리할지에 대해서 전체적으로 설정할 수 있습니다.

    기본적으로 수많은 Appender 가 제공되고 있습니다.

    • ConsoleAppender
    • FileAppender
    • RollingFileAppender
    • AsyncAppender
    • DBAppender
    • SMTPAppender
    • SocketAppender
    • SyslogAppender

    저희는 Slack에 알림을 주는 것이 목적이기 때문에, SlackAppender를 사용하면 될 것 같습니다.

    하지만 SlackAppender는 제공되고 있지 않기에 직접 구현을 해야 하는데요

    이를 구현했을 때, Slack API 가 끝날 때까지, 계속 기다리고 있을 필요가 없기에, AsyncAppender를 사용하는 것이 좋을 것 같습니다.

    사용 방법은 다음과 같습니다. xml 기반으로 가능한데요

    <configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>myapp.log</file>
    <encoder>
    <pattern>%logger{35} -%kvp -%msg%n</pattern>
    </encoder>
    </appender>

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE" />
    </appender>

    <root level="DEBUG">
    <appender-ref ref="ASYNC" />
    </root>
    </configuration>

    만약 여기에 있는 기능들로 부족하다면, 직접 Appender 를 구현해서 사용할 수도 있습니다.

    직접 구현하려면 AppenderBase를 상속받아서 구현하면 됩니다.

    이 클래스는 필요한 부분이 대부분 구현되어 있고, appender 만 구현하면 바로 사용할 수 있습니다. 당연하지만 필요하다면 override 도 가능하죠

    Layout

    Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 합니다.

    Appender는 로그를 어디에 출력할지를 결정하는 역할을 하고, Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 하도록 하는 것이 이상적이지만

    Logback 은 Appender에서 Layout 을 직접 지정할 수 있도록 해주고 있습니다.

    따라서, 직접 Layout 을 만들지 않고, Appender 에서 기존에 이미 있는 패턴만 사용하려고 합니다

    Encoder

    Encoder는 Layout 과 비슷한 역할을 합니다.

    Layout 은 로그를 어떤 형식으로 출력할지를 결정하는 역할을 하고, Encoder 는 실제 byte 형태로 변환하는 역할을 합니다.

    Slack의 webhook을 사용할 것이지만, AppenderBase를 사용하기에, 이번에는 사용할 수 없습니다.

    Filter

    Filter는 로그를 어떤 조건에 따라서 출력할지를 결정하는 역할을 합니다.

    Filter 는 Appender를 등록하며 같이 등록할 수 있는데요

    이번 프로젝트에서는 Level 이 ERROR 이상인 것만 출력하도록 하고 싶기에, LevelFilter를 사용하면 좋을 것 같습니다.

    <configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>INFO</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
    </filter>
    <encoder>
    <pattern>
    %-4relative [%thread] %-5level %logger{30} -%kvp -%msg%n
    </pattern>
    </encoder>
    </appender>
    <root level="DEBUG">
    <appender-ref ref="CONSOLE" />
    </root>
    </configuration>

    와 비슷하게 사용할 수 있어 보입니다.

    그러면 실제로 프로젝트에서 error 발생 시 slack으로 알림을 주는 것을 구현해 보도록 하겠습니다.

    슬랙에 추가하는 방법

    이 블로그를 보고서 작성했습니다

    실제 구현

    구현된 결과물은 아래와 같습니다

    slack appender

    SlackAppender 구현하기

    public class SlackAppender extends AppenderBase<ILoggingEvent> {

    @Override
    protected void append(final ILoggingEvent eventObject) {
    final var restTemplate = new RestTemplate();
    final var url = "https://hooks.slack.com/services/";
    final Map<String, Object> body = createSlackErrorBody(eventObject);
    restTemplate.postForEntity(url, body, String.class);
    }

    private Map<String, Object> createSlackErrorBody(final ILoggingEvent eventObject) {
    final String message = createMessage(eventObject);
    return Map.of(
    "attachments", List.of(
    Map.of(
    "fallback", "요청을 실패했어요 :cry:",
    "color", "#2eb886",
    "pretext", "에러가 발생했어요 확인해주세요 :cry:",
    "author_name", "car-ffeine",
    "text", message,
    "fields", List.of(
    Map.of(
    "title", "우선순위",
    "value", "High",
    "short", false
    ),
    Map.of(
    "title", "서버 환경",
    "value", "local",
    "short", false
    )
    ),
    "ts", eventObject.getTimeStamp()
    )
    )
    );
    }

    private String createMessage(final ILoggingEvent eventObject) {
    final String baseMessage = "에러가 발생했습니다.\n";
    final String pattern = baseMessage + "```%s %s %s [%s] - %s```";
    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    return String.format(pattern,
    simpleDateFormat.format(eventObject.getTimeStamp()),
    eventObject.getLevel(),
    eventObject.getThreadName(),
    eventObject.getLoggerName(),
    eventObject.getFormattedMessage());
    }
    }

    이 과정에서 url을 직접 입력하시면 됩니다.

    그리고, 이렇게 만든 SlackAppender를 logback-spring.xml 에 등록하면 됩니다.

    <?xml version="1.0" encoding="UTF-8"?>

    <configuration scan="true" scanPeriod="60 seconds">
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <include resource="org/springframework/boot/logging/logback/file-appender.xml"/>
    <root level="INFO">
    <appender-ref ref="FILE"/>
    <appender-ref ref="CONSOLE"/>
    </root>
    <appender name="SLACK_APPENDER" class="racingcar.SlackAppender">
    </appender>
    <appender name="ASYNC_SLACK_APPENDER" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="SLACK_APPENDER"/>
    </appender>
    <logger name="racingcar" level="ERROR" additivity="true">
    <appender-ref ref="ASYNC_SLACK_APPENDER"/>

    </logger>

    </configuration>

    이렇게 하면, racingcar 패키지에서 에러가 발생할 때만 slack으로 알림을 받을 수 있습니다.

    결론

    slack appender

    이번 글에서는 log 레벨에 따라 slack 으로 알림을 받는 방법을 알아보았습니다.

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/tags/spring/page/3.html b/tags/spring/page/3.html index 568a8f8..fc15519 100644 --- a/tags/spring/page/3.html +++ b/tags/spring/page/3.html @@ -5,13 +5,13 @@ "Spring" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "Spring" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    누누
    박스터

    안녕하세요 카페인팀 누누입니다

    이번에는 대량의 데이터를 DB에 넣는 과정을 최적화하는 과정에서 알게 된 내용을 공유하려고 합니다

    이번 최적화의 목표

    전기차 충전소에 대한 공공 데이터를 가져오고, 그 데이터를 DB 에 넣는 과정을 최적화해보자

    대량의 데이터를 삽입하는 과정

    저희 팀의 요구사항을 간단하게 정리하면 다음과 같습니다

    1. 대량의 데이터를 공공 데이터에서 전기차 충전소와 전기차 충전기에 대한 데이터를 가져온다
      • 충전소는 6만 개, 충전기는 23만 개의 데이터가 존재한다.
      • 한 번에 가져올 수 있는 양은 9999개 까지다.
    2. 이 데이터를 DB에 넣는다
      • 충전소와 충전기는 1:N 관계이다

    최적화 전은 어떤 상황이었는데?

    before_optimize

    위 사진을 잘 보시면 아실 수 있으시겠지만, 2000개를 저장하는데, 231.762 초가 사용되었습니다.

    물론 출력을 위한 시간도 포함되었기에, 230초 정도라고 생각하셔도 좋습니다

    1만 개라면? 231.762초 * 5 = 1,158.81초

    23만 개라면? 1158.81 * 23 = 26,652.63초

    시간으로 바꿔보면 7.4 시간이 걸린다는 것을 볼 수 있습니다

    이 과정에서 볼 수 있는 문제점

    1. 데이터를 저장할 때마다, 새로운 Transaction 이 생성된다.

    어떻게 개선할 수 있을까?

    데이터를 저장할 때마다, 새로운 Transaction 이 생성되는 것을 방지하기 위해, 전체를 하나의 트랜잭션으로 묶는다

    전체를 한 트랜잭션으로 묶은 버전

    all_in_transaction

    이 과정에서 2000개를 저장하는데 65초 가 사용되었습니다.

    1만 개라면? 65초 * 5 = 325초

    23만 개라면? 325초 * 23 = 7,475초

    시간으로 바꿔보면 2시간이 걸린다는 것을 볼 수 있습니다

    전체적으로 3배 정도 빨라졌습니다

    이 과정에서 볼 수 있는 문제점

    1. 23만 개의 저장이 모두 한 트랜잭션이 되어서, 하나가 실패하면 23만개를 새로 저장해야 하는 상황에 처한다

    어떻게 개선할 수 있을까?

    23만개의 저장이 모두 한 트랜잭션이 되는 것을 방지하기 위해, 1만 개씩 영속화시킨다

    1만 개가 한 트랜잭션으로 묶인 버전

    separateTransaction

    성능상으로 개선한 부분은 그렇게 크지 않지만, 실패했을 때, 1만 개만 다시 저장하면 되기에, 훨씬 빠르게 복구가 가능합니다.

    여기서 PageNo라는 클래스는, i를 바로 참조했을 경우, effectively final을 보장할 수 없어서 만들었습니다.

    성능은 전체를 한 트랜잭션으로 묶은 버전과 큰 차이가 나지 않습니다.

    이 과정에서 볼 수 있는 문제점

    1. id 생성 전략이 GenerationType.IDENTITY 이기에, 데이터를 저장할 때마다, DB에서 id를 생성해야 한다.

    JPA에 있는 쓰기 지연을 전혀 활용할 수 없고, DB에서 id를 생성하기 위해, DB와 매번 통신을 해야 한다.

    어떻게 개선할 수 있을까?

    id를 미리 생성해서, DB 에서 id 를 생성하는 과정을 생략한다

    ID 생성 전략을 GenerationType.Table의 형태로 바꿔서, DB에서 id를 생성하는 과정을 줄여서, 성능을 개선한다

    1만 개가 한 트랜잭션으로 묶이고, id를 미리 생성한 버전

    이때 batch size를 1000 단위로 설정해서 1000개씩 id 가 늘어나도록 설정했다

    charger_generatorstation_generator

    spring.jdbc.template.fetch-size=10000

    10000batch_size

    1자리 숫자는 앞에서부터 n(만개)를 의미하고, 2번째 숫자는 1만 개를 저장하는 데 걸린 시간(ms)을 의미합니다.

    처음 1만 개는 142초가 걸리고, 2만 개는 285초가 걸렸습니다.

    23만 개라면? 142 * 26 = 3,266초

    처음과 비교하자면 7.4시간이 걸리는 것에서 54분 정도 걸리는 것으로 개선되었습니다.

    이 과정에서 볼 수 있는 문제점

    하나의 스레드에서만 동작하기에, 성능이 개선되었지만, 여전히 느립니다.

    하나의 스레드에서만 동작하기에, 하나의 커넥션을 사용하게 됩니다.

    어떻게 개선할 수 있을까?

    여러 스레드에서 동작하게 하고, 여러 커넥션을 사용하게 합니다.

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전

    multi_thread

    이 버전에서 89991 개를 저장하는데 총 157초가 걸렸습니다.

    23만 개라면? 157 * 3 = 471초

    시간으로 바꿔보면 5분도 채 걸리지 않는 시간이죠

    이 과정에서 볼 수 있는 문제점

    hikari connection pool 사이즈를 10으로 설정했는데, 10개의 커넥션을 사용하면서 저장을 하다 보니, 10개의 커넥션을 모두 사용하고 나서, 11번째부터는 커넥션을 가져오기 위해, 기다려야 하는 상황이 발생합니다.

    어떻게 개선할 수 있을까?

    hikari connection pool 사이즈를 25로 설정해서, 25개의 커넥션을 사용하도록 합니다.

    spring.datasource.hikari.maximum-pool-size=25

    여러 스레드에서 동작하고, 여러 커넥션을 사용하는 버전 2

    multi_thread2

    총 13만 개의 데이터를 저장하는데, 147초가 걸리고, db 인스턴스의 cpu 사용률이 100%에 가까워져서 ec2 가 다운되었습니다.

    이 과정에서 볼 수 있는 문제점

    db의 cpu 사용량을 고려하지 않고, 23만 개가 조금 넘는 데이터를 25개의 커넥션을 활용해 저장하려고 했습니다

    결론

    1. 데이터를 저장할 때마다, transaction을 사용하지 말자
    2. 데이터를 저장할 때마다, id를 생성하지 말자
    3. 여러 스레드에서 동작하고, 여러 커넥션을 사용하자
    4. db의 cpu 사용량을 고려하자

    긴 글 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git a/tags/styled-components.html b/tags/styled-components.html index f158f52..883f0a9 100644 --- a/tags/styled-components.html +++ b/tags/styled-components.html @@ -5,13 +5,13 @@ "styled-components" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "styled-components" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 2분
    야미

    왜 styled-components인가?


    여러 CSS-in-JS 중 styled-components를 선택한 이유는 다음과 같다.

    1. 컴포넌트 안에 관련 CSS를 작성할 수 있어 컴포넌트별 디자인 코드 확인 및 수정이 용이하다.

    2. 혹자는 코드 가독성이 안 좋아진다고도 하지만, 개인적으로는 태그를 더 시맨틱 하게 작성할 수 있어서 좋다고 느꼈다.

    3. 팀원들 모두 styled-components가 익숙하다.

    4. 지금까지 사용하면서 불편한 점을 못 느꼈다.


    styled-components와 emotion은 기능도, 작성법도 상당히 유사하다.

    그래서 이번에는 styled-components 대신 emotion을 써볼까도 생각했었다.

    하지만 emotion에서만 사용 가능하던 *CSS Props라는 편리한 기능을

    styled-components(v5.2.0 이상)에서 쓸 수 있게 되기도 했고,

    '새로운 기술 공부를 해보면 좋을 것 같다'는 이유를 제외하고는

    딱히 emotion을 사용할 필요성을 못 느껴 styled-components를 채택했다.

    // *CSS Props 예시

    const buttonStyle = css`
    font-size: 18px;
    color: white;
    background: black;
    `;

    const ClickButton = styled.button<{ css: CSSProp }>`
    width: 100px;

    ${({ css }) => css}
    `;

    <ClickButton css={buttonStyle}>Click me!</ClickButton>;
    - + \ No newline at end of file diff --git a/tags/subnet.html b/tags/subnet.html index 631361d..eec06c6 100644 --- a/tags/subnet.html +++ b/tags/subnet.html @@ -5,13 +5,13 @@ "subnet" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "subnet" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    어떤 문제가 있었나요?

    우아한테크코스에서 private 서브넷에 db 인스턴스를 두고, 보안을 위해 외부에서 접속을 차단하려고 했습니다.

    이 과정에서 총 2가지의 문제점이 있었습니다.

    1. private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었습니다.
    2. public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안되었습니다.

    이 부분을 어떻게 해결했는지 알아보도록 하겠습니다.

    아래의 모든 설명은 AWS 를 기준으로 합니다.

    private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었다.

    해결 방법

    public ip 자동할당을 해주지 않아서, 인터넷에 연결이 안 되었습니다.

    이를 해결하기 위해 public ip 자동할당을 해주었습니다.

    왜 public ip를 할당했더니 문제가 해결되었을까요?

    private 서브넷이란?

    정말 간단하게 설명했을 때

    private 서브넷은 인터넷에 연결되지 않은 서브넷입니다.

    조금 자세하게 들어가 보도록 하겠습니다

    private 서브넷은 인터넷 게이트웨이가 연결되지 않은 서브넷입니다.

    aws 공식문서에서 사진을 통해 보면 아래와 같이 되어있습니다

    private subnet

    public 서브넷에만 인터넷 게이트웨이가 연결되어 있고, private 서브넷에는 인터넷 게이트웨이가 연결되어있지 않습니다.

    private 서브넷에 인터넷 게이트웨이가 연결되어 있지 않다고 했을 때, 기본적으로 인터넷에 접속이 안됩니다.

    mysql을 설치할 때도, 인터넷에 접속을 해야하는데, 인터넷에 접속이 안되니 설치가 안되는 것입니다.

    어? 인터넷 자체가 접근이 안되면 어떻게 설치하나요?

    정말 원시적으로 해결하기 위해서는 public 서브넷에 인스턴스를 하나 더 만들어서, mysql 을 압축해서 scp를 통해 private 서브넷에 있는 인스턴스에 전송하고, 압축을 풀어서 설치하는 방법이 있습니다.

    하지만 이 방법은 너무 원시적이고, 비효율적입니다.

    그래서 인터넷으로 요청을 보낼 수 있도록 만드는 과정이 필요합니다.

    인터넷으로 요청을 보낼 수 있도록 만드는 과정

    인터넷으로 요청을 보낼 수 있도록 만드는 과정은 크게 2가지가 있습니다.

    private 서브넷을 public 서브넷으로 바꾸기

    보안을 위해서 private 서브넷에 두려고 했던 것을 public 서브넷으로 바꾼다는 부분은 매우 위험합니다.

    그래서 이 방법은 보통 사용하지 않습니다.

    NAT 인스턴스(Gateway) 만들기

    NAT 인스턴스는 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들어주는 인스턴스입니다.

    인터넷에 접속을 하기 위해서는 public ip 가 필요합니다.

    따라서 NAT 인스턴스, NAT 게이트웨이는 public 서브넷에 존재해야 합니다.

    어? NAT 인스턴스를 통해서 바로 통신이 가능하면 왜 private 서브넷이 필요한가요? 그냥 다 public 서브넷에 두면 되지 않나요?

    NAT 인스턴스, NAT Gateway는 내부에서 출발한 트래픽만 통과할 수 있도록 설정이 되어있습니다.

    예를 들면 private 서브넷에 인스턴스에 접속해서 직접 mysql download 요청을 했을 때만 허용이 됩니다.

    외부에서 바로 private 인스턴스로 접근할 수는 없습니다.

    NAT 인스턴스만 설정을 하면 바로 연결이 되나요?

    public ip도 자동 할당을 해줘야 합니다

    public ip 가 필요한 이유

    NAT 인스턴스를 통해서 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들었는데, 왜 public ip 가 필요할까요?

    외부 인터넷과 통신을 할 때 public ip 가 필요합니다.

    NAT 인스턴스 혹은 NAT 게이트웨이가 인터넷과 통신할 때, NAT 인스턴스의 public ip + private ip를 통해서 통신을 하지 않습니다.

    내부 인스턴스의 public ip 를 통해서 통신을 하게 되어있습니다.

    따라서 NAT 인스턴스와 내부 인스턴스 모두 public ip 가 필요합니다.

    이 과정을 통해서 1번 문제를 해결할 수 있었습니다.

    이제 2번째 문제를 해결해 보도록 하겠습니다.

    public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안 되는 문제

    public 서브넷에 있는 서버가 private 서브넷에 있는 서버에 접속을 하려고 했는데, 접속이 안 되는 문제가 있었습니다.

    해결 방법

    해결 방법에는 2가지 과정이 있습니다.

    public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해 주기

    기본적으로 public 서브넷에 있는 인스턴스의 보안 그룹에는 private 서브넷에 있는 인스턴스의 보안 그룹이 추가되어있지 않습니다.

    따라서 public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.

    private ip를 통해서 접속하기

    public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속할 때, public ip 를 통해서 접속을 하면 안 됩니다.

    public ip를 통해서 접속하는 과정을 자세하게 알아보겠습니다.

    1. public 서브넷에 있는 인스턴스가 public ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
    2. 라우팅 테이블에서 public ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
    3. 라우터를 통해서 외부 인터넷으로 나가게 됩니다.
    4. 트래픽이 NAT 인스턴스에 도착합니다.
    5. NAT 인스턴스는 내부에서 출발한 트래픽이 아니기 때문에, 트래픽을 거부합니다.

    이 과정이 일어나기에, public ip 를 통해서 접속을 하면 안 됩니다.

    private ip를 통해서 접근하면 어떻게 되는지 알아보겠습니다

    1. public 서브넷에 있는 인스턴스가 private ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
    2. 라우팅 테이블에서 private ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
    3. 라우터를 거쳐서 private 서브넷의 라우터로 이동합니다.
    4. private 서브넷의 라우터는 private 서브넷에 있는 인스턴스에게 트래픽을 전달합니다.
    5. private 서브넷에 있는 인스턴스는 트래픽을 받아서 처리합니다.

    이 과정을 통해서 2번 문제를 해결할 수 있었습니다.

    요약

    1. private 서브넷에 있는 인스턴스가 인터넷에 접속을 하려면 NAT 인스턴스 혹은 NAT 게이트웨이가 필요합니다.
    2. private 서브넷에 있는 인스턴스도 public ip 가 필요합니다.
    3. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 하려면 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.
    4. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 할 때, private ip 를 통해서 접속을 해야 합니다.
    - + \ No newline at end of file diff --git a/tags/tanstack-query.html b/tags/tanstack-query.html index 076bbd8..1efa9b1 100644 --- a/tags/tanstack-query.html +++ b/tags/tanstack-query.html @@ -5,13 +5,13 @@ "tanstack query" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "tanstack query" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    가브리엘

    안녕하세요? 카페인 팀 FE에서 상태관리 라이브러리를 어떻게 해야할 지 고민 끝에 서드파티 라이브러리가 필요하게 되어 글을 작성하게됐습니다.

    서버 상태와 클라이언트 상태의 구분

    서버상태와 UI상태를 이해하는 것은 굉장히 중요했습니다. 데이터를 송수신하는 작업과 상태를 관리하는 작업은 유기적으로 동작해야했습니다. 기존에는 상태와 데이터 송수신 과정을 분리해서 생각했다면, 현대의 react 프로젝트들은 서버와 동기화를 해야할 상태그렇지 않은 상태로 분리해서 생각해야 합니다.

    React에서 어떤 데이터를 상태로 다뤄야 하는가에 대해서는 여러 의견이 나올 수 있다고 생각하지만 상태가 특성을 가지고 있는가에 대해서는 대부분 특성이 있다고 동의할 것입니다. 이 글에서는 React의 상태란 무엇인가?에 대해서 다루지 않고 React의 상태의 특성에 대해서만 언급을 하려고 합니다.

    상태의 특성으로는 크게 두 가지가 있습니다.

    클라이언트 상태

    클라이언트 상태는 컴포넌트들 간에 어떤 값을 공유해야하면서 오로지 React DOM 내부에서만 CRUD가 일어나는 상태를 의미합니다. 이 상태들은 React DOM 외부 세계와 크게 관련이 없으며 동기적으로 반영됩니다. 대표적으로는 UI를 조작하는 상태들이 될 것입니다. 클라이언트 상태들은 대부분 장기적으로 유지될 필요가 없기에 화면을 벗어나거나 세션이 끊기는 경우 사라져도 괜찮은 경우가 많습니다.

    서버 상태

    서버 상태는 React의 바깥 세상(서버)에 존재하는 데이터가 React의 상태 관리와 비동기적으로 동기화 된 것을 의미합니다. 어떤 상태가 외부에서 관리되는 데이터와 반드시 연동되어야 한다면 이는 곧 서버 상태임을 의미합니다. React의 상태를 CRUD 하는 것 뿐만 아닌, 서버에서도 항상 같은 일이 일어나야 합니다. 서버 상태는 장기적으로 유지되어야 하며, 세션에서 벗어나더라도 서버로 부터 복구를 해야 합니다.

    기존의 상태 관리 라이브러리들은 리액트의 전역에서 상태를 조작하는 것에 특화되어있고, 비동기적인 상태 관리도 지원하여 서버와의 통신이 가능합니다. 하지만 대부분의 라이브러리들은 클라이언트 상태를 조작하는 것에 초점이 맞춰져있습니다.

    더군다나 클라이언트 상태와 서버 상태가 하는 일이 명확하게 다른 상황에서 이 둘을 한 곳에서 관리하는 것 보다는 완벽하게 분리하는 것이 더 나을 것입니다. 따라서 서버 상태를 관리하는 것에 중점을 둔 라이브러리들이 등장하였습니다. 대표적인 라이브러리로는 RTK Query, Tanstack Query, SWR 등이 있습니다.

    왜 Tanstack Query였나?

    vs RTK Query

    RTK Query는 RTK를 반드시 사용해야 하는 것은 아니지만 RTK를 타겟으로 나온 서버 상태 관리 라이브러리입니다. 카페인 팀에서는 클라이언트 상태를 관리하기 위해 라이브러리를 사용하지 않습니다. 더욱이 Redux의 복잡한 코드 구성과 방대한 보일러 플레이트는 매력적이지 않았습니다. tanstack query에서는 무한 데이터 페칭을 지원하기 위해 Infinite Queries가 있지만 RTK Query는 그렇지 않았습니다.

    vs SWR

    SWR도 하나의 좋은 선택지였지만, 전역 상태 관리 라이브러리들이 범용적으로 지원하는 셀렉터 기능을 지원하지 않았습니다. 또, 가비지 컬렉터의 부재도 아쉬웠습니다. 재요청을 하기 위한 stale time 설정이나 쿼리 취소 기능이 없는 점도 매력적이지 않았습니다.

    카페인 팀에서 하려는 일은요…

    저희 카페인 팀의 프로젝트는 실시간 전기자동차 충전소 지도 및 사용 통계 조회 서비스 로 지도 기반의 프로젝트입니다. 서버 상태를 적극적으로 다뤄야 하는 상황에서 Tanstack Query를 서버 상태 관리 라이브러리로 선정하게 됐습니다.

    메인 기능 중 Tanstack Query가 핵심으로 사용될 것 같은 기능은 다음과 같습니다.

    • 지도에서 충전소 조회
      • 현재 접속한 클라이언트에 렌더링 된 지도 화면(디스플레이)의 크기에 따른 GPS좌표를 알아내어 서버로 부터 충전소 정보를 수신 받습니다. 즉, 화면이 이동하게 되면 사용자가 바라보고 있는 영역이 변하므로 새로운 요청을 보내게 됩니다.
      • 서버에서 수신한 충전소 정보는 실시간 사용 현황도 반영되어있으므로 주기적인 업데이트도 필요합니다.
      • 빈번한 데이터의 변화가 필요하며 그만큼 통신 실패 등 에러가 발생할 가능성도 많아지게 됩니다.
      • 사용자의 빠른 지도 이동이 발생하는 경우를 대응할 수 있어야 합니다.
    • 전국 충전소 검색기
      • 원하는 충전소 검색을 하는 기능을 지원합니다. 전국 단위로 검색 결과를 수신하는 기능입니다.
      • 네이버와 구글 검색창 처럼 사용자가 input 창에 검색어를 입력할 때 마다 검색 결과가 동적으로 표시되어야 합니다.
      • 빈번한 데이터의 변화가 필요하고, 사용자의 빠른 타이핑으로 인해 잦은 검색이 발생하는 경우를 대응할 수 있어야 합니다.
      • 이를 위해 데이터를 캐싱할 필요도 있다고 생각합니다.

    프로젝트에서 클라이언트와 서버와의 통신이 어쩌다 한번 일어난다면 굳이 라이브러리가 필요가 없겠지만, 서버의 데이터 전적으로 의존해야 하는 저희 프로젝트 특성상 Tanstack Query의 여러 기능이 생산성에 많은 도움이 될 것으로 기대합니다.

    - + \ No newline at end of file diff --git a/tags/test.html b/tags/test.html index f5ea3ea..3861301 100644 --- a/tags/test.html +++ b/tags/test.html @@ -5,7 +5,7 @@ "test" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -22,7 +22,7 @@ 하지만 Storybook을 이용하면 특정 컴포넌트를 Storybook 위에 올려놓고 테스트를 할 수 있어 빠르게 작업이 가능합니다. 인터렉션이나 웹접근성을 확인해주는 플러그인도 존재하여 프론트엔드 개발에서 굉장히 중요한 역할로 부상했습니다.

    저희 팀은 이외에 Cypress를 사용하는 것도 고려하였으나, 지도와 결합된 애플리케이션을 테스트하기에 다소 어려움이 있어 위 라이브러리들을 개발에 활용했습니다.

    저희는 위 테스팅 라이브러리들을 원활히 활용하기 위해 테스트 자동화를 구축했습니다.

    Jest와 React Testing Library 테스트 자동화

    name: frontend-test

    on:
    pull_request:
    branches:
    - main
    - develop
    paths:
    - frontend/**
    - .github/**

    permissions:
    contents: read

    jobs:
    test:
    name: test-when-pull-request
    runs-on: ubuntu-latest
    environment: test
    defaults:
    run:
    working-directory: ./frontend
    steps:
    - name: Checkout PR
    uses: actions/checkout@v2
    - name: Install dependencies
    run: npm install
    - name: Test
    run: npm run test

    이벤트 트리거 설정

    pull_request 이벤트가 발생하였을 때, 해당 이벤트가 main 브랜치와 develop 브랜치에서만 동작합니다.

    변경 사항 경로 제한

    테스트를 실행할 때는 frontend 디렉토리와 .github 디렉토리 내의 파일들을 고려하도록 했습니다. 백엔드와의 환경 분리를 위해 이러한 접근 제한을 했습니다.

    권한 설정

    permissions은 읽기 권한만 설정되어 있어 코드나 파일을 변경을 방지합니다.

    작업(Job) 설정

    test라는 이름의 작업을 정의하였고, 이 작업에서는 Ubuntu 환경에서 테스트를 실행합니다. test라는 이름의 환경 변수를 사용합니다. 테스트는 (카페인 팀 레포지토리의) frontend 디렉토리에서 작업하도록 하였습니다.

    스텝(Step) 설정

    코드를 체크아웃하고, 의존성을 설치하며, 테스트를 실행하는 세 가지 단계로 구성되어 있습니다.

    이러한 설정을 통해 PR에 코드가 올라올 때 자동으로 프론트엔드 테스트가 실행됩니다.

    이러한 테스트 자동화 전략은 프론트엔드 애플리케이션을 안정적이게 개발하고 유지할 수 있도록 도와줍니다.

    Storybook의 빌드 자동화

    name: storybook-deploy

    on:
    pull_request:
    branches:
    - develop
    paths:
    - frontend/**
    - .github/**

    jobs:
    build:
    runs-on: ubuntu-22.04
    defaults:
    run:
    working-directory: ./frontend
    steps:
    - name: Setup Repository
    uses: actions/checkout@v3

    - name: Set up Node
    uses: actions/setup-node@v3
    with:
    node-version: 18.16.0

    - name: Install dependencies
    run: npm install

    - name: Cache node_modules
    id: cache
    uses: actions/cache@v3
    with:
    path: '**/node_modules'
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
    ${{ runner.os }}-node-

    - name: storybook build
    run: npm run build-storybook

    - name: Upload storybook build files to temp artifact
    uses: actions/upload-artifact@v3
    with:
    name: Storybook
    path: frontend/storybook-static
    deploy:
    needs: build
    runs-on: self-hosted
    steps:
    - name: Remove previous version app
    working-directory: .
    run: rm -rf dist

    - name: Download the built file to AWS
    uses: actions/download-artifact@v3
    with:
    name: Storybook
    path: frontend/dev/dist

    - name: Move folder
    working-directory: frontend/dev/
    run: |
    rm -rf /home/ubuntu/dist/*
    cp -r ./dist /home/ubuntu

    - name: comment PR
    uses: thollander/actions-comment-pull-request@v1
    env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    with:
    message: '🚀storybook: https://storybook.carffe.in/'

    비슷한 코드이지만, 매번 PR이 열릴 때 마다 스토리북이 자동으로 빌드 및 배포됩니다. 배포가 완료되면 배포된 URL을 알려 코드 리뷰할 때 참고할 수 있도록 돕습니다.

    이상 카페인 팀에서 사용하고 있는 테스팅 라이브러리와 테스트 자동화 방법을 알아봤습니다.

    - + \ No newline at end of file diff --git a/tags/test/page/2.html b/tags/test/page/2.html index 9bac5fe..eaabbe5 100644 --- a/tags/test/page/2.html +++ b/tags/test/page/2.html @@ -5,7 +5,7 @@ "test" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -17,7 +17,7 @@ 아까 environment 속성을 보면 test라고 설정해놓은 것을 볼 수 있습니다. 해당 환경이 여기에 적용됩니다.

    Branch rule 정의하기

    이번에는 해당 Repository의 Settings -> Branches 탭으로 들어갑니다. 그리고 원하는 branch에 들어가 edit 버튼을 누릅니다.

    그리고 사진과 같이 Require deployments to succeed before merging 속성을 클릭합니다. 그리고 아래와 같이 어떤 환경을 적용할 것인지 선택할 수 있습니다.

    이 속성은 해당 배포가 성공해야 merge 할 수 있도록 브랜치를 보호하는 기능입니다.

    그리고 저희는 frontend와 backend Job의 환경을 둘 다 test라는 이름으로 정의했기 때문에 하나의 environment만 선택해도 둘 다 적용되는 효과를 볼 수 있습니다. branch rule

    적용 후

    아래와 같이 merge가 안된다는 글과 빨간색으로 경고 표시를 해주고 있습니다. blocked

    결론

    간단한 github action을 통해서 생산성을 많이 올릴 수 있는 좋은 기능인 것 같습니다. 다른 팀들도 이 기능을 도입하여 사용하는 것을 추천드립니다.

    - + \ No newline at end of file diff --git a/tags/to-list.html b/tags/to-list.html index 5200b6c..82ea03f 100644 --- a/tags/to-list.html +++ b/tags/to-list.html @@ -5,13 +5,13 @@ "toList" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "toList" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 6분
    누누

    우아한테크코스에서 자바 11을 사용하는 것이 너무 익숙해진 상황이어서, java 11 대신 java 17을 쓰려면 쓰는 대신, 왜 java 17을 쓰면 좋은지에 대해서 설득을 하는 시간이 있어야 하는데요

    처음에는 단순히 record 클래스가 좋아요, collect(Collectors.toList()); 대신 toList() 만으로 해결할 수 있어서 좋아요

    까지밖에 설명할 수 없었습니다.

    이것만으로 동의를 해줘서 일단 java 17 을 사용하기로 했지만, 이번 기회에 조금 더 자세하게 알아보려고 합니다

    Java 17 과 Java 11의 중요한 차이들

    기능적인 부분과, 숨겨진 부분을 나누어볼 수 있을 것 같습니다.

    기능적인 차이점

    언제나 직접 차이를 보면 더 직관적이기 때문에, 직접 코드를 보면서 설명을 해보려고 합니다

    record 클래스

    간단한 dto 클래스를 만들었을 때 코드가 정말 간단해지는 것을 확인할 수 있습니다

    Java 11

    public class Dto {
    private final int data;

    public Dto(int data) {
    this.data = data;
    }

    public int getData() {
    return data;
    }
    }

    lombok 을 사용했을 때


    @Getter
    @AllArgsConstructor
    public class Dto {
    private final int data;
    }

    Java17

    public record Record(int data) {
    }

    이렇게 보면 훨씬 간단해진 것을 볼 수 있습니다

    예상되는 문제점

    objectMapper를 사용하면 어떻게 되나요? noArgsConstructor 가 필요하지 않나요?

    class RecordTest {

    @Test
    void objectMapper_로_변환() throws JsonProcessingException {
    // given
    ObjectMapper objectMapper = new ObjectMapper();
    Record record = new Record(1);

    // when
    String json = objectMapper.writeValueAsString(record);

    // then
    assertEquals("{\"data\":1}", json);
    }

    @Test
    void string_에서_객체로_변환() throws JsonProcessingException {
    // given
    String json = "{\"data\":1}";
    ObjectMapper objectMapper = new ObjectMapper();

    // when
    Record record = objectMapper.readValue(json, Record.class);

    // then
    assertEquals(1, record.data());
    }
    }

    이 테스트에서 볼 수 있는 것처럼 성공적으로 deserialize, serialize 가 가능합니다

    toList() method

    Java 11

    이 부분도 정말 편의성이 높다고 생각하는 부분 중 하나인데요

    Collectors.toList() 대신 toList() 만으로도 사용이 가능합니다

    public class ToListWith11 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .collect(Collectors.toList());
    System.out.println(result);
    }
    }

    Java 17

    public class ToListWith17 {

    public static void main(String[] args) {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    List<Integer> result = list.stream()
    .filter(i -> i > 3)
    .toList();
    System.out.println(result);
    }
    }

    switch expression

    Java 11

    우테코에서는 switch, case 를 싫어하기에 볼 수는 없겠지만

    switch 문에도 정말 편하게 바뀌었는데요

    public class SwitchWith11 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = 0;
    switch (day) {
    case "Monday":
    result = 1;
    break;
    case "Tuesday":
    result = 2;
    break;
    case "Wednesday":
    result = 3;
    break;
    case "Thursday":
    result = 4;
    break;
    case "Friday":
    result = 5;
    break;
    case "Saturday":
    result = 6;
    break;
    case "Sunday":
    result = 7;
    break;
    }
    System.out.println(result);
    }
    }

    Java 17

    public class SwitchWith17 {

    public static void main(String[] args) {
    String day = "Sunday";
    int result = switch (day) {
    case "Monday" -> 1;
    case "Tuesday" -> 2;
    case "Wednesday" -> 3;
    case "Thursday" -> 4;
    case "Friday" -> 5;
    case "Saturday" -> 6;
    case "Sunday" -> 7;
    default -> 0;
    };
    System.out.println(result);
    }
    }

    코드 량이 엄청 줄어든 것을 확인하실 수 있습니다

    instanceof pattern matching

    물론 instanceof 를 사용할 경우가 많은가? 하면 많지는 않겠지만

    아래와 같이 변경되었습니다

    Java 11

    public class InstanceOfWith11 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.toUpperCase());
    }
    }
    }

    Java 17

    public class InstanceOfWith17 {

    public static void main(String[] args) {
    Object obj = "Hello";
    if (obj instanceof String str) {
    System.out.println(str.toUpperCase());
    }
    }
    }

    number format

    이 기능은 12에 나왔는데요

    언어별로 숫자를 표현하는 방식이 다르지만, 쉽게 표현할 수 있도록 도와주는 기능입니다

    Java 17

    public class NumberFormatterWith11 {
    public static void main(String[] args) {
    int number = 1_000_000;

    String result = NumberFormat.getCompactNumberInstance(Locale.KOREA, NumberFormat.Style.LONG).format(number);

    System.out.println(result.equals("100만"));
    }
    }

    나머지 부분은 사실 그렇게 큰 역할을 할 것 같지는 않아서 생략하겠습니다

    숨겨진 부분들

    gc throughput

    위의 사진은 gc 의 버전별 처리량입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 15% 정도 향상되었고, java 11과는 10% 정도 향상되었습니다.

    gc latency

    위의 사진은 gc의 버전별 지연시간입니다.

    G1 GC 를 기준으로 본다면 Java8 과의 차이는 30% 정도 향상되었고, java 11과는 25% 정도 향상되었습니다.

    이와 같이, 단순하게 새로운 기능만 추가되는 것이 아니라 꾸준히 성능도 향상되고 있습니다.

    이런 부분을 고려했을 때, Java 17을 사용하는 것이 좋을 것 같습니다.

    참고

    - + \ No newline at end of file diff --git a/tags/trouble-shooting.html b/tags/trouble-shooting.html index 5c99f96..4752516 100644 --- a/tags/trouble-shooting.html +++ b/tags/trouble-shooting.html @@ -5,7 +5,7 @@ "trouble-shooting" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -31,7 +31,7 @@ 하지만 직접 확인해보기 전까지는 확신할 수 없으니 간단히 Runtime 클래스에서 제공해주는 totalMemory(), freeMemory() 메서드를 통해 알아보겠습니다.

        @Test
    void 페이징을_사용한_조회() {
    List<Station> stations = stationRepository.findAllByOrder(Pageable.ofSize(1000));

    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();
    System.out.println("paging 사용 중인 메모리: " + ((total - free) / 1024 / 1024) + "MB");
    }

    @Test
    void 페이징을_사용하지_않고_조회() {
    List<Station> stations = stationRepository.findAllFetch();

    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();

    System.out.println("findAll() 사용 중인 메모리: " + ((total - free) / 1024 / 1024) + "MB");
    }

    findAll paging 확연히 차이가 나는 것을 확인할 수 있습니다.

    물론 테스트코드에서는 23만건의 API 요청은 같은 조건이니 배제하고 확인했습니다.

    이로써 하나의 문제가 또 해결된 것 같습니다.

    아직 배우는 단계라 혹시 틀린 점이 있다면 지적 부탁드리겠습니다.

    Reference

    - + \ No newline at end of file diff --git a/tags/trouble-shooting/page/2.html b/tags/trouble-shooting/page/2.html index 2d4a3cb..15feb44 100644 --- a/tags/trouble-shooting/page/2.html +++ b/tags/trouble-shooting/page/2.html @@ -5,7 +5,7 @@ "trouble-shooting" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -26,7 +26,7 @@ 트랜잭션을 오래 가지고 있으면 Lock을 가지고 있는 시간이 오래걸립니다. 그래서 트랜잭션을 작게 분리할 수 있습니다. 페이징을 통해 트랜잭션을 작게 분리하다보면 쿼리가 여러번 나가 성능상 문제가 생길 수 있을 것 같습니다.
  • INSERT ~~ ON DUPLICATE KEY UPDATE ~~ 사용하지 않기 해당 sql이 아닌 INSERT IGNORE을 사용하여 추가된 정보만 넣고, update는 다른 작업으로 분리하기
  • 이런 방법들을 사용하면 될 것 같았습니다. 그 중 저는 현재는 간단하게 2번째 방법이 제일 나을 것 같다는 생각에 쿼리를 수정했습니다.

    그리고 문제를 해결했습니다. 해당 문제가 발생하게 되어 좀 더 재밌는 것들을 고민하고 공부할 수 있는 저희 팀에게 감사하고 모르는 키워드를 많이 알려준 누누에게 감사합니다.

    아직 배우는 단계라 정확한 정보가 아닐 수 있습니다. 부족한 부분에 대해 많은 지적 부탁드립니다.

    - + \ No newline at end of file diff --git a/tags/use-sync-external-state.html b/tags/use-sync-external-state.html index a901ec9..dfc3f6d 100644 --- a/tags/use-sync-external-state.html +++ b/tags/use-sync-external-state.html @@ -5,13 +5,13 @@ "useSyncExternalState" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "useSyncExternalState" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 13분
    센트

    1. 개요

    기존의 구조에서는 마커 하나를 렌더링하기 위해 다음과 같은 과정을 거쳤다.

    1. StationMarkersContainer 컴포넌트에서 충전소 정보 요청
    2. 충전소 정보를 props로 넘겨 Marker 컴포넌트 호출
    3. 지도에 부착될 DOM요소 생성
    4. createRoot를 통해 리액트 root 생성
    5. 2번에서 생성한 DOM 요소를 전달해 구글 지도 api의 Marker 생성자 함수 호출
    6. 3번에서 생성했던 root의 render 메서드 호출
    7. 마커 인스턴스 전역 상태에 새로 생성한 마커 추가

    위 과정을 거쳤을 때의 마커 렌더링 모습을 보면 다음과 같다.

    before

    마커들이 한번에 렌더링 되는 것이 아니라 산발적으로 렌더링 되는 모습을 확인할 수 있다.

    2. 문제 원인 분석

    마커를 렌더링 하기 위해 거치는 과정을 분석해 보았다.

    1 ~ 3 과정에서는 성능에 크게 영향을 끼칠 요소가 없지만 4번 과정은 일반적인 리액트 프로젝트를 개발할 때 겪는 과정이 아니다. 따라서 createRoot를 통해 많은 개수의 루트를 생성했을 때의 영향에 대해 알아보았다.

    image

    리액트 공식 문서를 보니 페이지의 일부에 리액트를 뿌려서 사용하는 경우에는 루트를 필요한 만큼 생성해도 된다는 이야기가 포함되어 있었다. 따라서 4번 과정 또한 문제의 원인이라고 볼 수 없었다.

    5번 과정은 구글 지도에 마커를 특정 위도 경도에 위치시키기 위해서 어쩔 수 없이 거쳐야 하는 과정이므로 이 과정은 문제가 있더라도 개선이 불가능해 일단 고려하지 않았다.

    6번 과정은 4번 과정에서 생성했던 리액트 루트의 render 메서드를 호출해 실제로 화면에 리액트 컴포넌트를 그리도록 하는 과정이다. 이 과정 또한 리액트 컴포넌트를 화면에 렌더링하기 위해선 어쩔 수 없이 거쳐야 하는 과정이므로 고려하지 않았다.

    하지만 6번 과정에서 리액트 컴포넌트를 직접 그리는 것이 아니라 구글 지도 api의 기본 마커를 사용하면 성능을 향상시킬 수 있지 않냐고 반문할 수도 있을 것이다. 이전에는 이러한 방식을 사용해 마커를 렌더링 했었다. 우리의 서비스는 현재 사용 가능한 충전소 개수를 마커를 통해서도 전달하기 때문에 이를 고려해 기본 마커를 사용할 때 다음의 두 가지 문제가 생긴다.

    1. 사용 가능한 충전소 개수를 기본 마커에 렌더링 할 때 성능이 매우 좋지 않다.
    2. 마커의 디자인을 바꾸고자 할 때 변경에 대응하기 어렵다.

    따라서 마커는 리액트 루트의 render 메서드를 호출해 리액트 컴포넌트를 렌더링하는 것으로 결정했다.

    마지막으로 남은 7번 과정에서는 useSyncExternalState 훅을 사용해 전역적으로 관리하고 있던 상태에 수정을 가하는 연산을 수행한다. 이 과정은 이전에도 성능 저하를 유발할 것으로 예상되던 부분이었다. (하단 링크 참고)

    useSyncExternalStore 훅을 통해 구독한 state가 한번에 업데이트 되는 이유

    요청의 결과로 받아온 마커 정보의 개수가 100개라고 가정해보자. 우리는 이제 마커를 렌더링 할 것이다. 첫 번째 마커의 렌더링을 위해 1번 ~ 6번의 과정을 거친 후 7번 과정을 수행한다. 그러면 리액트 입장에서는 리액트 루트의 render 메서드 호출에 대한 동작을 수행해야 하고, 새로운 마커 인스턴스에 대한 전역 상태를 변경시키는 동작을 수행해야 한다. 리액트가 이 과정을 100번 반복하고 나면 우리는 비로소 모든 마커가 화면에 렌더링 된 모습을 볼 수 있을 것이다.

    나는 이 부분에서 성능 저하의 요소가 있다고 생각했다. 리액트에서의 상태 변화는 곧 리액트 내부의 렌더링을 위한 로직이 수행되게 함을 의미하고, 이 과정을 개선 이전에는 마커의 개수만큼 반복하고 있었던 것이다. 여기까지 생각해보니 전역 상태 변화에 대해 리액트가 렌더링을 위한 연산을 진행할 동안에는 마커의 렌더링(render 메서드 호출)이 멈추는 것이 아닐까 하는 생각이 들었다.

    그래서 크롬 개발자 도구의 퍼포먼스 탭을 들어가 보니 산발적으로 발생하던 마커 렌더링의 문제 원인이 짐작했던 그 원인임을 확인할 수 있었다.

    image

    프레임 이미지 하단을 보면 산발적인 마커 렌더링이 수행될 때마다 수반되는 어떤 함수 호출이 있음을 확인할 수 있다.

    image

    이 부분이 문제의 함수 호출 부분이다. 자세히 살펴보면 상단에 performWorkUntilDeadline이란 함수가 호출됨을 볼 수 있다.

    image

    performWorkUntilDeadline 라는 함수를 조금 알아보니 해당 함수는 간단히 말해 리액트에서 state의 변경이 한번에 많이 발생할 때 5ms의 데드라인 시간을 줄 때 사용하는 함수라는 것을 알게 되었다. 문제의 원인이라고 생각했던 마커 개수 만큼의 전역 상태 변화가 실제로 마커 렌더링을 잠시 중단하게 만들고 있음을 알게 되었다.

    3. 문제 해결

    앞서 분석한 문제를 개선해보고자 마커 렌더링에 필요한 충전소 정보 배열을 부모 컴포넌트에서 받아와 각 충전소 정보를 자식 컴포넌트에 넘겨주고, 자식 컴포넌트에서 마커 생성과 렌더링 로직을 수행하던 기존의 방식을 부수고 부모 컴포넌트에서 모든 것을 일괄 처리하는 방식으로 고쳐보았다.

    고치는 과정에서 기존 방식에서는 리액트 생명 주기에 의존하여 화면에 보여지지 않는 마커를 지워주던 로직을 이제는 모두 직접 구현해야 했다.

    이전의 영역과 겹치는 부분에 있는 충전소는 다시 그리지 않고, 영역 밖의 충전소를 나타내는 마커는 지워주고, 이전의 영역과 겹치지 않는 새로 받아온 충전소는 그리도록 다음과 같이 메서드를 분리해보았다.

    • 기존과 겹치지 않는 새로운 영역에 대한 마커를 생성하는 메서드
    • 기존과 겹쳐지는 영역에 대한 마커들을 반환하는 메서드
    • 새로운 영역 밖에 있는 마커들을 지워주는 메서드
    • 새롭게 생성된 마커를 화면에 렌더링하는 메서드

    이 메서드들을 커스텀 훅으로 분리해 부모 컴포넌트에서 이를 활용하도록 하여 다소 복잡할 수 있는 마커 렌더링 로직을 선언적으로 구현할 수 있도록 했다.

    결과적으로 기존에 사용되던 기능들을 그대로 사용할 수 있으면서 화면에 마커가 산발적으로 렌더링 되던 문제가 해결 되었고, 부가적인 효과로 전체 마커의 렌더링 시점도 앞당길 수 있게 되었다. + 기존에는 구조적인 문제로 연산량이 너무 많아 클러스터링이 늦어져 이를 도입할 수 없었던 문제를 구조 수정으로 인해 적용할 수 있게 되었다.

    작업한 PR

    https://github.com/woowacourse-teams/2023-car-ffeine/pull/737

    결과 분석 (performance 탭 활용)

    before

    마커 조회 요청이 종료된 시점: 약 2499ms

    image

    첫 마커 렌더링 시점: 3093ms

    image

    모든 마커 렌더링 종료 시점: 약 3611ms

    image

    처음으로 마커가 렌더링 될 때까지 소요된 시간: 594ms

    모든 마커 렌더링에 소요된 시간: 1112ms

    after

    마커 조회 요청의 시작점: 약 1875ms

    image

    모든 마커 렌더링 종료 시점: 2395ms

    image

    처음으로 마커가 렌더링 될 때까지 소요된 시간: 519ms

    모든 마커 렌더링에 소요된 시간: 519ms

    개선 결과

    처음으로 마커가 렌더링 되는 시점은 두 방식 모두 비슷한 결과를 보인다. 하지만 개선 후 방식은 한번에 모든 마커가 렌더링 되는 방식이고, 개선 이전의 방식은 산발적으로 마커가 렌더링 되는 방식이므로 개선 후의 방식에서 전체 마커를 렌더링 하는 시점이 훨씬 빨라지게 되었다.

    결과적으로 전체 마커가 렌더링 되는 속도 약 55.6% 단축하게 되었다. 이 결과는 마커가 늘어날 수록 더욱 차이가 극적으로 벌어질 것으로 예상된다.

    before

    before

    after

    after

    - + \ No newline at end of file diff --git a/tags/use-sync-external-store.html b/tags/use-sync-external-store.html index 2a7c11b..541e23e 100644 --- a/tags/use-sync-external-store.html +++ b/tags/use-sync-external-store.html @@ -5,7 +5,7 @@ "useSyncExternalStore" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -24,7 +24,7 @@ 빨간색은 개발자가 직접 건들지 못하지만 간접적으로 사용할 수 있는 영역 노란색은 React 18 엔진의 영역입니다.

    이외에 제공되는 다른 커스텀 훅들도 거의 비슷한 구조를 띄고 있습니다.

    // 추가로 구현할 수 있는 함수들

    export const useSetExternalState = <T>(store: DataObserver<T>) => {
    const { setState } = store;

    return setState;
    };

    export const useExternalValue = <T>(store: DataObserver<T>) => {
    const { subscribe, getState } = store;
    const state = useSyncExternalStore(subscribe, getState);

    return state;
    };

    // 바닐라JS 영역에서 자연스러운 읽기를 지원하는 함수

    export const getStoreSnapshot = <T>(store: DataObserver<T>) => {
    return store.getState();
    };

    더 다양한 예제는 여기에서 확인할 수 있고 작성한 라이브러리 코드 전문은 여기에서 확인할 수 있습니다.

    겨우 파일 수십 줄로 만든 초경량 상태관리 라이브러리였습니다

    - + \ No newline at end of file diff --git a/tags/use-sync-external-store/page/2.html b/tags/use-sync-external-store/page/2.html index 57cc4ed..0c01699 100644 --- a/tags/use-sync-external-store/page/2.html +++ b/tags/use-sync-external-store/page/2.html @@ -5,13 +5,13 @@ "useSyncExternalStore" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "useSyncExternalStore" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 18분
    센트

    Untitled

    위 이미지는 현재까지 구현한 지도의 모습이다. 구현된 기능은 다음과 같다.

    • 충전소 정보를 서버에 요청해 받아온 충전소 정보를 바탕으로 화면에 마커를 표시하는 기능
    • 화면이 이동하거나 줌인, 줌 아웃을 할 시 화면의 마커 정보가 최신화 되는 기능
    • 마커 정보를 최신화 할 때 화면에서 사라진 마커를 dom에서 제거하는 기능
    • 마커 정보를 최신화 할 때 이전 화면에서도 있었던 마커를 재생성 하지 않는 기능
    • 마커를 클릭했을 시 해당 마커에 대한 간단 정보를 모달로 띄워주는 기능
    • 화면에 표시된 마커들에 대한 충전소 정보를 리스트로 보여주는 기능

    이번에 새로 추가하고자 한 기능은 다음과 같다.

    • 충전소 리스트에서 충전소를 선택하면 화면의 중심이 선택한 충전소 마커로 이동하고, 충전소의 간단 정보를 모달로 띄워주는 기능

    위 기능을 구현하기 위해선 google maps api의 InfoWindow객체를 이용해야 한다. 사용 방식은 다음과 같다.

    const infowindow = new google.maps.InfoWindow({
    content: contentString,
    ariaLabel: 'Uluru',
    });

    const marker = new google.maps.Marker({
    position: uluru,
    map,
    title: 'Uluru (Ayers Rock)',
    });

    infowindow.open({
    anchor: marker,
    map,
    });

    간단하게 요약하자면 다음과 같다.

    • InfoWindow 생성자 함수를 통해 infoWindow 인스턴스를 생성한다.
      • 생성시 dom 요소 혹은 string을 전달해 infoWindow가 생성될 dom위치를 지정해준다.
    • marker 인스턴스를 infoWindow 인스턴스의 open 메서드에 인자로 전달한다.
    • infoWindow 생성 시 전달했던 dom요소의 위치가 marker의 위치로 고정되면서 화면에 그려진다.

    Untitled

    충전소 정보를 보여주는 위 StationList 컴포넌트는 충전소 정보에 접근할 때 react-query를 통해 서버 상태를 직접 내려 받아 컴포넌트 내부 리스트를 렌더링 한다.

    또한, StationMarkersContainer에서도 충전소 정보를 react-query의 서버 상태에서 참조해 마커를 렌더링 하고 있다.

    따라서 StationList 컴포넌트와 StationMarkersContainer는 각각 따로 서버 상태에 접근해 렌더링을 수행하고 있으므로 둘 사이에는 어떠한 연결 고리가 없다.

    여기서 문제가 발생하게 되었다.


    현재까지의 코드에서는 infoWindow인스턴스를 StationMarkersContainer컴포넌트에서 생성한다. 이를 하위 컴포넌트인 StationMarker에 내려주고, 이 컴포넌트 내부에서 marker인스턴스를 생성한다.

    이번에 구현하기로 한 기능은 StationList의 항목 중 하나를 선택했을 시 선택된 충전소에 해당하는 마커에 간단 정보 모달이 뜨며 화면을 해당 마커가 중심으로 오도록 이동 시키는 것이었다.

    하지만 지금의 코드 구조상 StationListStationMarkersContainer사이에는 어떠한 연결 고리도 없으므로 infoWindowmarkerStationList는 접근할 수 없는 상태가 된다.

    이를 해결하기 위해서 다음과 같은 방법을 사용하기로 했다.

    • infoWindow인스턴스를 root 단에서 생성해 전역적으로 관리한다.
    • 생성될 marker 인스턴스들을 배열 형태의 전역 상태로 관리한다.

    위 내용을 말로만 본다면 별로 어려울 것 없어 보이지만 실제 구현을 진행해보니 내부적으로 큰 문제가 두 가지 존재했다.

    1. 따로 모듈을 분리해 infoWindow를 생성할 수 없다.
    2. marker인스턴스를 생성하는 주체가 StationMarkersContainer가 되어서는 안된다.

    각각의 문제점을 살펴보자.


    1. 따로 모듈을 분리해 infoWindow를 생성할 수 없다.

    infoWinodw를 전역 상태로 만들어 사용하기 위해 처음으로 했던 생각은 infoWindowStore.ts로 모듈을 분리하여 infoWindow를 생성해 store의 초기값으로 지정하는 것이었다.

    위 생각을 가지고 그대로 구현해보았더니 google을 참조할 수 없다는 에러가 발생했다. InfoWindow생성자 함수는 google.maps.InfoWindow를 통해 접근할 수 있기 때문에 해당 에러는 infoWindow인스턴스를 생성할 수 없다는 것을 의미했다.

    google을 참조할 수 없는지 이유를 분석해보니 이유는 다음과 같았다.

    우리 팀이 구글 지도 로드를 위해 선택한 라이브러리는 @googlemaps/react-wrapper이다. 이 라이브러리의 동작을 살펴보면 다음과 같다.

    • Wrapper컴포넌트가 @googlemaps/js-loader라이브러리의 Loader생성자 함수를 호출한다.
    • 생성된 loader인스턴스의 load메서드를 실행시켜 지도의 로딩 작업을 시작한다.
      • load 메서드는 최종적으로 Promise<typeof google>을 반환하는데, 지도 로드에 성공하면 resolve(window.google) 을 실행시켜 google을 전역적으로 사용 가능하도록 만들어준다.
    • 지도의 로딩이 완료되면 Wrapperrender props를 통해 받은 콜백 함수를 실행시킨다.
      • render콜백 함수는 로딩 상태를 나타내는 Status를 파라미터로 넘겨 받아 호출된다.

    최종적으로 render를 실행 시켰을 때 반환 되는 컴포넌트에서는 google 로딩 되어 전역적으로 접근이 가능함을 보장할 수 있으므로 이때부터 google에 접근이 가능해진다. → 따라서 Wrapper를 통해 반환되는 컴포넌트의 하위 컴포넌트에서 google.maps.Map생성자 함수를 사용해 지도를 생성할 수 있게 된다.

    infoWindow를 생성하기 위해 만든 새로운 모듈은 첫 import시기에 평가될 것이기 때문에 Wrapper의 하위 컴포넌트에서 import를 수행한다면 로드가 완료된 이후 시점일 것이므로 window.google이 등록되어 google에 접근이 가능할 것으로 예상했다.

    하지만 웹팩을 통한 번들링 과정에서 모듈이 뒤섞여 파일의 평가 시기를 보장할 수 없어져 새로 만든 모듈에서는 google에 대한 접근이 불가능해지게 되었다. 웹팩을 좀 더 공부해본다면 이 문제를 해결할 수 있을 것 같았지만, 너무 지엽적인 부분에서 많은 시간을 들이기 보단 기존에 개발하던 방식을 통해 문제를 해결해보기로 결정했다.

    최종적으로 문제를 해결한 방식은 다음과 같다.

    • InfoWindow생성자 함수를 호출할 CarFfeineInfoWindowInitializer컴포넌트를 만든다.
    • Wrapper로 감싸진 컴포넌트 하위에 CarFfeineInfoWindowInitializer 컴포넌트를 추가한다.
    • google에 접근이 가능한 상태를 보장받은 CarFfeineInfoWindowInitializer내부에서 infoWindow인스턴스를 생성한다.
    • storeinfoWindow인스턴스를 set해주어 전역적으로 infoWindow를 사용 가능하도록 한다.

    2. marker인스턴스를 생성하는 주체가 StationMarkersContainer가 되어서는 안된다.

    이번 팀 프로젝트에서 지도를 구현하기 위해 google maps api를 사용하게 되었다. 뜬금없이 이 이야기를 한 이유는 다음과 같다.

    • google maps api는 바닐라 자바스크립트를 기반으로 동작한다.
    • 이번 팀 프로젝트는 리액트를 기반으로 개발을 진행할 것이다.
    • 지도를 그리기 위해서 바닐라 자바스크립트와 리액트의 적절한 조화가 필요하다.
    • 다소 혼란스러울 수 있는 지도의 조작 방식을 리액트와 조화롭게 사용하기 위해서 컴포넌트 설계시 컴포넌트의 책임을 확실하게 구분해야겠다는 생각을 하게 되었다.

    이 컴포넌트의 책임에 대한 문제로 인해 marker 인스턴스를 생성하는 주체에 대해 많은 고민을 하게 되었다.

    일단 원래 코드 구조에서 마커를 그리기 위해 컴포넌트를 다음과 같이 추상화 했다.

    • StationMarkersContainer 컴포넌트
      • 리액트 쿼리를 통해 받아온 서버 상태(충전소 정보 배열)로 StationMarker를 호출한다.
    • StationMarker 컴포넌트
      • 상위에서 내려받은 충전소 정보 props를 통해 marker 인스턴스를 생성한다. (google maps api에서는 인스턴스 생성이 곧 렌더링을 의미한다)
      • 생성한 marker 인스턴스에 infoWindow 인스턴스의 open 메서드를 트리거 하는 클릭 이벤트 리스너를 추가해준다.
      • useEffect의 클린업 함수를 이용해 충전소 정보가 최신화 되었을 때 마커가 더이상 화면에 보이지 않는다면 marker 인스턴스의 setMap(null) 메서드를 호출해 google maps api에서 마커를 지우도록 한다. (마커 렌더링 최적화)

    간략히 설명하자면 StationMarkersContainer 컴포넌트는 충전소 정보를 서버에서 받아 StationMarker를 호출하는 역할만을 수행하고, 마커에 대한 모든 세부 로직은 StationMarker가 수행하도록 컴포넌트를 추상화 해보았다.

    이름에서도 드러나듯 StationMarker 컴포넌트가 marker 인스턴스를 생성하는 주체가 되어야 바닐라 자바스크립트와 리액트의 혼종인 이 프로젝트의 코드를 추후 유지보수 할 때 문제가 없으리라 판단했다.

    하지만 이렇게 추상화 된 컴포넌트들은 marker 인스턴스를 배열 형식의 전역 상태에 담아 관리하고자 할 때 문제가 되었다.


    일단 먼저 서버에서 내려 받은 충전소 정보를 station이라고 하자, 우리는 이 station을 통해 marker 인스턴스를 생성하고자 한다.

    이때 생각 할 수 있는 가장 간단한 방법은 station에서 map 메서드를 통해 marker 인스턴스를 생성하여 이 marker 인스턴스를 하위 컴포넌트인 StationMarker에 넘겨주는 방식일 것이다.

    하지만 이 방식은 인스턴스를 생성하는 것이 곧 화면에 렌더링을 발생시키는 것을 의미하는 google maps api의 특성상 우리가 처음 설계한 컴포넌트의 책임을 반하는 구조를 만들어내게 된다.

    자세히 설명해보자면 마커의 렌더링은 StationMarkersContainer가 수행하고 있는데 화면에 보이지 않는 마커를 지우는 역할은 StationMarker컴포넌트가 수행하고 있고, 이벤트 핸들러의 추가 역시 마커가 생성된 이후에 하위 컴포넌트에서 이를 수행하는 괴상한 코드가 만들어지게 된다.

    추후 코드의 유지보수성을 위해선 피해야 할 방식임이 명확했다.

    해결 방식을 고민해보다가 다음과 같은 해결 방안을 생각하게 되었다.

    StationMarker 컴포넌트의 역할

    • marker 인스턴스를 생성한다.
    • marker 인스턴스의 이벤트 핸들러를 추가한다.
    • 생성된 marker 인스턴스를 배열 형식의 전역 상태에 추가한다.
    • 충전소 정보가 최신화 되었을 때 마커가 화면에 보이지 않는 상태가 되었다면 marker 인스턴스를 전역 상태에서 삭제한다.

    위와 같이 StationMarker 의 역할을 잡게 되면 기존의 컴포넌트 설계 구조를 해치지 않으면서 전역 상태에 marker인스턴스를 잘 추가할 수 있게 된다. 하지만 이렇게 되면 StationMarker 컴포넌트는 다음의 큰 문제들을 가지게 된다.

    1. marker들을 가지는 전역 상태를 구독하고 있는 컴포넌트가 새로 생성되는 마커의 개수만큼 리렌더링 된다.
    2. 현재 사용하고 있는 전역 상태 관리 도구의 특성상 이전 상태를 참조해와야 marker를 추가할 수 있게 되는데, 이 때 이전 상태가 최신의 상태임을 보장하지 못할 수 있다.

    이 두 문제를 해결할 방식을 고민해보았을 때 다음과 같은 결론에 도달하게 되었다.

    • 현재 사용하고 있는 전역 상태 관리 도구는 React 18에 새로 추가된 useSyncExternalState 훅을 기반으로 recoil과 비슷하게 사용할 수 있도록 계층을 분리하여 만든 도구이다.
    • 기존에 사용하던 전역 상태 관리 도구의 메서드 useExternalState, useExternalValue, useSetExternalState 이외에 store 인스턴스에 직접 접근하여 최신의 상태를 참조하는 getStoreSnapShot 메서드를 추가한다.
    • store에 직접 접근해 받아온 최신의 상태는 바닐라 자바스크립트 객체 이므로 리액트의 리렌더링을 발생 시키지 않는다.
    • 리렌더링으로 인한 문제점들을 getStoreSnapShot 메서드를 추가함으로써 해결할 수 있다.

    새로운 기능 추가를 위해 마주했던 앞선 두 가지의 문제와 해결 방식을 살펴 보았다. 그래서 최종적으로 이전까지 계속해서 고민해왔던 문제를 해결한 과정을 간추려보자면 다음과 같다.

    • 충전소 정보를 서버에서 받아와 렌더링 하는 StationList 컴포넌트에서 marker 인스턴스 배열을 저장하고 있는 store인스턴스에 직접 접근해 최신의 marker인스턴스들을 가져온다.
    • 충전소 목록에서 사용자가 충전소를 클릭했을 때 전역으로 관리되는 infoWindow 인스턴스의 open메서드에 marker 인스턴스들 중 선택된 marker를 전달해 간단 정보 모달을 띄워준다.
    - + \ No newline at end of file diff --git a/tags/vpc.html b/tags/vpc.html index bc22511..ef3cd2a 100644 --- a/tags/vpc.html +++ b/tags/vpc.html @@ -5,13 +5,13 @@ "vpc" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "vpc" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    어떤 문제가 있었나요?

    우아한테크코스에서 private 서브넷에 db 인스턴스를 두고, 보안을 위해 외부에서 접속을 차단하려고 했습니다.

    이 과정에서 총 2가지의 문제점이 있었습니다.

    1. private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었습니다.
    2. public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안되었습니다.

    이 부분을 어떻게 해결했는지 알아보도록 하겠습니다.

    아래의 모든 설명은 AWS 를 기준으로 합니다.

    private 서브넷에 인스턴스가 인터넷에서 mysql을 설치할 수 없었다.

    해결 방법

    public ip 자동할당을 해주지 않아서, 인터넷에 연결이 안 되었습니다.

    이를 해결하기 위해 public ip 자동할당을 해주었습니다.

    왜 public ip를 할당했더니 문제가 해결되었을까요?

    private 서브넷이란?

    정말 간단하게 설명했을 때

    private 서브넷은 인터넷에 연결되지 않은 서브넷입니다.

    조금 자세하게 들어가 보도록 하겠습니다

    private 서브넷은 인터넷 게이트웨이가 연결되지 않은 서브넷입니다.

    aws 공식문서에서 사진을 통해 보면 아래와 같이 되어있습니다

    private subnet

    public 서브넷에만 인터넷 게이트웨이가 연결되어 있고, private 서브넷에는 인터넷 게이트웨이가 연결되어있지 않습니다.

    private 서브넷에 인터넷 게이트웨이가 연결되어 있지 않다고 했을 때, 기본적으로 인터넷에 접속이 안됩니다.

    mysql을 설치할 때도, 인터넷에 접속을 해야하는데, 인터넷에 접속이 안되니 설치가 안되는 것입니다.

    어? 인터넷 자체가 접근이 안되면 어떻게 설치하나요?

    정말 원시적으로 해결하기 위해서는 public 서브넷에 인스턴스를 하나 더 만들어서, mysql 을 압축해서 scp를 통해 private 서브넷에 있는 인스턴스에 전송하고, 압축을 풀어서 설치하는 방법이 있습니다.

    하지만 이 방법은 너무 원시적이고, 비효율적입니다.

    그래서 인터넷으로 요청을 보낼 수 있도록 만드는 과정이 필요합니다.

    인터넷으로 요청을 보낼 수 있도록 만드는 과정

    인터넷으로 요청을 보낼 수 있도록 만드는 과정은 크게 2가지가 있습니다.

    private 서브넷을 public 서브넷으로 바꾸기

    보안을 위해서 private 서브넷에 두려고 했던 것을 public 서브넷으로 바꾼다는 부분은 매우 위험합니다.

    그래서 이 방법은 보통 사용하지 않습니다.

    NAT 인스턴스(Gateway) 만들기

    NAT 인스턴스는 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들어주는 인스턴스입니다.

    인터넷에 접속을 하기 위해서는 public ip 가 필요합니다.

    따라서 NAT 인스턴스, NAT 게이트웨이는 public 서브넷에 존재해야 합니다.

    어? NAT 인스턴스를 통해서 바로 통신이 가능하면 왜 private 서브넷이 필요한가요? 그냥 다 public 서브넷에 두면 되지 않나요?

    NAT 인스턴스, NAT Gateway는 내부에서 출발한 트래픽만 통과할 수 있도록 설정이 되어있습니다.

    예를 들면 private 서브넷에 인스턴스에 접속해서 직접 mysql download 요청을 했을 때만 허용이 됩니다.

    외부에서 바로 private 인스턴스로 접근할 수는 없습니다.

    NAT 인스턴스만 설정을 하면 바로 연결이 되나요?

    public ip도 자동 할당을 해줘야 합니다

    public ip 가 필요한 이유

    NAT 인스턴스를 통해서 private 서브넷에 있는 인스턴스가 인터넷에 접속할 수 있도록 만들었는데, 왜 public ip 가 필요할까요?

    외부 인터넷과 통신을 할 때 public ip 가 필요합니다.

    NAT 인스턴스 혹은 NAT 게이트웨이가 인터넷과 통신할 때, NAT 인스턴스의 public ip + private ip를 통해서 통신을 하지 않습니다.

    내부 인스턴스의 public ip 를 통해서 통신을 하게 되어있습니다.

    따라서 NAT 인스턴스와 내부 인스턴스 모두 public ip 가 필요합니다.

    이 과정을 통해서 1번 문제를 해결할 수 있었습니다.

    이제 2번째 문제를 해결해 보도록 하겠습니다.

    public 서브넷에 있는 인스턴스에서 private 서브넷에 있는 인스턴스에 접속이 안 되는 문제

    public 서브넷에 있는 서버가 private 서브넷에 있는 서버에 접속을 하려고 했는데, 접속이 안 되는 문제가 있었습니다.

    해결 방법

    해결 방법에는 2가지 과정이 있습니다.

    public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해 주기

    기본적으로 public 서브넷에 있는 인스턴스의 보안 그룹에는 private 서브넷에 있는 인스턴스의 보안 그룹이 추가되어있지 않습니다.

    따라서 public 서브넷에 있는 인스턴스의 보안 그룹에 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.

    private ip를 통해서 접속하기

    public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속할 때, public ip 를 통해서 접속을 하면 안 됩니다.

    public ip를 통해서 접속하는 과정을 자세하게 알아보겠습니다.

    1. public 서브넷에 있는 인스턴스가 public ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
    2. 라우팅 테이블에서 public ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
    3. 라우터를 통해서 외부 인터넷으로 나가게 됩니다.
    4. 트래픽이 NAT 인스턴스에 도착합니다.
    5. NAT 인스턴스는 내부에서 출발한 트래픽이 아니기 때문에, 트래픽을 거부합니다.

    이 과정이 일어나기에, public ip 를 통해서 접속을 하면 안 됩니다.

    private ip를 통해서 접근하면 어떻게 되는지 알아보겠습니다

    1. public 서브넷에 있는 인스턴스가 private ip 를 통해서 private 서브넷에 있는 인스턴스에 접속을 시도합니다.
    2. 라우팅 테이블에서 private ip 일 경우에 어떻게 처리할지에 대한 정보를 찾습니다.
    3. 라우터를 거쳐서 private 서브넷의 라우터로 이동합니다.
    4. private 서브넷의 라우터는 private 서브넷에 있는 인스턴스에게 트래픽을 전달합니다.
    5. private 서브넷에 있는 인스턴스는 트래픽을 받아서 처리합니다.

    이 과정을 통해서 2번 문제를 해결할 수 있었습니다.

    요약

    1. private 서브넷에 있는 인스턴스가 인터넷에 접속을 하려면 NAT 인스턴스 혹은 NAT 게이트웨이가 필요합니다.
    2. private 서브넷에 있는 인스턴스도 public ip 가 필요합니다.
    3. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 하려면 private 서브넷에 있는 인스턴스의 보안 그룹을 추가해주어야 합니다.
    4. public 서브넷에 있는 인스턴스가 private 서브넷에 있는 인스턴스에 접속을 할 때, private ip 를 통해서 접속을 해야 합니다.
    - + \ No newline at end of file diff --git a/tags/webpack.html b/tags/webpack.html index 7484d46..5780c43 100644 --- a/tags/webpack.html +++ b/tags/webpack.html @@ -5,13 +5,13 @@ "webpack" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "webpack" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 5분
    센트

    웹팩에서 msw 설정

    이번 팀 프로젝트는 CRA와 같은 보일러 플레이트 코드를 사용하지 못하게 제한이 있다. 또한 요즘 많이 사용된다는 Vite의 사용도 제한이 있고, 웹팩으로 프로젝트를 시작하도록 강제하고 있다.

    팀원 모두 한 번도 웹팩을 통해 프로젝트를 시작해본 경험이 없어 프론트엔드 팀원 각자 개인 레포에서 웹팩 공부를 진행한 후 어느정도 진척이 있을 때 팀 레포에 프로젝트를 시작하기로 했다.

    다행히 웹팩으로 시작하는 프로젝트에 대한 많은 참고 자료들이 있어 첫 리액트 프로젝트 화면을 띄우는데 까지는 그리 오랜 시간이 걸리지 않았다. 그렇게 모든 팀원이 첫 웹팩 프로젝트를 성공시킨 후 모여 팀 프로젝트 초기 설정을 시작해보았다.

    eslint, prettier, 웹팩 등등 여러 설정들을 하고 필요한 패키지를 설치하는데 문제가 발생했다. 큰 데이터를 다루는 백엔드의 개발 속도를 고려해 프론트엔드 개발을 진행하기 위해서 미션중에 배웠던 MSW 라이브러리를 사용하기로 결정했는데, 이 라이브러리가 우리 팀의 개발 환경에서 동작하지 않았다.

    왜 동작하지 않는지 원인을 찾아보니 MSW service worker 파일을 찾을 수 없다는 오류 메세지가 나오는 것을 확인할 수 있었다. 원인을 더 자세히 알아보니 public 폴더에 있는 파일들은 웹팩이 번들링을 진행할 때 포함이 되지 않는다는 것을 알 수 있었고, 이를 어떻게 해결할 지 팀원들과 방법을 찾아보았다.

    약 한시간쯤 지났을 무렵 copy-webpack-plugin 패키지를 통해 public 경로에 있는 파일들도 빌드 폴더에 포함시킬 수 있다는 것을 알게 되었다. 하지만 이 copy-webpack-plugin에 대한 사용법이 미숙해 public 폴더에 있는 mockServiceWorker.js 파일만 빌드 폴더로 옮겼어야 했는데 index.html과 같은 다른 파일들 까지 한꺼번에 빌드 폴더로 옮겨지게 되었다.

    이런 저런 방법들을 시도해보다 webpack.config.js 파일의 plugins에 아래와 같은 설정을 추가 해주어 MSW를 프로젝트에 적용할 수 있게 되었다.

    new CopyWebpackPlugin({
    patterns: [
    { from: 'public/mockServiceWorker.js', to: '.' }, // msw service worker
    ],
    }),

    설정을 간단히 보면 public 경로에 있는 mockServiceWorker.js 파일을 빌드 후 폴더의 루트 디렉토리에 추가해준다는 설정이다.

    문제 상황과 해결 방법을 간단하게 다시 정리해보면 다음과 같다.

    1. MSW를 적용해보려고 함.
    2. 웹팩에서 개발 서버를 열었을 때 MSW 실행을 위해 필요한 mockServiceWorker.js 파일을 찾을 수 없다는 오류가 발생함.
    3. 문제의 원인은 웹팩에서 번들링을 진행할 때 public 폴더 하위 경로에 있는 파일들을 무시하기 때문이었음.
    4. 문제를 해결하기 위해 public 경로에 있는 mockServiceWorker.js 파일을 번들링 후 폴더의 루트 디렉토리에 저장하도록 하는 설정을 추가해줌.
    - + \ No newline at end of file diff --git a/tags/world.html b/tags/world.html index 1fffc8b..dda04dc 100644 --- a/tags/world.html +++ b/tags/world.html @@ -5,7 +5,7 @@ "world" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -21,7 +21,7 @@ 거기에 file을 만듭니다. 파일 이름이 중요한데요 V1__init.sql 이러한 방식으로 V{version 숫자}__{어떠한 파일인지에 대한 이름}.sql 언더스코어 2개는 필수로 작성해야합니다.

    create table member(
    id bigint auto_increment primary key,
    name varchar(255) null,
    );

    이렇게 V1__init.sql에 대한 파일을 작성했습니다. 이제는 email을 추가한다는 요구사항을 반영해보겠습니다.

    ALTER TABLE member
    ADD COLUMN email varchar(255);

    이렇게 새로운 파일을 만들어서 해당 스크립트를 작성했습니다. 파일명이 중요한데요, 이전 파일의 숫자보다 +1 이 되는 숫자를 V 뒤에 붙입니다.

    따라서 이번 파일은 V2__add_column_email.sql 이라고 만들었습니다.

    그럼 이제 또 시간이 지나 회원이 많아졌습니다. 하지만 email이 없는 사용자도 많습니다. 이 상황에서 email을 not null로 변경해야한다는 요구사항이 생겼습니다.

    그러면 아래와 같이 반영할 수 있습니다.

    ALTER TABLE member
    MODIFY email VARCHAR(20) NOT NULL default 'default'

    이렇게 V3__add_constraints.sql 파일을 만들었습니다. 그러면 null이 있던 row들은 email이 default가 되고 not null 제약조건이 활성화 된 것을 볼 수 있습니다.

    그러면 주어진 요구사항은 모두 만족할 수 있습니다. 거기에다 v1, v2, v3 가 나뉘어져있어서 어느 커밋부터 해당 sql이 추가되었는지도 확인할 수 있습니다.

    그리고 ddl-auto update를 사용하면 반영되지 않았던 제약조건의 추가도 확인할 수 있습니다. 그러면 ddl-auto의 속성을 validate로 변경하여, db schema와 entity의 필드가 다르면 어플리케이션이 실행되지 않도록 해서 좀 더 안전한 개발을 할 수 있습니다.

    결론

    flyway는 roll back을 하는 것이 유료라서, production 서버에서 혹은 롤백을 해야하는 일이 있는 서버에서는 사용하는 것이 좋지 않지만, 이와 같이 데이터를 drop 할 수 없는 상황이라면, 사용하지 않을 이유가 없어보이는 좋은 도구입니다.

    짧은 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git a/tags/world/page/2.html b/tags/world/page/2.html index 7eb8329..1bf3fa8 100644 --- a/tags/world/page/2.html +++ b/tags/world/page/2.html @@ -5,13 +5,13 @@ "world" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +
    - + \ No newline at end of file diff --git a/tags/zero-time.html b/tags/zero-time.html index df6a237..44d4bf6 100644 --- a/tags/zero-time.html +++ b/tags/zero-time.html @@ -5,7 +5,7 @@ "zero-time" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "zero-time" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 9분
    제이

    안녕하세요! 카페인팀의 제이입니다.

    저희 카페인 팀에서 무중단 배포를 진행했습니다. 어떤 과정으로 진행을 했는지 작성해보도록 하겠습니다!


    기존 배포 방식과 문제점

    먼저 카페인 팀의 기존 배포 방식은 다음과 같습니다.

    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근하여서 기존에 띄워진 서버를 다운 시킵니다.
    4. Docker Hub에 업로드한 Docker image를 pull해서 서버를 가동시킵니다.

    이런 과정으로 배포 스크립트가 작성되어 있습니다. 하지만 이 방법은 기존 서버를 다운 시키고 새로운 서버를 띄울 때 다운 타임이 존재한다는 문제점이 있습니다.

    사용자 입장에서는 잘 사용하고 있는데 갑자기 서비스가 작동되지 않는다면 서비스에 대한 신뢰성이 낮아질 수도 있고 이런 이유로 이탈할 수도 있습니다.

    기존 문제를 해결하기

    저희는 먼저 제한된 EC2 인스턴스로 인해 롤링 배포의 장점을 가져갈 수 없었고, 카나리 방식 또한 저희 서비스에서 필요로한 전략이 아니기 때문에 비교적 롤백도 빠른 Blue/Green 전략을 선택하였습니다.

    저희의 Blue/Green 무중단 배포 시나리오는 다음과 같습니다. 편의를 위해서 [기존 서버(기존 포트) / 새로운 서버(새로운 포트)] 라는 명칭을 사용하겠습니다.


    1. Target branch에 push가 되면 Github Actions가 작동합니다.
    2. Target branch의 소스 코드가 빌드되어서 Docker Hub 에 올라가게 됩니다.
    3. Github Actions의 self-hosted runner를 통해 infra 서버에서 prod 서버로 접근해서 Docker Hub에 업로드한 새로운 버전의 Image를 pull 해옵니다.
    4. 만약 8080 포트에 기존 서버가 띄워져 있으면 8081 포트를 새로운 서버가 띄워질 포트로 지정해주고, 반대로 8081 포트에 기존 서버가 띄워져 있으면 8080 포트에 새로운 서버가 띄워질 포트로 지정해줍니다.
    5. 미리 Docker Hub에 업로드한 Docker image를 [image+port]라는 네이밍으로 pull을 한 후 새로운 포트로 서버를 가동시킵니다.
    6. 새로운 서버가 제대로 가동 됐는지 확인하기 위해서 헬스 체크를 진행합니다. 20번 동안 서버가 정상 동작하는지 Spring Actuactor를 통해서 확인을 합니다.
    7. 정상 작동이 됐음을 확인하면 현재 인스턴스에는 2대의 서버가 띄워져있고 요청은 여전히 기존 서버로 들어가게 됩니다. 따라서 Nginx를 통해 포트포워딩을 새로운 서버의 포트로 지정해주고 기존 서버는 내려줍니다.

    여기까지가 카페인 팀의 시나리오입니다. 그렇다면 하나씩 스크립트를 확인해보겠습니다. 설명은 주석으로 달아두겠습니다 :)

    backend-deploy.yml

    (Github Actions에서 사용)

    name: deploy

    # 1. prod/backend branch에 push 작업이 일어나면 해당 작업을 수행한다
    on:
    push:
    branches:
    - prod/backend

    jobs:
    docker-build:
    runs-on: ubuntu-latest
    defaults:
    run:
    working-directory: ./backend

    steps:
    # 2. 도커 허브에 로그인
    - name: Log in to Docker Hub
    uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
    with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_PASSWORD }}
    - uses: actions/checkout@v3

    # 3. JDK 17 설치 및 빌드 (프로젝트 Java version)
    - name: Set up JDK 17
    uses: actions/setup-java@v3
    with:
    java-version: '17'
    distribution: 'adopt'

    - name: Gradle Caching
    uses: actions/cache@v3
    with:
    path: |
    ~/.gradle/caches
    ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
    ${{ runner.os }}-gradle-

    - name: Grant execute permission for gradlew
    run: chmod +x gradlew
    - name: Build for asciiDoc
    run: ./gradlew bootjar

    - name: Build with Gradle
    run: ./gradlew bootjar

    # 4. 산출물을 Image로 빌드 후 Docker Hub에 Image Push하기
    - name: Extract metadata (tags, labels) for Docker
    id: meta
    uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
    with:
    images: woowacarffeine/backend

    - name: Build and push Docker image
    uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
    with:
    context: .
    file: ./backend/Dockerfile
    push: true
    platforms: linux/arm64
    tags: woowacarffeine/backend:latest
    labels: ${{ steps.meta.outputs.labels }}


    deploy:
    # 5. Self-hosted 작동 -> infra 인스턴스에서 작동됨
    runs-on: self-hosted
    if: ${{ needs.docker-build.result == 'success' }}
    needs: [ docker-build ]
    steps:

    # 6. infra 인스턴스에서 prod 인스턴스로 접근 (아래부터는 prod 서버 내에서 작업)
    - name: Join EC2 prod server
    uses: appleboy/ssh-action@master
    env:
    JASYPT_KEY: ${{ secrets.JASYPT_KEY }}
    DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
    DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
    with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USERNAME }}
    key: ${{ secrets.SERVER_KEY }}
    port: ${{ secrets.SERVER_PORT }}
    envs: JASYPT_KEY, DATABASE_USERNAME, DATABASE_PASSWORD

    script: |

    # 7. Docker Hub에서 Image를 pull해온다
    sudo docker pull woowacarffeine/backend:latest

    # 8. 만약 8080 포트가 켜져 있으면 새로운 서버의 포트는 8081로 설정
    if sudo docker ps | grep ":8080"; then
    export BEFORE_PORT=8080
    export NEW_PORT=8081
    export NEW_ACTUATOR_PORT=8089

    # 9. 만약 8081 포트가 켜져 있으면 새로운 서버의 포트는 8080로 설정
    else
    export BEFORE_PORT=8081
    export NEW_PORT=8080
    export NEW_ACTUATOR_PORT=8088
    fi

    # 10. Docker로 새로운 서버를 띄운다.
    sudo docker run -d -p $NEW_PORT:8080 -p $NEW_ACTUATOR_PORT:8088 \
    -e "SPRING_PROFILE=prod" \
    -e "ENCRYPT_KEY=${{secrets.JASYPT_KEY}}" \
    -e "DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}}" \
    -e "DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}}" \
    -e "REPLICA_DATABASE_USERNAME=${{secrets.REPLICA_DB_USER_NAME}}" \
    -e "REPLICA_DATABASE_PASSWORD=${{secrets.REPLICA_DB_USER_PASSWORD}}" \
    -e "SLACK_WEBHOOK_URL=${{secrets.SLACK_WEBHOOK_URL}}" \
    --name backend$NEW_PORT \
    woowacarffeine/backend:latest

    # 11. prod 인스턴스에 있는 bluegreen.sh 를 작동한다. (이 때 port 값을 같이 넣어준다.)
    sudo sh /home/ubuntu/bluegreen.sh $BEFORE_PORT $NEW_PORT $NEW_ACTUATOR_PORT



    bluegreen.sh

    (prod 인스턴스 내부에 존재)

    #!/bin/bash

    # 1. Github Actions를 통해 넘겨 받은 환경변수 값
    BEFORE_PORT=$1
    NEW_PORT=$2
    NEW_ACTUATOR_PORT=$3

    echo "기존 포트 : $BEFORE_PORT"
    echo "새로운 포트: $NEW_PORT"
    echo "새로운 ACTUATOR_PORT: $NEW_ACTUATOR_PORT"


    # 2. 20번 동안 헬스 체크를 진행
    count=0
    for count in {0..20}
    do
    echo "서버 상태 확인(${count}/20)";

    # 3. 새로운 서버가 작동되는지 Actuator를 통해 값을 받아옴
    STATUS=$(curl -s http://127.0.0.1:${NEW_ACTUATOR_PORT}/actuator/health-check)

    # 4. Actuator를 통해 성공적으로 서버가 띄워지지 않은 경우
    if [ "${STATUS}" != '{"status":"up"}' ]
    then
    # 5. 10초를 기다린 후 다시 헬스 체크를 진행한다.
    sleep 10
    continue
    else
    # 6. 헬스 체크를 통해 새로운 서버가 성공적으로 작동된다면 멈춘다.
    break
    fi
    done


    # 7. 20번의 헬스 체크를 하는 동안 새로운 서버가 제대로 작동되지 않은 경우 종료
    if [ $count -eq 20 ]
    then
    echo "새로운 서버 배포를 실패했습니다."
    exit 1
    fi


    # 8. 새로운 서버가 성공적으로 작동한 경우
    # Nginx를 통해 포트포워딩을 기존 포트에서 새로운 포트로 변경해준다.
    # 이 부분은 .inc 파일을 통해 Nginx에서 주입 받아서 포트만 변경해도 됩니다!
    export BACKEND_PORT=$NEW_PORT
    envsubst '${BACKEND_PORT}' < backend.template > backend.conf
    sudo mv backend.conf /etc/nginx/conf.d/
    sudo nginx -s reload


    # 9. 기존 서버를 내려주고, 도커 리소스를 정리해준다
    docker stop backend$BEFORE_PORT
    sudo docker container prune -f

    이렇게 카페인 팀에서는 무중단 배포를 도입할 수 있었습니다.

    긴 글 읽어주셔서 감사합니다 :)

    - + \ No newline at end of file diff --git "a/tags/\352\265\254\352\270\200-\354\247\200\353\217\204.html" "b/tags/\352\265\254\352\270\200-\354\247\200\353\217\204.html" index 0595c17..e15319a 100644 --- "a/tags/\352\265\254\352\270\200-\354\247\200\353\217\204.html" +++ "b/tags/\352\265\254\352\270\200-\354\247\200\353\217\204.html" @@ -5,13 +5,13 @@ "구글 지도" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "구글 지도" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 18분
    가브리엘

    안녕하세요? 카페인 팀에서 사용한 지도 시스템에 대해서 소개하려고 합니다.

    지도 기능에서 가장 핵심인 기능 두 가지를 뽑자면, 지도 그 자체와 지도 위에 그려지는 마커를 뽑을 수 있을 것입니다. 지도 위에 마커를 그리는 일은 그다지 어렵지 않고, documents 에 있는 예제들을 잘 따라하면 누구나 충분히 구현할 수 있을 것입니다.

    no offset

    하지만 마커의 갯수가 과도하게 많다면 어떤 전략을 세울 수 있을까요?

    카페인 팀에서는요 ...

    카페인 서비스에서 지도는 굉장히 중요한 요소 중 하나였습니다. 사용자들이 궁금한 장소의 주변에 있는 충전소를 시각적으로 제공해주기 위해서는 지도를 잘 제어할 수 있어야 했습니다. 특히 전국에 이미 수만 대의 충전소가 보급이 된 상황에서 충전소 마커를 모두 그려주기 위해서는 많은 제약이 있었고, 마커를 적당한 수준으로 렌더링 하려면 클라이언트와 서버 간에 특별한 작업이 필요했습니다.

    어떤 전략을 펼쳤는지 소개하기에 앞서 미리 말씀드리지만, 저희 팀에서 취한 지도 관리 전략은 모든 프로젝트에 유효하지 않을 것입니다. 지도 위에 한번에 표현할 마커의 갯수가 수백 개 이하라면, 서버에 데이터가 과도하게 많은 것이 아니라면 오히려 이러한 전략이 사용자 경험을 해칠 수 있을 것입니다. (환경이 원활하다면 데이터를 가능한 많이 보여주는 것이 좋을테니깐요.)

    또, 이 글에서는 Google Maps API를 기준으로 설명하고 있지만, 지원하는 기능이 일부 다르더라도 대부분의 지도 API에서 사용이 가능한 전략일 것입니다. 참고로 개인적으로 사용 해본 여러 벤더 사의 지도 API들은 모두 이와 유사한 기능을 제공했습니다.

    좌표란 무엇일까?

    아마 어린 시절부터 우리나라에는 특별히 38선이라는 것이 존재한다는 사실을 교육받기에 좌표계라는 것이 있다는 사실은 누구나 알 것입니다. 하지만 당장 위도와 경도를 구분지으라고 하면 어떤 선이 위선이고 경선인지 헷갈리기에 찍어야 할 것입니다. 따라서 이 선이 어떤 선인지, 어떤 값을 얘기하려는 것인지 사진과 함께 간단히 설명하겠습니다.

    no offset

    사진을 보시면 아시겠지만 위도란, 남북의 위치를 나타내는 데 사용됩니다. 경도는 동서의 위치를 나타내는 데 사용됩니다. 대부분의 공식 문서가 영어로 작성되어있고, 코드에서도 이를 나타내는 것이 중요하기에 영문 표기법까지 소개를 하자면 위도는 Latitude, 경도는 Longitude로 표기합니다. 이유는 모르겠지만 제공되는 변수나 메서드 명으로 lat, lng라고 줄여서 표기하기도 합니다.

    no offset

    위도와 경도만 알면, 지구 위의 어떤 위치를 나타낼 수 있습니다.

    따라서, 어떤 마커를 어떤 위치에 찍을 것인지는 위도와 경도 값으로 결정할 수 있게 되겠죠?

    사용자가 어딜 보고 있을까?

    지도 api에서 제공해주는 메서드를 활용하면 사용자의 디바이스가 어느 위치를 보고 있는지 알 수 있습니다.

    let map = /* 어디선가 생성된 구글 맵 객체 */
    const center = map.getCenter();
    console.log(center.lng()); // 디바이스 중심의 longitude
    console.log(center.lat()); // 디바이스 중심의 latitude

    지도 객체로 부터 중심점을 알게되면 해당 디바이스의 중심의 좌표를 알아낼 수 있게 됩니다.

    no offset

    사용자의 디바이스는 얼마나 넓게 보고 있을까?

    지도 api에서 제공해주는 메서드를 활용하면 사용자의 디바이스가 어떤 영역을 보고 있는지도 알게 됩니다. 지도 api 마다 제공하는 스펙이 다르지만, 대부분은 어떤 식으로든 알려줍니다.

    google maps API에서는 디스플레이의 북동쪽 끝 점의 좌표와, 남서쪽 끝 점의 좌표를 제공해줍니다.

    const map = /* 어디선가 생성된 구글 맵 객체 */
    const bounds = map.getBounds();
    console.log(bounds.getNorthEast().lng(), bounds.getNorthEast().lat()); // 디바이스 1사분면 끝 점의 longitude와 latitude
    console.log(bounds.getSouthWest().lng(), bounds.getSouthWest().lat()); // 디바이스 3사분면 끝 점의 longitude와 latitude

    no offset

    편의상 좌표를 다음과 같이 정의해보겠습니다.

    • 중심 점 p0: (x0, y0)
    • 디바이스의 제 1사분면 끝점 p2: (x2, y2)
    • 디바이스의 제 3사분면 끝점 p1: (x1, y1)
    위 정의는 아래에서도 계속 설명 될 점과 좌표 입니다.

    이렇게 알아낸 값으로 사용자 디바이스의 영역을 알게 됐습니다.

    저희 카페인 팀에서는 이 값을 좀 더 효율적으로 다루기 위해 delta 개념을 도입했습니다.

    화면에서 보고 있는 영역을 확대/축소 하면 어떤 특징을 보일까?

    delta 설명을 앞서, 사용자의 디바이스 영역과 확대 수준에 따른 실제 좌표에 대해 알아보려고 합니다.

    사용자가 화면을 얼마나 넓게 보고 있는지를 쉽게 알기 위해서는 끝점들의 수치를 계산해줄 필요가 있었습니다.

    사진은 사용자가 디바이스를 통해 바라 보고 있는 중심 좌표와 그 끝 점을 의미합니다.

    no offset

    예를 들어 사용자가 지도를 많이 축소한 경우에는 중심 점 p0은 그대로지만 양 끝점 p1, p2의 위치가 점점 중심 점 p0으로 부터 멀어질 것입니다.

    반면에 사용자가 지도를 많이 확대한 경우에는 중심 점 p0은 그대로지만 양 끝점 p1, p2의 위치가 점점 중심점과 가까워질 것입니다.

    no offset

    양 사진 모두 중심 점 p0는 그대로지만, 디바이스의 확대 수준으로 인해 양 끝점인 p1과 p2가 달라진 모습을 보인 것입니다.

    즉, 이런 결론을 내릴 수 있습니다.

    1. 양 끝점 p1, p2가 중심 점 p0으로 부터 멀어질 수록 지도를 축소한 것이다.
    2. 양 끝점 p1, p2가 중심 점 p0으로 부터 가까워 수록 지도를 확대한 것이다.

    이 때 디바이스의 디스플레이가 위도 경도 상으로 얼마나 멀어져있는지를 수치화하면 편하게 다룰 수 있습니다.

    확대 수준을 수치화 할 수 없을까?

    사용자의 디스플레이의 중심 점 p0을 기준으로 하여 양 끝점 p1, p2이 얼마나 멀어져있는지에 따라 지도의 영역 뿐만 아니라 얼마나 많이 확대 되었는지 여부를 알게 됐습니다.

    그렇다면 이를 좀 더 효율적인 방법으로 나타내려면 어떤 전략을 취할 수 있을까요?

    사용자 디스플레이를 조금 더 자세히 살펴보겠습니다.

    no offset

    중학교 시절 배웠던 좌표 평면계를 떠올려보면 화면에서 얻을 수 있는 좌표들은 위와 같습니다. 여기에서 각 점의 수직/수평의 변화량인 delta를 알아보면 어떨까요?

    경도 델타 (longitudeDelta)

    p2와 p0의 경도 거리, 그리고 p1과 p0의 경도 거리는 같습니다.

    즉, x2 - x0 === x0 - x1 이라는 결론을 얻을 수 있습니다.

    이를 longitudeDelta로 정의하겠습니다.

    위도 델타 (latitudeDelta)

    p2와 p0의 위도 거리, 그리고 p1과 p0의 위도 거리는 같습니다.

    즉, y2 - y0 === y0 - y1 이라는 결론을 얻을 수 있습니다.

    이를 latitudeDelta로 정의하겠습니다.

    no offset

    코드로 알아보면 다음과 같습니다.

    const map = /* 어디선가 생성된 구글 맵 객체 */
    const bounds = map.getBounds();
    const longitudeDelta = (bounds.getNorthEast().lng() - bounds.getSouthWest().lng()) / 2; // 경도 변화량
    const latitudeDelta = (bounds.getNorthEast().lat() - bounds.getSouthWest().lat()) / 2; // 위도 변화량

    드디어 클라이언트에서 델타 값을 생성할 수 있게 되었습니다.

    그렇다면 왜 이렇게 굳이 델타 값을 생성한 것일까요?

    delta의 유용한 점 1: 원래 의도한 값을 복원하기 쉽다.

    서버의 입장에서는 중심 좌표와 델타 값만 알면 정확한 영역만큼 데이터를 호출할 수 있게 됩니다.

    예를 들어 클라이언트에서 서버로 다음과 같은 파라미터를 넘겨줬다고 가정해보겠습니다.

    {
    "longitude": 127,
    "latitude": 37,
    "longitudeDelta": 0.1,
    "longitudeDelta": 0.2,
    }

    그렇다면 서버에서는 다음과 같이 해석할 수 있게 됩니다.

    const maxLongitude = longitude + longitudeDelta;
    const minLongitude = longitude - longitudeDelta;
    const maxLatitude = latitude + latitudeDelta;
    const minLatitude = latitude - latitudeDelta;

    (javascript 기준으로 작성했습니다.)

    이렇게 알아낸 경계 값을 가지고 다음과 같은 sql문을 작성할 수 있게 될 것입니다.

    SELECT * FROM stations WHERE latitude >= :minLatitude AND latitude <= :maxLatitude AND longitude >= :minLongitude AND longitude <= :maxLongitude;

    no offset

    즉, 위 그림처럼, 원하는 영역만큼만 정확하게 데이터를 호출할 수 있게 됩니다.

    delta의 유용한 점 2: 델타가 무분별하게 커지는 것을 막기 쉽다.

    예를 들어 사용자가 지도를 축소하여 한반도를 디스플레이에 가득 채운다면 서버가 어떻게 될까요?

    이러한 행위를 막는 가장 쉬운 방법은 지도 api에서 지원하는 줌 레벨을 제한 하는 것입니다. 후술하겠지만 줌 레벨은 디스플레이의 해상도를 고려하지 못합니다.

    따라서 근본적으로 델타가 일정 값 이상 요청되지 못하도록, 혹은 연산되지 못하도록 막게 할 수 있습니다.

    물론 델타가 없더라도 델타 값을 추정하여 연산할 수 있겠지만, 이를 수치화 해서 관리한다면 클라이언트와 서버 모두 지도를 손쉽게 통제하는 것이 가능하게 됩니다.

    예를 들어 다음과 같이 델타 값을 고정하여 요청 영역을 제한할(요청을 보내지 않거나 고정된 사이즈로만 요청을 보낼) 수 있습니다.

    {
    longitude,
    latitude,
    longitudeDelta: longitudeDelta < 0.008 ? longitudeDelta : 0.008,
    latitudeDelta: latitudeDelta < 0.004 ? latitudeDelta : 0.004,
    }

    특정 수치를 넘기지 못하게 처리할 때 눈에 보이는 변수로 취급하기 쉽습니다. (즉, 매번 계산하지 않아도 됩니다.)

    디바이스 크기 관련 문제도 있습니다.

    분명히 같은 줌 레벨이지만, 디바이스의 크기나 해상도에 따라 지도가 보여지는 정도가 다릅니다.

    no offset

    위 사진은 구글에서 제공하는 zoom 레벨을 동일하게 맞춘 후, 여러 디바이스에서 호출한 것입니다.

    줌 레벨을 통해서 요청을 제한하다보면 여러 해상도를 제어하기 어렵습니다.

    no offset

    실제로 카페인 팀에서는 고해상도 모니터를 대응하기 위해 델타 값이 너무 크게 되면 요청의 제한을 하고 있습니다. 사진에서 보시다시피 고해상도 모니터의 경우, 너무 넓은 범위를 요청한다 싶으면 중심점으로 부터 일정 거리만 보여주도록 하고 있습니다.

    (참고로 줌 레벨에 따른 요청도 덤으로 제한하고 있어서 멀리서 호출하는 행위도 금지하고 있습니다.)

    delta의 유용한 점 3: 적당한 범위를 정해주기 편하다

    위 예제에서는 정확한 범위만큼 요청하는 것을 예제로 하지만, 프로젝트에 따라서 조금 더 넓은 영역을 호출하고 싶을 때가 있을 것입니다.

    no offset

    예를 들어 현재 사용자의 디바이스 크기보다 살짝 큰 범위의 데이터를 미리 로드해 놓으면 사용자가 좁은 움직임을 보일 때 불필요한 재 렌더링을 줄여서 더 빠른 렌더링이 가능하게 됩니다.

    사실 이 기법은 프로젝트마다 다르겠지만, 카페인 팀에서는 한번 불러온 마커를 매번 해제 하지 않고 이전 요청 데이터와 다음 요청 데이터를 비교하여 달라진 마커만을 정확하게 탈부착하는 작업을 진행하고 있습니다.

    이런 기법을 활용하면 사용자가 좁은 범위에서 움직임을 보였을 때, 기존에 불러온 마커를 메모리에서 탈락시키지 않으므로 사용자 경험을 개선할 수도 있을 것입니다.

    마커를 상태에 연동하여 정확하게 메모리에서 탈부착 시키는 전략에 대한 글은 이후에 작성할 예정입니다.

    긴 글 읽어주셔서 감사합니다.

    - + \ No newline at end of file diff --git "a/tags/\353\260\251\353\254\270\354\236\220-\353\266\204\354\204\235.html" "b/tags/\353\260\251\353\254\270\354\236\220-\353\266\204\354\204\235.html" index 02b3404..630a059 100644 --- "a/tags/\353\260\251\353\254\270\354\236\220-\353\266\204\354\204\235.html" +++ "b/tags/\353\260\251\353\254\270\354\236\220-\353\266\204\354\204\235.html" @@ -5,7 +5,7 @@ "방문자 분석" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -20,7 +20,7 @@ no offset no offset no offset

    집계 된 자료처럼 방문자들이 단순 방문만 한 것이 아니라, 수 많은 이벤트를 발생시키고 평균 참여 시간도 상당 부분 확보했음을 확인할 수 있습니다.

    - + \ No newline at end of file diff --git "a/tags/\353\260\260\355\217\254.html" "b/tags/\353\260\260\355\217\254.html" index 421fc51..33b29f9 100644 --- "a/tags/\353\260\260\355\217\254.html" +++ "b/tags/\353\260\260\355\217\254.html" @@ -5,13 +5,13 @@ "배포" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "배포" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    안녕하세요 우아한테크코스 카페인팀 누누입니다

    이번에 카페인 팀에서 배포 아키텍처를 결정하게 되었던 과정에 대해서 정리를 해보고 싶어서 글을 쓰게 되었습니다.

    아키텍처와 서버가 배포되는 과정을 보여드리면서 시작하도록 하겠습니다

    배포 아키텍처

    서버가 배포되는 과정은 다음과 같습니다.

    server image

    우아한테크코스 인스턴스에 대한 소개

    우테코에서 선택할 수 있는 인스턴스는 총 2가지 종류입니다.

    1. 퍼블릭 서브넷에 있는 인스턴스
      • 캠퍼스에서만 SSH 접근이 가능한 인스턴스입니다.
      • 미리 열려있는 포트들만 허용이 되어 있습니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다
    2. 프라이빗 서브넷에 있는 인스턴스
      • 퍼블릭 서브넷에 있는 인스턴스를 통해서만 접근이 가능합니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다.

    1번 인스턴스를 2개 사용 가능하고, 2번 인스턴스를 1개 사용 가능합니다.

    권장되는 환경에서 1개는 db 서버로 사용하고, 나머지 2개는 자유롭게 사용이 가능했습니다.

    그전에 알면 좋아요

    여기서는 Self Hosted Runner를 사용했는데요.

    Self Hosted Runner에 대한 내용은 여기 에 잘 나와있습니다.

    외부 IP로부터 SSH 접근이 불가능하기에, Self Hosted Runner 나, Jenkins 같은 방법을 사용할 수 있었는데, 러닝 커브를 고려해서 Self Hosted Runner를 선택하게 되었습니다.

    배포 아키텍처에 대한 고민

    저희 팀이 이번 아키텍처를 만들기 위해서 고민했던 점들은 다음과 같습니다.

    1. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
    3. 2차 데모데이까지의 과제인 개발 서버를 어떻게 구성할 수 있을까?

    여기서 1번을 가장 먼저 생각한 아키텍처를 구성하게 되었는데, 다음과 같습니다.

    선택의 기준이 되었던 것은 총 3가지였습니다.

    1. DB는 프라이빗 서브넷에 위치시키고, 우리 인스턴스를 거쳐서만 접근이 가능하게 한다.
      • 이 부분은 보안을 위해서 어쩔 수 없이 선택하게 된 부분입니다.이 부분을 고려하다 보니, 최소한으로 구성할 수 있는 구조가 db 용 private 인스턴스 1개, 그리고 우리가 사용할 public 인스턴스 1개가 됩니다
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
      • 개발용 인스턴스에 CD 툴이나, 모니터링 툴을 설치하게 되면, 운영 서버에도 동일하게 작업을 해야 합니다.
      • 이 부분을 최소화하기 위해서, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 되었습니다.
    3. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
      • 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 않는다면 개발용 인스턴스에서 장애가 발생했을 때, CD 툴과 모니터링 툴에도 영향을 미치게 됩니다. 이 부분을 생각했을 때도, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리해야 한다고 결정하게 되었습니다
      • 한 부분의 장애가 다른 툴까지 사용할 수 없게 만들게 되어서, 롤백이나, 상황 파악을 하기 힘들게 만들게 됩니다.

    이런 과정들을 생각했을 때, 인스턴스 1개를 개발 서버용으로, 인스턴스 1개를 CD 툴과 모니터링 툴을 설치한 인스턴스로 사용하게 되었습니다.

    실제 내부 구성은 어떻게 될까요?

    개발 서버

    이 인스턴스에는 총 2가지 기능이 들어가 있습니다.

    1. 프론트 서버
      • react로 되어있는 프론트엔드 코드를 사용자에게 전달해 주는 역할을 합니다.
    2. 백엔드 서버
      • spring으로 되어있는 api 서버입니다.

    물론, 이렇게 하면 두 곳 중 한 곳에 장애가 발생했을 때, 프론트 서버와 백엔드 서버가 모두 영향을 받게 됩니다.

    같이 관리하게 된 첫 번째 이유로 비용이 들기 때문에 비용의 문제를 고려하게 되었습니다. 개발 서버에서 프론트 서버와 백엔드 서버를 관리하게 되었습니다.

    두 번째 이유로는, 아직 프로젝트 초창기 이기 때문에, 백엔드에서 장애가 났을 때, 프론트에서 일정 이상의 에러 처리가 불가능했습니다.

    프로젝트가 많이 진행되었다면, 프론트엔드만으로 혹은 장애가 나지 않은 서버를 활용해 에러 처리를 할 수 있지만, 아직은 그런 기능을 구현하지 못했습니다.

    이와는 별개로 실행 시 편의를 위해서 도커를 사용해 개발 서버를 관리하고 있습니다.

    CD 툴과 모니터링 툴

    이 인스턴스에는 총 3가지 기능이 들어가 있습니다.

    1. CD 툴
      • 위에서 설명드린 것처럼, self hosted runner 가 동작하게 되어있습니다
    2. 보안을 위한 리버스 프록시
      • 저희 프로젝트에서 구글 지도를 사용하게 되는데, 이때 API 키를 사용하게 됩니다. 이렇게 하면, API 키를 노출시키지 않고, 사용할 수 있습니다.
      • 이 API 키를 노출시키지 않기 위해서, 리버스 프록시를 하나 두고, 여기서 API 키를 추가해 요청을 보내는 방식으로 구성하게 되었습니다.
    3. 모니터링 툴
      • 저희 프로젝트에서 아직 도입하지 않았지만, 현재 이슈로는 올라가 있는 상태입니다.
      • Actuator, 프로메테우스, 그라파나 이 3가지를 활용해서 모니터링 툴을 구성하게 될 예정입니다

    위 기능들이 한 인스턴스에 모여있기에, 위의 기능들은 추후에 운영 서버가 추가되었을 때, 중복으로 관리하지 않아도 됩니다.

    배포 과정 더 자세히 알아보기

    아래에 사진에서 보이는 과정을 통해서 배포를 진행하고 있는데요

    server image

    1. 사용자가 push를 하면, github actions에서 도커 빌드를 진행하고, 도커 허브에 이미지를 올립니다.
    2. 도커 허브에 이미지가 올라간 이후에, self hosted runner 가 작동을 시작합니다.
    3. 개발용 인스턴스에 접근해서, 이미지를 받고, 컨테이너를 실행합니다.

    이런 과정을 통해서, 개발용 인스턴스에 배포를 진행하고 있습니다.

    느낀 점

    좋은 아키텍처를 설계하기 위해서는 고려해야 할 점들이 정말 많다는 것을 다시 한번 느꼈습니다.

    운영 서버가 추가된다던가, 인스턴스가 늘어나고, 줄어드는 상황에 유연하게 대처할 수 있도록 설계를 해야 한다는 것을 다시 한번 느꼈습니다.

    중복으로 관리될 포인트를 줄여야 한다는 것도 다시 한번 느낄 수 있었고요

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git "a/tags/\354\204\234\353\262\204-\353\266\200\355\225\230-\354\244\204\354\235\264\352\270\260.html" "b/tags/\354\204\234\353\262\204-\353\266\200\355\225\230-\354\244\204\354\235\264\352\270\260.html" index 6d2ee15..a98a158 100644 --- "a/tags/\354\204\234\353\262\204-\353\266\200\355\225\230-\354\244\204\354\235\264\352\270\260.html" +++ "b/tags/\354\204\234\353\262\204-\353\266\200\355\225\230-\354\244\204\354\235\264\352\270\260.html" @@ -5,7 +5,7 @@ "서버 부하 줄이기" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -21,7 +21,7 @@ 이 정보를 제외하고 마커를 띄우기 위해 필요한 최소한의 정보를 조회하도록 수정해 서버의 부하를 낮췄습니다.

    이러한 변경으로 인해 충전소 조회 API의 성능이 개선되었습니다. 필요한 정보만을 조회하므로써 데이터베이스의 부하를 줄이고 응답 시간을 단축할 수 있게 되었습니다. 또한, 프론트엔드에서는 필요한 정보만을 호출하여 불필요한 데이터를 받아오지 않아도 되므로 클라이언트 측의 성능도 향상되었습니다.

    - + \ No newline at end of file diff --git "a/tags/\354\204\234\353\262\204.html" "b/tags/\354\204\234\353\262\204.html" index ec93611..780e726 100644 --- "a/tags/\354\204\234\353\262\204.html" +++ "b/tags/\354\204\234\353\262\204.html" @@ -5,7 +5,7 @@ "서버" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -26,7 +26,7 @@ 이를 통해서 Ver2에 비해서 1초정도 줄었습니다.

    Ver4. 이전 방식 + Fetch Join 사용하기 (약 6초)

    마지막 방법은 조회 과정의 시간 단축입니다.

    처음에 Stations를 findAll()하는 쿼리를 확인해보니 N+1 문제가 발생하고 있었습니다. 그 이유는 Station에서 Chargers를 지연로딩으로 설정 했는데, 이를 그대로 get 메서드를 통해 조회해서 해당 문제가 발생했습니다.

    List<ChargeStation> findAll(); // 기존

    @Query("SELECT DISTINCT c FROM ChargeStation c JOIN FETCH c.chargers"); // Fetch Join 적용
    List<ChargeStation> findAll();

    따라서 위에 코드와 같이 Fetch Join을 이용해서 처음에 데이터를 가져왔습니다. 이렇게 효율적인 조회로 변경하면서 시간을 많이 줄일 수 있었습니다.

    지금까지의 방법을 정리를 하자면

    Ver1 과 같은 방식에서는 업데이트 과정에서 JPA의 식별자에 따른 처리 방식으로 인해 [SELECT + UPDATE] or [SELECT + INSERT] 와 같이 쿼리가 두 번씩 나갔습니다.

    그래서 Ver3까지 개선을 하기 위해서 저장과 업데이트를 한 번에 JDBC를 이용해서 Batch로 처리해주는 방식을 선택했고,

    변경 감지 + 배치 데이터를 모으기 위해서 자료구조를 이용해서 시간을 조금씩 단축 했습니다.

    마지막으로 Ver4에서는 findAll()에서 발생하는 N+1의 문제를 해결하면서 시간을 단축했습니다.

    이런 과정을 통해서 동일 작업을 14초에서 6초 정도로 줄일 수 있었습니다!

    - + \ No newline at end of file diff --git "a/tags/\354\204\234\353\262\204/page/2.html" "b/tags/\354\204\234\353\262\204/page/2.html" index 45451ae..e7e0e14 100644 --- "a/tags/\354\204\234\353\262\204/page/2.html" +++ "b/tags/\354\204\234\353\262\204/page/2.html" @@ -5,13 +5,13 @@ "서버" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "서버" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    안녕하세요 우아한테크코스 카페인팀 누누입니다

    이번에 카페인 팀에서 배포 아키텍처를 결정하게 되었던 과정에 대해서 정리를 해보고 싶어서 글을 쓰게 되었습니다.

    아키텍처와 서버가 배포되는 과정을 보여드리면서 시작하도록 하겠습니다

    배포 아키텍처

    서버가 배포되는 과정은 다음과 같습니다.

    server image

    우아한테크코스 인스턴스에 대한 소개

    우테코에서 선택할 수 있는 인스턴스는 총 2가지 종류입니다.

    1. 퍼블릭 서브넷에 있는 인스턴스
      • 캠퍼스에서만 SSH 접근이 가능한 인스턴스입니다.
      • 미리 열려있는 포트들만 허용이 되어 있습니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다
    2. 프라이빗 서브넷에 있는 인스턴스
      • 퍼블릭 서브넷에 있는 인스턴스를 통해서만 접근이 가능합니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다.

    1번 인스턴스를 2개 사용 가능하고, 2번 인스턴스를 1개 사용 가능합니다.

    권장되는 환경에서 1개는 db 서버로 사용하고, 나머지 2개는 자유롭게 사용이 가능했습니다.

    그전에 알면 좋아요

    여기서는 Self Hosted Runner를 사용했는데요.

    Self Hosted Runner에 대한 내용은 여기 에 잘 나와있습니다.

    외부 IP로부터 SSH 접근이 불가능하기에, Self Hosted Runner 나, Jenkins 같은 방법을 사용할 수 있었는데, 러닝 커브를 고려해서 Self Hosted Runner를 선택하게 되었습니다.

    배포 아키텍처에 대한 고민

    저희 팀이 이번 아키텍처를 만들기 위해서 고민했던 점들은 다음과 같습니다.

    1. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
    3. 2차 데모데이까지의 과제인 개발 서버를 어떻게 구성할 수 있을까?

    여기서 1번을 가장 먼저 생각한 아키텍처를 구성하게 되었는데, 다음과 같습니다.

    선택의 기준이 되었던 것은 총 3가지였습니다.

    1. DB는 프라이빗 서브넷에 위치시키고, 우리 인스턴스를 거쳐서만 접근이 가능하게 한다.
      • 이 부분은 보안을 위해서 어쩔 수 없이 선택하게 된 부분입니다.이 부분을 고려하다 보니, 최소한으로 구성할 수 있는 구조가 db 용 private 인스턴스 1개, 그리고 우리가 사용할 public 인스턴스 1개가 됩니다
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
      • 개발용 인스턴스에 CD 툴이나, 모니터링 툴을 설치하게 되면, 운영 서버에도 동일하게 작업을 해야 합니다.
      • 이 부분을 최소화하기 위해서, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 되었습니다.
    3. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
      • 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 않는다면 개발용 인스턴스에서 장애가 발생했을 때, CD 툴과 모니터링 툴에도 영향을 미치게 됩니다. 이 부분을 생각했을 때도, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리해야 한다고 결정하게 되었습니다
      • 한 부분의 장애가 다른 툴까지 사용할 수 없게 만들게 되어서, 롤백이나, 상황 파악을 하기 힘들게 만들게 됩니다.

    이런 과정들을 생각했을 때, 인스턴스 1개를 개발 서버용으로, 인스턴스 1개를 CD 툴과 모니터링 툴을 설치한 인스턴스로 사용하게 되었습니다.

    실제 내부 구성은 어떻게 될까요?

    개발 서버

    이 인스턴스에는 총 2가지 기능이 들어가 있습니다.

    1. 프론트 서버
      • react로 되어있는 프론트엔드 코드를 사용자에게 전달해 주는 역할을 합니다.
    2. 백엔드 서버
      • spring으로 되어있는 api 서버입니다.

    물론, 이렇게 하면 두 곳 중 한 곳에 장애가 발생했을 때, 프론트 서버와 백엔드 서버가 모두 영향을 받게 됩니다.

    같이 관리하게 된 첫 번째 이유로 비용이 들기 때문에 비용의 문제를 고려하게 되었습니다. 개발 서버에서 프론트 서버와 백엔드 서버를 관리하게 되었습니다.

    두 번째 이유로는, 아직 프로젝트 초창기 이기 때문에, 백엔드에서 장애가 났을 때, 프론트에서 일정 이상의 에러 처리가 불가능했습니다.

    프로젝트가 많이 진행되었다면, 프론트엔드만으로 혹은 장애가 나지 않은 서버를 활용해 에러 처리를 할 수 있지만, 아직은 그런 기능을 구현하지 못했습니다.

    이와는 별개로 실행 시 편의를 위해서 도커를 사용해 개발 서버를 관리하고 있습니다.

    CD 툴과 모니터링 툴

    이 인스턴스에는 총 3가지 기능이 들어가 있습니다.

    1. CD 툴
      • 위에서 설명드린 것처럼, self hosted runner 가 동작하게 되어있습니다
    2. 보안을 위한 리버스 프록시
      • 저희 프로젝트에서 구글 지도를 사용하게 되는데, 이때 API 키를 사용하게 됩니다. 이렇게 하면, API 키를 노출시키지 않고, 사용할 수 있습니다.
      • 이 API 키를 노출시키지 않기 위해서, 리버스 프록시를 하나 두고, 여기서 API 키를 추가해 요청을 보내는 방식으로 구성하게 되었습니다.
    3. 모니터링 툴
      • 저희 프로젝트에서 아직 도입하지 않았지만, 현재 이슈로는 올라가 있는 상태입니다.
      • Actuator, 프로메테우스, 그라파나 이 3가지를 활용해서 모니터링 툴을 구성하게 될 예정입니다

    위 기능들이 한 인스턴스에 모여있기에, 위의 기능들은 추후에 운영 서버가 추가되었을 때, 중복으로 관리하지 않아도 됩니다.

    배포 과정 더 자세히 알아보기

    아래에 사진에서 보이는 과정을 통해서 배포를 진행하고 있는데요

    server image

    1. 사용자가 push를 하면, github actions에서 도커 빌드를 진행하고, 도커 허브에 이미지를 올립니다.
    2. 도커 허브에 이미지가 올라간 이후에, self hosted runner 가 작동을 시작합니다.
    3. 개발용 인스턴스에 접근해서, 이미지를 받고, 컨테이너를 실행합니다.

    이런 과정을 통해서, 개발용 인스턴스에 배포를 진행하고 있습니다.

    느낀 점

    좋은 아키텍처를 설계하기 위해서는 고려해야 할 점들이 정말 많다는 것을 다시 한번 느꼈습니다.

    운영 서버가 추가된다던가, 인스턴스가 늘어나고, 줄어드는 상황에 유연하게 대처할 수 있도록 설계를 해야 한다는 것을 다시 한번 느꼈습니다.

    중복으로 관리될 포인트를 줄여야 한다는 것도 다시 한번 느낄 수 있었고요

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git "a/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230.html" "b/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230.html" index c0a7a03..df2e876 100644 --- "a/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230.html" +++ "b/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230.html" @@ -5,7 +5,7 @@ "서비스 경험" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -14,8 +14,9 @@ no offset no offset no offset -no offset

    - +no offset +no offset

    + \ No newline at end of file diff --git "a/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230/page/2.html" "b/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230/page/2.html" index cc67e8a..974e5b3 100644 --- "a/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230/page/2.html" +++ "b/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230/page/2.html" @@ -5,7 +5,7 @@ "서비스 경험" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "서비스 경험" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 15분
    가브리엘
    센트

    안녕하세요? 센트와 가브리엘 입니다.

    저희 카페인 팀에서는 지난번 카페인 서비스 1차 체험 진행 이후 일부 기능 개선이 있었습니다. 기능 개선의 유용성을 판별하고자 카페인 서비스 2차 체험을 다녀왔습니다.

    저희 팀에서 1차 체험 이후 개선한 사항은 다음과 같습니다.

    1. 지역검색

    no offset

    • 이제는 검색어를 입력하는 경우, 전국 도시의 주소가 같이 제공됩니다.

    2. 충전소 마커를 확인할 수 있는 지도 영역 확장

    no offset

    (기존에는 위 사진보다 좁은 영역만을 호출하는 것이 허용되었다.)

    • 모바일에서 좀 더 넓은 영역을 호출하는 것을 허용했습니다. 원래는 디바이스 너비를 고려하지 않고 줌 레벨 기준으로 요청을 제한했으나, 이제는 사용자 디바이스에 보이는 지도의 영역 크기를 기반으로 요청을 제한하는 방식을 도입했습니다.
    • 기존에 사용하던 마커의 단점은, 그 크기가 너무 크다는 것이었습니다. 이로인해 더 넓은 영역을 보여주는 경우에 마커들이 겹치는 현상이 있었는데요, 이를 수정하기 위해 특정 영역 크기 이상에서는 마커를 좀 더 간소화 된 디자인으로 보이도록 개선하였습니다.
    • 마커 사이즈가 작아지면서 사용 가능한 충전기 개수가 더이상 들어갈 공간이 없어졌습니다. 따라서 마커 색상은 그대로 유지를 하되, 인포 윈도우에 현재 사용 가능한 충전기 개수를 보여주는 방식으로 디자인을 개선하였습니다.

    체험 규칙 설정

    개선한 기능이 실제로 유용한지 확인해보기 위해 저희는 카페인 서비스 2차 체험의 규칙을 다음과 같이 설정했습니다.

    저희는 좀 더 의미있는 경험을 하기위해 1차 체험 때 정했던 규칙에 더해서 다음과 같은 추가 규칙을 설정하였습니다.

    중간에 목표 지점이 많이 변경된다

    지난 카페인 서비스 1차 체험에서는 지역 검색이 없어 목표 지점을 찾는 것이 불편했습니다. 1차 체험 이후 지역 검색이 추가 되었으므로 이 기능이 얼마나 유용한지 경험해보고자 이 규칙을 설정했습니다.

    추가로 목표 지점 주변의 충전소를 확인할 때 새로 추가된 지도 영역 확장이 얼마나 유용한지도 경험해보고자 했습니다.

    체험 개요

    no offset

    1. 잠실역 출발
    2. 하남 만두집
    3. 다음 목적지 설정
    4. 판교

    체험 후기

    잠실역 출발

    no offset

    쏘카에서 EV6를 대여해서 가브리엘, 센트, 키아라가 잠실역에서 출발하였습니다. 저녁 퇴근 이후에 남이섬을 가려고 목적지를 설정하였으나 배가 너무 고파서 가는 길에 식사를 하자고 얘기가 나왔습니다.

    하남 만두집

    따라서 진정한 처음 목적지는 스타필드였으나, 가브리엘은 동네 주민이라 스타필드를 너무 잘 알고 있었습니다. 따라서 스타필드에 전기차 충전소가 어디에 있는지도 알고있으므로 목적지를 급하게 변경하기로 했습니다. 이 때 목적지 변경을 위해 주변 식당을 둘러보던 중에 괜찮은 식당을 발견해서 해당 식당을 기준으로 주변 충전소를 확인해보기로 했습니다.

    no offset

    식당 주변을 가기 위해 지역 검색을 처음으로 사용하여 식당과 가까운 지역을 탐색할 수 있었습니다.

    이 과정에서 식당에는 충전소가 없다는 사실을 알게되어, 근처 충전소를 찾아보기 위해서 지도를 축소했더니 1차 체험때와는 달리 더 넓은 영역을 보여줬습니다. 이전에는 마커 자체가 보이지 않아 답답하였으나, 이제는 더 넓은 영역을 조회할 수 있게 되어 편리했습니다.

    지난 체험 이후로 피드백을 자체 수집하여 개발한 기능들이 편하다는 것을 식당에 가는 길에 느낄 수 있었습니다.

    다음 목적지 설정

    no offset

    하남 만두집에서 식사를 하다가 알게된 사실은, 남이섬은 생각보다 너무 멀다는 것이었습니다. 식사를 마치고 남이섬에 가면, 충전도 제대로 못하고 돌아올 판이었습니다.

    식사를 하면서 다른 목적지를 알아봤는데, 가브리엘이 예전에 가봤던 곳 중에서 남양주의 물의 정원이 시간을 떼우기 좋다는 소리를 하였습니다. 따라서 물의 정원을 검색해보았습니다.

    놀랍게도 물의정원은 검색결과에 없었습니다!

    어쩔 수 없이 카카오 지도로 물의 정원 위치를 확인하여 주소를 알아내었고, 이 주소를 카페인 검색창에 넣었습니다. 저희는 이 과정에서 카페인 서비스는 업체명 조회가 안된다는 것이 치명적인 단점이라고 생각했습니다. 다만, 이 기능은 검색 할 때마다 많은 비용이 청구되어 현실적으로 지금 당장 기능을 넣는 것은 어렵다고 판단했습니다.

    결국 주소 검색을 통해 물의 정원과 가장 가까운 충전소를 알아내었습니다.

    그런데! 지도를 축소해서 확인해 보니 해당 충전소는 물의 정원과 생각보다 멀었습니다.

    no offset

    no offset

    무려 걸어서 30분이나 걸리는 충전소였습니다!

    전기차 충전을 위해 왕복 1시간이나 걸리는 거리를 걸을 수 없다고 생각하였습니다.

    물론 지난 체험에서 전기차가 생각보다 배터리가 오래간다는 사실을 알고 있었지만, 만약 저희처럼 충전이 급한 사용자라면 목적지를 포기할 수 밖에 없겠구나 라는 생각이 들었습니다.

    마지막으로 정한 목적지는, 의외의 결정이었습니다.

    굉장히 발전된 첨단 도시로 알려진 판교였습니다!

    사실은 앞으로 갈지도 모르는 판교를 미리 구경이나 해보자는게 이유였지만 비밀입니다(?)

    일단 판교역은 IT서비스 회사들이 많이 몰려있는 곳이었습니다.

    따라서 저희는 판교역을 카페인 검색창에 검색했습니다.

    no offset

    지도를 판교역으로 이동하여 외부인 개방인 충전소를 찾았는데, 판교공영주차장이 보여서 해당 충전소를 목적지로 잡고 출발했습니다.

    판교

    하남에서 판교를 가기 위해서는 서하남IC를 지나야했습니다.

    가는 길에 우리 서비스에 나오는 정보와 실제 정보가 일치하는지 점검차 서하남 간이 휴게소를 들려봤습니다.

    이 휴게소에도 충전소가 있다고 검색이 되었기 때문입니다!

    no offset

    검색 당시에는 2대의 충전기가 있다고 나왔고, 둘다 사용이 가능하다고 되어있었는데 실제로 확인해보니 일치하는 것을 확인했습니다.

    먼 길을 달려 판교에 도착하였습니다.

    주차장에 들어오기 전, 카페인 서비스를 확인해보니 판교공영주차장의 충전기 총 12기 중 10기가 사용가능한 상태였습니다.

    정작 들어와서 보니 입구부터 너무 많은 전기차들이 충전기를 사용중이었습니다.

    뭔가 이상하다 싶었지만, 아직 서버에 반영이 안된건가? 하면서 비어있는 충전기를 찾았습니다.

    no offset no offset

    충전기를 꽂고 나서 알게된 것은 카페인 서비스에 나온 충전소 회사명과 방금 꽂은 충전기 회사명이 다르다는 것이었습니다.

    알고보니 음성 인식으로 네비에 검색한 충전소는 판교공영주차장이 아닌 판교역 환승 주차장이라 엉뚱한 곳으로 온 것이었습니다!!!

    다행인 점은 우리 서비스에서 제공하는 충전기 사용 여부 정보가 잘못된 것이 아니었다는 것이었습니다.

    그래서 애초에 가고자 했던 판교공영주자창에 대한 카페인 서비스의 정보가 실제와 동일한지 확인해보러 걸어서 이동했습니다. (바로 앞에 있었기 때문입니다.)

    no offset no offset

    도착해보니 1층의 충전기들이 모두 공사중이었고, 서비스의 정보가 실제로도 불일치 하는 줄 알았습니다. 다시 상세 정보를 보니 3~6층에 충전기들에 대한 정보라는 것이 명시되어 있었고, 실제로도 이와 동일한 것을 확인했습니다.

    no offset

    저희는 시간이 너무 흘러 다시 잠실로 돌아와 차를 반납하고 체험을 마무리 했습니다.

    결론

    불편했던 점

    • 디바이스에 보여지는 지도 영역 확장시에 원하는 정보를 볼 수 없는 것이 불편했다.
      • 지도를 확대해주세요 모달이 뜨고, 원래 있던 충전소 마커가 전부 사라진다.
    • 현재 나의 위치를 알아볼 수 있는 수단이 없어 불편했다.
      • 현위치를 나타내는 핀 (1차 체험기에서도 언급했던 부분)
      • 내 위치를 상대적으로 알 수 있는 랜드마크의 부족
    • 특정 장소(매장명) 검색이 안돼서 카페인 서비스만으로 목적지를 찾아가기 불편했다.
      • 카카오맵 등을 활용해 특정 장소 검색을 진행해야 했다.

    다음 목표

    앞선 불편했던점을 개선하기 위해 다음과 같은 기능 개선을 추가로 진행할 예정입니다.

    • 디바이스에 보여지는 지도 영역 확장에 제한이 생기지 않게 충전소 마커 클러스터링을 우선적으로 도입한다.
    • 현재 나의 위치를 알아볼 수 있도록 지하철 역과 같은 랜드마커를 지웠던 것을 롤백한다.

    카페인 서비스만으로 목적지를 찾아갈 수 있도록 하기 위해서 특정 장소 검색을 추가하고 싶지만, 해당 기능을 구현하기 위해선 검색당 비용이 많이 청구되는 장소 검색 API를 추가해야 했기에 현실적으로 지금 당장 구현하기 어렵다고 판단했습니다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git "a/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230/page/3.html" "b/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230/page/3.html" index 5ab736c..24a9429 100644 --- "a/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230/page/3.html" +++ "b/tags/\354\204\234\353\271\204\354\212\244-\352\262\275\355\227\230/page/3.html" @@ -5,7 +5,7 @@ "서비스 경험" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -27,7 +27,7 @@ 차주 분과 인터뷰 하고 싶었지만, 차 내부에서 너무 바빠보이셔서 그럴 수 없었습니다.

    전기차 충전을 기다리면서 무엇을 할 수 있을까요? 이 분은 다행히도 업무를 보고 계셨지만, 다른 차주들은 무엇을 하고 보낼지 궁금해졌습니다.

    no offset

    휴게소에는 충전소가 하나 더 있었습니다.

    한 곳은 사용중이지만, 다른 한 곳은 사용할 수 있었습니다.

    저희는 이 충전소를 사용해보기로 했습니다.

    no offset

    사용할 수 있으니깐 들어가봐야지! 하고 도착한 순간 아차 싶었습니다.

    "아, 충전소가 외부인 사용 금지일 수 있었지?"

    저희는 분명히 서비스를 직접 개발했으니깐 다 알고 있던 사항이었지만, 전혀 생각치 못했습니다.

    서비스를 개발하는 내내 외부인 개방 충전소에 대한 중요성을 간파하였고, 이 기능을 넣었으면서도 사용하지 않고 충전소를 방문한 것이었습니다.

    바로 앞에 있어서 다행이었지만, 어찌됐든 이 충전소를 사용할 수 없었습니다.

    따라서 저희는 휴게소를 떠나는 내내 이 문제에 대해서 토론을 할 수 밖에 없었습니다.

    분명 우리가 만든 서비스인데 왜 놓쳤을까?

    맛있는 점심

    no offset

    파주닭국수 본점에서 맛있는 식사를 했습니다.

    비록 식당에는 전기차 충전소가 없었지만, 인근에 충전소가 있어 실험을 하나 해볼 수 있었습니다.

    인근 충전소와 식당의 거리가 가까워 보이는데, 과연 걸어갈 수 있을까?

    실제로 걷지는 않았습니다만 차 타면서 지나가면서 확인해본 결과 직접 걸을 수 없는 거리였습니다. (굉장히 걷기 싫은 수준의 먼 거리였습니다.)

    집에 있는 PHEV를 탈 기회가 많아 전기차 충전소를 자주 방문했던 저는 이런 점을 잘 알고 있었습니다.

    다행히 이 부분을 잘 알고 있었기에 저희는 이 부분을 서비스에 반영하였고, 모든 데이터를 포기하지 않았던 것이 옳은 선택이었다는 것을 확인하게 되었습니다.

    no offset

    식사가 끝나고 드디어 마장호수로 출발하게 되었습니다.

    마장호수 도착

    마장호수에 도착하자마자 충전소에 방문했습니다.

    no offset

    통계에서는 사용률이 적을 것이라고 하였는데 저희만 있었습니다.

    no offset no offset

    2기 중 1곳을 저희가 사용하였고, 마장호수를 돌았습니다.

    no offset

    약 50분 간 산책을 하고, 돌아와보니 충전기 다 되어있었습니다.

    사실 마장호수 까지 오는 내내 든 생각이었지만, 전기차의 배터리가 생각보다 오래 간다는 생각이 들었습니다.

    일부러 회생제동 기능도 끄고, 에어컨을 강하게 틀어서 배터리를 소진하려고 하였으나, 85km를 주행하는 동안 겨우 20%를 소모하였습니다.

    충전기를 꽂을 때 50%였으나, 호수를 한바퀴 돌고 오니 이미 100%가 되어있었습니다.

    여담이지만, 저희가 돌아왔을 때 옆 자리에는 전기 화물차가 있어 충전소가 가득 찼습니다.

    또, 앱에서도 충전기 사용 여부가 업데이트 되는 것을 확인했습니다.

    no offset

    배터리 성능에는 좋지 않고 가격도 비싸서 이를 자주 사용하는 것은 좋지 않겠지만, 급한 사람들은 급속 충전기를 사용하면 되겠구나 싶었습니다.

    따라서 급속과 완속은 더더욱 다른 개념으로 봐야겠다는 생각이 들었습니다.

    제가 그동안 경험했던 전기차 충전소는 완속 기준이었기에 신선한 경험이었습니다.

    선릉으로 돌아오다

    no offset

    선릉으로 돌아와서 차량을 반납하였습니다.

    저희는 이번 여정을 통해 카페인 서비스에서 어떤 점을 개선해야할지 좀 더 명확하게 알게되었습니다.

    1. 현재 서비스에서 제공하는 기능들로 충전소를 검색하는 것은 가능하며, 충전소의 위치를 정확하게 파악하는 것도 가능하다.
    2. 하지만 충전소가 없는 목적지는 검색할 수 없고, 현 위치가 어디인지 가늠하기가 어려워진다.
    3. 충전소를 사용할 수 있다고 표기되어 있더라도 외부인 개방이 아닐 수 있다. 정보가 정확히 제공됨에도 불구하고 이를 단번에 눈치채기 어렵다.
    4. 이러한 문제를 예상하여 외부인 개방 여부를 필터링 할 수 있는 기능을 제공하고 있음에도 불구하고 사용하지 않았다.
    5. 충전소의 통계 자료의 적중률은 높았으나, 좀 더 많은 충전소를 들려 확인해봐야 할 것 같았다.
    6. 전기자동차는 생각보다 오래가고 상품성이 있었다. 주행 능력도 충분하고, 인프라가 잘 되어있다. 이걸 왜 욕하지? 라는 생각이 들었다.
    7. 지도 확대 허용 범위가 너무 좁아서 사용하는데 불편한건 실제 상황에서 더 불편했다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git "a/tags/\354\225\204\355\202\244\355\205\215\354\262\230.html" "b/tags/\354\225\204\355\202\244\355\205\215\354\262\230.html" index bc2a271..17806db 100644 --- "a/tags/\354\225\204\355\202\244\355\205\215\354\262\230.html" +++ "b/tags/\354\225\204\355\202\244\355\205\215\354\262\230.html" @@ -5,13 +5,13 @@ "아키텍처" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "아키텍처" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    안녕하세요 우아한테크코스 카페인팀 누누입니다

    이번에 카페인 팀에서 배포 아키텍처를 결정하게 되었던 과정에 대해서 정리를 해보고 싶어서 글을 쓰게 되었습니다.

    아키텍처와 서버가 배포되는 과정을 보여드리면서 시작하도록 하겠습니다

    배포 아키텍처

    서버가 배포되는 과정은 다음과 같습니다.

    server image

    우아한테크코스 인스턴스에 대한 소개

    우테코에서 선택할 수 있는 인스턴스는 총 2가지 종류입니다.

    1. 퍼블릭 서브넷에 있는 인스턴스
      • 캠퍼스에서만 SSH 접근이 가능한 인스턴스입니다.
      • 미리 열려있는 포트들만 허용이 되어 있습니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다
    2. 프라이빗 서브넷에 있는 인스턴스
      • 퍼블릭 서브넷에 있는 인스턴스를 통해서만 접근이 가능합니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다.

    1번 인스턴스를 2개 사용 가능하고, 2번 인스턴스를 1개 사용 가능합니다.

    권장되는 환경에서 1개는 db 서버로 사용하고, 나머지 2개는 자유롭게 사용이 가능했습니다.

    그전에 알면 좋아요

    여기서는 Self Hosted Runner를 사용했는데요.

    Self Hosted Runner에 대한 내용은 여기 에 잘 나와있습니다.

    외부 IP로부터 SSH 접근이 불가능하기에, Self Hosted Runner 나, Jenkins 같은 방법을 사용할 수 있었는데, 러닝 커브를 고려해서 Self Hosted Runner를 선택하게 되었습니다.

    배포 아키텍처에 대한 고민

    저희 팀이 이번 아키텍처를 만들기 위해서 고민했던 점들은 다음과 같습니다.

    1. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
    3. 2차 데모데이까지의 과제인 개발 서버를 어떻게 구성할 수 있을까?

    여기서 1번을 가장 먼저 생각한 아키텍처를 구성하게 되었는데, 다음과 같습니다.

    선택의 기준이 되었던 것은 총 3가지였습니다.

    1. DB는 프라이빗 서브넷에 위치시키고, 우리 인스턴스를 거쳐서만 접근이 가능하게 한다.
      • 이 부분은 보안을 위해서 어쩔 수 없이 선택하게 된 부분입니다.이 부분을 고려하다 보니, 최소한으로 구성할 수 있는 구조가 db 용 private 인스턴스 1개, 그리고 우리가 사용할 public 인스턴스 1개가 됩니다
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
      • 개발용 인스턴스에 CD 툴이나, 모니터링 툴을 설치하게 되면, 운영 서버에도 동일하게 작업을 해야 합니다.
      • 이 부분을 최소화하기 위해서, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 되었습니다.
    3. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
      • 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 않는다면 개발용 인스턴스에서 장애가 발생했을 때, CD 툴과 모니터링 툴에도 영향을 미치게 됩니다. 이 부분을 생각했을 때도, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리해야 한다고 결정하게 되었습니다
      • 한 부분의 장애가 다른 툴까지 사용할 수 없게 만들게 되어서, 롤백이나, 상황 파악을 하기 힘들게 만들게 됩니다.

    이런 과정들을 생각했을 때, 인스턴스 1개를 개발 서버용으로, 인스턴스 1개를 CD 툴과 모니터링 툴을 설치한 인스턴스로 사용하게 되었습니다.

    실제 내부 구성은 어떻게 될까요?

    개발 서버

    이 인스턴스에는 총 2가지 기능이 들어가 있습니다.

    1. 프론트 서버
      • react로 되어있는 프론트엔드 코드를 사용자에게 전달해 주는 역할을 합니다.
    2. 백엔드 서버
      • spring으로 되어있는 api 서버입니다.

    물론, 이렇게 하면 두 곳 중 한 곳에 장애가 발생했을 때, 프론트 서버와 백엔드 서버가 모두 영향을 받게 됩니다.

    같이 관리하게 된 첫 번째 이유로 비용이 들기 때문에 비용의 문제를 고려하게 되었습니다. 개발 서버에서 프론트 서버와 백엔드 서버를 관리하게 되었습니다.

    두 번째 이유로는, 아직 프로젝트 초창기 이기 때문에, 백엔드에서 장애가 났을 때, 프론트에서 일정 이상의 에러 처리가 불가능했습니다.

    프로젝트가 많이 진행되었다면, 프론트엔드만으로 혹은 장애가 나지 않은 서버를 활용해 에러 처리를 할 수 있지만, 아직은 그런 기능을 구현하지 못했습니다.

    이와는 별개로 실행 시 편의를 위해서 도커를 사용해 개발 서버를 관리하고 있습니다.

    CD 툴과 모니터링 툴

    이 인스턴스에는 총 3가지 기능이 들어가 있습니다.

    1. CD 툴
      • 위에서 설명드린 것처럼, self hosted runner 가 동작하게 되어있습니다
    2. 보안을 위한 리버스 프록시
      • 저희 프로젝트에서 구글 지도를 사용하게 되는데, 이때 API 키를 사용하게 됩니다. 이렇게 하면, API 키를 노출시키지 않고, 사용할 수 있습니다.
      • 이 API 키를 노출시키지 않기 위해서, 리버스 프록시를 하나 두고, 여기서 API 키를 추가해 요청을 보내는 방식으로 구성하게 되었습니다.
    3. 모니터링 툴
      • 저희 프로젝트에서 아직 도입하지 않았지만, 현재 이슈로는 올라가 있는 상태입니다.
      • Actuator, 프로메테우스, 그라파나 이 3가지를 활용해서 모니터링 툴을 구성하게 될 예정입니다

    위 기능들이 한 인스턴스에 모여있기에, 위의 기능들은 추후에 운영 서버가 추가되었을 때, 중복으로 관리하지 않아도 됩니다.

    배포 과정 더 자세히 알아보기

    아래에 사진에서 보이는 과정을 통해서 배포를 진행하고 있는데요

    server image

    1. 사용자가 push를 하면, github actions에서 도커 빌드를 진행하고, 도커 허브에 이미지를 올립니다.
    2. 도커 허브에 이미지가 올라간 이후에, self hosted runner 가 작동을 시작합니다.
    3. 개발용 인스턴스에 접근해서, 이미지를 받고, 컨테이너를 실행합니다.

    이런 과정을 통해서, 개발용 인스턴스에 배포를 진행하고 있습니다.

    느낀 점

    좋은 아키텍처를 설계하기 위해서는 고려해야 할 점들이 정말 많다는 것을 다시 한번 느꼈습니다.

    운영 서버가 추가된다던가, 인스턴스가 늘어나고, 줄어드는 상황에 유연하게 대처할 수 있도록 설계를 해야 한다는 것을 다시 한번 느꼈습니다.

    중복으로 관리될 포인트를 줄여야 한다는 것도 다시 한번 느낄 수 있었고요

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git "a/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244.html" "b/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244.html" index 05b9355..9f3f798 100644 --- "a/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244.html" +++ "b/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244.html" @@ -5,7 +5,7 @@ "우아한테크코스" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -26,7 +26,7 @@ 이를 통해서 Ver2에 비해서 1초정도 줄었습니다.

    Ver4. 이전 방식 + Fetch Join 사용하기 (약 6초)

    마지막 방법은 조회 과정의 시간 단축입니다.

    처음에 Stations를 findAll()하는 쿼리를 확인해보니 N+1 문제가 발생하고 있었습니다. 그 이유는 Station에서 Chargers를 지연로딩으로 설정 했는데, 이를 그대로 get 메서드를 통해 조회해서 해당 문제가 발생했습니다.

    List<ChargeStation> findAll(); // 기존

    @Query("SELECT DISTINCT c FROM ChargeStation c JOIN FETCH c.chargers"); // Fetch Join 적용
    List<ChargeStation> findAll();

    따라서 위에 코드와 같이 Fetch Join을 이용해서 처음에 데이터를 가져왔습니다. 이렇게 효율적인 조회로 변경하면서 시간을 많이 줄일 수 있었습니다.

    지금까지의 방법을 정리를 하자면

    Ver1 과 같은 방식에서는 업데이트 과정에서 JPA의 식별자에 따른 처리 방식으로 인해 [SELECT + UPDATE] or [SELECT + INSERT] 와 같이 쿼리가 두 번씩 나갔습니다.

    그래서 Ver3까지 개선을 하기 위해서 저장과 업데이트를 한 번에 JDBC를 이용해서 Batch로 처리해주는 방식을 선택했고,

    변경 감지 + 배치 데이터를 모으기 위해서 자료구조를 이용해서 시간을 조금씩 단축 했습니다.

    마지막으로 Ver4에서는 findAll()에서 발생하는 N+1의 문제를 해결하면서 시간을 단축했습니다.

    이런 과정을 통해서 동일 작업을 14초에서 6초 정도로 줄일 수 있었습니다!

    - + \ No newline at end of file diff --git "a/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244/page/2.html" "b/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244/page/2.html" index 6c680a6..02d81c3 100644 --- "a/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244/page/2.html" +++ "b/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244/page/2.html" @@ -5,13 +5,13 @@ "우아한테크코스" 태그로 연결된 2개 게시물개의 게시물이 있습니다. | CAR-FFEINE - +

    "우아한테크코스" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 11분
    누누

    안녕하세요 우아한테크코스 카페인팀 누누입니다

    이번에 카페인 팀에서 배포 아키텍처를 결정하게 되었던 과정에 대해서 정리를 해보고 싶어서 글을 쓰게 되었습니다.

    아키텍처와 서버가 배포되는 과정을 보여드리면서 시작하도록 하겠습니다

    배포 아키텍처

    서버가 배포되는 과정은 다음과 같습니다.

    server image

    우아한테크코스 인스턴스에 대한 소개

    우테코에서 선택할 수 있는 인스턴스는 총 2가지 종류입니다.

    1. 퍼블릭 서브넷에 있는 인스턴스
      • 캠퍼스에서만 SSH 접근이 가능한 인스턴스입니다.
      • 미리 열려있는 포트들만 허용이 되어 있습니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다
    2. 프라이빗 서브넷에 있는 인스턴스
      • 퍼블릭 서브넷에 있는 인스턴스를 통해서만 접근이 가능합니다.
      • 같은 서브넷에 있는 인스턴스끼리는 모든 포트가 허용되어 있습니다.

    1번 인스턴스를 2개 사용 가능하고, 2번 인스턴스를 1개 사용 가능합니다.

    권장되는 환경에서 1개는 db 서버로 사용하고, 나머지 2개는 자유롭게 사용이 가능했습니다.

    그전에 알면 좋아요

    여기서는 Self Hosted Runner를 사용했는데요.

    Self Hosted Runner에 대한 내용은 여기 에 잘 나와있습니다.

    외부 IP로부터 SSH 접근이 불가능하기에, Self Hosted Runner 나, Jenkins 같은 방법을 사용할 수 있었는데, 러닝 커브를 고려해서 Self Hosted Runner를 선택하게 되었습니다.

    배포 아키텍처에 대한 고민

    저희 팀이 이번 아키텍처를 만들기 위해서 고민했던 점들은 다음과 같습니다.

    1. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
    3. 2차 데모데이까지의 과제인 개발 서버를 어떻게 구성할 수 있을까?

    여기서 1번을 가장 먼저 생각한 아키텍처를 구성하게 되었는데, 다음과 같습니다.

    선택의 기준이 되었던 것은 총 3가지였습니다.

    1. DB는 프라이빗 서브넷에 위치시키고, 우리 인스턴스를 거쳐서만 접근이 가능하게 한다.
      • 이 부분은 보안을 위해서 어쩔 수 없이 선택하게 된 부분입니다.이 부분을 고려하다 보니, 최소한으로 구성할 수 있는 구조가 db 용 private 인스턴스 1개, 그리고 우리가 사용할 public 인스턴스 1개가 됩니다
    2. 운영 서버를 나중에 추가하게 되었을 때, 어떻게 중복으로 관리되는 부분을 최소화할 수 있을까?
      • 개발용 인스턴스에 CD 툴이나, 모니터링 툴을 설치하게 되면, 운영 서버에도 동일하게 작업을 해야 합니다.
      • 이 부분을 최소화하기 위해서, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 되었습니다.
    3. 어떻게 하면 장애의 영향을 최소화할 수 있을까?
      • 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리하게 않는다면 개발용 인스턴스에서 장애가 발생했을 때, CD 툴과 모니터링 툴에도 영향을 미치게 됩니다. 이 부분을 생각했을 때도, 개발용 인스턴스와, CD 툴, 모니터링 툴을 설치한 인스턴스를 분리해야 한다고 결정하게 되었습니다
      • 한 부분의 장애가 다른 툴까지 사용할 수 없게 만들게 되어서, 롤백이나, 상황 파악을 하기 힘들게 만들게 됩니다.

    이런 과정들을 생각했을 때, 인스턴스 1개를 개발 서버용으로, 인스턴스 1개를 CD 툴과 모니터링 툴을 설치한 인스턴스로 사용하게 되었습니다.

    실제 내부 구성은 어떻게 될까요?

    개발 서버

    이 인스턴스에는 총 2가지 기능이 들어가 있습니다.

    1. 프론트 서버
      • react로 되어있는 프론트엔드 코드를 사용자에게 전달해 주는 역할을 합니다.
    2. 백엔드 서버
      • spring으로 되어있는 api 서버입니다.

    물론, 이렇게 하면 두 곳 중 한 곳에 장애가 발생했을 때, 프론트 서버와 백엔드 서버가 모두 영향을 받게 됩니다.

    같이 관리하게 된 첫 번째 이유로 비용이 들기 때문에 비용의 문제를 고려하게 되었습니다. 개발 서버에서 프론트 서버와 백엔드 서버를 관리하게 되었습니다.

    두 번째 이유로는, 아직 프로젝트 초창기 이기 때문에, 백엔드에서 장애가 났을 때, 프론트에서 일정 이상의 에러 처리가 불가능했습니다.

    프로젝트가 많이 진행되었다면, 프론트엔드만으로 혹은 장애가 나지 않은 서버를 활용해 에러 처리를 할 수 있지만, 아직은 그런 기능을 구현하지 못했습니다.

    이와는 별개로 실행 시 편의를 위해서 도커를 사용해 개발 서버를 관리하고 있습니다.

    CD 툴과 모니터링 툴

    이 인스턴스에는 총 3가지 기능이 들어가 있습니다.

    1. CD 툴
      • 위에서 설명드린 것처럼, self hosted runner 가 동작하게 되어있습니다
    2. 보안을 위한 리버스 프록시
      • 저희 프로젝트에서 구글 지도를 사용하게 되는데, 이때 API 키를 사용하게 됩니다. 이렇게 하면, API 키를 노출시키지 않고, 사용할 수 있습니다.
      • 이 API 키를 노출시키지 않기 위해서, 리버스 프록시를 하나 두고, 여기서 API 키를 추가해 요청을 보내는 방식으로 구성하게 되었습니다.
    3. 모니터링 툴
      • 저희 프로젝트에서 아직 도입하지 않았지만, 현재 이슈로는 올라가 있는 상태입니다.
      • Actuator, 프로메테우스, 그라파나 이 3가지를 활용해서 모니터링 툴을 구성하게 될 예정입니다

    위 기능들이 한 인스턴스에 모여있기에, 위의 기능들은 추후에 운영 서버가 추가되었을 때, 중복으로 관리하지 않아도 됩니다.

    배포 과정 더 자세히 알아보기

    아래에 사진에서 보이는 과정을 통해서 배포를 진행하고 있는데요

    server image

    1. 사용자가 push를 하면, github actions에서 도커 빌드를 진행하고, 도커 허브에 이미지를 올립니다.
    2. 도커 허브에 이미지가 올라간 이후에, self hosted runner 가 작동을 시작합니다.
    3. 개발용 인스턴스에 접근해서, 이미지를 받고, 컨테이너를 실행합니다.

    이런 과정을 통해서, 개발용 인스턴스에 배포를 진행하고 있습니다.

    느낀 점

    좋은 아키텍처를 설계하기 위해서는 고려해야 할 점들이 정말 많다는 것을 다시 한번 느꼈습니다.

    운영 서버가 추가된다던가, 인스턴스가 늘어나고, 줄어드는 상황에 유연하게 대처할 수 있도록 설계를 해야 한다는 것을 다시 한번 느꼈습니다.

    중복으로 관리될 포인트를 줄여야 한다는 것도 다시 한번 느낄 수 있었고요

    긴 글을 읽어주셔서 감사합니다

    - + \ No newline at end of file diff --git "a/tags/\354\232\260\355\205\214\354\275\224.html" "b/tags/\354\232\260\355\205\214\354\275\224.html" index 7390d86..31cf2a5 100644 --- "a/tags/\354\232\260\355\205\214\354\275\224.html" +++ "b/tags/\354\232\260\355\205\214\354\275\224.html" @@ -5,7 +5,7 @@ "우테코" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "우테코" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 4분
    가브리엘

    지난 4주 간 변경사항 및 신규 기능을 소개합니다!!

    이번 업데이트에는 사용자 경험을 직접 수집하여 피드백을 반영하였답니다!

    마커 클러스터링

    마커 클러스터링이란?

    마커 클러스터링은 지도에 표시되는 마커들을 클러스터로 묶어서 표시하는 것을 말합니다. 마커 클러스터링을 사용하면 지도에 표시되는 마커의 수를 줄일 수 있습니다. 마커 클러스터링은 지도에 표시되는 마커의 수가 많을 때 유용하게 사용할 수 있습니다.

    어디에서 확인할 수 있나요?

    지도를 축소하는 경우, 마커가 클러스터로 묶여 표시됩니다. 클러스터를 클릭하면 해당 지역으로 확대됩니다.

    지역 클러스터링 서버 클러스터링

    도시 검색 기능

    도시 검색 기능이란?

    기존 검색창은 충전소의 이름과 주소를 기반으로 한 검색이 가능했습니다.

    이제는 대한민국의 주요 도시들을 검색할 수 있는 기능이 추가되었습니다.

    원하는 지역을 검색하고, 해당 지역으로 빠르게 이동할 수 있으며 지도 조작에 많은 도움이 됩니다.

    어디에서 확인할 수 있나요?

    검색창에 원하는 지역을 입력하면 바로 확인할 수 있습니다.

    도시 검색결과

    디자인 개선

    인포 윈도우가 개선되었어요!

    기존 인포 윈도우는 충전소의 이름과 주소만을 표시하고 있었습니다.

    이제는 사용량을 제공하며, 길찾기 기능도 제공합니다.

    인포 윈도우

    충전소 사용 통계 정보 디자인이 변경되었어요

    통계 정보

    새로워진 탭 디자인과 색상을 적용하였습니다.

    충전소 마커가 이원화 되었습니다.

    충전소 마커 충전소 마커

    지도를 축소할 수록 마커가 도로를 가리는 현상이 있어 사이즈가 대폭 축소되었습니다.

    단, 확대하는 경우에는 기존과 동일한 형태의 마커를 제공합니다.

    - + \ No newline at end of file diff --git "a/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260.html" "b/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260.html" index 9f84750..93d0921 100644 --- "a/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260.html" +++ "b/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260.html" @@ -5,7 +5,7 @@ "전기차 사용기" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -14,8 +14,9 @@ no offset no offset no offset -no offset

    - +no offset +no offset

    + \ No newline at end of file diff --git "a/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260/page/2.html" "b/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260/page/2.html" index cf9611e..768f966 100644 --- "a/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260/page/2.html" +++ "b/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260/page/2.html" @@ -5,7 +5,7 @@ "전기차 사용기" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "전기차 사용기" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 15분
    가브리엘
    센트

    안녕하세요? 센트와 가브리엘 입니다.

    저희 카페인 팀에서는 지난번 카페인 서비스 1차 체험 진행 이후 일부 기능 개선이 있었습니다. 기능 개선의 유용성을 판별하고자 카페인 서비스 2차 체험을 다녀왔습니다.

    저희 팀에서 1차 체험 이후 개선한 사항은 다음과 같습니다.

    1. 지역검색

    no offset

    • 이제는 검색어를 입력하는 경우, 전국 도시의 주소가 같이 제공됩니다.

    2. 충전소 마커를 확인할 수 있는 지도 영역 확장

    no offset

    (기존에는 위 사진보다 좁은 영역만을 호출하는 것이 허용되었다.)

    • 모바일에서 좀 더 넓은 영역을 호출하는 것을 허용했습니다. 원래는 디바이스 너비를 고려하지 않고 줌 레벨 기준으로 요청을 제한했으나, 이제는 사용자 디바이스에 보이는 지도의 영역 크기를 기반으로 요청을 제한하는 방식을 도입했습니다.
    • 기존에 사용하던 마커의 단점은, 그 크기가 너무 크다는 것이었습니다. 이로인해 더 넓은 영역을 보여주는 경우에 마커들이 겹치는 현상이 있었는데요, 이를 수정하기 위해 특정 영역 크기 이상에서는 마커를 좀 더 간소화 된 디자인으로 보이도록 개선하였습니다.
    • 마커 사이즈가 작아지면서 사용 가능한 충전기 개수가 더이상 들어갈 공간이 없어졌습니다. 따라서 마커 색상은 그대로 유지를 하되, 인포 윈도우에 현재 사용 가능한 충전기 개수를 보여주는 방식으로 디자인을 개선하였습니다.

    체험 규칙 설정

    개선한 기능이 실제로 유용한지 확인해보기 위해 저희는 카페인 서비스 2차 체험의 규칙을 다음과 같이 설정했습니다.

    저희는 좀 더 의미있는 경험을 하기위해 1차 체험 때 정했던 규칙에 더해서 다음과 같은 추가 규칙을 설정하였습니다.

    중간에 목표 지점이 많이 변경된다

    지난 카페인 서비스 1차 체험에서는 지역 검색이 없어 목표 지점을 찾는 것이 불편했습니다. 1차 체험 이후 지역 검색이 추가 되었으므로 이 기능이 얼마나 유용한지 경험해보고자 이 규칙을 설정했습니다.

    추가로 목표 지점 주변의 충전소를 확인할 때 새로 추가된 지도 영역 확장이 얼마나 유용한지도 경험해보고자 했습니다.

    체험 개요

    no offset

    1. 잠실역 출발
    2. 하남 만두집
    3. 다음 목적지 설정
    4. 판교

    체험 후기

    잠실역 출발

    no offset

    쏘카에서 EV6를 대여해서 가브리엘, 센트, 키아라가 잠실역에서 출발하였습니다. 저녁 퇴근 이후에 남이섬을 가려고 목적지를 설정하였으나 배가 너무 고파서 가는 길에 식사를 하자고 얘기가 나왔습니다.

    하남 만두집

    따라서 진정한 처음 목적지는 스타필드였으나, 가브리엘은 동네 주민이라 스타필드를 너무 잘 알고 있었습니다. 따라서 스타필드에 전기차 충전소가 어디에 있는지도 알고있으므로 목적지를 급하게 변경하기로 했습니다. 이 때 목적지 변경을 위해 주변 식당을 둘러보던 중에 괜찮은 식당을 발견해서 해당 식당을 기준으로 주변 충전소를 확인해보기로 했습니다.

    no offset

    식당 주변을 가기 위해 지역 검색을 처음으로 사용하여 식당과 가까운 지역을 탐색할 수 있었습니다.

    이 과정에서 식당에는 충전소가 없다는 사실을 알게되어, 근처 충전소를 찾아보기 위해서 지도를 축소했더니 1차 체험때와는 달리 더 넓은 영역을 보여줬습니다. 이전에는 마커 자체가 보이지 않아 답답하였으나, 이제는 더 넓은 영역을 조회할 수 있게 되어 편리했습니다.

    지난 체험 이후로 피드백을 자체 수집하여 개발한 기능들이 편하다는 것을 식당에 가는 길에 느낄 수 있었습니다.

    다음 목적지 설정

    no offset

    하남 만두집에서 식사를 하다가 알게된 사실은, 남이섬은 생각보다 너무 멀다는 것이었습니다. 식사를 마치고 남이섬에 가면, 충전도 제대로 못하고 돌아올 판이었습니다.

    식사를 하면서 다른 목적지를 알아봤는데, 가브리엘이 예전에 가봤던 곳 중에서 남양주의 물의 정원이 시간을 떼우기 좋다는 소리를 하였습니다. 따라서 물의 정원을 검색해보았습니다.

    놀랍게도 물의정원은 검색결과에 없었습니다!

    어쩔 수 없이 카카오 지도로 물의 정원 위치를 확인하여 주소를 알아내었고, 이 주소를 카페인 검색창에 넣었습니다. 저희는 이 과정에서 카페인 서비스는 업체명 조회가 안된다는 것이 치명적인 단점이라고 생각했습니다. 다만, 이 기능은 검색 할 때마다 많은 비용이 청구되어 현실적으로 지금 당장 기능을 넣는 것은 어렵다고 판단했습니다.

    결국 주소 검색을 통해 물의 정원과 가장 가까운 충전소를 알아내었습니다.

    그런데! 지도를 축소해서 확인해 보니 해당 충전소는 물의 정원과 생각보다 멀었습니다.

    no offset

    no offset

    무려 걸어서 30분이나 걸리는 충전소였습니다!

    전기차 충전을 위해 왕복 1시간이나 걸리는 거리를 걸을 수 없다고 생각하였습니다.

    물론 지난 체험에서 전기차가 생각보다 배터리가 오래간다는 사실을 알고 있었지만, 만약 저희처럼 충전이 급한 사용자라면 목적지를 포기할 수 밖에 없겠구나 라는 생각이 들었습니다.

    마지막으로 정한 목적지는, 의외의 결정이었습니다.

    굉장히 발전된 첨단 도시로 알려진 판교였습니다!

    사실은 앞으로 갈지도 모르는 판교를 미리 구경이나 해보자는게 이유였지만 비밀입니다(?)

    일단 판교역은 IT서비스 회사들이 많이 몰려있는 곳이었습니다.

    따라서 저희는 판교역을 카페인 검색창에 검색했습니다.

    no offset

    지도를 판교역으로 이동하여 외부인 개방인 충전소를 찾았는데, 판교공영주차장이 보여서 해당 충전소를 목적지로 잡고 출발했습니다.

    판교

    하남에서 판교를 가기 위해서는 서하남IC를 지나야했습니다.

    가는 길에 우리 서비스에 나오는 정보와 실제 정보가 일치하는지 점검차 서하남 간이 휴게소를 들려봤습니다.

    이 휴게소에도 충전소가 있다고 검색이 되었기 때문입니다!

    no offset

    검색 당시에는 2대의 충전기가 있다고 나왔고, 둘다 사용이 가능하다고 되어있었는데 실제로 확인해보니 일치하는 것을 확인했습니다.

    먼 길을 달려 판교에 도착하였습니다.

    주차장에 들어오기 전, 카페인 서비스를 확인해보니 판교공영주차장의 충전기 총 12기 중 10기가 사용가능한 상태였습니다.

    정작 들어와서 보니 입구부터 너무 많은 전기차들이 충전기를 사용중이었습니다.

    뭔가 이상하다 싶었지만, 아직 서버에 반영이 안된건가? 하면서 비어있는 충전기를 찾았습니다.

    no offset no offset

    충전기를 꽂고 나서 알게된 것은 카페인 서비스에 나온 충전소 회사명과 방금 꽂은 충전기 회사명이 다르다는 것이었습니다.

    알고보니 음성 인식으로 네비에 검색한 충전소는 판교공영주차장이 아닌 판교역 환승 주차장이라 엉뚱한 곳으로 온 것이었습니다!!!

    다행인 점은 우리 서비스에서 제공하는 충전기 사용 여부 정보가 잘못된 것이 아니었다는 것이었습니다.

    그래서 애초에 가고자 했던 판교공영주자창에 대한 카페인 서비스의 정보가 실제와 동일한지 확인해보러 걸어서 이동했습니다. (바로 앞에 있었기 때문입니다.)

    no offset no offset

    도착해보니 1층의 충전기들이 모두 공사중이었고, 서비스의 정보가 실제로도 불일치 하는 줄 알았습니다. 다시 상세 정보를 보니 3~6층에 충전기들에 대한 정보라는 것이 명시되어 있었고, 실제로도 이와 동일한 것을 확인했습니다.

    no offset

    저희는 시간이 너무 흘러 다시 잠실로 돌아와 차를 반납하고 체험을 마무리 했습니다.

    결론

    불편했던 점

    • 디바이스에 보여지는 지도 영역 확장시에 원하는 정보를 볼 수 없는 것이 불편했다.
      • 지도를 확대해주세요 모달이 뜨고, 원래 있던 충전소 마커가 전부 사라진다.
    • 현재 나의 위치를 알아볼 수 있는 수단이 없어 불편했다.
      • 현위치를 나타내는 핀 (1차 체험기에서도 언급했던 부분)
      • 내 위치를 상대적으로 알 수 있는 랜드마크의 부족
    • 특정 장소(매장명) 검색이 안돼서 카페인 서비스만으로 목적지를 찾아가기 불편했다.
      • 카카오맵 등을 활용해 특정 장소 검색을 진행해야 했다.

    다음 목표

    앞선 불편했던점을 개선하기 위해 다음과 같은 기능 개선을 추가로 진행할 예정입니다.

    • 디바이스에 보여지는 지도 영역 확장에 제한이 생기지 않게 충전소 마커 클러스터링을 우선적으로 도입한다.
    • 현재 나의 위치를 알아볼 수 있도록 지하철 역과 같은 랜드마커를 지웠던 것을 롤백한다.

    카페인 서비스만으로 목적지를 찾아갈 수 있도록 하기 위해서 특정 장소 검색을 추가하고 싶지만, 해당 기능을 구현하기 위해선 검색당 비용이 많이 청구되는 장소 검색 API를 추가해야 했기에 현실적으로 지금 당장 구현하기 어렵다고 판단했습니다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git "a/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260/page/3.html" "b/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260/page/3.html" index 6dcd8e1..f3b3c6c 100644 --- "a/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260/page/3.html" +++ "b/tags/\354\240\204\352\270\260\354\260\250-\354\202\254\354\232\251\352\270\260/page/3.html" @@ -5,7 +5,7 @@ "전기차 사용기" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -27,7 +27,7 @@ 차주 분과 인터뷰 하고 싶었지만, 차 내부에서 너무 바빠보이셔서 그럴 수 없었습니다.

    전기차 충전을 기다리면서 무엇을 할 수 있을까요? 이 분은 다행히도 업무를 보고 계셨지만, 다른 차주들은 무엇을 하고 보낼지 궁금해졌습니다.

    no offset

    휴게소에는 충전소가 하나 더 있었습니다.

    한 곳은 사용중이지만, 다른 한 곳은 사용할 수 있었습니다.

    저희는 이 충전소를 사용해보기로 했습니다.

    no offset

    사용할 수 있으니깐 들어가봐야지! 하고 도착한 순간 아차 싶었습니다.

    "아, 충전소가 외부인 사용 금지일 수 있었지?"

    저희는 분명히 서비스를 직접 개발했으니깐 다 알고 있던 사항이었지만, 전혀 생각치 못했습니다.

    서비스를 개발하는 내내 외부인 개방 충전소에 대한 중요성을 간파하였고, 이 기능을 넣었으면서도 사용하지 않고 충전소를 방문한 것이었습니다.

    바로 앞에 있어서 다행이었지만, 어찌됐든 이 충전소를 사용할 수 없었습니다.

    따라서 저희는 휴게소를 떠나는 내내 이 문제에 대해서 토론을 할 수 밖에 없었습니다.

    분명 우리가 만든 서비스인데 왜 놓쳤을까?

    맛있는 점심

    no offset

    파주닭국수 본점에서 맛있는 식사를 했습니다.

    비록 식당에는 전기차 충전소가 없었지만, 인근에 충전소가 있어 실험을 하나 해볼 수 있었습니다.

    인근 충전소와 식당의 거리가 가까워 보이는데, 과연 걸어갈 수 있을까?

    실제로 걷지는 않았습니다만 차 타면서 지나가면서 확인해본 결과 직접 걸을 수 없는 거리였습니다. (굉장히 걷기 싫은 수준의 먼 거리였습니다.)

    집에 있는 PHEV를 탈 기회가 많아 전기차 충전소를 자주 방문했던 저는 이런 점을 잘 알고 있었습니다.

    다행히 이 부분을 잘 알고 있었기에 저희는 이 부분을 서비스에 반영하였고, 모든 데이터를 포기하지 않았던 것이 옳은 선택이었다는 것을 확인하게 되었습니다.

    no offset

    식사가 끝나고 드디어 마장호수로 출발하게 되었습니다.

    마장호수 도착

    마장호수에 도착하자마자 충전소에 방문했습니다.

    no offset

    통계에서는 사용률이 적을 것이라고 하였는데 저희만 있었습니다.

    no offset no offset

    2기 중 1곳을 저희가 사용하였고, 마장호수를 돌았습니다.

    no offset

    약 50분 간 산책을 하고, 돌아와보니 충전기 다 되어있었습니다.

    사실 마장호수 까지 오는 내내 든 생각이었지만, 전기차의 배터리가 생각보다 오래 간다는 생각이 들었습니다.

    일부러 회생제동 기능도 끄고, 에어컨을 강하게 틀어서 배터리를 소진하려고 하였으나, 85km를 주행하는 동안 겨우 20%를 소모하였습니다.

    충전기를 꽂을 때 50%였으나, 호수를 한바퀴 돌고 오니 이미 100%가 되어있었습니다.

    여담이지만, 저희가 돌아왔을 때 옆 자리에는 전기 화물차가 있어 충전소가 가득 찼습니다.

    또, 앱에서도 충전기 사용 여부가 업데이트 되는 것을 확인했습니다.

    no offset

    배터리 성능에는 좋지 않고 가격도 비싸서 이를 자주 사용하는 것은 좋지 않겠지만, 급한 사람들은 급속 충전기를 사용하면 되겠구나 싶었습니다.

    따라서 급속과 완속은 더더욱 다른 개념으로 봐야겠다는 생각이 들었습니다.

    제가 그동안 경험했던 전기차 충전소는 완속 기준이었기에 신선한 경험이었습니다.

    선릉으로 돌아오다

    no offset

    선릉으로 돌아와서 차량을 반납하였습니다.

    저희는 이번 여정을 통해 카페인 서비스에서 어떤 점을 개선해야할지 좀 더 명확하게 알게되었습니다.

    1. 현재 서비스에서 제공하는 기능들로 충전소를 검색하는 것은 가능하며, 충전소의 위치를 정확하게 파악하는 것도 가능하다.
    2. 하지만 충전소가 없는 목적지는 검색할 수 없고, 현 위치가 어디인지 가늠하기가 어려워진다.
    3. 충전소를 사용할 수 있다고 표기되어 있더라도 외부인 개방이 아닐 수 있다. 정보가 정확히 제공됨에도 불구하고 이를 단번에 눈치채기 어렵다.
    4. 이러한 문제를 예상하여 외부인 개방 여부를 필터링 할 수 있는 기능을 제공하고 있음에도 불구하고 사용하지 않았다.
    5. 충전소의 통계 자료의 적중률은 높았으나, 좀 더 많은 충전소를 들려 확인해봐야 할 것 같았다.
    6. 전기자동차는 생각보다 오래가고 상품성이 있었다. 주행 능력도 충분하고, 인프라가 잘 되어있다. 이걸 왜 욕하지? 라는 생각이 들었다.
    7. 지도 확대 허용 범위가 너무 좁아서 사용하는데 불편한건 실제 상황에서 더 불편했다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git "a/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261.html" "b/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261.html" index 933e3c1..f540a67 100644 --- "a/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261.html" +++ "b/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261.html" @@ -5,7 +5,7 @@ "전기차 충전소 앱" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "전기차 충전소 앱" 태그로 연결된 4개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 4분
    가브리엘

    지난 4주 간 변경사항 및 신규 기능을 소개합니다!!

    이번 업데이트에는 사용자 경험을 직접 수집하여 피드백을 반영하였답니다!

    마커 클러스터링

    마커 클러스터링이란?

    마커 클러스터링은 지도에 표시되는 마커들을 클러스터로 묶어서 표시하는 것을 말합니다. 마커 클러스터링을 사용하면 지도에 표시되는 마커의 수를 줄일 수 있습니다. 마커 클러스터링은 지도에 표시되는 마커의 수가 많을 때 유용하게 사용할 수 있습니다.

    어디에서 확인할 수 있나요?

    지도를 축소하는 경우, 마커가 클러스터로 묶여 표시됩니다. 클러스터를 클릭하면 해당 지역으로 확대됩니다.

    지역 클러스터링 서버 클러스터링

    도시 검색 기능

    도시 검색 기능이란?

    기존 검색창은 충전소의 이름과 주소를 기반으로 한 검색이 가능했습니다.

    이제는 대한민국의 주요 도시들을 검색할 수 있는 기능이 추가되었습니다.

    원하는 지역을 검색하고, 해당 지역으로 빠르게 이동할 수 있으며 지도 조작에 많은 도움이 됩니다.

    어디에서 확인할 수 있나요?

    검색창에 원하는 지역을 입력하면 바로 확인할 수 있습니다.

    도시 검색결과

    디자인 개선

    인포 윈도우가 개선되었어요!

    기존 인포 윈도우는 충전소의 이름과 주소만을 표시하고 있었습니다.

    이제는 사용량을 제공하며, 길찾기 기능도 제공합니다.

    인포 윈도우

    충전소 사용 통계 정보 디자인이 변경되었어요

    통계 정보

    새로워진 탭 디자인과 색상을 적용하였습니다.

    충전소 마커가 이원화 되었습니다.

    충전소 마커 충전소 마커

    지도를 축소할 수록 마커가 도로를 가리는 현상이 있어 사이즈가 대폭 축소되었습니다.

    단, 확대하는 경우에는 기존과 동일한 형태의 마커를 제공합니다.

    - + \ No newline at end of file diff --git "a/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/2.html" "b/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/2.html" index 2e790b5..4bd0c01 100644 --- "a/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/2.html" +++ "b/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/2.html" @@ -5,7 +5,7 @@ "전기차 충전소 앱" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -14,8 +14,9 @@ no offset no offset no offset -no offset

    - +no offset +no offset

    + \ No newline at end of file diff --git "a/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/3.html" "b/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/3.html" index 495bf51..d6de8c9 100644 --- "a/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/3.html" +++ "b/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/3.html" @@ -5,7 +5,7 @@ "전기차 충전소 앱" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "전기차 충전소 앱" 태그로 연결된 4개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 15분
    가브리엘
    센트

    안녕하세요? 센트와 가브리엘 입니다.

    저희 카페인 팀에서는 지난번 카페인 서비스 1차 체험 진행 이후 일부 기능 개선이 있었습니다. 기능 개선의 유용성을 판별하고자 카페인 서비스 2차 체험을 다녀왔습니다.

    저희 팀에서 1차 체험 이후 개선한 사항은 다음과 같습니다.

    1. 지역검색

    no offset

    • 이제는 검색어를 입력하는 경우, 전국 도시의 주소가 같이 제공됩니다.

    2. 충전소 마커를 확인할 수 있는 지도 영역 확장

    no offset

    (기존에는 위 사진보다 좁은 영역만을 호출하는 것이 허용되었다.)

    • 모바일에서 좀 더 넓은 영역을 호출하는 것을 허용했습니다. 원래는 디바이스 너비를 고려하지 않고 줌 레벨 기준으로 요청을 제한했으나, 이제는 사용자 디바이스에 보이는 지도의 영역 크기를 기반으로 요청을 제한하는 방식을 도입했습니다.
    • 기존에 사용하던 마커의 단점은, 그 크기가 너무 크다는 것이었습니다. 이로인해 더 넓은 영역을 보여주는 경우에 마커들이 겹치는 현상이 있었는데요, 이를 수정하기 위해 특정 영역 크기 이상에서는 마커를 좀 더 간소화 된 디자인으로 보이도록 개선하였습니다.
    • 마커 사이즈가 작아지면서 사용 가능한 충전기 개수가 더이상 들어갈 공간이 없어졌습니다. 따라서 마커 색상은 그대로 유지를 하되, 인포 윈도우에 현재 사용 가능한 충전기 개수를 보여주는 방식으로 디자인을 개선하였습니다.

    체험 규칙 설정

    개선한 기능이 실제로 유용한지 확인해보기 위해 저희는 카페인 서비스 2차 체험의 규칙을 다음과 같이 설정했습니다.

    저희는 좀 더 의미있는 경험을 하기위해 1차 체험 때 정했던 규칙에 더해서 다음과 같은 추가 규칙을 설정하였습니다.

    중간에 목표 지점이 많이 변경된다

    지난 카페인 서비스 1차 체험에서는 지역 검색이 없어 목표 지점을 찾는 것이 불편했습니다. 1차 체험 이후 지역 검색이 추가 되었으므로 이 기능이 얼마나 유용한지 경험해보고자 이 규칙을 설정했습니다.

    추가로 목표 지점 주변의 충전소를 확인할 때 새로 추가된 지도 영역 확장이 얼마나 유용한지도 경험해보고자 했습니다.

    체험 개요

    no offset

    1. 잠실역 출발
    2. 하남 만두집
    3. 다음 목적지 설정
    4. 판교

    체험 후기

    잠실역 출발

    no offset

    쏘카에서 EV6를 대여해서 가브리엘, 센트, 키아라가 잠실역에서 출발하였습니다. 저녁 퇴근 이후에 남이섬을 가려고 목적지를 설정하였으나 배가 너무 고파서 가는 길에 식사를 하자고 얘기가 나왔습니다.

    하남 만두집

    따라서 진정한 처음 목적지는 스타필드였으나, 가브리엘은 동네 주민이라 스타필드를 너무 잘 알고 있었습니다. 따라서 스타필드에 전기차 충전소가 어디에 있는지도 알고있으므로 목적지를 급하게 변경하기로 했습니다. 이 때 목적지 변경을 위해 주변 식당을 둘러보던 중에 괜찮은 식당을 발견해서 해당 식당을 기준으로 주변 충전소를 확인해보기로 했습니다.

    no offset

    식당 주변을 가기 위해 지역 검색을 처음으로 사용하여 식당과 가까운 지역을 탐색할 수 있었습니다.

    이 과정에서 식당에는 충전소가 없다는 사실을 알게되어, 근처 충전소를 찾아보기 위해서 지도를 축소했더니 1차 체험때와는 달리 더 넓은 영역을 보여줬습니다. 이전에는 마커 자체가 보이지 않아 답답하였으나, 이제는 더 넓은 영역을 조회할 수 있게 되어 편리했습니다.

    지난 체험 이후로 피드백을 자체 수집하여 개발한 기능들이 편하다는 것을 식당에 가는 길에 느낄 수 있었습니다.

    다음 목적지 설정

    no offset

    하남 만두집에서 식사를 하다가 알게된 사실은, 남이섬은 생각보다 너무 멀다는 것이었습니다. 식사를 마치고 남이섬에 가면, 충전도 제대로 못하고 돌아올 판이었습니다.

    식사를 하면서 다른 목적지를 알아봤는데, 가브리엘이 예전에 가봤던 곳 중에서 남양주의 물의 정원이 시간을 떼우기 좋다는 소리를 하였습니다. 따라서 물의 정원을 검색해보았습니다.

    놀랍게도 물의정원은 검색결과에 없었습니다!

    어쩔 수 없이 카카오 지도로 물의 정원 위치를 확인하여 주소를 알아내었고, 이 주소를 카페인 검색창에 넣었습니다. 저희는 이 과정에서 카페인 서비스는 업체명 조회가 안된다는 것이 치명적인 단점이라고 생각했습니다. 다만, 이 기능은 검색 할 때마다 많은 비용이 청구되어 현실적으로 지금 당장 기능을 넣는 것은 어렵다고 판단했습니다.

    결국 주소 검색을 통해 물의 정원과 가장 가까운 충전소를 알아내었습니다.

    그런데! 지도를 축소해서 확인해 보니 해당 충전소는 물의 정원과 생각보다 멀었습니다.

    no offset

    no offset

    무려 걸어서 30분이나 걸리는 충전소였습니다!

    전기차 충전을 위해 왕복 1시간이나 걸리는 거리를 걸을 수 없다고 생각하였습니다.

    물론 지난 체험에서 전기차가 생각보다 배터리가 오래간다는 사실을 알고 있었지만, 만약 저희처럼 충전이 급한 사용자라면 목적지를 포기할 수 밖에 없겠구나 라는 생각이 들었습니다.

    마지막으로 정한 목적지는, 의외의 결정이었습니다.

    굉장히 발전된 첨단 도시로 알려진 판교였습니다!

    사실은 앞으로 갈지도 모르는 판교를 미리 구경이나 해보자는게 이유였지만 비밀입니다(?)

    일단 판교역은 IT서비스 회사들이 많이 몰려있는 곳이었습니다.

    따라서 저희는 판교역을 카페인 검색창에 검색했습니다.

    no offset

    지도를 판교역으로 이동하여 외부인 개방인 충전소를 찾았는데, 판교공영주차장이 보여서 해당 충전소를 목적지로 잡고 출발했습니다.

    판교

    하남에서 판교를 가기 위해서는 서하남IC를 지나야했습니다.

    가는 길에 우리 서비스에 나오는 정보와 실제 정보가 일치하는지 점검차 서하남 간이 휴게소를 들려봤습니다.

    이 휴게소에도 충전소가 있다고 검색이 되었기 때문입니다!

    no offset

    검색 당시에는 2대의 충전기가 있다고 나왔고, 둘다 사용이 가능하다고 되어있었는데 실제로 확인해보니 일치하는 것을 확인했습니다.

    먼 길을 달려 판교에 도착하였습니다.

    주차장에 들어오기 전, 카페인 서비스를 확인해보니 판교공영주차장의 충전기 총 12기 중 10기가 사용가능한 상태였습니다.

    정작 들어와서 보니 입구부터 너무 많은 전기차들이 충전기를 사용중이었습니다.

    뭔가 이상하다 싶었지만, 아직 서버에 반영이 안된건가? 하면서 비어있는 충전기를 찾았습니다.

    no offset no offset

    충전기를 꽂고 나서 알게된 것은 카페인 서비스에 나온 충전소 회사명과 방금 꽂은 충전기 회사명이 다르다는 것이었습니다.

    알고보니 음성 인식으로 네비에 검색한 충전소는 판교공영주차장이 아닌 판교역 환승 주차장이라 엉뚱한 곳으로 온 것이었습니다!!!

    다행인 점은 우리 서비스에서 제공하는 충전기 사용 여부 정보가 잘못된 것이 아니었다는 것이었습니다.

    그래서 애초에 가고자 했던 판교공영주자창에 대한 카페인 서비스의 정보가 실제와 동일한지 확인해보러 걸어서 이동했습니다. (바로 앞에 있었기 때문입니다.)

    no offset no offset

    도착해보니 1층의 충전기들이 모두 공사중이었고, 서비스의 정보가 실제로도 불일치 하는 줄 알았습니다. 다시 상세 정보를 보니 3~6층에 충전기들에 대한 정보라는 것이 명시되어 있었고, 실제로도 이와 동일한 것을 확인했습니다.

    no offset

    저희는 시간이 너무 흘러 다시 잠실로 돌아와 차를 반납하고 체험을 마무리 했습니다.

    결론

    불편했던 점

    • 디바이스에 보여지는 지도 영역 확장시에 원하는 정보를 볼 수 없는 것이 불편했다.
      • 지도를 확대해주세요 모달이 뜨고, 원래 있던 충전소 마커가 전부 사라진다.
    • 현재 나의 위치를 알아볼 수 있는 수단이 없어 불편했다.
      • 현위치를 나타내는 핀 (1차 체험기에서도 언급했던 부분)
      • 내 위치를 상대적으로 알 수 있는 랜드마크의 부족
    • 특정 장소(매장명) 검색이 안돼서 카페인 서비스만으로 목적지를 찾아가기 불편했다.
      • 카카오맵 등을 활용해 특정 장소 검색을 진행해야 했다.

    다음 목표

    앞선 불편했던점을 개선하기 위해 다음과 같은 기능 개선을 추가로 진행할 예정입니다.

    • 디바이스에 보여지는 지도 영역 확장에 제한이 생기지 않게 충전소 마커 클러스터링을 우선적으로 도입한다.
    • 현재 나의 위치를 알아볼 수 있도록 지하철 역과 같은 랜드마커를 지웠던 것을 롤백한다.

    카페인 서비스만으로 목적지를 찾아갈 수 있도록 하기 위해서 특정 장소 검색을 추가하고 싶지만, 해당 기능을 구현하기 위해선 검색당 비용이 많이 청구되는 장소 검색 API를 추가해야 했기에 현실적으로 지금 당장 구현하기 어렵다고 판단했습니다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git "a/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/4.html" "b/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/4.html" index 00ff827..4df05e4 100644 --- "a/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/4.html" +++ "b/tags/\354\240\204\352\270\260\354\260\250-\354\266\251\354\240\204\354\206\214-\354\225\261/page/4.html" @@ -5,7 +5,7 @@ "전기차 충전소 앱" 태그로 연결된 4개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -27,7 +27,7 @@ 차주 분과 인터뷰 하고 싶었지만, 차 내부에서 너무 바빠보이셔서 그럴 수 없었습니다.

    전기차 충전을 기다리면서 무엇을 할 수 있을까요? 이 분은 다행히도 업무를 보고 계셨지만, 다른 차주들은 무엇을 하고 보낼지 궁금해졌습니다.

    no offset

    휴게소에는 충전소가 하나 더 있었습니다.

    한 곳은 사용중이지만, 다른 한 곳은 사용할 수 있었습니다.

    저희는 이 충전소를 사용해보기로 했습니다.

    no offset

    사용할 수 있으니깐 들어가봐야지! 하고 도착한 순간 아차 싶었습니다.

    "아, 충전소가 외부인 사용 금지일 수 있었지?"

    저희는 분명히 서비스를 직접 개발했으니깐 다 알고 있던 사항이었지만, 전혀 생각치 못했습니다.

    서비스를 개발하는 내내 외부인 개방 충전소에 대한 중요성을 간파하였고, 이 기능을 넣었으면서도 사용하지 않고 충전소를 방문한 것이었습니다.

    바로 앞에 있어서 다행이었지만, 어찌됐든 이 충전소를 사용할 수 없었습니다.

    따라서 저희는 휴게소를 떠나는 내내 이 문제에 대해서 토론을 할 수 밖에 없었습니다.

    분명 우리가 만든 서비스인데 왜 놓쳤을까?

    맛있는 점심

    no offset

    파주닭국수 본점에서 맛있는 식사를 했습니다.

    비록 식당에는 전기차 충전소가 없었지만, 인근에 충전소가 있어 실험을 하나 해볼 수 있었습니다.

    인근 충전소와 식당의 거리가 가까워 보이는데, 과연 걸어갈 수 있을까?

    실제로 걷지는 않았습니다만 차 타면서 지나가면서 확인해본 결과 직접 걸을 수 없는 거리였습니다. (굉장히 걷기 싫은 수준의 먼 거리였습니다.)

    집에 있는 PHEV를 탈 기회가 많아 전기차 충전소를 자주 방문했던 저는 이런 점을 잘 알고 있었습니다.

    다행히 이 부분을 잘 알고 있었기에 저희는 이 부분을 서비스에 반영하였고, 모든 데이터를 포기하지 않았던 것이 옳은 선택이었다는 것을 확인하게 되었습니다.

    no offset

    식사가 끝나고 드디어 마장호수로 출발하게 되었습니다.

    마장호수 도착

    마장호수에 도착하자마자 충전소에 방문했습니다.

    no offset

    통계에서는 사용률이 적을 것이라고 하였는데 저희만 있었습니다.

    no offset no offset

    2기 중 1곳을 저희가 사용하였고, 마장호수를 돌았습니다.

    no offset

    약 50분 간 산책을 하고, 돌아와보니 충전기 다 되어있었습니다.

    사실 마장호수 까지 오는 내내 든 생각이었지만, 전기차의 배터리가 생각보다 오래 간다는 생각이 들었습니다.

    일부러 회생제동 기능도 끄고, 에어컨을 강하게 틀어서 배터리를 소진하려고 하였으나, 85km를 주행하는 동안 겨우 20%를 소모하였습니다.

    충전기를 꽂을 때 50%였으나, 호수를 한바퀴 돌고 오니 이미 100%가 되어있었습니다.

    여담이지만, 저희가 돌아왔을 때 옆 자리에는 전기 화물차가 있어 충전소가 가득 찼습니다.

    또, 앱에서도 충전기 사용 여부가 업데이트 되는 것을 확인했습니다.

    no offset

    배터리 성능에는 좋지 않고 가격도 비싸서 이를 자주 사용하는 것은 좋지 않겠지만, 급한 사람들은 급속 충전기를 사용하면 되겠구나 싶었습니다.

    따라서 급속과 완속은 더더욱 다른 개념으로 봐야겠다는 생각이 들었습니다.

    제가 그동안 경험했던 전기차 충전소는 완속 기준이었기에 신선한 경험이었습니다.

    선릉으로 돌아오다

    no offset

    선릉으로 돌아와서 차량을 반납하였습니다.

    저희는 이번 여정을 통해 카페인 서비스에서 어떤 점을 개선해야할지 좀 더 명확하게 알게되었습니다.

    1. 현재 서비스에서 제공하는 기능들로 충전소를 검색하는 것은 가능하며, 충전소의 위치를 정확하게 파악하는 것도 가능하다.
    2. 하지만 충전소가 없는 목적지는 검색할 수 없고, 현 위치가 어디인지 가늠하기가 어려워진다.
    3. 충전소를 사용할 수 있다고 표기되어 있더라도 외부인 개방이 아닐 수 있다. 정보가 정확히 제공됨에도 불구하고 이를 단번에 눈치채기 어렵다.
    4. 이러한 문제를 예상하여 외부인 개방 여부를 필터링 할 수 있는 기능을 제공하고 있음에도 불구하고 사용하지 않았다.
    5. 충전소의 통계 자료의 적중률은 높았으나, 좀 더 많은 충전소를 들려 확인해봐야 할 것 같았다.
    6. 전기자동차는 생각보다 오래가고 상품성이 있었다. 주행 능력도 충분하고, 인프라가 잘 되어있다. 이걸 왜 욕하지? 라는 생각이 들었다.
    7. 지도 확대 허용 범위가 너무 좁아서 사용하는데 불편한건 실제 상황에서 더 불편했다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git "a/tags/\354\240\204\354\227\255-\354\203\201\355\203\234-\352\264\200\353\246\254.html" "b/tags/\354\240\204\354\227\255-\354\203\201\355\203\234-\352\264\200\353\246\254.html" index 597214a..d2a7d3f 100644 --- "a/tags/\354\240\204\354\227\255-\354\203\201\355\203\234-\352\264\200\353\246\254.html" +++ "b/tags/\354\240\204\354\227\255-\354\203\201\355\203\234-\352\264\200\353\246\254.html" @@ -5,7 +5,7 @@ "전역 상태 관리" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -24,7 +24,7 @@ 빨간색은 개발자가 직접 건들지 못하지만 간접적으로 사용할 수 있는 영역 노란색은 React 18 엔진의 영역입니다.

    이외에 제공되는 다른 커스텀 훅들도 거의 비슷한 구조를 띄고 있습니다.

    // 추가로 구현할 수 있는 함수들

    export const useSetExternalState = <T>(store: DataObserver<T>) => {
    const { setState } = store;

    return setState;
    };

    export const useExternalValue = <T>(store: DataObserver<T>) => {
    const { subscribe, getState } = store;
    const state = useSyncExternalStore(subscribe, getState);

    return state;
    };

    // 바닐라JS 영역에서 자연스러운 읽기를 지원하는 함수

    export const getStoreSnapshot = <T>(store: DataObserver<T>) => {
    return store.getState();
    };

    더 다양한 예제는 여기에서 확인할 수 있고 작성한 라이브러리 코드 전문은 여기에서 확인할 수 있습니다.

    겨우 파일 수십 줄로 만든 초경량 상태관리 라이브러리였습니다

    - + \ No newline at end of file diff --git "a/tags/\354\240\204\354\227\255\354\203\201\355\203\234.html" "b/tags/\354\240\204\354\227\255\354\203\201\355\203\234.html" index d689875..c530ae7 100644 --- "a/tags/\354\240\204\354\227\255\354\203\201\355\203\234.html" +++ "b/tags/\354\240\204\354\227\255\354\203\201\355\203\234.html" @@ -5,7 +5,7 @@ "전역상태" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -24,7 +24,7 @@ 빨간색은 개발자가 직접 건들지 못하지만 간접적으로 사용할 수 있는 영역 노란색은 React 18 엔진의 영역입니다.

    이외에 제공되는 다른 커스텀 훅들도 거의 비슷한 구조를 띄고 있습니다.

    // 추가로 구현할 수 있는 함수들

    export const useSetExternalState = <T>(store: DataObserver<T>) => {
    const { setState } = store;

    return setState;
    };

    export const useExternalValue = <T>(store: DataObserver<T>) => {
    const { subscribe, getState } = store;
    const state = useSyncExternalStore(subscribe, getState);

    return state;
    };

    // 바닐라JS 영역에서 자연스러운 읽기를 지원하는 함수

    export const getStoreSnapshot = <T>(store: DataObserver<T>) => {
    return store.getState();
    };

    더 다양한 예제는 여기에서 확인할 수 있고 작성한 라이브러리 코드 전문은 여기에서 확인할 수 있습니다.

    겨우 파일 수십 줄로 만든 초경량 상태관리 라이브러리였습니다

    - + \ No newline at end of file diff --git "a/tags/\354\271\264\355\216\230\354\235\270.html" "b/tags/\354\271\264\355\216\230\354\235\270.html" index 7e2068a..b72356b 100644 --- "a/tags/\354\271\264\355\216\230\354\235\270.html" +++ "b/tags/\354\271\264\355\216\230\354\235\270.html" @@ -5,7 +5,7 @@ "카페인" 태그로 연결된 5개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "카페인" 태그로 연결된 5개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 4분
    가브리엘

    지난 4주 간 변경사항 및 신규 기능을 소개합니다!!

    이번 업데이트에는 사용자 경험을 직접 수집하여 피드백을 반영하였답니다!

    마커 클러스터링

    마커 클러스터링이란?

    마커 클러스터링은 지도에 표시되는 마커들을 클러스터로 묶어서 표시하는 것을 말합니다. 마커 클러스터링을 사용하면 지도에 표시되는 마커의 수를 줄일 수 있습니다. 마커 클러스터링은 지도에 표시되는 마커의 수가 많을 때 유용하게 사용할 수 있습니다.

    어디에서 확인할 수 있나요?

    지도를 축소하는 경우, 마커가 클러스터로 묶여 표시됩니다. 클러스터를 클릭하면 해당 지역으로 확대됩니다.

    지역 클러스터링 서버 클러스터링

    도시 검색 기능

    도시 검색 기능이란?

    기존 검색창은 충전소의 이름과 주소를 기반으로 한 검색이 가능했습니다.

    이제는 대한민국의 주요 도시들을 검색할 수 있는 기능이 추가되었습니다.

    원하는 지역을 검색하고, 해당 지역으로 빠르게 이동할 수 있으며 지도 조작에 많은 도움이 됩니다.

    어디에서 확인할 수 있나요?

    검색창에 원하는 지역을 입력하면 바로 확인할 수 있습니다.

    도시 검색결과

    디자인 개선

    인포 윈도우가 개선되었어요!

    기존 인포 윈도우는 충전소의 이름과 주소만을 표시하고 있었습니다.

    이제는 사용량을 제공하며, 길찾기 기능도 제공합니다.

    인포 윈도우

    충전소 사용 통계 정보 디자인이 변경되었어요

    통계 정보

    새로워진 탭 디자인과 색상을 적용하였습니다.

    충전소 마커가 이원화 되었습니다.

    충전소 마커 충전소 마커

    지도를 축소할 수록 마커가 도로를 가리는 현상이 있어 사이즈가 대폭 축소되었습니다.

    단, 확대하는 경우에는 기존과 동일한 형태의 마커를 제공합니다.

    - + \ No newline at end of file diff --git "a/tags/\354\271\264\355\216\230\354\235\270/page/2.html" "b/tags/\354\271\264\355\216\230\354\235\270/page/2.html" index da1e1b8..35c95bb 100644 --- "a/tags/\354\271\264\355\216\230\354\235\270/page/2.html" +++ "b/tags/\354\271\264\355\216\230\354\235\270/page/2.html" @@ -5,7 +5,7 @@ "카페인" 태그로 연결된 5개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -14,8 +14,9 @@ no offset no offset no offset -no offset

    - +no offset +no offset

    + \ No newline at end of file diff --git "a/tags/\354\271\264\355\216\230\354\235\270/page/3.html" "b/tags/\354\271\264\355\216\230\354\235\270/page/3.html" index cc128a4..71eebc8 100644 --- "a/tags/\354\271\264\355\216\230\354\235\270/page/3.html" +++ "b/tags/\354\271\264\355\216\230\354\235\270/page/3.html" @@ -5,7 +5,7 @@ "카페인" 태그로 연결된 5개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "카페인" 태그로 연결된 5개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 15분
    가브리엘
    센트

    안녕하세요? 센트와 가브리엘 입니다.

    저희 카페인 팀에서는 지난번 카페인 서비스 1차 체험 진행 이후 일부 기능 개선이 있었습니다. 기능 개선의 유용성을 판별하고자 카페인 서비스 2차 체험을 다녀왔습니다.

    저희 팀에서 1차 체험 이후 개선한 사항은 다음과 같습니다.

    1. 지역검색

    no offset

    • 이제는 검색어를 입력하는 경우, 전국 도시의 주소가 같이 제공됩니다.

    2. 충전소 마커를 확인할 수 있는 지도 영역 확장

    no offset

    (기존에는 위 사진보다 좁은 영역만을 호출하는 것이 허용되었다.)

    • 모바일에서 좀 더 넓은 영역을 호출하는 것을 허용했습니다. 원래는 디바이스 너비를 고려하지 않고 줌 레벨 기준으로 요청을 제한했으나, 이제는 사용자 디바이스에 보이는 지도의 영역 크기를 기반으로 요청을 제한하는 방식을 도입했습니다.
    • 기존에 사용하던 마커의 단점은, 그 크기가 너무 크다는 것이었습니다. 이로인해 더 넓은 영역을 보여주는 경우에 마커들이 겹치는 현상이 있었는데요, 이를 수정하기 위해 특정 영역 크기 이상에서는 마커를 좀 더 간소화 된 디자인으로 보이도록 개선하였습니다.
    • 마커 사이즈가 작아지면서 사용 가능한 충전기 개수가 더이상 들어갈 공간이 없어졌습니다. 따라서 마커 색상은 그대로 유지를 하되, 인포 윈도우에 현재 사용 가능한 충전기 개수를 보여주는 방식으로 디자인을 개선하였습니다.

    체험 규칙 설정

    개선한 기능이 실제로 유용한지 확인해보기 위해 저희는 카페인 서비스 2차 체험의 규칙을 다음과 같이 설정했습니다.

    저희는 좀 더 의미있는 경험을 하기위해 1차 체험 때 정했던 규칙에 더해서 다음과 같은 추가 규칙을 설정하였습니다.

    중간에 목표 지점이 많이 변경된다

    지난 카페인 서비스 1차 체험에서는 지역 검색이 없어 목표 지점을 찾는 것이 불편했습니다. 1차 체험 이후 지역 검색이 추가 되었으므로 이 기능이 얼마나 유용한지 경험해보고자 이 규칙을 설정했습니다.

    추가로 목표 지점 주변의 충전소를 확인할 때 새로 추가된 지도 영역 확장이 얼마나 유용한지도 경험해보고자 했습니다.

    체험 개요

    no offset

    1. 잠실역 출발
    2. 하남 만두집
    3. 다음 목적지 설정
    4. 판교

    체험 후기

    잠실역 출발

    no offset

    쏘카에서 EV6를 대여해서 가브리엘, 센트, 키아라가 잠실역에서 출발하였습니다. 저녁 퇴근 이후에 남이섬을 가려고 목적지를 설정하였으나 배가 너무 고파서 가는 길에 식사를 하자고 얘기가 나왔습니다.

    하남 만두집

    따라서 진정한 처음 목적지는 스타필드였으나, 가브리엘은 동네 주민이라 스타필드를 너무 잘 알고 있었습니다. 따라서 스타필드에 전기차 충전소가 어디에 있는지도 알고있으므로 목적지를 급하게 변경하기로 했습니다. 이 때 목적지 변경을 위해 주변 식당을 둘러보던 중에 괜찮은 식당을 발견해서 해당 식당을 기준으로 주변 충전소를 확인해보기로 했습니다.

    no offset

    식당 주변을 가기 위해 지역 검색을 처음으로 사용하여 식당과 가까운 지역을 탐색할 수 있었습니다.

    이 과정에서 식당에는 충전소가 없다는 사실을 알게되어, 근처 충전소를 찾아보기 위해서 지도를 축소했더니 1차 체험때와는 달리 더 넓은 영역을 보여줬습니다. 이전에는 마커 자체가 보이지 않아 답답하였으나, 이제는 더 넓은 영역을 조회할 수 있게 되어 편리했습니다.

    지난 체험 이후로 피드백을 자체 수집하여 개발한 기능들이 편하다는 것을 식당에 가는 길에 느낄 수 있었습니다.

    다음 목적지 설정

    no offset

    하남 만두집에서 식사를 하다가 알게된 사실은, 남이섬은 생각보다 너무 멀다는 것이었습니다. 식사를 마치고 남이섬에 가면, 충전도 제대로 못하고 돌아올 판이었습니다.

    식사를 하면서 다른 목적지를 알아봤는데, 가브리엘이 예전에 가봤던 곳 중에서 남양주의 물의 정원이 시간을 떼우기 좋다는 소리를 하였습니다. 따라서 물의 정원을 검색해보았습니다.

    놀랍게도 물의정원은 검색결과에 없었습니다!

    어쩔 수 없이 카카오 지도로 물의 정원 위치를 확인하여 주소를 알아내었고, 이 주소를 카페인 검색창에 넣었습니다. 저희는 이 과정에서 카페인 서비스는 업체명 조회가 안된다는 것이 치명적인 단점이라고 생각했습니다. 다만, 이 기능은 검색 할 때마다 많은 비용이 청구되어 현실적으로 지금 당장 기능을 넣는 것은 어렵다고 판단했습니다.

    결국 주소 검색을 통해 물의 정원과 가장 가까운 충전소를 알아내었습니다.

    그런데! 지도를 축소해서 확인해 보니 해당 충전소는 물의 정원과 생각보다 멀었습니다.

    no offset

    no offset

    무려 걸어서 30분이나 걸리는 충전소였습니다!

    전기차 충전을 위해 왕복 1시간이나 걸리는 거리를 걸을 수 없다고 생각하였습니다.

    물론 지난 체험에서 전기차가 생각보다 배터리가 오래간다는 사실을 알고 있었지만, 만약 저희처럼 충전이 급한 사용자라면 목적지를 포기할 수 밖에 없겠구나 라는 생각이 들었습니다.

    마지막으로 정한 목적지는, 의외의 결정이었습니다.

    굉장히 발전된 첨단 도시로 알려진 판교였습니다!

    사실은 앞으로 갈지도 모르는 판교를 미리 구경이나 해보자는게 이유였지만 비밀입니다(?)

    일단 판교역은 IT서비스 회사들이 많이 몰려있는 곳이었습니다.

    따라서 저희는 판교역을 카페인 검색창에 검색했습니다.

    no offset

    지도를 판교역으로 이동하여 외부인 개방인 충전소를 찾았는데, 판교공영주차장이 보여서 해당 충전소를 목적지로 잡고 출발했습니다.

    판교

    하남에서 판교를 가기 위해서는 서하남IC를 지나야했습니다.

    가는 길에 우리 서비스에 나오는 정보와 실제 정보가 일치하는지 점검차 서하남 간이 휴게소를 들려봤습니다.

    이 휴게소에도 충전소가 있다고 검색이 되었기 때문입니다!

    no offset

    검색 당시에는 2대의 충전기가 있다고 나왔고, 둘다 사용이 가능하다고 되어있었는데 실제로 확인해보니 일치하는 것을 확인했습니다.

    먼 길을 달려 판교에 도착하였습니다.

    주차장에 들어오기 전, 카페인 서비스를 확인해보니 판교공영주차장의 충전기 총 12기 중 10기가 사용가능한 상태였습니다.

    정작 들어와서 보니 입구부터 너무 많은 전기차들이 충전기를 사용중이었습니다.

    뭔가 이상하다 싶었지만, 아직 서버에 반영이 안된건가? 하면서 비어있는 충전기를 찾았습니다.

    no offset no offset

    충전기를 꽂고 나서 알게된 것은 카페인 서비스에 나온 충전소 회사명과 방금 꽂은 충전기 회사명이 다르다는 것이었습니다.

    알고보니 음성 인식으로 네비에 검색한 충전소는 판교공영주차장이 아닌 판교역 환승 주차장이라 엉뚱한 곳으로 온 것이었습니다!!!

    다행인 점은 우리 서비스에서 제공하는 충전기 사용 여부 정보가 잘못된 것이 아니었다는 것이었습니다.

    그래서 애초에 가고자 했던 판교공영주자창에 대한 카페인 서비스의 정보가 실제와 동일한지 확인해보러 걸어서 이동했습니다. (바로 앞에 있었기 때문입니다.)

    no offset no offset

    도착해보니 1층의 충전기들이 모두 공사중이었고, 서비스의 정보가 실제로도 불일치 하는 줄 알았습니다. 다시 상세 정보를 보니 3~6층에 충전기들에 대한 정보라는 것이 명시되어 있었고, 실제로도 이와 동일한 것을 확인했습니다.

    no offset

    저희는 시간이 너무 흘러 다시 잠실로 돌아와 차를 반납하고 체험을 마무리 했습니다.

    결론

    불편했던 점

    • 디바이스에 보여지는 지도 영역 확장시에 원하는 정보를 볼 수 없는 것이 불편했다.
      • 지도를 확대해주세요 모달이 뜨고, 원래 있던 충전소 마커가 전부 사라진다.
    • 현재 나의 위치를 알아볼 수 있는 수단이 없어 불편했다.
      • 현위치를 나타내는 핀 (1차 체험기에서도 언급했던 부분)
      • 내 위치를 상대적으로 알 수 있는 랜드마크의 부족
    • 특정 장소(매장명) 검색이 안돼서 카페인 서비스만으로 목적지를 찾아가기 불편했다.
      • 카카오맵 등을 활용해 특정 장소 검색을 진행해야 했다.

    다음 목표

    앞선 불편했던점을 개선하기 위해 다음과 같은 기능 개선을 추가로 진행할 예정입니다.

    • 디바이스에 보여지는 지도 영역 확장에 제한이 생기지 않게 충전소 마커 클러스터링을 우선적으로 도입한다.
    • 현재 나의 위치를 알아볼 수 있도록 지하철 역과 같은 랜드마커를 지웠던 것을 롤백한다.

    카페인 서비스만으로 목적지를 찾아갈 수 있도록 하기 위해서 특정 장소 검색을 추가하고 싶지만, 해당 기능을 구현하기 위해선 검색당 비용이 많이 청구되는 장소 검색 API를 추가해야 했기에 현실적으로 지금 당장 구현하기 어렵다고 판단했습니다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git "a/tags/\354\271\264\355\216\230\354\235\270/page/4.html" "b/tags/\354\271\264\355\216\230\354\235\270/page/4.html" index f268928..30dbb8c 100644 --- "a/tags/\354\271\264\355\216\230\354\235\270/page/4.html" +++ "b/tags/\354\271\264\355\216\230\354\235\270/page/4.html" @@ -5,7 +5,7 @@ "카페인" 태그로 연결된 5개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -27,7 +27,7 @@ 차주 분과 인터뷰 하고 싶었지만, 차 내부에서 너무 바빠보이셔서 그럴 수 없었습니다.

    전기차 충전을 기다리면서 무엇을 할 수 있을까요? 이 분은 다행히도 업무를 보고 계셨지만, 다른 차주들은 무엇을 하고 보낼지 궁금해졌습니다.

    no offset

    휴게소에는 충전소가 하나 더 있었습니다.

    한 곳은 사용중이지만, 다른 한 곳은 사용할 수 있었습니다.

    저희는 이 충전소를 사용해보기로 했습니다.

    no offset

    사용할 수 있으니깐 들어가봐야지! 하고 도착한 순간 아차 싶었습니다.

    "아, 충전소가 외부인 사용 금지일 수 있었지?"

    저희는 분명히 서비스를 직접 개발했으니깐 다 알고 있던 사항이었지만, 전혀 생각치 못했습니다.

    서비스를 개발하는 내내 외부인 개방 충전소에 대한 중요성을 간파하였고, 이 기능을 넣었으면서도 사용하지 않고 충전소를 방문한 것이었습니다.

    바로 앞에 있어서 다행이었지만, 어찌됐든 이 충전소를 사용할 수 없었습니다.

    따라서 저희는 휴게소를 떠나는 내내 이 문제에 대해서 토론을 할 수 밖에 없었습니다.

    분명 우리가 만든 서비스인데 왜 놓쳤을까?

    맛있는 점심

    no offset

    파주닭국수 본점에서 맛있는 식사를 했습니다.

    비록 식당에는 전기차 충전소가 없었지만, 인근에 충전소가 있어 실험을 하나 해볼 수 있었습니다.

    인근 충전소와 식당의 거리가 가까워 보이는데, 과연 걸어갈 수 있을까?

    실제로 걷지는 않았습니다만 차 타면서 지나가면서 확인해본 결과 직접 걸을 수 없는 거리였습니다. (굉장히 걷기 싫은 수준의 먼 거리였습니다.)

    집에 있는 PHEV를 탈 기회가 많아 전기차 충전소를 자주 방문했던 저는 이런 점을 잘 알고 있었습니다.

    다행히 이 부분을 잘 알고 있었기에 저희는 이 부분을 서비스에 반영하였고, 모든 데이터를 포기하지 않았던 것이 옳은 선택이었다는 것을 확인하게 되었습니다.

    no offset

    식사가 끝나고 드디어 마장호수로 출발하게 되었습니다.

    마장호수 도착

    마장호수에 도착하자마자 충전소에 방문했습니다.

    no offset

    통계에서는 사용률이 적을 것이라고 하였는데 저희만 있었습니다.

    no offset no offset

    2기 중 1곳을 저희가 사용하였고, 마장호수를 돌았습니다.

    no offset

    약 50분 간 산책을 하고, 돌아와보니 충전기 다 되어있었습니다.

    사실 마장호수 까지 오는 내내 든 생각이었지만, 전기차의 배터리가 생각보다 오래 간다는 생각이 들었습니다.

    일부러 회생제동 기능도 끄고, 에어컨을 강하게 틀어서 배터리를 소진하려고 하였으나, 85km를 주행하는 동안 겨우 20%를 소모하였습니다.

    충전기를 꽂을 때 50%였으나, 호수를 한바퀴 돌고 오니 이미 100%가 되어있었습니다.

    여담이지만, 저희가 돌아왔을 때 옆 자리에는 전기 화물차가 있어 충전소가 가득 찼습니다.

    또, 앱에서도 충전기 사용 여부가 업데이트 되는 것을 확인했습니다.

    no offset

    배터리 성능에는 좋지 않고 가격도 비싸서 이를 자주 사용하는 것은 좋지 않겠지만, 급한 사람들은 급속 충전기를 사용하면 되겠구나 싶었습니다.

    따라서 급속과 완속은 더더욱 다른 개념으로 봐야겠다는 생각이 들었습니다.

    제가 그동안 경험했던 전기차 충전소는 완속 기준이었기에 신선한 경험이었습니다.

    선릉으로 돌아오다

    no offset

    선릉으로 돌아와서 차량을 반납하였습니다.

    저희는 이번 여정을 통해 카페인 서비스에서 어떤 점을 개선해야할지 좀 더 명확하게 알게되었습니다.

    1. 현재 서비스에서 제공하는 기능들로 충전소를 검색하는 것은 가능하며, 충전소의 위치를 정확하게 파악하는 것도 가능하다.
    2. 하지만 충전소가 없는 목적지는 검색할 수 없고, 현 위치가 어디인지 가늠하기가 어려워진다.
    3. 충전소를 사용할 수 있다고 표기되어 있더라도 외부인 개방이 아닐 수 있다. 정보가 정확히 제공됨에도 불구하고 이를 단번에 눈치채기 어렵다.
    4. 이러한 문제를 예상하여 외부인 개방 여부를 필터링 할 수 있는 기능을 제공하고 있음에도 불구하고 사용하지 않았다.
    5. 충전소의 통계 자료의 적중률은 높았으나, 좀 더 많은 충전소를 들려 확인해봐야 할 것 같았다.
    6. 전기자동차는 생각보다 오래가고 상품성이 있었다. 주행 능력도 충분하고, 인프라가 잘 되어있다. 이걸 왜 욕하지? 라는 생각이 들었다.
    7. 지도 확대 허용 범위가 너무 좁아서 사용하는데 불편한건 실제 상황에서 더 불편했다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git "a/tags/\354\271\264\355\216\230\354\235\270/page/5.html" "b/tags/\354\271\264\355\216\230\354\235\270/page/5.html" index 18d490c..b805a0e 100644 --- "a/tags/\354\271\264\355\216\230\354\235\270/page/5.html" +++ "b/tags/\354\271\264\355\216\230\354\235\270/page/5.html" @@ -5,7 +5,7 @@ "카페인" 태그로 연결된 5개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -20,7 +20,7 @@ no offset no offset no offset

    집계 된 자료처럼 방문자들이 단순 방문만 한 것이 아니라, 수 많은 이벤트를 발생시키고 평균 참여 시간도 상당 부분 확보했음을 확인할 수 있습니다.

    - + \ No newline at end of file diff --git "a/tags/\355\205\214\354\212\244\355\212\270.html" "b/tags/\355\205\214\354\212\244\355\212\270.html" index 0803609..533145e 100644 --- "a/tags/\355\205\214\354\212\244\355\212\270.html" +++ "b/tags/\355\205\214\354\212\244\355\212\270.html" @@ -5,7 +5,7 @@ "테스트" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -22,7 +22,7 @@ 하지만 Storybook을 이용하면 특정 컴포넌트를 Storybook 위에 올려놓고 테스트를 할 수 있어 빠르게 작업이 가능합니다. 인터렉션이나 웹접근성을 확인해주는 플러그인도 존재하여 프론트엔드 개발에서 굉장히 중요한 역할로 부상했습니다.

    저희 팀은 이외에 Cypress를 사용하는 것도 고려하였으나, 지도와 결합된 애플리케이션을 테스트하기에 다소 어려움이 있어 위 라이브러리들을 개발에 활용했습니다.

    저희는 위 테스팅 라이브러리들을 원활히 활용하기 위해 테스트 자동화를 구축했습니다.

    Jest와 React Testing Library 테스트 자동화

    name: frontend-test

    on:
    pull_request:
    branches:
    - main
    - develop
    paths:
    - frontend/**
    - .github/**

    permissions:
    contents: read

    jobs:
    test:
    name: test-when-pull-request
    runs-on: ubuntu-latest
    environment: test
    defaults:
    run:
    working-directory: ./frontend
    steps:
    - name: Checkout PR
    uses: actions/checkout@v2
    - name: Install dependencies
    run: npm install
    - name: Test
    run: npm run test

    이벤트 트리거 설정

    pull_request 이벤트가 발생하였을 때, 해당 이벤트가 main 브랜치와 develop 브랜치에서만 동작합니다.

    변경 사항 경로 제한

    테스트를 실행할 때는 frontend 디렉토리와 .github 디렉토리 내의 파일들을 고려하도록 했습니다. 백엔드와의 환경 분리를 위해 이러한 접근 제한을 했습니다.

    권한 설정

    permissions은 읽기 권한만 설정되어 있어 코드나 파일을 변경을 방지합니다.

    작업(Job) 설정

    test라는 이름의 작업을 정의하였고, 이 작업에서는 Ubuntu 환경에서 테스트를 실행합니다. test라는 이름의 환경 변수를 사용합니다. 테스트는 (카페인 팀 레포지토리의) frontend 디렉토리에서 작업하도록 하였습니다.

    스텝(Step) 설정

    코드를 체크아웃하고, 의존성을 설치하며, 테스트를 실행하는 세 가지 단계로 구성되어 있습니다.

    이러한 설정을 통해 PR에 코드가 올라올 때 자동으로 프론트엔드 테스트가 실행됩니다.

    이러한 테스트 자동화 전략은 프론트엔드 애플리케이션을 안정적이게 개발하고 유지할 수 있도록 도와줍니다.

    Storybook의 빌드 자동화

    name: storybook-deploy

    on:
    pull_request:
    branches:
    - develop
    paths:
    - frontend/**
    - .github/**

    jobs:
    build:
    runs-on: ubuntu-22.04
    defaults:
    run:
    working-directory: ./frontend
    steps:
    - name: Setup Repository
    uses: actions/checkout@v3

    - name: Set up Node
    uses: actions/setup-node@v3
    with:
    node-version: 18.16.0

    - name: Install dependencies
    run: npm install

    - name: Cache node_modules
    id: cache
    uses: actions/cache@v3
    with:
    path: '**/node_modules'
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
    ${{ runner.os }}-node-

    - name: storybook build
    run: npm run build-storybook

    - name: Upload storybook build files to temp artifact
    uses: actions/upload-artifact@v3
    with:
    name: Storybook
    path: frontend/storybook-static
    deploy:
    needs: build
    runs-on: self-hosted
    steps:
    - name: Remove previous version app
    working-directory: .
    run: rm -rf dist

    - name: Download the built file to AWS
    uses: actions/download-artifact@v3
    with:
    name: Storybook
    path: frontend/dev/dist

    - name: Move folder
    working-directory: frontend/dev/
    run: |
    rm -rf /home/ubuntu/dist/*
    cp -r ./dist /home/ubuntu

    - name: comment PR
    uses: thollander/actions-comment-pull-request@v1
    env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    with:
    message: '🚀storybook: https://storybook.carffe.in/'

    비슷한 코드이지만, 매번 PR이 열릴 때 마다 스토리북이 자동으로 빌드 및 배포됩니다. 배포가 완료되면 배포된 URL을 알려 코드 리뷰할 때 참고할 수 있도록 돕습니다.

    이상 카페인 팀에서 사용하고 있는 테스팅 라이브러리와 테스트 자동화 방법을 알아봤습니다.

    - + \ No newline at end of file diff --git "a/tags/\355\224\274\353\223\234\353\260\261.html" "b/tags/\355\224\274\353\223\234\353\260\261.html" index 545d44d..3ecd4db 100644 --- "a/tags/\355\224\274\353\223\234\353\260\261.html" +++ "b/tags/\355\224\274\353\223\234\353\260\261.html" @@ -5,7 +5,7 @@ "피드백" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -14,8 +14,9 @@ no offset no offset no offset -no offset

    - +no offset +no offset

    + \ No newline at end of file diff --git "a/tags/\355\224\274\353\223\234\353\260\261/page/2.html" "b/tags/\355\224\274\353\223\234\353\260\261/page/2.html" index 959256a..c69b68d 100644 --- "a/tags/\355\224\274\353\223\234\353\260\261/page/2.html" +++ "b/tags/\355\224\274\353\223\234\353\260\261/page/2.html" @@ -5,7 +5,7 @@ "피드백" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -13,7 +13,7 @@

    "피드백" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

    모든 태그 보기

    · 약 15분
    가브리엘
    센트

    안녕하세요? 센트와 가브리엘 입니다.

    저희 카페인 팀에서는 지난번 카페인 서비스 1차 체험 진행 이후 일부 기능 개선이 있었습니다. 기능 개선의 유용성을 판별하고자 카페인 서비스 2차 체험을 다녀왔습니다.

    저희 팀에서 1차 체험 이후 개선한 사항은 다음과 같습니다.

    1. 지역검색

    no offset

    • 이제는 검색어를 입력하는 경우, 전국 도시의 주소가 같이 제공됩니다.

    2. 충전소 마커를 확인할 수 있는 지도 영역 확장

    no offset

    (기존에는 위 사진보다 좁은 영역만을 호출하는 것이 허용되었다.)

    • 모바일에서 좀 더 넓은 영역을 호출하는 것을 허용했습니다. 원래는 디바이스 너비를 고려하지 않고 줌 레벨 기준으로 요청을 제한했으나, 이제는 사용자 디바이스에 보이는 지도의 영역 크기를 기반으로 요청을 제한하는 방식을 도입했습니다.
    • 기존에 사용하던 마커의 단점은, 그 크기가 너무 크다는 것이었습니다. 이로인해 더 넓은 영역을 보여주는 경우에 마커들이 겹치는 현상이 있었는데요, 이를 수정하기 위해 특정 영역 크기 이상에서는 마커를 좀 더 간소화 된 디자인으로 보이도록 개선하였습니다.
    • 마커 사이즈가 작아지면서 사용 가능한 충전기 개수가 더이상 들어갈 공간이 없어졌습니다. 따라서 마커 색상은 그대로 유지를 하되, 인포 윈도우에 현재 사용 가능한 충전기 개수를 보여주는 방식으로 디자인을 개선하였습니다.

    체험 규칙 설정

    개선한 기능이 실제로 유용한지 확인해보기 위해 저희는 카페인 서비스 2차 체험의 규칙을 다음과 같이 설정했습니다.

    저희는 좀 더 의미있는 경험을 하기위해 1차 체험 때 정했던 규칙에 더해서 다음과 같은 추가 규칙을 설정하였습니다.

    중간에 목표 지점이 많이 변경된다

    지난 카페인 서비스 1차 체험에서는 지역 검색이 없어 목표 지점을 찾는 것이 불편했습니다. 1차 체험 이후 지역 검색이 추가 되었으므로 이 기능이 얼마나 유용한지 경험해보고자 이 규칙을 설정했습니다.

    추가로 목표 지점 주변의 충전소를 확인할 때 새로 추가된 지도 영역 확장이 얼마나 유용한지도 경험해보고자 했습니다.

    체험 개요

    no offset

    1. 잠실역 출발
    2. 하남 만두집
    3. 다음 목적지 설정
    4. 판교

    체험 후기

    잠실역 출발

    no offset

    쏘카에서 EV6를 대여해서 가브리엘, 센트, 키아라가 잠실역에서 출발하였습니다. 저녁 퇴근 이후에 남이섬을 가려고 목적지를 설정하였으나 배가 너무 고파서 가는 길에 식사를 하자고 얘기가 나왔습니다.

    하남 만두집

    따라서 진정한 처음 목적지는 스타필드였으나, 가브리엘은 동네 주민이라 스타필드를 너무 잘 알고 있었습니다. 따라서 스타필드에 전기차 충전소가 어디에 있는지도 알고있으므로 목적지를 급하게 변경하기로 했습니다. 이 때 목적지 변경을 위해 주변 식당을 둘러보던 중에 괜찮은 식당을 발견해서 해당 식당을 기준으로 주변 충전소를 확인해보기로 했습니다.

    no offset

    식당 주변을 가기 위해 지역 검색을 처음으로 사용하여 식당과 가까운 지역을 탐색할 수 있었습니다.

    이 과정에서 식당에는 충전소가 없다는 사실을 알게되어, 근처 충전소를 찾아보기 위해서 지도를 축소했더니 1차 체험때와는 달리 더 넓은 영역을 보여줬습니다. 이전에는 마커 자체가 보이지 않아 답답하였으나, 이제는 더 넓은 영역을 조회할 수 있게 되어 편리했습니다.

    지난 체험 이후로 피드백을 자체 수집하여 개발한 기능들이 편하다는 것을 식당에 가는 길에 느낄 수 있었습니다.

    다음 목적지 설정

    no offset

    하남 만두집에서 식사를 하다가 알게된 사실은, 남이섬은 생각보다 너무 멀다는 것이었습니다. 식사를 마치고 남이섬에 가면, 충전도 제대로 못하고 돌아올 판이었습니다.

    식사를 하면서 다른 목적지를 알아봤는데, 가브리엘이 예전에 가봤던 곳 중에서 남양주의 물의 정원이 시간을 떼우기 좋다는 소리를 하였습니다. 따라서 물의 정원을 검색해보았습니다.

    놀랍게도 물의정원은 검색결과에 없었습니다!

    어쩔 수 없이 카카오 지도로 물의 정원 위치를 확인하여 주소를 알아내었고, 이 주소를 카페인 검색창에 넣었습니다. 저희는 이 과정에서 카페인 서비스는 업체명 조회가 안된다는 것이 치명적인 단점이라고 생각했습니다. 다만, 이 기능은 검색 할 때마다 많은 비용이 청구되어 현실적으로 지금 당장 기능을 넣는 것은 어렵다고 판단했습니다.

    결국 주소 검색을 통해 물의 정원과 가장 가까운 충전소를 알아내었습니다.

    그런데! 지도를 축소해서 확인해 보니 해당 충전소는 물의 정원과 생각보다 멀었습니다.

    no offset

    no offset

    무려 걸어서 30분이나 걸리는 충전소였습니다!

    전기차 충전을 위해 왕복 1시간이나 걸리는 거리를 걸을 수 없다고 생각하였습니다.

    물론 지난 체험에서 전기차가 생각보다 배터리가 오래간다는 사실을 알고 있었지만, 만약 저희처럼 충전이 급한 사용자라면 목적지를 포기할 수 밖에 없겠구나 라는 생각이 들었습니다.

    마지막으로 정한 목적지는, 의외의 결정이었습니다.

    굉장히 발전된 첨단 도시로 알려진 판교였습니다!

    사실은 앞으로 갈지도 모르는 판교를 미리 구경이나 해보자는게 이유였지만 비밀입니다(?)

    일단 판교역은 IT서비스 회사들이 많이 몰려있는 곳이었습니다.

    따라서 저희는 판교역을 카페인 검색창에 검색했습니다.

    no offset

    지도를 판교역으로 이동하여 외부인 개방인 충전소를 찾았는데, 판교공영주차장이 보여서 해당 충전소를 목적지로 잡고 출발했습니다.

    판교

    하남에서 판교를 가기 위해서는 서하남IC를 지나야했습니다.

    가는 길에 우리 서비스에 나오는 정보와 실제 정보가 일치하는지 점검차 서하남 간이 휴게소를 들려봤습니다.

    이 휴게소에도 충전소가 있다고 검색이 되었기 때문입니다!

    no offset

    검색 당시에는 2대의 충전기가 있다고 나왔고, 둘다 사용이 가능하다고 되어있었는데 실제로 확인해보니 일치하는 것을 확인했습니다.

    먼 길을 달려 판교에 도착하였습니다.

    주차장에 들어오기 전, 카페인 서비스를 확인해보니 판교공영주차장의 충전기 총 12기 중 10기가 사용가능한 상태였습니다.

    정작 들어와서 보니 입구부터 너무 많은 전기차들이 충전기를 사용중이었습니다.

    뭔가 이상하다 싶었지만, 아직 서버에 반영이 안된건가? 하면서 비어있는 충전기를 찾았습니다.

    no offset no offset

    충전기를 꽂고 나서 알게된 것은 카페인 서비스에 나온 충전소 회사명과 방금 꽂은 충전기 회사명이 다르다는 것이었습니다.

    알고보니 음성 인식으로 네비에 검색한 충전소는 판교공영주차장이 아닌 판교역 환승 주차장이라 엉뚱한 곳으로 온 것이었습니다!!!

    다행인 점은 우리 서비스에서 제공하는 충전기 사용 여부 정보가 잘못된 것이 아니었다는 것이었습니다.

    그래서 애초에 가고자 했던 판교공영주자창에 대한 카페인 서비스의 정보가 실제와 동일한지 확인해보러 걸어서 이동했습니다. (바로 앞에 있었기 때문입니다.)

    no offset no offset

    도착해보니 1층의 충전기들이 모두 공사중이었고, 서비스의 정보가 실제로도 불일치 하는 줄 알았습니다. 다시 상세 정보를 보니 3~6층에 충전기들에 대한 정보라는 것이 명시되어 있었고, 실제로도 이와 동일한 것을 확인했습니다.

    no offset

    저희는 시간이 너무 흘러 다시 잠실로 돌아와 차를 반납하고 체험을 마무리 했습니다.

    결론

    불편했던 점

    • 디바이스에 보여지는 지도 영역 확장시에 원하는 정보를 볼 수 없는 것이 불편했다.
      • 지도를 확대해주세요 모달이 뜨고, 원래 있던 충전소 마커가 전부 사라진다.
    • 현재 나의 위치를 알아볼 수 있는 수단이 없어 불편했다.
      • 현위치를 나타내는 핀 (1차 체험기에서도 언급했던 부분)
      • 내 위치를 상대적으로 알 수 있는 랜드마크의 부족
    • 특정 장소(매장명) 검색이 안돼서 카페인 서비스만으로 목적지를 찾아가기 불편했다.
      • 카카오맵 등을 활용해 특정 장소 검색을 진행해야 했다.

    다음 목표

    앞선 불편했던점을 개선하기 위해 다음과 같은 기능 개선을 추가로 진행할 예정입니다.

    • 디바이스에 보여지는 지도 영역 확장에 제한이 생기지 않게 충전소 마커 클러스터링을 우선적으로 도입한다.
    • 현재 나의 위치를 알아볼 수 있도록 지하철 역과 같은 랜드마커를 지웠던 것을 롤백한다.

    카페인 서비스만으로 목적지를 찾아갈 수 있도록 하기 위해서 특정 장소 검색을 추가하고 싶지만, 해당 기능을 구현하기 위해선 검색당 비용이 많이 청구되는 장소 검색 API를 추가해야 했기에 현실적으로 지금 당장 구현하기 어렵다고 판단했습니다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git "a/tags/\355\224\274\353\223\234\353\260\261/page/3.html" "b/tags/\355\224\274\353\223\234\353\260\261/page/3.html" index e7592a2..2465b2b 100644 --- "a/tags/\355\224\274\353\223\234\353\260\261/page/3.html" +++ "b/tags/\355\224\274\353\223\234\353\260\261/page/3.html" @@ -5,7 +5,7 @@ "피드백" 태그로 연결된 3개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -27,7 +27,7 @@ 차주 분과 인터뷰 하고 싶었지만, 차 내부에서 너무 바빠보이셔서 그럴 수 없었습니다.

    전기차 충전을 기다리면서 무엇을 할 수 있을까요? 이 분은 다행히도 업무를 보고 계셨지만, 다른 차주들은 무엇을 하고 보낼지 궁금해졌습니다.

    no offset

    휴게소에는 충전소가 하나 더 있었습니다.

    한 곳은 사용중이지만, 다른 한 곳은 사용할 수 있었습니다.

    저희는 이 충전소를 사용해보기로 했습니다.

    no offset

    사용할 수 있으니깐 들어가봐야지! 하고 도착한 순간 아차 싶었습니다.

    "아, 충전소가 외부인 사용 금지일 수 있었지?"

    저희는 분명히 서비스를 직접 개발했으니깐 다 알고 있던 사항이었지만, 전혀 생각치 못했습니다.

    서비스를 개발하는 내내 외부인 개방 충전소에 대한 중요성을 간파하였고, 이 기능을 넣었으면서도 사용하지 않고 충전소를 방문한 것이었습니다.

    바로 앞에 있어서 다행이었지만, 어찌됐든 이 충전소를 사용할 수 없었습니다.

    따라서 저희는 휴게소를 떠나는 내내 이 문제에 대해서 토론을 할 수 밖에 없었습니다.

    분명 우리가 만든 서비스인데 왜 놓쳤을까?

    맛있는 점심

    no offset

    파주닭국수 본점에서 맛있는 식사를 했습니다.

    비록 식당에는 전기차 충전소가 없었지만, 인근에 충전소가 있어 실험을 하나 해볼 수 있었습니다.

    인근 충전소와 식당의 거리가 가까워 보이는데, 과연 걸어갈 수 있을까?

    실제로 걷지는 않았습니다만 차 타면서 지나가면서 확인해본 결과 직접 걸을 수 없는 거리였습니다. (굉장히 걷기 싫은 수준의 먼 거리였습니다.)

    집에 있는 PHEV를 탈 기회가 많아 전기차 충전소를 자주 방문했던 저는 이런 점을 잘 알고 있었습니다.

    다행히 이 부분을 잘 알고 있었기에 저희는 이 부분을 서비스에 반영하였고, 모든 데이터를 포기하지 않았던 것이 옳은 선택이었다는 것을 확인하게 되었습니다.

    no offset

    식사가 끝나고 드디어 마장호수로 출발하게 되었습니다.

    마장호수 도착

    마장호수에 도착하자마자 충전소에 방문했습니다.

    no offset

    통계에서는 사용률이 적을 것이라고 하였는데 저희만 있었습니다.

    no offset no offset

    2기 중 1곳을 저희가 사용하였고, 마장호수를 돌았습니다.

    no offset

    약 50분 간 산책을 하고, 돌아와보니 충전기 다 되어있었습니다.

    사실 마장호수 까지 오는 내내 든 생각이었지만, 전기차의 배터리가 생각보다 오래 간다는 생각이 들었습니다.

    일부러 회생제동 기능도 끄고, 에어컨을 강하게 틀어서 배터리를 소진하려고 하였으나, 85km를 주행하는 동안 겨우 20%를 소모하였습니다.

    충전기를 꽂을 때 50%였으나, 호수를 한바퀴 돌고 오니 이미 100%가 되어있었습니다.

    여담이지만, 저희가 돌아왔을 때 옆 자리에는 전기 화물차가 있어 충전소가 가득 찼습니다.

    또, 앱에서도 충전기 사용 여부가 업데이트 되는 것을 확인했습니다.

    no offset

    배터리 성능에는 좋지 않고 가격도 비싸서 이를 자주 사용하는 것은 좋지 않겠지만, 급한 사람들은 급속 충전기를 사용하면 되겠구나 싶었습니다.

    따라서 급속과 완속은 더더욱 다른 개념으로 봐야겠다는 생각이 들었습니다.

    제가 그동안 경험했던 전기차 충전소는 완속 기준이었기에 신선한 경험이었습니다.

    선릉으로 돌아오다

    no offset

    선릉으로 돌아와서 차량을 반납하였습니다.

    저희는 이번 여정을 통해 카페인 서비스에서 어떤 점을 개선해야할지 좀 더 명확하게 알게되었습니다.

    1. 현재 서비스에서 제공하는 기능들로 충전소를 검색하는 것은 가능하며, 충전소의 위치를 정확하게 파악하는 것도 가능하다.
    2. 하지만 충전소가 없는 목적지는 검색할 수 없고, 현 위치가 어디인지 가늠하기가 어려워진다.
    3. 충전소를 사용할 수 있다고 표기되어 있더라도 외부인 개방이 아닐 수 있다. 정보가 정확히 제공됨에도 불구하고 이를 단번에 눈치채기 어렵다.
    4. 이러한 문제를 예상하여 외부인 개방 여부를 필터링 할 수 있는 기능을 제공하고 있음에도 불구하고 사용하지 않았다.
    5. 충전소의 통계 자료의 적중률은 높았으나, 좀 더 많은 충전소를 들려 확인해봐야 할 것 같았다.
    6. 전기자동차는 생각보다 오래가고 상품성이 있었다. 주행 능력도 충분하고, 인프라가 잘 되어있다. 이걸 왜 욕하지? 라는 생각이 들었다.
    7. 지도 확대 허용 범위가 너무 좁아서 사용하는데 불편한건 실제 상황에서 더 불편했다.

    이상 카페인 사용기였습니다.

    - + \ No newline at end of file diff --git "a/tags/\355\230\221\354\227\205.html" "b/tags/\355\230\221\354\227\205.html" index 5255f8c..a5f7bb9 100644 --- "a/tags/\355\230\221\354\227\205.html" +++ "b/tags/\355\230\221\354\227\205.html" @@ -5,7 +5,7 @@ "협업" 태그로 연결된 1개 게시물개의 게시물이 있습니다. | CAR-FFEINE - + @@ -21,7 +21,7 @@ 이 정보를 제외하고 마커를 띄우기 위해 필요한 최소한의 정보를 조회하도록 수정해 서버의 부하를 낮췄습니다.

    이러한 변경으로 인해 충전소 조회 API의 성능이 개선되었습니다. 필요한 정보만을 조회하므로써 데이터베이스의 부하를 줄이고 응답 시간을 단축할 수 있게 되었습니다. 또한, 프론트엔드에서는 필요한 정보만을 호출하여 불필요한 데이터를 받아오지 않아도 되므로 클라이언트 측의 성능도 향상되었습니다.

    - + \ No newline at end of file