<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>yesolje</title>
    <link>https://yesolje.tistory.com/</link>
    <description>Spring, Java 를 비롯한 웹개발 지식</description>
    <language>ko</language>
    <pubDate>Thu, 21 May 2026 02:02:40 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yesolje</managingEditor>
    <image>
      <title>yesolje</title>
      <url>https://tistory1.daumcdn.net/tistory/7449021/attach/70b75c64778c4e378c3617e56f42932f</url>
      <link>https://yesolje.tistory.com</link>
    </image>
    <item>
      <title>GitLab Runner와 Docker를 활용한 개발 서버 CI/CD 파이프라인 구축</title>
      <link>https://yesolje.tistory.com/19</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;제가 이번에 맡은 프로젝트는 &lt;b data-index-in-node=&quot;9&quot; data-path-to-node=&quot;3&quot;&gt;4개의 독립된 서비스가 유기적으로 맞물려 돌아가는 구조&lt;/b&gt;로 설계되었습니다. 서비스 특성상 개발과 시연이 매우 빠른 주기로 반복되었고, 그만큼 코드 수정과 배포가 빈번하게 일어날 수밖에 없었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;하지만 프로젝트마다 배포 방식이 다르고, 특히 &lt;b data-index-in-node=&quot;26&quot; data-path-to-node=&quot;4&quot;&gt;운영 서버와 개발 서버의 환경 차이&lt;/b&gt;까지 존재하여 매번 수동으로 배포하기에는 한계가 있었습니다. 잦은 배포 과정에서 발생하는 인적 실수를 방지하고, 개발자가 오직 코드 구현에만 집중할 수 있는 환경을 만들기 위해 'Git Push만으로 완료되는 자동화 시스템'이 절실했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하고자 &lt;b data-index-in-node=&quot;9&quot; data-path-to-node=&quot;5&quot;&gt;GitLab Runner&lt;/b&gt;를 중심으로 전체 CI/CD 파이프라인을 구축했습니다. 각 서비스의 특성에 맞춰 프론트엔드는 &lt;b data-index-in-node=&quot;74&quot; data-path-to-node=&quot;5&quot;&gt;PM2&lt;/b&gt;를, 백엔드와 워커(Worker)는 &lt;b data-index-in-node=&quot;97&quot; data-path-to-node=&quot;5&quot;&gt;Docker Compose&lt;/b&gt;를 활용하여 배포 프로세스를 표준화했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 4개의 프로젝트가 혼재된 환경에서 어떻게 배포 효율을 높였는지, 그 구축 과정을 정리한 기록입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;아키텍쳐 설계&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1421&quot; data-origin-height=&quot;929&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kVmeO/dJMcafewMho/djTlKM1GVauon6nPkSKwg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kVmeO/dJMcafewMho/djTlKM1GVauon6nPkSKwg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kVmeO/dJMcafewMho/djTlKM1GVauon6nPkSKwg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkVmeO%2FdJMcafewMho%2FdjTlKM1GVauon6nPkSKwg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1421&quot; height=&quot;929&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1421&quot; data-origin-height=&quot;929&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시나리오는 다음과 같은 단계로 진행됩니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;개발자가 GitLab 서버의 develop 브랜치에 코드를 푸시&lt;/li&gt;
&lt;li&gt;GitLab은 Webhook을 통해 개발 서버의 GitLab Runner에게 빌드 트리거 전송&lt;/li&gt;
&lt;li&gt;Runner가 해당 Repository에서 최신 코드를 Git Pull하여 소스코드 디렉토리에 반영&lt;/li&gt;
&lt;li&gt;프로젝트 유형에 따라 분기:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Frontend&lt;/span&gt;: npm 빌드 후 PM2를 통해 프로세스 재시작 (포트 2025)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Backend/Workers&lt;/span&gt;: Docker CLI를 통해 컨테이너 이미지 빌드 및 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Backend는 Nginx(80/443)를 통해 외부 요청 수신, PostgreSQL 및 Redis와 연동&lt;/li&gt;
&lt;li&gt;Encoding Worker와 Provisioning Worker는 Backend 및 Redis와 통신하며 작업 수행&lt;/li&gt;
&lt;li&gt;배포 완료 후 헬스체크를 통해 서비스 정상 동작 여부 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 아키텍처는 다음과 같은 이점을 제공합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자동화된 배포:&lt;/b&gt; 코드 푸시만으로 빌드부터 배포까지 자동 수행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빠른 피드백:&lt;/b&gt; develop 브랜치 변경 사항을 즉시 개발 서버에 반영&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관된 배포 환경:&lt;/b&gt; Docker Compose를 통한 컨테이너 기반 배포로 환경 일관성 확보&lt;/li&gt;
&lt;li&gt;&lt;b&gt;롤백 가능성:&lt;/b&gt; 이전 이미지를 -prev 태그로 보관하여 신속한 롤백 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 안정성:&lt;/b&gt; 헬스체크 및 재시도 로직으로 배포 실패 조기 감지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인적 오류 최소화:&lt;/b&gt; 수동 배포 절차 제거로 휴먼 에러 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;구조 및 기술 스택&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gitlab repository&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 : &lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-frontend.git&quot;&gt;http://x.x.x.x/solutions/[project]/prod-frontend.git&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 : &lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-backend.git&quot;&gt;http:// &lt;/a&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-frontend.git&quot;&gt;x.x.x.x&lt;/a&gt;&lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-backend.git&quot;&gt; /solutions/ &lt;/a&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-frontend.git&quot;&gt;[project]&lt;/a&gt;&lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-backend.git&quot;&gt; / &lt;/a&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-frontend.git&quot;&gt;prod&lt;/a&gt;&lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-backend.git&quot;&gt; -backend.git&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동영상 인코딩 : &lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/encoding-server.git&quot;&gt;http://1 &lt;/a&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-frontend.git&quot;&gt;x.x.x.x&lt;/a&gt;&lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/encoding-server.git&quot;&gt; /solutions/ &lt;/a&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-frontend.git&quot;&gt;[project]&lt;/a&gt;&lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/encoding-server.git&quot;&gt; /encoding-server.git&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로비저닝 : &lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/provisioning-worker.git&quot;&gt;http:// &lt;/a&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-frontend.git&quot;&gt;x.x.x.x&lt;/a&gt;&lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/provisioning-worker.git&quot;&gt; /solutions/ &lt;/a&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;http://192.168.11.130/solutions/csp-itr00/itraining-frontend.git&quot;&gt;[project]&lt;/a&gt;&lt;a href=&quot;http://192.168.11.130/solutions/csp-itr00/provisioning-worker.git&quot;&gt; /provisioning-worker.git&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대상 브랜치: 각 레포지토리의 develop 브랜치&lt;/li&gt;
&lt;li&gt;배포 대상 서버: ubuntu 24.04 LTS&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt; prod.dev
├──  prod-frontend -------&amp;gt; git 으로 관리되는 소스코드 경로
│   ├── .env -------------------&amp;gt; 로컬설정용, 개발서버에서는 참조하지 않음
│   ├── .gitlab-ci.yml ---------&amp;gt; GitlabRunner 빌드 및 배포 방식을 설정하는 문서
│   ├── .dockerignore 
│   └── .gitignore
├──  prod-backend
├──  encoding-worker
├──  provisioning-worker
└──  docker ---------------------&amp;gt; 도커 관련 설정 파일 위치
		├── docker-compose.yml  ------&amp;gt; 도커 컨테이너 빌드 설정 파일
		├── dockerfile.backend  ------&amp;gt; docker-compose 가 참조하는 프로젝트별 컨테이너 설정 파일
		├── dockerfile.encoding-worker
		├── dockerfile.provisioning-worker
		└── .env ---------------------&amp;gt; 개발서버 설정용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GitLab Runner 채택의 이유&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;통합 관리 :&lt;/b&gt; 현재 개발팀이 차용하고 있는 형상관리 툴인 GitLab과 네이티브 통합되어 있어 별도의 CI/CD 도구 없이 GitLab 저장소와 자동으로 연동되며, 즉각적인 빌드 트리거와 파이프라인 파악이 용이하다는 장점이 있어 채택하게 되었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연한 실행 :&lt;/b&gt; Shell Executor를 통해 서버에 설치된 도구들을 직접 활용할 수 있어 설정이 간단하고, 빌드 속도가 빠르며, 개발 서버 환경에 최적화된 배포 자동화를 구현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;gitlab runner version : 18.8.0-1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Docker compose 채택의 이유&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;다중 컨테이너 관리 :&lt;/b&gt; 백엔드와 워커(Worker) 등 4개의 서비스가 유기적으로 연결되어야 했기에, 각 서비스를 독립된 컨테이너로 띄우되 네트워크와 볼륨을 한 번에 정의할 수 있는 Docker Compose가 가장 적합했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동일한 환경 유지 :&lt;/b&gt; 로컬, 개발기, 운영기 간 환경 차이로 발생하는 에러를 원천 차단하여, 시연 중 발생할 수 있는 '환경 변수' 리스크를 최소화했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PM2 채택의 이유 &lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;경량화 및 무중단 배포:&lt;/b&gt; 프론트엔드 서비스의 경우, 컨테이너화보다 Node.js 프로세스 매니저인 PM2를 직접 활용하는 것이 리소스 효율 면에서 유리하다고 판단했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;빠른 롤백과 모니터링:&lt;/b&gt; 잦은 수정이 일어나는 시연 단계에서 간단한 명령어로 프로세스를 재시작하고 상태를 즉각 확인할 수 있어 선택했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;상세 설치 과정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) APT 저장소 추가&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitLab 공식&amp;nbsp;저장소 추가&lt;/li&gt;
&lt;li&gt;GPG 키 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) GitLab Runner&amp;nbsp;설치&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;apt install gitlab-runner&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 시스템&amp;nbsp;사용자 설정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gitlab-runner&amp;nbsp;사용자 생성&lt;/li&gt;
&lt;li&gt;docker&amp;nbsp;그룹 추가 (Docker 명령 실행용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4) GitLab Runner 등록&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gitlab-runner register&amp;nbsp;실행&lt;/li&gt;
&lt;li&gt;GitLab 서버 URL 입력&lt;/li&gt;
&lt;li&gt;Registration&amp;nbsp;Token 입력&lt;/li&gt;
&lt;li&gt;Executor 선택:&amp;nbsp;shell&lt;/li&gt;
&lt;li&gt;Runner 이름 지정:&amp;nbsp;ubuntu&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(5) Systemd 서비스&amp;nbsp;설정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스&amp;nbsp;파일 생성/수정&lt;/li&gt;
&lt;li&gt;서비스 활성화 및 시작:&amp;nbsp;systemctl&amp;nbsp;enable --now&amp;nbsp;gitlab-runner&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(6) Git 설정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gitlab-runner&amp;nbsp;사용자용 Git 설정&lt;/li&gt;
&lt;li&gt;Credential&amp;nbsp;helper 설정 (자동&amp;nbsp;인증)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;배포 방식&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;frontend&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포 방식:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PM2를 사용한 프로세스&amp;nbsp;관리&lt;/li&gt;
&lt;li&gt;Next.js&amp;nbsp;빌드 후 정적 파일 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파이프라인 구조:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Build Stage:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;npm ci로 의존성&amp;nbsp;설치&lt;/li&gt;
&lt;li&gt;npm&amp;nbsp;run build로 Next.js 빌드&lt;/li&gt;
&lt;li&gt;빌드 산출물(.next, public)을&amp;nbsp;tar로 압축&lt;/li&gt;
&lt;li&gt;Artifacts로&amp;nbsp;fe_build.tar&amp;nbsp;저장 (1시간 유지)&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Deploy&amp;nbsp;Stage:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PM2 프로세스 중지&amp;nbsp;(pm2 stop)&lt;/li&gt;
&lt;li&gt;기존 빌드 파일 삭제&lt;/li&gt;
&lt;li&gt;새 빌드 압축 해제&lt;/li&gt;
&lt;li&gt;Production 의존성만 설치&amp;nbsp;(npm install --production)&lt;/li&gt;
&lt;li&gt;PM2로 새 프로세스 시작 (포트 2025)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시 활용:&amp;nbsp;node_modules/,&amp;nbsp;.next/cache/&amp;nbsp;캐싱&lt;/li&gt;
&lt;li&gt;Zero-downtime: PM2 재시작으로 다운타임 최소화&lt;/li&gt;
&lt;li&gt;브랜치:&amp;nbsp;develop,&amp;nbsp;rebuild-project&amp;nbsp;브랜치에서 자동 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포 경로:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소스코드:&amp;nbsp;/itraining.dev/itraining-frontend&lt;/li&gt;
&lt;li&gt;실행 포트: 2025&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;backend&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포 방식:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PM2를 사용한 프로세스&amp;nbsp;관리&lt;/li&gt;
&lt;li&gt;Next.js&amp;nbsp;빌드 후 정적 파일 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파이프라인 구조:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Build Stage:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;npm ci로 의존성&amp;nbsp;설치&lt;/li&gt;
&lt;li&gt;npm&amp;nbsp;run build로 Next.js 빌드&lt;/li&gt;
&lt;li&gt;빌드 산출물(.next, public)을&amp;nbsp;tar로 압축&lt;/li&gt;
&lt;li&gt;Artifacts로&amp;nbsp;fe_build.tar&amp;nbsp;저장 (1시간 유지)&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Deploy&amp;nbsp;Stage:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PM2 프로세스 중지&amp;nbsp;(pm2 stop)&lt;/li&gt;
&lt;li&gt;기존 빌드 파일 삭제&lt;/li&gt;
&lt;li&gt;새 빌드 압축 해제&lt;/li&gt;
&lt;li&gt;Production 의존성만 설치&amp;nbsp;(npm install --production)&lt;/li&gt;
&lt;li&gt;PM2로 새 프로세스 시작 (포트 2025)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시 활용:&amp;nbsp;node_modules/,&amp;nbsp;.next/cache/&amp;nbsp;캐싱&lt;/li&gt;
&lt;li&gt;Zero-downtime: PM2 재시작으로 다운타임 최소화&lt;/li&gt;
&lt;li&gt;브랜치:&amp;nbsp;develop,&amp;nbsp;rebuild-project&amp;nbsp;브랜치에서 자동 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포 경로:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소스코드:&amp;nbsp;/itraining.dev/itraining-frontend&lt;/li&gt;
&lt;li&gt;실행 포트: 2025&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Trouble Shooting&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 구축 과정에서 발생한 주요 기술적 이슈와 이를 해결하기 위해 적용한 전략들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size20&quot;&gt;1. GitLab Runner의 Docker 실행 권한 문제&lt;/h4&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5&quot;&gt;[문제]&lt;/b&gt; 파이프라인 실행 시 docker: command not found 또는 permission denied 에러가 발생하며 빌드가 중단되는 현상이 있었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;92&quot; data-path-to-node=&quot;5&quot;&gt;[원인]&lt;/b&gt; GitLab Runner 프로세스가 기본적으로 gitlab-runner 유저 권한으로 실행되는데, 해당 유저가 Docker 데몬에 접근할 수 있는 권한이 없었기 때문입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;194&quot; data-path-to-node=&quot;5&quot;&gt;[해결]&lt;/b&gt; gitlab-runner 사용자를 docker 그룹에 추가하여 sudo 권한 없이도 Docker 명령을 수행할 수 있도록 조치했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1771238819730&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo usermod -aG docker gitlab-runner&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;이후 서비스를 재시작하여 파이프라인이 호스트의 Docker 엔진을 정상적으로 제어할 수 있음을 확인했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size20&quot;&gt;2. 빌드 실패 코드의 배포 및 서버 장애 방지&lt;/h4&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9&quot;&gt;[문제]&lt;/b&gt; 문법 오류나 테스트를 통과하지 못한 코드가 머지(Merge)되어 배포될 경우, 실시간 시연 중인 개발 서버에 즉각적인 장애가 발생할 리스크가 있었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;91&quot; data-path-to-node=&quot;9&quot;&gt;[해결]&lt;/b&gt; &lt;b data-index-in-node=&quot;96&quot; data-path-to-node=&quot;9&quot;&gt;'선 파이프라인, 후 머지'&lt;/b&gt; 원칙을 세우고 GitLab 설정을 강화했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;Merge Request(MR) 기반 워크플로우:&lt;/b&gt; 코드 변경 시 반드시 MR을 생성하도록 강제하고, 파이프라인이 자동으로 먼저 수행되게 설정했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;Pipeline Success 필수 설정:&lt;/b&gt; GitLab의 'Settings &amp;gt; Merge Requests'에서 &lt;b data-index-in-node=&quot;62&quot; data-path-to-node=&quot;10,1,0&quot;&gt;&quot;Pipelines must succeed&quot;&lt;/b&gt; 옵션을 활성화하여, 빌드나 테스트가 실패한 코드는 머지 버튼 자체가 활성화되지 않도록 '품질 게이트'를 구축했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size20&quot;&gt;3. 다중 컨테이너 간의 정교한 실행 의존성 관리&lt;/h4&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12&quot;&gt;[문제]&lt;/b&gt; docker-compose up 실행 시, 데이터베이스(PostgreSQL, Redis)가 완전히 준비되기 전에 백엔드 서비스가 먼저 실행되어 커넥션 에러(Connection Refused)로 프로세스가 종료되는 현상이 발생했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;136&quot; data-path-to-node=&quot;12&quot;&gt;[해결]&lt;/b&gt; 단순한 실행 순서 보장을 넘어, 서비스의 **'상태(Health)'**를 체크하는 로직을 도입했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,0,0&quot;&gt;depends_on 확장:&lt;/b&gt; condition: service_healthy 옵션을 사용하여 의존성을 정의했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,1,0&quot;&gt;Healthcheck 정의:&lt;/b&gt; PostgreSQL과 Redis에 각각 healthcheck 로직을 추가하여, 해당 서비스가 단순히 '실행 중'이 아니라 '쿼리를 받을 수 있는 상태'가 되었을 때 비로소 백엔드와 프로비저닝(Provisioning) 서비스가 구동되도록 설계했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;회고와 향후 과제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 CI/CD 파이프라인 구축은 단순히 '편리함'을 넘어, 프로젝트 전체의 개발 문화와 안정성을 재정립하는 계기가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4&quot;&gt;1. &quot;배포 공포증&quot;의 해소&lt;/b&gt; 수동 배포 시절에는 &quot;내가 명령어를 잘못 치면 어떡하지?&quot; 혹은 &quot;환경 변수가 꼬이면 어떡하지?&quot;라는 불안감이 늘 있었습니다. 하지만 자동화된 품질 게이트를 구축한 뒤로는, 시스템이 검증해 준다는 믿음 아래 더 과감하게 코드를 수정하고 빠르게 피드백을 반영할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5&quot;&gt;2. 인프라와 운영에 대한 깊은 이해&lt;/b&gt; 4개의 프로젝트가 얽힌 구조를 하나하나 뜯어보며, Docker의 네트워크 구조와 프로세스 관리(PM2)의 디테일을 깊이 있게 학습할 수 있었습니다. 특히 컨테이너 간의 의존성 문제를 해결하며 '애플리케이션의 생애 주기'를 고려한 설계가 얼마나 중요한지 체감했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6&quot;&gt;3. 남은 과제&lt;/b&gt; 현재는 단일 서버 기반의 Docker Compose 구조이지만, 향후 트래픽이 늘어난다면&amp;nbsp;&lt;b data-index-in-node=&quot;105&quot; data-path-to-node=&quot;6&quot;&gt;Kubernetes&lt;/b&gt;로의 확장도 고민해 볼 필요가 있습니다. 또한, 지금보다 더 촘촘한 유닛 테스트 코드를 파이프라인에 녹여내어 '코드 퀄리티' 향상을 고려할 수도 있습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;결국 도구는 수단일 뿐, 중요한 것은 팀이 일할 수 있는 환경을 최적의 만드는 것을 다시 한번 배운 소중한 경험이었습니다.&lt;/p&gt;</description>
      <category>기술</category>
      <author>yesolje</author>
      <guid isPermaLink="true">https://yesolje.tistory.com/19</guid>
      <comments>https://yesolje.tistory.com/19#entry19comment</comments>
      <pubDate>Mon, 16 Feb 2026 19:09:07 +0900</pubDate>
    </item>
    <item>
      <title>Scapy를 활용한 실시간 TCP 패킷 수집과 Kafka‑DB 데이터 파이프라인 구축</title>
      <link>https://yesolje.tistory.com/18</link>
      <description>&lt;h1&gt;들어가며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 트래픽 분석은 사이버 보안과 디지털 포렌식에 있어 필수적입니다. 특히 다크웹 활동을 추적하고 분석하는 과정에서 TCP 패킷에 포함된 페이로드와 raw 데이터를 수집하는 것은 불법 활동의 패턴을 파악하는 데 중요한 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서는 회사의 다크웹 추적 애플리케이션 개발의 일환으로, Python의 Scapy 라이브러리를 활용하여 다크웹 네트워크 요청에서 발생하는 TCP 패킷을 수집하고, Kafka를 통해 데이터를 전송하며, 최종적으로 데이터베이스에 저장하는 파이프라인을 구축하였습니다&lt;/p&gt;
&lt;h1&gt;아키텍쳐 설계&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 시스템은 다음과 같은 세 가지 주요 컴포넌트로 구성되어 있습니다 :&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled_diagram___Mermaid_Chart-2025-07-24-032933.png&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;3839&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HZuwk/btsPwDO8BTv/ncHmoWICPLlucoJPNnC691/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HZuwk/btsPwDO8BTv/ncHmoWICPLlucoJPNnC691/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HZuwk/btsPwDO8BTv/ncHmoWICPLlucoJPNnC691/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHZuwk%2FbtsPwDO8BTv%2FncHmoWICPLlucoJPNnC691%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;344&quot; height=&quot;1234&quot; data-filename=&quot;Untitled_diagram___Mermaid_Chart-2025-07-24-032933.png&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;3839&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 수집 레이어&lt;/b&gt;: Scapy를 활용한 TCP 패킷 스니핑 및 Queue를 통한 비동기 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 전송 레이어&lt;/b&gt;: Kafka Producer를 통한 메시지 큐잉 시스템&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 저장 레이어&lt;/b&gt;: Kafka Consumer와 데이터베이스 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시나리오는 다음과 같은 단계로 진행됩니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 웹 서버에 HTTP 요청을 보냄&lt;/li&gt;
&lt;li&gt;Scapy 스니퍼가 이 트래픽을 포착&lt;/li&gt;
&lt;li&gt;스니퍼는 패킷을 큐에 넣음&lt;/li&gt;
&lt;li&gt;별도의 스레드가 Kafka를 통해 원격 브로커에 메시지 전송&lt;/li&gt;
&lt;li&gt;KafkaConsumer가 이 메시지를 읽어 데이터베이스에 적재&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 아키텍처는 다음과 같은 이점을 제공합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 데이터 처리&lt;/li&gt;
&lt;li&gt;확장성 및 견고성&lt;/li&gt;
&lt;li&gt;컴포넌트 간 느슨한 결합(Loose Coupling)&lt;/li&gt;
&lt;li&gt;장애 복구 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;데이터베이스 스키마&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷 수집 시스템에서 캡처된 데이터는 다음과 같은 스키마로 데이터베이스에 저장됩니다:&lt;/p&gt;
&lt;table style=&quot;height: 212px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;컬럼명&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;데이터타입&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;INT(PK)&lt;/td&gt;
&lt;td&gt;고유 식별자(자동 증가)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;src_ip&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;VARCHAR(45)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;출발지 IP 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;src_port&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;INT&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;출발지 포트 번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;dst_ip&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;VARCHAR(45)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;목적지 IP 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;dst_port&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;INT&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;목적지 포트 번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;capture_time&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;DATETIME(3)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;패킷 캡처 시간 (밀리초 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;packet_size&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;INT&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;패킷 페이로드 크기 (바이트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;collector_ip&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;VARCHAR(45)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;수집기 IP 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;created_at&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;TIMESTAMP&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;DB 저장 시간 (기본값: CURRENT_TIMESTAMP)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;데이터 수집 : Scapy 와 Queue 를 이용한 TCP 패킷 스니핑&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scapy는 강력한 패킷 조작 및 분석 도구로, 이를 활용하여 네트워크 인터페이스에서 TCP 패킷을 실시간으로 캡처합니다. 주요 구현 포인트는 다음과 같습니다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from scapy.all import sniff, IP, TCP
from datetime import datetime
from queue import Queue, Empty
import threading

# 큐 설정 (최대 20,000개 패킷 저장)
packet_queue = Queue(maxsize=20000)

# 패킷 처리 함수
def packet_handler(packet):
    if packet.haslayer(IP) and packet.haslayer(TCP):
        ip_layer = packet[IP]
        tcp_layer = packet[TCP]

        # 페이로드가 있는 패킷만 처리
        if len(tcp_layer.payload) &amp;gt; 0:
            data = {
                &quot;src_ip&quot;: ip_layer.src,
                &quot;src_port&quot;: tcp_layer.sport,
                &quot;dst_ip&quot;: ip_layer.dst,
                &quot;dst_port&quot;: tcp_layer.dport,
                &quot;capture_time&quot;: datetime.fromtimestamp(packet.time).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
                &quot;packet_size&quot;: len(tcp_layer.payload)
            }
            try:
                packet_queue.put_nowait(data)
            except:
                # 큐가 가득 찬 경우, 오래된 데이터를 파일로 저장하고 새 데이터 추가
                try:
                    old_data = packet_queue.get_nowait()
                    save_overflow_data(old_data)
                    packet_queue.put_nowait(data)
                except:
                    # 그래도 안되면 새 데이터를 파일로 저장
                    save_overflow_data(data)

def main():
    global running

    # 스레드 시작
    processing_thread = threading.Thread(target=process_packets, daemon=True)
    processing_thread.start()

    try:
        # 패킷 캡처 시작
        sniff(iface=IFACE,
        filter=&quot;tcp dst port 8000 and tcp[13] &amp;amp; 8 != 0&quot;,
        prn=packet_handler,
        store=0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 멀티스레딩과 큐를 활용하여 패킷 캡처와 처리를 분리함으로써 성능 병목 현상을 방지했습니다. 특히 &lt;code&gt;maxsize=20000&lt;/code&gt;으로 큐 크기를 제한하여 메모리 사용량을 제어했습니다.&lt;/p&gt;
&lt;h1&gt;데이터 전송 : kafka Producer 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캡처된 패킷 데이터는 Kafka Producer를 통해 메시지 큐로 전송됩니다. 이는 시스템의 확장성과 안정성을 보장합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from kafka import KafkaProducer
import json
import threading
from queue import Queue, Empty

# Kafka Producer
producer = KafkaProducer(
    bootstrap_servers=KAFKA_BROKER,
    value_serializer=lambda v: json.dumps(v).encode('utf-8')
)

def kafka_sender():
    global running
    while running or not packet_queue.empty():
        try:
            data = packet_queue.get(timeout=1)

            try:
                producer.send(TOPIC_NAME, data)
            except Exception as e:
                print(f&quot;Kafka 전송 실패: {e}&quot;)
                save_error_backup(data)

            packet_queue.task_done()
        except Empty:
            continue&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구현에서는 다음과 같은 최적화 기법을 적용했습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비동기 전송&lt;/b&gt;: &lt;code&gt;packet_queue.get(timeout=1)&lt;/code&gt;으로 큐에서 데이터를 가져와 비동기적으로 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예외 처리&lt;/b&gt;: Kafka 전송 실패 시 &lt;code&gt;save_error_backup()&lt;/code&gt; 함수로 데이터 백업&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타임아웃 적용&lt;/b&gt;: &lt;code&gt;Empty&lt;/code&gt; 예외 처리를 통해 큐가 비어있을 때 불필요한 CPU 사용 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;데이터 수신 및 저장 : kafka Consumer 와 DB Insert&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Consumer는 메시지 큐에서 데이터를 가져와 데이터베이스에 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from kafka import KafkaConsumer
import json
import pymysql

# Kafka Consumer 설정
consumer = KafkaConsumer(
    'packet-logs',
    bootstrap_servers='localhost:9092',
    group_id='packet-log-consumer-group',
    value_deserializer=lambda m: json.loads(m.decode('utf-8')),
    auto_offset_reset='latest',
    enable_auto_commit=True
)

# MariaDB 연결
db_conn = pymysql.connect(
    host='localhost',
    user='collect_user',
    password='1234',
    database='collector_db'
)
cursor = db_conn.cursor()

try:
    print(&quot; Kafka Consumer 대기중  &quot;)
    for message in consumer:
        data = message.value
        print(&quot;받은 메시지:&quot;, data)

        sql = &quot;&quot;&quot;
            INSERT INTO packet_logs
            (src_ip, src_port, dst_ip, dst_port, capture_time, packet_size, collector_ip)
            VALUES (%s, %s, %s, %s, %s, %s, %s)
        &quot;&quot;&quot;

        try:
            cursor.execute(sql, (
                data['src_ip'],
                data['src_port'],
                data['dst_ip'],
                data['dst_port'],
                data['capture_time'],
                data['packet_size'],
                data['collector_ip']
            ))
            db_conn.commit()
            print(&quot;✅ DB 저장 완료&quot;)
        except Exception as e:
            print(&quot;❌ DB 저장 실패:&quot;, e)

finally:
    cursor.close()
    db_conn.close()
    consumer.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보면 다음과 같은 구현 특징을 확인할 수 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그룹 ID('packet-log-consumer-group')를 통한 메시지 소비 관리&lt;/li&gt;
&lt;li&gt;'latest' 오프셋 설정으로 최신 메시지부터 소비&lt;/li&gt;
&lt;li&gt;자동 커밋 기능 활성화를 통한 처리 간소화&lt;/li&gt;
&lt;li&gt;try-finally 구조를 통한 DB 연결 및 리소스 안전한 해제&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;시스템 성능 검증: tcpdump를 통한 패킷 손실 테스트&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현한 시스템의 실제 성능과 패킷 손실 여부를 검증하기 위해 tcpdump를 병행하여 패킷 수집을 진행했습니다. 이는 Scapy 기반 수집기가 모든 패킷을 성공적으로 캡처하는지 확인하기 위한 중요한 검증 단계였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 방법론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JMeter를 사용하여 다양한 부하 시나리오에서 테스트를 진행했습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;낮은 부하&lt;/b&gt;: 10개의 요청을 10초 동안 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중간 부하&lt;/b&gt;: 100개의 요청을 10초 동안 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;높은 부하&lt;/b&gt;: 1,000개의 요청을 5초 동안 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;극한 부하&lt;/b&gt;: 10,000개의 요청을 5초 동안 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 시나리오에서 다음과 같은 방식으로 검증을 수행했습니다:&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# tcpdump로 패킷 캡처 및 카운트
sudo tcpdump -i any 'tcp dst port 8000 and tcp[13] &amp;amp; 8 != 0' -w /tmp/test_dump.pcap

# 캡처된 패킷 수 확인
tcpdump -r /tmp/test_dump.pcap | wc -l

# 동시에 Scapy 기반 수집기 실행
python3 sniffer_producer_v3.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 부하 시나리오에서 Scapy 기반 수집기와 tcpdump의 패킷 수집 결과가 일치했습니다. 다음은 각 시나리오별 결과입니다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;부하 시나리오&lt;/td&gt;
&lt;td&gt;tcpdump 패킷수&lt;/td&gt;
&lt;td&gt;scapy 패킷수&lt;/td&gt;
&lt;td&gt;일치율&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;낮은 부하(10개/10초)&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중간 부하 (100개/10초)&lt;/td&gt;
&lt;td&gt;4427&lt;/td&gt;
&lt;td&gt;4427&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;높은 부하 (1,000개/5초)&lt;/td&gt;
&lt;td&gt;2277&lt;/td&gt;
&lt;td&gt;2277&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;극한 부하 (10,000개/5초)&lt;/td&gt;
&lt;td&gt;3495&lt;/td&gt;
&lt;td&gt;3495&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분석 및 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 검증 과정을 통해 다음과 같은 결론을 도출할 수 있었습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현된 Scapy 기반 패킷 수집기는 초당 2,000개 이상의 패킷도 손실 없이 처리 가능함&lt;/li&gt;
&lt;li&gt;Queue와 멀티스레딩 구현 방식이 실시간 패킷 처리에 적합함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 검증 결과는 프로덕션 환경에서도 안정적인 시스템 운영이 가능함을 보여주며, 향후 더 높은 부하 상황에서도 확장 가능한 아키텍처임을 입증합니다.&lt;/p&gt;
&lt;h1&gt;Trouble Shooting&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 진행 중 다음과 같은 문제에 직면했고 해결했습니다:&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패킷 손실 방지를 위한 큐 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 구현 시 큐 오버플로우를 대비하여 큐 크기를 &lt;code&gt;maxsize=20000&lt;/code&gt;으로 설정하고, 큐 오버플로우시 전송하지 못한 데이터를 json 파일로 저장하는 백업 매커니즘을 구현했습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 문제: 패킷 손실 발생
# 원인: 단일 쓰레드에서 패킷 캡처와 처리를 동시에 수행

# 큐 오버플로우 시 파일에 저장
def save_overflow_data(data):
    timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
    filename = f&quot;packet_overflow _err_bk_{timestamp}.json&quot;

    try:
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f&quot;큐 오버플로우 데이터 저장: {filename}&quot;)
    except Exception as e:
        print(f&quot;오버플로우 파일 저장 실패: {e}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka 연결 장애 대응&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 브로커 연결 실패나 메시지 전송 실패 시 데이터 손실을 방지하기 위한 백업 메커니즘을 구현했습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def save_error_backup(data):
    try:
        with open('kafka_error_backup.json', 'a') as f:
            f.write(json.dumps(data) + '\n')
    except Exception as e:
        print(f&quot;백업 저장 실패: {e}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 누수 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장시간 실행 시 메모리 사용량이 지속적으로 증가하는 문제가 발생했습니다. 이는 대량의 패킷 객체가 적절히 해제되지 않아 발생한 문제였습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 문제: 메모리 사용량 지속적 증가
# 원인: 패킷 객체가 메모리에 계속 남아있음

# 해결책: 명시적 객체 해제와 가비지 컬렉션 수행
import gc

def gc_worker():
    global running
    while running:
        time.sleep(30)  # 30초마다 GC 실행
        collected = gc.collect()
        print(f&quot;GC 실행: {collected}개 객체 정리&quot;)

def main():
    global running

    print(f&quot;패킷 스니퍼 시작 - 포트: {TARGET_PORT}, IP: {collector_ip}&quot;)

    # 스레드 시작
    kafka_thread = threading.Thread(target=kafka_sender, daemon=True)
    gc_thread = threading.Thread(target=gc_worker, daemon=True)

    kafka_thread.start()
    gc_thread.start()&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;회고와 향후 과제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트를 통해 실시간 네트워크 트래픽 수집 및 처리 파이프라인을 성공적으로 구축했습니다. 주요 성과는 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 처리 시스템 구현: tcpdump 테스트 결과와 같이 초당 최대 2,000개 이상의 패킷을 손실 없이 처리하는 안정적인 시스템 구축&lt;/li&gt;
&lt;li&gt;자원 효율성 달성: Queue와 멀티스레딩 아키텍처, 주기적 GC 실행을 통해 메모리 사용량을 일정하게 유지하며 장기간 실행 가능한 구조 설계&lt;/li&gt;
&lt;li&gt;장애 대응 메커니즘: 큐 오버플로우와 Kafka 연결 장애 시 JSON 파일 백업 시스템 등을 통해 데이터 손실을 최소화하는 견고한 아키텍처 구축&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향후 개선할 수 있는 부분은 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모니터링 및 알림 시스템&lt;/b&gt;: kafka 및 수집기 서버의 용량 이슈를 비롯한 이상 징후 발생 시 알림 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패킷 타임스탬프 개선&lt;/b&gt;: 현재 사용 중인 capture_time은 Scapy가 패킷을 캡처한 시간이지만, 이를 패킷 내부에 포함된 실제 전송 시각으로 대체하여 더 정확한 네트워크 활동 타임라인 분석 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 대규모 네트워크 환경에서 실시간 데이터 처리의 중요성과 도전 과제를 명확히 보여주었으며, 향후 이와 유사한 시스템을 구축할 때 귀중한 경험이 될 것입니다.&lt;/p&gt;</description>
      <category>기술</category>
      <author>yesolje</author>
      <guid isPermaLink="true">https://yesolje.tistory.com/18</guid>
      <comments>https://yesolje.tistory.com/18#entry18comment</comments>
      <pubDate>Thu, 24 Jul 2025 14:50:08 +0900</pubDate>
    </item>
    <item>
      <title>SpringCamp 2025 세션 요약</title>
      <link>https://yesolje.tistory.com/17</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btxoed/btsOWFsL9cr/jrKh2c0oYrUmfrfS6miM51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btxoed/btsOWFsL9cr/jrKh2c0oYrUmfrfS6miM51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btxoed/btsOWFsL9cr/jrKh2c0oYrUmfrfS6miM51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbtxoed%2FbtsOWFsL9cr%2FjrKh2c0oYrUmfrfS6miM51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;894&quot; height=&quot;465&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;465&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; 난 스프링에서 ml 서빙을 해봤어요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;ML 서빙이란 무엇이고, 기존 방식의 한계는 무엇일까?&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;머신러닝(ML) 서빙&lt;/span&gt;이란, &lt;span style=&quot;color: #409d00;&quot;&gt;학습된 모델을 실제 서비스 환경에 배포해 실시간 혹은 배치 방식으로 예측 결과를 제공하는 것&lt;/span&gt;&lt;/b&gt;을 의미한다. 예를 들어 사용자가 웹 서비스에서 상품 리뷰를 입력하면, 이를 실시간으로 분석하여 긍정/부정을 판단해주는 기능이 바로 ML 서빙의 대표적인 예다. 현재 대부분의 ML 서빙은 파이썬 기반으로 진행된다. 파이썬은 머신러닝 생태계에서 가장 널리 사용되는 언어이며, TensorFlow, PyTorch 등 풍부한 라이브러리와 프레임워크를 통해 빠르게 모델을 개발하고 실험할 수 있다.&lt;br /&gt;하지만 개발을 넘어 &amp;lsquo;서빙&amp;rsquo;의 단계로 진입하게 되면 상황은 달라진다. 파이썬은 인터프리터 언어이고, 대규모 트래픽이나 병렬 처리, 안정적인 배포 환경에서는 여러 제약이 발생한다. 특히 파이썬 기반의 서빙 엔진들은 대부분 네이티브 레벨에서 최적화되어야 하고, 장애 대응이나 성능 튜닝 시 고도의 전문성이 요구되며, 기술 지원이나 운영 비용 또한 부담이 된다. 이러한 이유로 ML을 서비스에 적용해보고 싶은 개발자들, 특히 백엔드 중심의 자바 개발자들에게는 파이썬 기반의 ML 서빙 환경은 높은 진입장벽으로 느껴질 수 있다.&lt;br /&gt;&lt;b&gt;JVM 기반 ML 서빙, 자바 개발자를 위한 대안&lt;/b&gt;&lt;br /&gt;이러한 문제의식 속에서 JVM 기반의 ML 서빙이 하나의 대안으로 제시되고 있다. 스프링 캠프 세션에서는 DJL(Deep Java Library)을 중심으로 자바 개발자가 기존 생태계를 활용해 머신러닝 모델을 서빙할 수 있는 방법이 소개되었다. DJL은 PyTorch, TensorFlow, ONNX 등 다양한 백엔드를 추상화하여 자바 코드로 쉽게 모델을 로딩하고 추론할 수 있게 해주는 라이브러리다. 이를 통해 자바 개발자는 익숙한 언어와 프레임워크 환경 내에서 ML 모델을 운영에 적용할 수 있으며, JVM 기반의 높은 안정성과 성능도 함께 누릴 수 있다.&lt;br /&gt;세션에서는 실제 리뷰 분석 모델을 예로 들어, 텍스트 리뷰 데이터를 입력받아 긍정/부정을 분류하는 과정을 DJL 기반으로 구현한 사례를 소개했다. 또한 GPU 자원 사용량 최적화, 배치 처리 설정, 병렬 추론 처리 등 고성능 서빙에 필요한 세부 조정도 자바 환경에서 충분히 가능하다는 점이 강조되었다. 단순히 &amp;lsquo;자바에서도 할 수 있다&amp;rsquo;는 수준이 아니라, 실제 서비스에 적용 가능한 수준의 실용성과 성능을 보여준 셈이다.&lt;br /&gt;이처럼 JVM 기반 ML 서빙은 파이썬 중심의 한계를 보완하면서, 기존 백엔드 개발자의 역량을 최대한 활용할 수 있게 해준다. ML이 더 이상 특정 언어나 전문가의 영역에만 국한되지 않고, 다양한 서비스 맥락에 융합될 수 있도록 돕는 현실적인 대안으로 떠오르고 있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; Amazone Q developer 와 함께하는 생성형 AI 시대 헤쳐나가기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;Amazon Q Developer: 안전한 생성형 AI 기반 개발 도우미&lt;/b&gt;&lt;br /&gt;Amazon Q Developer는 AWS가 제공하는 안전한 생성형 AI 기반 개발 지원 도구다. 이 툴의 핵심 목적은 개발자들이 더 빠르고 효율적으로 소프트웨어를 개발하고 유지보수할 수 있도록 돕는 데 있다. 특히 &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;코드 작성뿐만 아니라, 리팩토링, 테스트, 버전 업그레이드, 문서화 등 실무에서 자주 마주치는 반복적이고 번거로운 작업들을 AI가 자동으로 수행해준다.&lt;/span&gt;&lt;/b&gt; 단순한 코드 보완을 넘어, 실제 운영 환경에서 발생하는 문제들을 AI가 이해하고 조치할 수 있도록 설계된 것이 특징이다.&lt;br /&gt;Amazon Q Developer는 AWS 환경에 최적화되어 있으며 정확한 코딩 가이드와 추천을 제공하여 운영 안정성과 보안성을 동시에 확보할 수 있도록 돕는다. 일반 개발자뿐 아니라 인프라 개발자, 레거시 시스템 유지관리자들에게도 특히 유용하다. 예를 들어, &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;레거시 시스템의 자바 버전이나 스프링 부트 프레임워크를 최신으로 업그레이드하는 작업도 간단한 명령어 한 줄로 수행할 수 있으며&lt;/span&gt;&lt;/b&gt;, 기존 문서를 기반으로 LLM의 한계를 보완한 &amp;lsquo;에이전틱 AI&amp;rsquo; 구조를 통해 문맥 기반 대응력도 갖췄다.&lt;br /&gt;&lt;b&gt;에이전트 기반 구조와 유연한 도구 지원&lt;/b&gt;&lt;br /&gt;Amazon Q Developer의 가장 큰 장점 중 하나는 &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;목적별로 세분화된 에이전트를 제공&lt;/span&gt;&lt;/b&gt;한다는 점이다. 예를 들어 개발자 요구사항에 따라 코드 작성은 /dev, 테스트 자동화는 /test, 보안 점검은 /review, 문서 자동화는 /doc, 버전 업그레이드는 /transform 명령어로 각각 수행할 수 있다. 이처럼 각 기능에 특화된 에이전트들이 존재하며, 이들은 단순 실행에 그치지 않고 스스로 계획을 수립하고 반복 수행하는 &amp;lsquo;자율 행동형 AI&amp;rsquo;로 작동한다.&lt;br /&gt;또한 다양한 환경에서 유연하게 사용할 수 있다는 것도 강점이다. &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;명령형 CLI 툴로 제공&lt;/span&gt;&lt;/b&gt;되기 때문에 단순 채팅 방식으로 명령을 입력하는 것만으로 기능을 수행할 수 있으며, IDE에 종속되지 않고 보다 빠르고 정확한 코드 생성을 기대할 수 있다. 지원하는 개발 환경도 폭넓다. &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;Eclipse, IntelliJ, VS Code 등 주요 IDE에서 플러그인 또는 확장 프로그램을 통해 연동이 가능하며,&lt;/span&gt;&lt;/b&gt; 개발자는 본인의 스타일에 맞는 환경에서 Amazon Q Developer의 모든 기능을 누릴 수 있다. 시스템 프롬프트나 동작 룰도 사용자 정의가 가능하여, 팀이나 조직의 코드 규칙에 맞춘 설정도 할 수 있다.&lt;br /&gt;&lt;b&gt;실제 사용성과 비용 측면의 경쟁력&lt;/b&gt;&lt;br /&gt;최근 많은 개발자들이 활용하는 AI 코딩 툴 중 하나인 Cursor와 비교해보더라도, Amazon Q Developer는 기능적 측면에서 전혀 밀리지 않는다. &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;특히 무료 티어에서도 제공되는 기능들이 매우 강력하며,&lt;/span&gt;&lt;/b&gt; 소규모 프로젝트나 개인 개발자 수준에서는 비용 부담 없이도 충분한 생산성 향상을 체감할 수 있다. 예를 들어 코드 자동 완성, 테스트 코드 생성, 보안 취약점 진단, 문서 생성 등 대부분의 작업을 프리 티어에서도 빠르게 수행할 수 있으며, 기업용 유료 플랜은 월 $19 정도로 합리적인 가격에 제공된다.&lt;br /&gt;또한 AWS 인프라에 최적화되어 있다는 점은 단순한 AI 도우미를 넘어 실질적인 DevOps 및 클라우드 통합 작업에 강점을 제공한다. 다양한 언어와 프레임워크를 지원하고, CLI 기반 툴로 IDE와 독립적으로 작동하기 때문에, 특정 개발 도구에 종속되지 않고 더 빠르고 정확한 대응이 가능하다. 이러한 점에서 Amazon Q Developer는 단순한 편의성 이상의 가치를 지니며, 특히 Cursor를 이미 써본 사용자라면 그 효율성과 통합성 측면에서 확실한 차이를 체감할 수 있을 것이다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; Talk Session - 함께 성장하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;함께 성장하는 개발 문화, 신뢰와 피드백에서 시작된다&lt;/b&gt;&lt;br /&gt;이번 토크 세션은 도메인 지식의 공유, 팀 내 신뢰 구축, 심리적 안정감, 그리고 진정성 있는 피드백의 중요성에 대한 깊이 있는 이야기로 채워졌다. 단순히 기술 역량을 넘어서, 함께 성장하는 조직을 만들기 위해 우리가 무엇을 해야 하는지를 다뤘다.&lt;br /&gt;먼저, 주니어든 시니어든 함께 일하기 위해서는 조직만의 도메인 지식을 잘 전파하는 것이 중요하다는 이야기가 나왔다. 새로운 동료가 합류했을 때, 서비스를 이해하고 빠르게 적응할 수 있도록 돕는 것은 팀 전체의 성장 기반이 된다.&lt;br /&gt;또한 팀 분위기 속에서 공유와 피드백이 얼마나 자연스럽게 이루어지는지가 개발 문화의 핵심이라는 점도 강조되었다. 문화가 없는 조직일수록, 외부 커뮤니티 활동이나 소규모의 변화 시도부터 시작해보는 것이 좋은 방법이 될 수 있다는 제안도 있었다.&lt;br /&gt;변화를 만들기 위해선 먼저 신뢰를 쌓아야 하며, 그 과정에서 작은 성공 경험이 큰 자신감으로 이어질 수 있다. 특히 심리적 안정감이 높은 조직일수록 생산성이 높아지는데, 이는 리더의 역할이 크다는 점도 짚어주었다.&lt;br /&gt;피드백에 있어서도 진심 어린 태도와 상대방에 대한 존중이 중요하다. 부정적인 피드백이라도 상대방의 성장을 진심으로 바란다는 마음이 바탕이 되면, 그 피드백은 관계를 해치기보다 오히려 신뢰를 높이는 계기가 될 수 있다. 칭찬과 긍정적인 말로 시작하는 습관도 피드백을 부드럽게 만든다.&lt;br /&gt;결국 좋은 개발 문화는 &quot;기술만이 아니라 사람을 향한 관심&quot;에서 출발한다는 메시지가 전해졌다. 실력이 부족해도 괜찮다. 중요한 건 함께 나아가려는 태도와, 그 안에서 조금씩 시도해보는 용기다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; 실전 MSA 트랜잭션 개발 가이드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;마이크로서비스에서의 트랜잭션 처리와 데이터 책임 설계&lt;/b&gt;&lt;br /&gt;마이크로서비스 아키텍처(MSA)는 시스템을 서비스 단위로 나누어 각 팀이 자율적으로 개발하고 운영할 수 있도록 돕는 구조다. 이를 실현하려면 서비스 간 경계가 명확해야 하며, 그중에서도 &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;데이터 오너십&lt;/span&gt;&lt;/b&gt;이 핵심 원칙 중 하나로 꼽힌다. &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;각 서비스는 자신이 소유한 데이터만 수정할 수 있고, 다른 서비스의 데이터는 조회(read)만 가능해야 한다.&lt;/span&gt;&lt;/b&gt; 예를 들어 고객 정보를 담당하는 서비스만 고객 테이블을 수정할 수 있고, 나머지 서비스는 복제하거나 조회만 허용하는 방식이다.&lt;br /&gt;하지만 이렇게 DB를 분리하면, 여러 서비스에 걸쳐 데이터를 수정해야 할 경우 트랜잭션 일관성을 보장하기 어려워진다. 전통적인 2PC(2-Phase Commit) 기반의 분산 트랜잭션은 운영 복잡성과 성능 저하 문제 때문에 권장되지 않는다. 그래서 이 세션에서는&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt; '피벗 트랜잭션(Pivot Transaction)&amp;rsquo;&lt;/span&gt;&lt;/b&gt;이라는 현실적인 대안이 소개되었다. 이는 논리적인 트랜잭션의 기준점을 두고, 그 이전까지는 트랜잭션을 커밋하고 이후는 이벤트 기반으로 처리하는 방식이다. 예를 들어 수강신청 &amp;rarr; 커뮤니티 등록 &amp;rarr; 실습 등록 순서에서 실습 등록만 실패해도 앞선 작업은 롤백하지 않고 유지한 채, 나머지는 보상 처리나 재시도로 해결한다.&lt;br /&gt;트랜잭션 충돌이나 데이터 경쟁 상태를 막기 위해 &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;SELECT FOR UPDATE와 같은 DB 락, 시멘틱 락(의미 기반 중간 상태 설정), TCC 패턴(Try-Confirm-Cancel) 등 다양한 전략이 활용된다.&lt;/span&gt;&lt;/b&gt; 특히 MSA 환경에서는 격리 수준이 낮기 때문에 이런 조치들이 중요하다.&lt;br /&gt;또한 신뢰할 수 있는 트랜잭션 처리를 위해서는 멱등성(idempotency)도 중요하다.&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt; 동일한 요청이 중복으로 들어왔을 때도 결과가 일관되게 나와야 하며, 실패한 요청이 재시도되더라도 시스템이 이상 없이 동작해야 한다.&lt;/span&gt;&lt;/b&gt; 이를 위해 요청 전에 상태를 기록하고, 결과를 받아서 갱신하는 방식으로 트랜잭션 흐름을 설계하는 것이 권장된다. 또한 이벤트에 버전 정보를 포함하면 처리 순서를 통제할 수 있어 충돌 방지에 유용하다.&lt;br /&gt;결국 마이크로서비스에서 중요한 건 단순히 나누는 것이 아니라, 분리된 시스템 간에도 신뢰성과 일관성을 어떻게 유지할 것인가에 대한 설계와 운영 전략이다. 이번 세션은 실무에서 마주하는 다양한 트랜잭션 문제에 대한 구체적 대응법을 제시하며, MSA를 안전하게 도입하고 운영하기 위한 기반을 정리하는 데 큰 도움이 되었다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; 레일웨이&amp;nbsp;지향&amp;nbsp;프로그래밍과&amp;nbsp;스프링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이 세션에서는 소프트웨어 로직을 마치 선로처럼 설계해, 성공과 실패의 경로를 명확히 분리해 다루는 &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;Railway Oriented Programming 기법을 소개했다.&lt;/span&gt;&lt;/b&gt; 핵심은 &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&quot;예외를 값처럼 다루자&quot;는 함수형 프로그래밍 철학과, 타입 시스템을 활용한 안전한 예외처리 방식이다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;기존의 예외 처리 방식은 throw, try-catch로 흐름을 중단시키는 구조였다면, ROP는 Success와 Failure라는 두 개의 선로를 정의하고, 예외를 값으로 처리해 흐름을 분기시키되 끊지 않는 방식을 사용한다. 이때 중요한 개념이 &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;모나드(Monad)&lt;/span&gt;&lt;/b&gt;이며, &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;이는 값과 그 처리 과정을 감싼 컨테이너로, flatMap 같은 함수로 합성 가능하다.&lt;/span&gt;&lt;/b&gt; 예를 들어 .recover, .fold, .ignore 같은 패턴을 사용하면 복구하거나 무시하면서 로직을 계속 이어갈 수 있다.&lt;br /&gt;ROP의 설계 방식은 계약에 의한 설계(Design by Contract) 개념과도 맞닿아 있다. &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;입력값의 유효성 검증을 &amp;lsquo;전제조건&amp;rsquo;, 출력값의 검증을 &amp;lsquo;사후조건&amp;rsquo;이라 보고, 타입 수준에서 이 계약을 명시함으로써 &lt;/span&gt;&lt;/b&gt;컴파일 타임부터 오류 가능성을 줄일 수 있다. 이는 &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;@Validated, Optional, Result 등의 개념과도 잘 어울린다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;다만, ROP에도 제약이 있다. 예외를 값으로 처리하는 구조는 Spring의 @Transactional 같은&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt; 트랜잭션 처리에서는 문제가 될 수 있다.&lt;/span&gt;&lt;/b&gt; 이유는 예외가 실제로 throw되지 않으면 rollback이 발생하지 않기 때문. 이 경우에는 실제 예외를 던지는 방식이 필요하다.&lt;br /&gt;자바에서도 함수형 예외처리를 위해 Vavr 같은 라이브러리를 사용할 수 있다. 이 라이브러리는 Try, Either, Option 등 모나드 유틸리티를 제공하며, 자바에서도 불변성과 함수형 흐름을 구현할 수 있게 해준다.&lt;br /&gt;결국 이 세션의 메시지는 명확하다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예외도 값처럼 다루자&lt;/li&gt;
&lt;li&gt;흐름은 끊지 말고 분기하자&lt;/li&gt;
&lt;li&gt;타입 시스템과 계약 기반 설계를 적극 활용하자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROP는 모든 상황에 적합하지는 않지만, 명확한 분기 처리, 예측 가능한 흐름, 테스트 용이성이 중요한 도메인에서는 강력한 설계 방식이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; 후기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 가보는 컨퍼런스였는데 참가비가 아깝지 않은 알차고 빡빡한(?) 시간이라 좋았다. 강연자가 참가자를 가르친다는 느낌보다는 본인들이 직면했던 문제들과 해결방법을 수평적으로 공유하는 느낌이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 amazone q developer 와 talk session 을 인상깊게 들었다. 바이브 코딩이 들어올 수 없는 성역(?) 같은 이클립스에도 생성형 ai 의 도움을 받을 수 있게 되어 내 업무에도 활용할 수 있는 포인트가 되겠다 싶었다.&lt;/p&gt;
&lt;p data-end=&quot;230&quot; data-start=&quot;65&quot; data-ke-size=&quot;size16&quot;&gt;또, Talk Session에서는 시니어 개발자들도 소통과 협업을 매우 중요하게 생각한다는 인상을 받았다. 그만큼 기존 방식을 바꾸는 일은 신중하게 접근해야 한다는 메시지도 강하게 와 닿았고, 나 역시 함께 일하는 동료에게 신뢰를 줄 수 있는 개발자가 되어야겠다고 다짐했다.&lt;/p&gt;</description>
      <author>yesolje</author>
      <guid isPermaLink="true">https://yesolje.tistory.com/17</guid>
      <comments>https://yesolje.tistory.com/17#entry17comment</comments>
      <pubDate>Mon, 30 Jun 2025 01:12:52 +0900</pubDate>
    </item>
    <item>
      <title>Spring의 기능 확장 - 상속과 템플릿 메소드 패턴</title>
      <link>https://yesolje.tistory.com/16</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1357&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BssDb/btsOnccylar/lBbeB5kF8q4qTK4KXnpkrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BssDb/btsOnccylar/lBbeB5kF8q4qTK4KXnpkrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BssDb/btsOnccylar/lBbeB5kF8q4qTK4KXnpkrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBssDb%2FbtsOnccylar%2FlBbeB5kF8q4qTK4KXnpkrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1357&quot; height=&quot;759&quot; data-origin-width=&quot;1357&quot; data-origin-height=&quot;759&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스에 대한 관심사를 분리하기 위한 방법 중 하나는 상속이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슈퍼클래스의 기본적인 로직의 흐름 외에, 변경/확장의 가능성이 있는 일부 기능을 추상메소드로 만들어 놓고, 구현체에서 이를 재정의하여 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 변경이 용이하고, 확장이 쉬운 코드를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔️ &lt;b&gt;템플릿 메소드 패턴(팩토리 메소드 패턴)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤, 서브클래스에서 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 예시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- UserDao(슈퍼클래스)&lt;/p&gt;
&lt;pre id=&quot;code_1748774866826&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tobySpringStudy.user.dao;

import com.tobySpringStudy.user.domain.User;

import java.sql.*;

public abstract class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        .......
    }

    public User get(String id) throws ClassNotFoundException, SQLException{
        Connection c = getConnection(); 
        .......

    }

    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- NUserDao(서브클래스)&lt;/p&gt;
&lt;pre id=&quot;code_1748774922692&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.tobySpringStudy.user.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class NUserDao extends UserDao{

    public Connection getConnection() throws ClassNotFoundException, SQLException {
        
        //N사의 DB connection 생성 코드
        Class.forName(&quot;org.h2.Driver&quot;);
        Connection c = DriverManager.getConnection(
                &quot;jdbc:h2:tcp://localhost/~/testDB&quot;, &quot;sa&quot;, &quot;&quot;
        );
        return c;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔️ &lt;b&gt;추상클래스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 인스턴스를 만들 수 없고, 하위 클래스로 상속하여 사용해야 하는 클래스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;abstract 키워드를 붙여 생성, extends 키워드를 붙여 상속&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드/일반 메서드/ 추상 메서드를 가질 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗&lt;b&gt;참고 : Java 의 접근 제어자&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.186%;&quot;&gt;접근제어자&lt;/td&gt;
&lt;td style=&quot;width: 17.7907%;&quot;&gt;같은 클래스&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;같은 패키지&lt;/td&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;다른 패키지의 자식 클래스&lt;/td&gt;
&lt;td style=&quot;width: 16.0465%;&quot;&gt;외부클래스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.186%;&quot;&gt;public&lt;/td&gt;
&lt;td style=&quot;width: 17.7907%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 16.0465%;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.186%;&quot;&gt;protected&lt;/td&gt;
&lt;td style=&quot;width: 17.7907%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 16.0465%;&quot;&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.186%;&quot;&gt;(default)&lt;/td&gt;
&lt;td style=&quot;width: 17.7907%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 16.0465%;&quot;&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.186%;&quot;&gt;private&lt;/td&gt;
&lt;td style=&quot;width: 17.7907%;&quot;&gt;✅&lt;/td&gt;
&lt;td style=&quot;width: 18.7209%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 16.0465%;&quot;&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗&lt;b&gt;참고 : 자바는 클래스의 다중상속을 허용하지 않는다?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 클래스가 여러 클래스로부터 상속 받는것을 허용하지 않는다는 뜻&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요할 경우, 인터페이스 구현을 통해 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #dddddd;&quot;&gt;&lt;s&gt;소잃고 외양간 고치기&lt;/s&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>기술</category>
      <author>yesolje</author>
      <guid isPermaLink="true">https://yesolje.tistory.com/16</guid>
      <comments>https://yesolje.tistory.com/16#entry16comment</comments>
      <pubDate>Sun, 1 Jun 2025 20:00:08 +0900</pubDate>
    </item>
    <item>
      <title>스프링 빈(Spring bean) 생명주기</title>
      <link>https://yesolje.tistory.com/15</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c68kuC/btsNPc6WFe8/S5zsKabHKLMSIKw0HTkgEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c68kuC/btsNPc6WFe8/S5zsKabHKLMSIKw0HTkgEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c68kuC/btsNPc6WFe8/S5zsKabHKLMSIKw0HTkgEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc68kuC%2FbtsNPc6WFe8%2FS5zsKabHKLMSIKw0HTkgEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;745&quot; height=&quot;323&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 스프링&amp;nbsp;빈(Bean)&amp;nbsp;이란?&lt;/b&gt; &lt;br /&gt;빈(Bean)은&amp;nbsp;스프링&amp;nbsp;컨테이너에&amp;nbsp;의해&amp;nbsp;관리되는&amp;nbsp;재사용&amp;nbsp;가능한&amp;nbsp;소프트웨어&amp;nbsp;컴포넌트이다.&amp;nbsp; &lt;br /&gt;즉,&amp;nbsp;스프링&amp;nbsp;컨테이너가&amp;nbsp;관리하는&amp;nbsp;자바&amp;nbsp;객체를&amp;nbsp;뜻하며,&amp;nbsp;하나&amp;nbsp;이상의&amp;nbsp;빈(Bean)을&amp;nbsp;관리한다. &lt;br /&gt;빈은 인스턴스화된 객체를 의미하며, 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.&amp;nbsp; &lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;new 로 생성한 인스턴스 &amp;rarr; 빈 아님&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;@Component , @Bean 등으로 생성한 인스턴스 &amp;rarr; 빈&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt;&lt;/b&gt; &lt;b&gt;스프링&amp;nbsp;빈(Bean)&amp;nbsp;사용이유&amp;nbsp;?&lt;/b&gt; &lt;br /&gt;&lt;br /&gt;가장&amp;nbsp;큰&amp;nbsp;이유는&amp;nbsp;스프링&amp;nbsp;간&amp;nbsp;객체가&amp;nbsp;의존관계를&amp;nbsp;관리하도록&amp;nbsp;하는&amp;nbsp;것에&amp;nbsp;가장&amp;nbsp;큰&amp;nbsp;목적이&amp;nbsp;있다.&amp;nbsp;&amp;nbsp;객체가&amp;nbsp;의존관계를&amp;nbsp;등록할&amp;nbsp;때&amp;nbsp;스프링&amp;nbsp;컨테이너에서&amp;nbsp;해당하는&amp;nbsp;빈을&amp;nbsp;찾고,&amp;nbsp;그&amp;nbsp;빈과&amp;nbsp;의존성을&amp;nbsp;만든다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt;&lt;/b&gt; &lt;b&gt;스프링 빈(Bean) 등록 방법&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt; &lt;/span&gt; xml에 직접 등록 &lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt; &lt;/span&gt; @Bean 어노테이션을 이용 &lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt; &lt;/span&gt; @Component, @Controller, @Service, @Repository 어노테이션을 이용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring&amp;nbsp;Boot&amp;nbsp;애플리케이션이&amp;nbsp;실행되면,&amp;nbsp;내부적으로는&amp;nbsp;SpringApplication.run()을&amp;nbsp;통해&amp;nbsp;ApplicationContext가&amp;nbsp;초기화된다. &lt;br /&gt;이&amp;nbsp;시점에&amp;nbsp;Spring은&amp;nbsp;classpath에서&amp;nbsp;@Component,&amp;nbsp;@Service,&amp;nbsp;@Controller,&amp;nbsp;@Configuration&amp;nbsp;등의&amp;nbsp;어노테이션이&amp;nbsp;붙은&amp;nbsp;클래스를&amp;nbsp;스캔해서&amp;nbsp;빈&amp;nbsp;정의로&amp;nbsp;등록하고,&amp;nbsp;이를&amp;nbsp;기반으로&amp;nbsp;빈을&amp;nbsp;생성한다. &lt;br /&gt;&lt;br /&gt;이때&amp;nbsp;대부분의&amp;nbsp;Bean은&amp;nbsp;싱글톤(Singleton)&amp;nbsp;스코프이므로,&amp;nbsp;컨테이너&amp;nbsp;초기화&amp;nbsp;시점에&amp;nbsp;미리&amp;nbsp;생성되어&amp;nbsp;ApplicationContext에&amp;nbsp;등록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 빈 생성 타이밍과 생명주기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨테이너 생성 &amp;rarr; 스프링 빈 생성 &amp;rarr; 의존관계 주입 &amp;rarr; 초기화 콜백 &amp;rarr; 앱 본연의 동작 수행 &amp;rarr; 소멸전 콜백&amp;rarr; 스프링 종료&lt;/p&gt;</description>
      <category>기술</category>
      <author>yesolje</author>
      <guid isPermaLink="true">https://yesolje.tistory.com/15</guid>
      <comments>https://yesolje.tistory.com/15#entry15comment</comments>
      <pubDate>Thu, 8 May 2025 20:41:39 +0900</pubDate>
    </item>
    <item>
      <title>캐시(Cache) 의 동작 원리</title>
      <link>https://yesolje.tistory.com/14</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 캐시란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 사용하는 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시는 저장 공간이 작고 비용이 비싼 대신 빠른 성능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 저장 공간은 속도와 용량 등 특성에 맞게 역할을 나눠서 사용하는데, 이를 메모리 계층 구조라고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;557&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p12FF/btsNG143l3w/Ze9H55Lh1PsJBeAVJpZzO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p12FF/btsNG143l3w/Ze9H55Lh1PsJBeAVJpZzO1/img.png&quot; data-alt=&quot;메모리 계층 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p12FF/btsNG143l3w/Ze9H55Lh1PsJBeAVJpZzO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp12FF%2FbtsNG143l3w%2FZe9H55Lh1PsJBeAVJpZzO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;557&quot; height=&quot;313&quot; data-origin-width=&quot;557&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메모리 계층 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터 메모리 계층 다이어그램에서 각 계층은 아래로 갈수록 더 느리지만 크고 저렴하며, 위로 갈 수록 더 빠르지만 작고 비싸며, CPU에 가까워지는 구조를 보여준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림처럼 데이터 처리속도를 높이기 위해 자주 사용되는 데이터를 캐시에 담아두고, 해당 데이터가 필요할 때 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 캐시를 사용하는 과정&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsw4Mj/btsNIfacNHb/eho1ppQrpr3NLauAaYFyu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsw4Mj/btsNIfacNHb/eho1ppQrpr3NLauAaYFyu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsw4Mj/btsNIfacNHb/eho1ppQrpr3NLauAaYFyu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbsw4Mj%2FbtsNIfacNHb%2Feho1ppQrpr3NLauAaYFyu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;414&quot; height=&quot;367&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 사용하는 과정은 다음 다이어그램과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 클라이언트가 서버로 데이터를 요청한다(ex. 이미지, 값 등등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 서버가 요청받은 데이터를 캐시메모리에 있는지 없는지 확인한다. 만약 데이터가 있다면 캐시 안에 있는 것을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 데이터가 없다면 DB 및 서버에 접근하여 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4) 가져온 데이터를 캐시에 저장한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 데이터 지역성 원리(Principle of Locality)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 데이터를 담기에 캐시의 저장 공간은 작다. 그렇기 때문에 많이 사용되는 소수의 데이터를 선별해야 하는데, 이때 사용되는 것이 지역성이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지역성은 우리가 어떤 데이터를 참조하면, 그 주변의 데이터를 또 참조할 가능성이 높다 라는 메모리 접근의 일반적인 성향을 의미하며, 시간 지역성(Temporal Locality) 와 공간 지역성(Spatial Locality) 으로 나눌 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/b&gt;시간 지역성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;최근 접근한 데이터에&lt;/b&gt;&lt;/span&gt; &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;다시 접근&lt;/span&gt;&lt;/b&gt;하는 경향&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 자주 사용하는 변수 값(count++, total += x, for 문의 i 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 로그인 후 세션 정보, 최근에 요청한 API 응답&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/b&gt;공간지역성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;최근 접근한 데이터의 주변 공간&lt;/b&gt;&lt;/span&gt;에 접근하는 경향&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 배열을 차례대로 검색할 때 가까운 메모리 공간에 연속적으로 접근함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 배열을 순차적으로 순회할 때 (arr[0] , arr[1], arr[2] ... )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐시의 효율 - 배열과 LinkedList&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시의 성능을 결정짓는 중요 요소는 데이터의 위치이다. 이와 관련해 배열과 LinkedList 는 메모리 배치 방식이 달라, 효율에서 큰 차이를 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열은 메모리상에 연속적인 공간에 저장된다. arr[0] 과 arr[1] 은 물리적으로 나란히 붙어있기 때문에 CPU가 이것을 읽을 때 캐시는 주변 데이터를 블록 단위로 함께 불러온다. 이후 근처 데이터에 접근했을 때 데이터가 이미 캐시에 있으므로 빠르게 처리할 수 있다. (공간 지역성)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, LinkedList 는 각각의 노드가 흩어진 메모리 공간에 저장되어 있고, 각 노드는 다음 노드의 주소(Pointer) 만 알고 있다. 즉, 캐시는 다음 노드의 위치를 미리 예측하거나 불러올 수 있다. 이로 인해 캐시 미스가 발생하고 성능이 저하된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, 순차 탐색이 많고 성능이 중요할 경우 배열을 사용해야 하고, 데이터의 삽입/삭제가 빈번할 경우에는 LinkedList 사용을 고려해야 한다. 즉, 실무에서는 데이터의 용도와 이용 방식에 따라 다른 자료구조를 이용하여, 캐시를 효율적으로 이용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기술</category>
      <author>yesolje</author>
      <guid isPermaLink="true">https://yesolje.tistory.com/14</guid>
      <comments>https://yesolje.tistory.com/14#entry14comment</comments>
      <pubDate>Thu, 1 May 2025 20:25:16 +0900</pubDate>
    </item>
    <item>
      <title>멀티모듈 구조로 Spring 프로젝트 리팩토링</title>
      <link>https://yesolje.tistory.com/13</link>
      <description>&lt;p data-end=&quot;217&quot; data-start=&quot;200&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;217&quot; data-start=&quot;200&quot; data-ke-size=&quot;size18&quot;&gt;오늘은 단일모듈 기반의 프로젝트를 멀티모듈 구조로 리팩토링한 경험과, 멀티모듈이 왜 필요한지 - 어떤 방식으로 구성해야 하는지 고민한 기록들을 정리해 보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;217&quot; data-start=&quot;200&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 멀티모듈이란&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;350&quot; data-start=&quot;237&quot; data-ke-size=&quot;size16&quot;&gt;멀티모듈은 하나의 큰 프로젝트를 &lt;b&gt;역할별로 작은 모듈(서브 프로젝트)&lt;/b&gt; 로 나누어 구성하는 방식을 말합니다. 사실, 대부분의 서비스는 단일 프로젝트로 구성하는 일이 드뭅니다. 프로젝트가 일정 규모 이상으로 커지게 되면 사용자와의 접점을 담당하는 서버, 각종 api 및 batch를 비롯한 시스템의 부피가 늘어나게 되고, 이 경우 단일 프로젝트에서 구성할 경우 동일한 Domain을 시스템 갯수만큼 복사 붙여넣기 해야 하기 때문입니다. 이를 보완하기 위해 프로젝트를 멀티모듈 구조로 구성하는 방법은 공통으로 사용하는 코드를 모아놓기 때문에 역할 분리 및 재사용성 측면에서 효과가 좋은 방법이라고 볼 수 있습니다. 멀티모듈 구조에서 각 모듈은 서로 독립적인 책임을 가지며, 필요에 따라 다른 모듈에 의존할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;423&quot; data-start=&quot;352&quot; data-ke-size=&quot;size16&quot;&gt;Java/Spring 환경에서는 Gradle이나 Maven을 사용해 &lt;b&gt;모듈 간의 의존성과 빌드 관리&lt;/b&gt;를 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;747&quot; data-start=&quot;727&quot; data-ke-size=&quot;size20&quot;&gt;  멀티모듈이 필요한 이유&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;976&quot; data-start=&quot;749&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;811&quot; data-start=&quot;749&quot;&gt;&lt;b&gt;역할 분리&lt;/b&gt;: 프로젝트의 각 계층(domain, web, batch 등)을 명확히 나눌 수 있어 유지보수가 용이해짐&lt;/li&gt;
&lt;li data-end=&quot;864&quot; data-start=&quot;812&quot;&gt;&lt;b&gt;재사용성 향상&lt;/b&gt;: 공통 로직을 common 모듈로 분리하여 여러 모듈에서 재사용 가능&lt;/li&gt;
&lt;li data-end=&quot;933&quot; data-start=&quot;865&quot;&gt;&lt;b&gt;의존성 제어&lt;/b&gt;: 어느 모듈이 어디에 의존하는지 명확히 할 수 있음&amp;nbsp;&lt;/li&gt;
&lt;li data-end=&quot;976&quot; data-start=&quot;934&quot;&gt;&lt;b&gt;배포 및 테스트 최적화&lt;/b&gt;: 특정 모듈만 따로 빌드하거나 테스트 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1053&quot; data-start=&quot;978&quot; data-ke-size=&quot;size16&quot;&gt;단일 모듈에서는 공통 코드가 뒤엉기거나, 계층 간 의존 관계가 꼬이기 쉬운데 멀티모듈 구조를 도입하면 이런 문제를 예방할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 멀티모듈의 구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 내가 참고한 멀티모듈 구조&amp;amp;설명&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 공부용으로 쓰는 소규모 프로젝트이지만, 후에 Batch 및 스케줄링 등등 별도의 시스템이 늘어난다는 가정하에 염두에 둔 계층 구조는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tEUyg/btsNmHZGwMO/aUlqou30kJ7muAi6qSmfbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tEUyg/btsNmHZGwMO/aUlqou30kJ7muAi6qSmfbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tEUyg/btsNmHZGwMO/aUlqou30kJ7muAi6qSmfbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtEUyg%2FbtsNmHZGwMO%2FaUlqou30kJ7muAi6qSmfbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;724&quot; height=&quot;365&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;storeManagingSystem &lt;br /&gt;&amp;nbsp;┣&amp;nbsp; &amp;nbsp;store-managing-api&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;larr;&amp;nbsp;실제&amp;nbsp;애플리케이션&amp;nbsp;실행&amp;nbsp;(Web,&amp;nbsp;Controller&amp;nbsp;등) &lt;br /&gt;&amp;nbsp;┣&amp;nbsp; &amp;nbsp;store-managing-core&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;larr;&amp;nbsp;도메인&amp;nbsp;로직,&amp;nbsp;Service,&amp;nbsp;Repository&amp;nbsp;등 &lt;br /&gt;&amp;nbsp;┣&amp;nbsp; &amp;nbsp;store-managing-common&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;larr;&amp;nbsp;공통&amp;nbsp;DTO,&amp;nbsp;Util,&amp;nbsp;Enum&amp;nbsp;등 &lt;br /&gt;&amp;nbsp;┗&amp;nbsp; &amp;nbsp;store-managing-batch&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;larr;&amp;nbsp;Spring&amp;nbsp;Batch&amp;nbsp;전용&amp;nbsp;Job/Step&amp;nbsp;정의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;603&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NmmeJ/btsNnwkxxqK/qrU9evnP4lxjdAlNCrfsfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NmmeJ/btsNnwkxxqK/qrU9evnP4lxjdAlNCrfsfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NmmeJ/btsNnwkxxqK/qrU9evnP4lxjdAlNCrfsfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNmmeJ%2FbtsNnwkxxqK%2FqrU9evnP4lxjdAlNCrfsfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;837&quot; height=&quot;603&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;603&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.단일 모듈 프로젝트 리팩토링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 마무리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기술</category>
      <author>yesolje</author>
      <guid isPermaLink="true">https://yesolje.tistory.com/13</guid>
      <comments>https://yesolje.tistory.com/13#entry13comment</comments>
      <pubDate>Wed, 16 Apr 2025 20:51:14 +0900</pubDate>
    </item>
    <item>
      <title>백준_2609_최대공약수와 최소공배수</title>
      <link>https://yesolje.tistory.com/12</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;풀이&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744014590571&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.IOException;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws IOException {
        /*
         * https://www.acmicpc.net/problem/2609
         * 최대공약수와 최소공배수
         */

        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();
        int result1 = 0 ;
        int result2 = a*b ;

        while(a%b != 0){
            int c = a%b;
            a = b;
            b = c;
        }

        result1 = b;
        result2 = result2 / b;
        

        System.out.println(result1);
        System.out.println(result2);
        


    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념정리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;❓&lt;/span&gt;&lt;/b&gt; 최대공약수(GCD) 구하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 최대공약수는 두 수 a , b 가 있을 때 둘다 나눌 수 있는 가장 큰 수이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 24와 18의 최대공약수는 24와 18을 모두 나눌 수 있고, 24 / 18 의 나머지 6도 나눌 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 따라서, 나머지가 0으로 떨어지는 수가 나올 때 까지 약수와 나머지 간의 나누기 연산을 반복한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;예시 1 : 24 / 18 -&amp;gt; 18 / 6 -&amp;gt; 나머지가 0 -&amp;gt; 최대공약수는 6&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;예시 2 : 42/ 30 -&amp;gt; 30/12 -&amp;gt; 12/6 -&amp;gt; 최대공약수는 6&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;❓&lt;/span&gt;&lt;/b&gt; 최소공배수(LCM) 구하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 최소공배수는 두 수 a, b 가 있을 때 공통배수 중에서 가장 작은 수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 확실한 공배수는 두 수 a와 b를 곱한 수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 이 수에는 최대공약수가 두번 들어가 있다. 따라서 중복된 최대공약수를 한번 나눠서 제거해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;예시 1: 12 * 18 = (6*2) * ( 6*3) = 6 * 6 * 2 * 3 -&amp;gt; 6을 한번 나누어 제거-&amp;gt; 최소공배수는 36&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>코딩테스트</category>
      <author>yesolje</author>
      <guid isPermaLink="true">https://yesolje.tistory.com/12</guid>
      <comments>https://yesolje.tistory.com/12#entry12comment</comments>
      <pubDate>Mon, 7 Apr 2025 17:46:27 +0900</pubDate>
    </item>
    <item>
      <title>프로그래머스_고득점kit_우선탐색_게임 맵 최단거리</title>
      <link>https://yesolje.tistory.com/11</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;풀이&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741838802226&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    static class Node {
        int x, y, dist;
        
        Node(int x, int y, int dist) {
            this.x = x;
            this.y = y;
            this.dist = dist;
        }
    }

    public int solution(int[][] maps) {
        int n = maps.length;
        int m = maps[0].length;
        
        int[][] directions = {{1,0}, {-1,0}, {0,1}, {0,-1}}; // 상, 하, 우, 좌
        boolean[][] visited = new boolean[n][m]; // 방문 여부
        
        Queue&amp;lt;Node&amp;gt; queue = new LinkedList&amp;lt;&amp;gt;();
        queue.offer(new Node(0, 0, 1));
        visited[0][0] = true;
        
        while (!queue.isEmpty()) {
            Node node = queue.poll();
            int x = node.x, y = node.y, dist = node.dist;
            
            if (x == n - 1 &amp;amp;&amp;amp; y == m - 1) return dist; // 도착하면 거리 반환
            
            for (int[] dir : directions) {
                int nx = x + dir[0];
                int ny = y + dir[1];
                
                if (nx &amp;gt;= 0 &amp;amp;&amp;amp; ny &amp;gt;= 0 &amp;amp;&amp;amp; nx &amp;lt; n &amp;amp;&amp;amp; ny &amp;lt; m) { // 범위 체크
                    if (!visited[nx][ny] &amp;amp;&amp;amp; maps[nx][ny] == 1) { // 방문 안 했고 길이면
                        queue.offer(new Node(nx, ny, dist + 1));
                        visited[nx][ny] = true;
                    }
                }
            }
        }
        
        return -1; // 도착점에 도달할 수 없는 경우
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념정리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;❓&lt;/span&gt;&lt;/b&gt; &lt;b&gt;너비우선탐색(BFS)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 시작으로부터 가까운 정점을 먼저 방문하고 멀리 떨어져 있는 곳을 나중에 방문하는 순회 방법이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 최단 거리를 찾을 때 DFS 보다 유리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; Queue 를 사용하여 한 번 방문한 곳은 다시 방문하지 않도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;❓&lt;/span&gt;&lt;/b&gt; &lt;b&gt;BFS 에서 Queue 를 사용하는 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; Queue 는 선입선출 방식으로 동작하기 때문에, 현재와 가장 가까운 위치부터 탐색한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt; 탐색 순서가 정해져 있기 때문에, 먼저 도착한 노드가 최단 경로로 온 노드임을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1345&quot; data-origin-height=&quot;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FNPkl/btsMItItFiv/RmM1PjvqdnRJy7hqQe9kCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FNPkl/btsMItItFiv/RmM1PjvqdnRJy7hqQe9kCk/img.png&quot; data-alt=&quot;갈림길에서 큐가 두개 쌓이고, 그 큐를 순서대로 처리한다. 노드가 하나의 길을 끝까지 파고 돌아오는 DFS 와 달리, BFS 에서는 가까운 지점부터 우선탐색한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FNPkl/btsMItItFiv/RmM1PjvqdnRJy7hqQe9kCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFNPkl%2FbtsMItItFiv%2FRmM1PjvqdnRJy7hqQe9kCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1345&quot; height=&quot;737&quot; data-origin-width=&quot;1345&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;갈림길에서 큐가 두개 쌓이고, 그 큐를 순서대로 처리한다. 노드가 하나의 길을 끝까지 파고 돌아오는 DFS 와 달리, BFS 에서는 가까운 지점부터 우선탐색한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;와... 이건 진짜 어려웠다ㅠㅠ&lt;/s&gt;&lt;/p&gt;</description>
      <category>코딩테스트</category>
      <author>yesolje</author>
      <guid isPermaLink="true">https://yesolje.tistory.com/11</guid>
      <comments>https://yesolje.tistory.com/11#entry11comment</comments>
      <pubDate>Thu, 13 Mar 2025 13:39:57 +0900</pubDate>
    </item>
    <item>
      <title>프로그래머스_고득점kit_우선탐색_타겟넘버</title>
      <link>https://yesolje.tistory.com/10</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;풀이&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741664732404&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    int count = 0; // 정답 개수를 저장하는 변수

    public int solution(int[] numbers, int target) {
        dfs(numbers, target, 0, 0); // DFS 탐색 시작
        return count;
    }

    private void dfs(int[] numbers, int target, int index, int sum) {
        // 1. 모든 숫자를 다 사용한 경우
        if (index == numbers.length) {
            if (sum == target) { // 목표 숫자와 같다면
                count++; // 정답 개수 증가
            }
            return; // 재귀 종료
        }

        // 2. 현재 숫자를 더하는 경우
        dfs(numbers, target, index + 1, sum + numbers[index]);

        // 3. 현재 숫자를 빼는 경우
        dfs(numbers, target, index + 1, sum - numbers[index]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념정리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;❓&lt;/span&gt;깊이우선탐색(DFS)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt;한방향으로 끝까지 가보고 , 다시 돌아와서 다른 방향도 탐색하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt;트리 형태로 모든 경우의 수를 탐색&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✔️&lt;/span&gt;함수 안에서 자기 자신을 호출하는 방식으로 동작(재귀)&lt;/p&gt;</description>
      <category>코딩테스트</category>
      <author>yesolje</author>
      <guid isPermaLink="true">https://yesolje.tistory.com/10</guid>
      <comments>https://yesolje.tistory.com/10#entry10comment</comments>
      <pubDate>Tue, 11 Mar 2025 12:48:47 +0900</pubDate>
    </item>
  </channel>
</rss>