<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>nippycloud 님의 블로그</title>
    <link>https://nippyclouding.tistory.com/</link>
    <description>since 1999</description>
    <language>ko</language>
    <pubDate>Thu, 14 May 2026 08:39:41 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>nippycloud</managingEditor>
    <image>
      <title>nippycloud 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8258127/attach/af2505350cf946e6aed60138be11ae4d</url>
      <link>https://nippyclouding.tistory.com</link>
    </image>
    <item>
      <title>[Kafka] Kafka 1</title>
      <link>https://nippyclouding.tistory.com/95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka : 대규모 데이터를 처리할 수 있는 메시지 큐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 큐 : 큐 형태의 자료구조에 데이터를 일시적으로 저장하는 '임시' 저장소, 비동기로 데이터를 처리할 수 있어 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 동기적 처리 : 순차처리, A 작업이 모두 끝난 뒤 B 작업 처리, Rest API 통신&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 비동기 처리 : 병렬 처리, A 작업을 시작한 직후 A 작업이 끝날 때까지 기다리지 않고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;B 작업도&lt;span&gt; 바로 실행, 메시지 큐 통신&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;비동기로 효율적으로 동작하기 때문에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;대규모 트래픽 처리 시&lt;span&gt; 유리하다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Broker : 카프카가 설치된 서버&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Cluster : 카프카가 설치된 서버 (브로커)들의 모음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우분투에 Kafka 설치 (우분투를 카프카 브로커로 세팅하는 방법)&lt;/p&gt;
&lt;pre id=&quot;code_1778138136357&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#1. JDK 설치 (Kafka는 JDK 기반 동작)
$ sudo apt update
$ sudo apt install openjdk-17-jdk
$ java -version # 잘 설치됐는 지 확인


#2. Kafka 설치
wget https://archive.apache.org/dist/kafka/4.0.0/kafka_2.13-4.0.0.tgz
# wget https://dlcdn.apache.org/kafka/4.0.0/kafka_2.13-4.0.0.tgz
# 아파치는 새로운 버전이 나오면 이전 주소 파일을 다른 저장소로 옮기거나 변경한다 =&amp;gt; 적절한 주소 확인 필요


#3. 압축 풀기
$ tar -xzf kafka_2.13-4.0.0.tgz # 압축 풀기
$ cd kafka_2.13-4.0.0 # 압축 푼 디렉토리로 이동


#4. Kafka를 실행시켰을 때 소요되는 메모리 줄이기 (JVM 메모리 튜닝), 설정하지 않으면 기본 1GB 할당
#kafka-server-start.sh는 실행될 때 KAFKA_HEAP_OPTS라는 변수에 적힌 값을 읽어서 JVM 메모리를 설정
#Xmx : 초기 메모리, Xms : 최대 메모리
$ export KAFKA_HEAP_OPTS=&quot;-Xmx400m -Xms400m&quot;


#5. swap 메모리 지정 : 최대 400메가 메모리 + 스왑 
$ sudo dd if=/dev/zero of=/swapfile bs=128M count=16 # 2GB 파일 생성
$ sudo chmod 600 /swapfile # 파일에 권한 부여
$ sudo mkswap /swapfile # 2GB 파일을 swap 공간의 형태로 전환
$ sudo swapon /swapfile # swap 활성화

# 시스템 부팅 시마다 swap 메모리가 자동으로 활성화 되도록 파일시스템 수정
# fstab : file system table, 부팅할 때 어떤 디스크, 장치를 어디에 연결(마운트)할지 기록한 파일
$ sudo vi /etc/fstab 
# vi 편집기에서 fstab 파일에 아래 내용을 추가하고 저장하기
# /swapfile 파일을 가져와서 swap 메모리 형태로 사용, 타입 형식은 스왑 형식이고 옵션은 default, 백업 x(0), 부팅 시 디스크 검사 x(0)
/swapfile swap swap defaults 0 0

$ free #(free -h) swap 메모리 설정이 잘 되었는지 확인

#6. 카프카 설정 수정
$ vi config/server.properties

# 외부에서 접근할 때 사용하는 주소, IP 수정
advertised.listeners=PLAINTEXT://{현재 카프카 리눅스의 IP}:9092,CONTROLLER://{현재 카프카 리눅스의 IP}:9093

#7. 카프카 로그 파일 설정 (카프카 서버 처음 실행 시에만 설정하면 된다.
$ KAFKA_CLUSTER_ID=&quot;$(bin/kafka-storage.sh random-uuid)&quot; # 랜덤 uuid 발행하여 카프카 클러스터 id로 지정
$ bin/kafka-storage.sh format --standalone -t $KAFKA_CLUSTER_ID -c config/server.properties
# KRaft 모드(주키퍼가 없는 독립 실행 모드)에서의 저장소 포맷 명령어

#8. 카프카 리눅스 서버 실행
$ cd ~/kafka_2.13-4.0.0
$ bin/kafka-server-start.sh -daemon config/server.properties # 백그라운드 실행
$ tail -f logs/kafkaServer.out # 카프카 서버 로그 확인 (정상 실행되었는지)
$ lsof -i:9092 #9092번 포트에서 카프카가 잘 동작하는지 확인

#9. 카프카 리눅스 서버 종료
$ bin/kafka-server-stop.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;kafka.png&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;247&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZI8u4/dJMcaaLZvTq/glkZcCaXlR5TkWIdO3C0j0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZI8u4/dJMcaaLZvTq/glkZcCaXlR5TkWIdO3C0j0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZI8u4/dJMcaaLZvTq/glkZcCaXlR5TkWIdO3C0j0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZI8u4%2FdJMcaaLZvTq%2FglkZcCaXlR5TkWIdO3C0j0%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;1157&quot; height=&quot;247&quot; data-filename=&quot;kafka.png&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;247&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여차저차해서 카프카 실행, 종료 성공 ..&lt;/p&gt;</description>
      <category>Infra/Kafka</category>
      <author>nippycloud</author>
      <guid isPermaLink="true">https://nippyclouding.tistory.com/95</guid>
      <comments>https://nippyclouding.tistory.com/95#entry95comment</comments>
      <pubDate>Thu, 7 May 2026 17:10:19 +0900</pubDate>
    </item>
    <item>
      <title>egovframe vscode initializr 3주차</title>
      <link>https://nippyclouding.tistory.com/94</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;해당 글은 교육 시간의 내용을 정리한 것이 아닌 스스로 학습한 것을 정리하는 글이다.&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;3주차 교육 시간 내용을 정리한 것은 아래 링크에 있다.&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;이번 주에는 본격적으로 md 파일을 파헤치기 시작했다.&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;먼저 README.MD 파일을 읽어본다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cz0srg/dJMcahYATge/TQTYck1Mqsj3UwGw3d5IUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cz0srg/dJMcahYATge/TQTYck1Mqsj3UwGw3d5IUK/img.png&quot; data-alt=&quot;Readme.md&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cz0srg/dJMcahYATge/TQTYck1Mqsj3UwGw3d5IUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcz0srg%2FdJMcahYATge%2FTQTYck1Mqsj3UwGw3d5IUK%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;957&quot; height=&quot;631&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Readme.md&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;'주요 기능' 부터 막혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;프로젝트 생성 기능, CRUD 코드 생성 기능 .. 이런 것들이 무슨 말이고 어디에서 사용되는지 이해가 잘 되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bp4yx/dJMcahYATgh/caQ2BkIL2Ze401zOfLzxR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bp4yx/dJMcahYATgh/caQ2BkIL2Ze401zOfLzxR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bp4yx/dJMcahYATgh/caQ2BkIL2Ze401zOfLzxR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBp4yx%2FdJMcahYATgh%2FcaQ2BkIL2Ze401zOfLzxR1%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;958&quot; height=&quot;844&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;923&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ds2qlt/dJMcahYATgk/w4geKjaYAtydxeoNwFx0U1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ds2qlt/dJMcahYATgk/w4geKjaYAtydxeoNwFx0U1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ds2qlt/dJMcahYATgk/w4geKjaYAtydxeoNwFx0U1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fds2qlt%2FdJMcahYATgk%2Fw4geKjaYAtydxeoNwFx0U1%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;634&quot; height=&quot;923&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;923&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;size14&quot;&gt;여기까지 무슨 말인지 잘 몰랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;아래에 '사용 예시' 가 있어서 이것을 읽었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;885&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kwWU8/dJMcajvkkS4/2ZQYqr1ZTAjbrrBeAKkT91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kwWU8/dJMcajvkkS4/2ZQYqr1ZTAjbrrBeAKkT91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kwWU8/dJMcajvkkS4/2ZQYqr1ZTAjbrrBeAKkT91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkwWU8%2FdJMcajvkkS4%2F2ZQYqr1ZTAjbrrBeAKkT91%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;980&quot; height=&quot;885&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;885&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;size14&quot;&gt;DDL을 어디에 입력하라는지도 잘 모르겠어서 Antigravity에 이 md 파일을 내가 써보기 위해서 어떻게 하면 되는지 물어보고는 해답을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;안티그라비티가 해당 프로젝트에서 VS Code 좌측 사이드바의 Run and Debug로 가서 Run Extension을 누르기만 하면 된다고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kB6Qy/dJMcadokJEc/mz4V9P9E8ByoS423J0JVo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kB6Qy/dJMcadokJEc/mz4V9P9E8ByoS423J0JVo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kB6Qy/dJMcadokJEc/mz4V9P9E8ByoS423J0JVo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkB6Qy%2FdJMcadokJEc%2Fmz4V9P9E8ByoS423J0JVo0%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;336&quot; height=&quot;177&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Run Extension을 실행하니 VS Code에서 4개의 터미널이 실행되며 새로운 VS Code 창이 하나 더 열렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;새로운 VS Code 창은 2주차때 혼자 공부하면서 마주한 익숙한 화면이었다. (VS Code에서 전자정부 프레임워크 프로젝트를 생성할 수 있는 화면)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xeWG4/dJMcaaFcNeq/wzuwF4fe9pK4sSFnFZ2Vz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xeWG4/dJMcaaFcNeq/wzuwF4fe9pK4sSFnFZ2Vz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xeWG4/dJMcaaFcNeq/wzuwF4fe9pK4sSFnFZ2Vz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxeWG4%2FdJMcaaFcNeq%2FwzuwF4fe9pK4sSFnFZ2Vz1%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;1261&quot; height=&quot;253&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;253&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;size14&quot;&gt;새로운 VS Code 창으로 넘어오면 바로 Code Generator로 넘어가는 것이 아니라 'Projects'에서 템플릿을 선택하고 &quot;반드시 템플릿을 먼저 생성&quot;해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(알고싶지 않았는데 고생하면서 알게 되었다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이후 Code Generator로 넘어와서 ReadMe.md 파일의 예시 DDL을 입력했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vhZyf/dJMcafGuYzR/km8bYg2ZuPcKNrA6WUs5v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vhZyf/dJMcafGuYzR/km8bYg2ZuPcKNrA6WUs5v0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vhZyf/dJMcafGuYzR/km8bYg2ZuPcKNrA6WUs5v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvhZyf%2FdJMcafGuYzR%2Fkm8bYg2ZuPcKNrA6WUs5v0%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;267&quot; height=&quot;836&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;1013&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vHuGQ/dJMcagMbvDi/guJC6eAgMjLOKroacKyNF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vHuGQ/dJMcagMbvDi/guJC6eAgMjLOKroacKyNF0/img.png&quot; data-alt=&quot;Output Path가 잘못 되었다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vHuGQ/dJMcagMbvDi/guJC6eAgMjLOKroacKyNF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvHuGQ%2FdJMcagMbvDi%2FguJC6eAgMjLOKroacKyNF0%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;667&quot; height=&quot;1013&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;1013&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Output Path가 잘못 되었다&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;size14&quot;&gt;Output Path를 맞추어주고, Generate를 클릭하면 생성할 예시 코드 파일들을 select하라고 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;u&gt;Output Path는 반드시 Projects에서 먼저 생성한 패키지 안으로 설정해야 한다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;11개 파일 모두를 select한 뒤 프로젝트를 생성한 다음 Output Path에서 새로운 파일이 생성되었는지 확인하고 그것을 VS Code로 다시 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w0kCy/dJMcajvkldx/m7UotxWK69TCKaZeLkIKX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w0kCy/dJMcajvkldx/m7UotxWK69TCKaZeLkIKX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w0kCy/dJMcajvkldx/m7UotxWK69TCKaZeLkIKX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw0kCy%2FdJMcajvkldx%2Fm7UotxWK69TCKaZeLkIKX1%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;1278&quot; height=&quot;816&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;816&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;size14&quot;&gt;새로 생성한 프로젝트를 들어가면 샘플 코드들이 생성된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;1023&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8nrA6/dJMcaaZsRvk/WCRzLF8uOOkmBUkEKkz4F0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8nrA6/dJMcaaZsRvk/WCRzLF8uOOkmBUkEKkz4F0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8nrA6/dJMcaaZsRvk/WCRzLF8uOOkmBUkEKkz4F0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8nrA6%2FdJMcaaZsRvk%2FWCRzLF8uOOkmBUkEKkz4F0%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;1179&quot; height=&quot;1023&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;1023&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;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #222222;&quot;&gt;다음으로 CONTRIBUTING.MD 파일을 읽어본다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #222222;&quot;&gt;작성중 ..&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>일반/오픈소스 컨트리뷰션 아카데미 2026 [체험형]</category>
      <author>nippycloud</author>
      <guid isPermaLink="true">https://nippyclouding.tistory.com/94</guid>
      <comments>https://nippyclouding.tistory.com/94#entry94comment</comments>
      <pubDate>Mon, 4 May 2026 22:04:42 +0900</pubDate>
    </item>
    <item>
      <title>egovframe vscode initializr 2주차</title>
      <link>https://nippyclouding.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;해당 글은 교육 시간의 내용을 정리한 것이 아닌 스스로 학습한 것을 정리하는 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;2주차 교육 시간 내용을 정리한 것은 아래 링크에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://hypnotic-mayonnaise-5c3.notion.site/2-34b319fec6fe8089a5b5f90568aa5424?source=copy_link&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hypnotic-mayonnaise-5c3.notion.site/2-34b319fec6fe8089a5b5f90568aa5424?source=copy_link&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777600285734&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;2주차 | Notion&quot; data-og-description=&quot;Hosted by Notion Sites &amp;mdash; The easiest way to get a website up and running.&quot; data-og-host=&quot;hypnotic-mayonnaise-5c3.notion.site&quot; data-og-source-url=&quot;https://hypnotic-mayonnaise-5c3.notion.site/2-34b319fec6fe8089a5b5f90568aa5424?source=copy_link&quot; data-og-url=&quot;https://hypnotic-mayonnaise-5c3.notion.site/2-34b319fec6fe8089a5b5f90568aa5424&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hs01E/dJMb9kT6las/CLetpIHvOBdCBlFx3gMUqK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/5SKCn/dJMb8SXBbvf/InOdDx7su4JW8sL2vYa0KK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://hypnotic-mayonnaise-5c3.notion.site/2-34b319fec6fe8089a5b5f90568aa5424?source=copy_link&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hypnotic-mayonnaise-5c3.notion.site/2-34b319fec6fe8089a5b5f90568aa5424?source=copy_link&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hs01E/dJMb9kT6las/CLetpIHvOBdCBlFx3gMUqK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/5SKCn/dJMb8SXBbvf/InOdDx7su4JW8sL2vYa0KK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2주차 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hosted by Notion Sites &amp;mdash; The easiest way to get a website up and running.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hypnotic-mayonnaise-5c3.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2주차 학습&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;egovframe vscode initializr 프로젝트에 기여를 하기 위해서는 이 프로젝트가 어떤 것인지 먼저 이해가 선행되어야 할 것이다고 생각해서 간단한 Health check API와 Controller - Service - Repository 계층 구도를 만들어보았다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 기본 설치와 Health check API&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;VS Code에서 &lt;span style=&quot;background-color: #ffffff; color: #616161; text-align: start;&quot;&gt;eGovFrame Initializr in VSCode&lt;/span&gt;와 &lt;span style=&quot;background-color: #ffffff; color: #616161; text-align: start;&quot;&gt;Extension Pack for Java를 확장에서 설치한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;2105&quot; data-origin-height=&quot;1108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pkr1n/dJMcabD3bws/FjgKtoYqa7mMqKVsfZ81R1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pkr1n/dJMcabD3bws/FjgKtoYqa7mMqKVsfZ81R1/img.png&quot; data-alt=&quot;eGovFrame Initializr in VSCode&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pkr1n/dJMcabD3bws/FjgKtoYqa7mMqKVsfZ81R1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpkr1n%2FdJMcabD3bws%2FFjgKtoYqa7mMqKVsfZ81R1%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;2105&quot; height=&quot;1108&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;2105&quot; data-origin-height=&quot;1108&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;eGovFrame Initializr in VSCode&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1708&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boWXlC/dJMcabD3bwP/Xbv6Wkn9nc2reDkqYN9nOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boWXlC/dJMcabD3bwP/Xbv6Wkn9nc2reDkqYN9nOk/img.png&quot; data-alt=&quot;Extension Pack for Java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boWXlC/dJMcabD3bwP/Xbv6Wkn9nc2reDkqYN9nOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboWXlC%2FdJMcabD3bwP%2FXbv6Wkn9nc2reDkqYN9nOk%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;1708&quot; height=&quot;536&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1708&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Extension Pack for Java&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;size14&quot;&gt;이후 좌측 사이드바에 &lt;i&gt;&lt;b&gt;e&amp;nbsp;&lt;/b&gt;&lt;/i&gt;가 생성된 것을 확인하고 클릭해서 프로젝트를 하나 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;가장 간단해 보이는 것으로는 'Web Project', 'Boot Web Project'가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;나는 Boot Web Project를 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;2514&quot; data-origin-height=&quot;1385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clUUvA/dJMcadBMnty/zVdxKJIG672e341KCs41vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clUUvA/dJMcadBMnty/zVdxKJIG672e341KCs41vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clUUvA/dJMcadBMnty/zVdxKJIG672e341KCs41vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclUUvA%2FdJMcadBMnty%2FzVdxKJIG672e341KCs41vk%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;2514&quot; height=&quot;1385&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;2514&quot; data-origin-height=&quot;1385&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;size14&quot;&gt;프로젝트를 생성하고 VS Code로 생성한 프로젝트를 다시 Open한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;example 패키지 하위에 controller 패키지를 생성 후 HealthCheckController.java 파일을 생성, 아래와 같은 healtch check API를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RnsuJ/dJMb99MXRAt/7EgFHXCVGsCNSzn1n7uT51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RnsuJ/dJMb99MXRAt/7EgFHXCVGsCNSzn1n7uT51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RnsuJ/dJMb99MXRAt/7EgFHXCVGsCNSzn1n7uT51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRnsuJ%2FdJMb99MXRAt%2F7EgFHXCVGsCNSzn1n7uT51%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;1714&quot; height=&quot;569&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;569&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;size14&quot;&gt;이후 EgovBootApplication.java에서 Run을 누르면 스프링 부트가 실행된다. (물론 Java가 설치되어있어야 한다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제2.png&quot; data-origin-width=&quot;2351&quot; data-origin-height=&quot;1318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ofqBz/dJMcacbSYTb/iNaghXEP6PwtQ5ualI2EFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ofqBz/dJMcacbSYTb/iNaghXEP6PwtQ5ualI2EFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ofqBz/dJMcacbSYTb/iNaghXEP6PwtQ5ualI2EFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FofqBz%2FdJMcacbSYTb%2FiNaghXEP6PwtQ5ualI2EFK%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;2351&quot; height=&quot;1318&quot; data-filename=&quot;무제2.png&quot; data-origin-width=&quot;2351&quot; data-origin-height=&quot;1318&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;size14&quot;&gt;작성한 API로 접속하면 health check API가 정상 동작하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgzxab/dJMcaiiRdkL/9lQGaymHMe9UpBRNvYARKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgzxab/dJMcaiiRdkL/9lQGaymHMe9UpBRNvYARKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgzxab/dJMcaiiRdkL/9lQGaymHMe9UpBRNvYARKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgzxab%2FdJMcaiiRdkL%2F9lQGaymHMe9UpBRNvYARKk%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;802&quot; height=&quot;195&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Controller - Service - Repository 계층 구조&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;pom.xml에 아래 의존성을 추가한다. (JPA, H2 DB)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777546833334&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-data-jpa&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.h2database&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;h2&amp;lt;/artifactId&amp;gt;
    &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 application.properties에서 기본 설정을 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777547007756&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.h2.console.enabled=true

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 기본적인 Entity, Controller, Service, Repository 계층 코드를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777547340288&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Entity
@Entity
@Getter
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    public Member(String name) {
        this.name = name;
    }
}

// controller
@RestController
@RequestMapping(&quot;/api/members&quot;)
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    // 데이터 저장 API (http://localhost:8080/api/members?name=kim)
    @PostMapping
    public Member createMember(@RequestParam String name) {
        return memberService.saveMember(name);
    }

    // 데이터 조회 API (http://localhost:8080/api/members)
    @GetMapping
    public List&amp;lt;Member&amp;gt; getMembers() {
        return memberService.getAllMembers();
    }
}

// service
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    public Member saveMember(String name) {
        Member member = new Member(name);
        return memberRepository.save(member); // DB에 저장
    }

    public List&amp;lt;Member&amp;gt; getAllMembers() {
        return memberRepository.findAll(); // DB에서 모두 조회
    }
}

// repository
public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
}



// 실행 파일
@SpringBootApplication
@EnableJpaRepositories(transactionManagerRef = &quot;jpaTransactionManager&quot;)
public class EgovBootApplication {

	public static void main(String[] args) {
		SpringApplication.run(EgovBootApplication.class, args);
	}

	@Bean
	@Primary
    public PlatformTransactionManager jpaTransactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}&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;size14&quot;&gt;실행 후 터미널에서 Post 요청 -&amp;gt; 정상 저장 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Him6G/dJMcahqKHdK/pXS3iSoyk7nouWBw4cNqr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Him6G/dJMcahqKHdK/pXS3iSoyk7nouWBw4cNqr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Him6G/dJMcahqKHdK/pXS3iSoyk7nouWBw4cNqr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHim6G%2FdJMcahqKHdK%2FpXS3iSoyk7nouWBw4cNqr1%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;1308&quot; height=&quot;264&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>일반/오픈소스 컨트리뷰션 아카데미 2026 [체험형]</category>
      <author>nippycloud</author>
      <guid isPermaLink="true">https://nippyclouding.tistory.com/93</guid>
      <comments>https://nippyclouding.tistory.com/93#entry93comment</comments>
      <pubDate>Thu, 30 Apr 2026 19:41:04 +0900</pubDate>
    </item>
    <item>
      <title>egovframe vscode initializr 1주차</title>
      <link>https://nippyclouding.tistory.com/92</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;2026년 4월 20일 ~ 2026년 5월 31일까지의 오픈소스 컨트리뷰션 아카데미 학습 내용 정리 카테고리&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;아래는 1주차 시간에 학습한 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://hypnotic-mayonnaise-5c3.notion.site/1-34b319fec6fe80948187e8c75fbf9861?source=copy_link&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hypnotic-mayonnaise-5c3.notion.site/1-34b319fec6fe80948187e8c75fbf9861?source=copy_link&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777602774790&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;1주차 | Notion&quot; data-og-description=&quot;SCM : Source Code Management&quot; data-og-host=&quot;hypnotic-mayonnaise-5c3.notion.site&quot; data-og-source-url=&quot;https://hypnotic-mayonnaise-5c3.notion.site/1-34b319fec6fe80948187e8c75fbf9861?source=copy_link&quot; data-og-url=&quot;https://hypnotic-mayonnaise-5c3.notion.site/1-34b319fec6fe80948187e8c75fbf9861&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/botGyL/dJMb8XR879N/J0taKGeMR7ILSqzja4mGhK/img.png?width=82&amp;amp;height=828&amp;amp;face=0_0_82_828,https://scrap.kakaocdn.net/dn/dllbaQ/dJMb8TCc4dM/hn4WVKiaugrkajblpUK5PK/img.png?width=82&amp;amp;height=828&amp;amp;face=0_0_82_828&quot;&gt;&lt;a href=&quot;https://hypnotic-mayonnaise-5c3.notion.site/1-34b319fec6fe80948187e8c75fbf9861?source=copy_link&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hypnotic-mayonnaise-5c3.notion.site/1-34b319fec6fe80948187e8c75fbf9861?source=copy_link&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/botGyL/dJMb8XR879N/J0taKGeMR7ILSqzja4mGhK/img.png?width=82&amp;amp;height=828&amp;amp;face=0_0_82_828,https://scrap.kakaocdn.net/dn/dllbaQ/dJMb8TCc4dM/hn4WVKiaugrkajblpUK5PK/img.png?width=82&amp;amp;height=828&amp;amp;face=0_0_82_828');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1주차 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SCM : Source Code Management&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hypnotic-mayonnaise-5c3.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;화요일은 간단한 디스코드 설정 테스트, 수요일은 오픈소스 컨트리뷰트 아카데미 발대식, 목요일은 깃에 대한 기본적인 개념을 익혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;토요일은 오프라인으로 위 링크에 대한 내용을 학습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;기억하면 좋을 명령어&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;rm -rf [파일, 디렉토리 이름] : 해당 파일 또는 디렉토리와 하위까지 모두 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;git reset --hard : 모두 되돌리기, 바로 직전 커밋으로 복구한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&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;merge와 rebase, squash&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size14&quot;&gt;Merge (일반 병합)&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size14&quot;&gt;- 두 브랜치의 변경 이력을 모두 남기고 '합쳐졌다는 흔적' [Merge Commit] 을 추가하는 방식&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size14&quot;&gt;&lt;u&gt;&lt;b&gt;A -&amp;gt; B -&amp;gt; C &amp;amp; B에서 X -&amp;gt; Y가 파생되었을 때 merge 시 A -&amp;gt; B -&amp;gt; C -&amp;gt; M (M은 커밋 C와 Y 변경 이력을 합친 새로운 merge 전용 커밋)&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size14&quot;&gt;- 장점 : 작업 시간 순서, 브랜치 분기, 병합 흐름이 모두 보존 =&amp;gt; '추적이 명확'&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size14&quot;&gt;- 단점 : 커밋 내역이 복잡하게 얽히고 의미 없는 merge 커밋이 쌓이면 히스토리가 지저분해질 수 있다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size14&quot;&gt;- 주로 여러 명이 공동 작업한 큰 단위의 기능을 합치거나 전체 작업 흐름 자체를 보존할 때 사용 (&lt;b&gt;혼자서 개발하는 로컬 브랜치에서는 rebase로도 충분&lt;/b&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size14&quot;&gt;Rebase (재배치)&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size14&quot;&gt;- 파생되어 작업한 브랜치의 시작점을 최신 코드로 옮겨 '처음부터 한 줄로 작업한 것처럼' 표시&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;u&gt;A -&amp;gt; B -&amp;gt; C &amp;amp; B에서 X -&amp;gt; Y가 파생되었을 때 rebase 시 A -&amp;gt; B -&amp;gt; C -&amp;gt; X -&amp;gt; Y&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size14&quot;&gt;- 장점 : 브랜치 갈래가 없어지고 일직선의 가시성이 좋은 커밋 히스토리를 유지할 수 있다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size14&quot;&gt;- 단점 : 커밋 기록이 바뀌기 때문에 다른 사람과 공유 중인 원격 브랜치에서 실행 시 문제 발생 가능 =&amp;gt; &lt;b&gt;원격 브랜치에서는 사용하지 않는 것이 일반적이다. &lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;(로컬 브랜치에서 사용할 때만 안전, 주로 로컬에서 원격 브랜치의 파생 브랜치를 생성하여 원격에 동기화하지 않고 로컬에서만 작업하는 브랜치에서 사용)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Squash (압축 병합)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 파생된 브랜치에서 작업한 자잘한 여러 개의 커밋을 하나의 큰 커밋으로 '압축' 하여 메인 브랜치에 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;u&gt;A -&amp;gt; B -&amp;gt; C &amp;amp; B에서 X -&amp;gt; Y가 파생되었을 때 squash 시 A -&amp;gt; B -&amp;gt; C -&amp;gt; D (D = B에서 X -&amp;gt; Y가 파생된 변경 이력들을 압축하여 C에 반영한 커밋)&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 장점 : 메인 브랜치에서 '완성된 기능' 단위의 커다란 커밋 하나만 남게 되어 프로젝트 진행 상황 파악이 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 단점 : 파생 브랜치에서 고민하며 남긴 세세한 작업 과정은 메인 브랜치에서 확인할 수 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 개인이 작업한 작은 기능, 버그 수정을 완료하고 메인 브랜치에 반영할 때 주로 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>일반/오픈소스 컨트리뷰션 아카데미 2026 [체험형]</category>
      <author>nippycloud</author>
      <guid isPermaLink="true">https://nippyclouding.tistory.com/92</guid>
      <comments>https://nippyclouding.tistory.com/92#entry92comment</comments>
      <pubDate>Thu, 30 Apr 2026 19:27:22 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Kafka Basic</title>
      <link>https://nippyclouding.tistory.com/91</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Kafka&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 디스크 기반의 메시징 시스템 저장소 (미들웨어) / 분산 메시지 브로커, 분산 스트리밍 플랫폼 이라고도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 실시간 메시징 서비스에 주로 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 메시지 브로커 : 각 서버 간 데이터를 송수신할 때 &lt;b&gt;중간 다리 역할, 중재자&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 데이터 생산자 Producer은 데이터를 Kafka에 보내고, 데이터 소비자 Consumer은 데이터를 kafka에서 꺼내어 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;Producer&lt;/b&gt;&amp;nbsp;: 카프카에게 데이터를 전달하는(push) 서버&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;Consumer&amp;nbsp;&lt;/b&gt;: 카프카에서 데이터를 빼내어 사용하는(pull) 서버&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&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;size18&quot;&gt;&lt;b&gt;아키텍쳐&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WOniD/dJMcacCYr9h/Rjivs1VmjtIc1QLNddfkg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WOniD/dJMcacCYr9h/Rjivs1VmjtIc1QLNddfkg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WOniD/dJMcacCYr9h/Rjivs1VmjtIc1QLNddfkg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWOniD%2FdJMcacCYr9h%2FRjivs1VmjtIc1QLNddfkg1%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;1000&quot; height=&quot;500&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Kafka와 RabbitMQ의 차이&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;- Kafka는 데이터를 생산, 소비하는 주체가 프로듀서와 컨슈머에게 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;프로듀서 서버가 직접 데이터를 push하고, 컨슈머 서버가 직접 데이터를 pull한다, 카프카는 가만히 있는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;컨슈머 서버가 데이터를 pull해도 데이터를 바로 지우지 않고 보관한다. (임시 보관, 주로 일주일)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;카프카의 목적은 '안정적으로 데이터를 발행, 소비, 보관' 하는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;- RabbitMQ는 데이터를 받으면 컨슈머 서버의 상태를 확인 후 RabbitMQ가 직접 컨슈머 서버에게 데이터를 전달한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;데이터가 들어오는 즉시 전달되기 때문에 응답 속도가 빠른 편이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;컨슈머 서버가 메시지를 정상 소비했다는 확인(ack)이 돌아오면 메모리에서 메시지를 즉시 삭제한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;래빗엠큐의 목적은 '메시지의 빠른 전달'이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;- Redis는 빠른 조회를 위한 캐시/저장소&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;- RabbitMQ는 '목적지까지 빠르고 확실하게 메시지를 전달'하기 위한 메시지 브로커&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;- Kafka는 '대량의 데이터를 안정적으로 전달'하기 위한 메시지 브로커&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Broker 브로커 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 소프트웨어가 설치된 '서버 한 대'를 의미, 데이터 저장소이자 전달자&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 프로듀서 서버(데이터를 보내는 서버)로부터 메시지를 받아서 저장, 컨슈머 서버(데이터를 사용하는 서버)가 메시지를 가져갈 수 있도록 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;메시지(데이터, 주로 JSON)는 브로커 내부의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;토픽(Topic)&lt;/b&gt;이라는 곳에 저장&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 브로커의 주요 업무 - 파티션 관리, Replication(다른 브로커에 데이터 복제), 컨트롤러 역할(파티션의 리더 선정, 다른 브로커 관리 ..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- &lt;b&gt;일반적으로 카프카를 운영할 때는 서버 한 대만 사용하지 않고 여러 대를 '묶어서' 사용한다. -&amp;gt; 클러스터&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Cluster 클러스터 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 Broker(카프카 서버)들의 집합&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 여러 대의 브로커(서버)가 하나로 묶여서 동작하는 상태&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 고가용성 : 브로커 하나가 문제가 생겨도 다른 브로커에서 데이터를 가지고 있으면 서비스가 중단되지 않는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 확장성 : 트래픽 증가 시 브로커 서버를 추가로 투입하여 처리량을 늘릴 수 있다. (최적의 복제와 분산 시스템)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 고성능 비동기 메시징 처리 시스템 : 프로듀서 서버는 데이터를 카프카에 push한 뒤 응답 결과를 기다리지 않고 비동기로 바로 다음 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;u&gt;일반적으로 안정성을 위해 적어도 3대 이상의 브로커로 클러스터를 구성한다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;카프카 특징&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 메시지 : 카프카에서 사용하는 &lt;u&gt;데이터의 최소 단위&lt;/u&gt;, 브로커 내부 '&lt;b&gt;토픽&lt;/b&gt;' 이라는 곳에 저장, &lt;b&gt;비구조화&lt;/b&gt;된 데이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;비구조화&lt;/b&gt; : RDB와 다르게 데이터의 양식이 매번 변경되어도 카프카의 토픽에 저장 가능&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;비구조화 특징이 있지만 컨슈머 서버 입장에서는 데이터 양식에 변경이 있을 경우 문제가 발생할 수 있기에 일반적으로 데이터를 정형화시켜둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;=&amp;gt; 비구조화가 가능하지만 정형화를 권장&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- RDB와 다르게 메시지를 영구보관하지 않는다. (임시 보관, 주로 일주일)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- &lt;b&gt;재처리 매커니즘&lt;/b&gt;을 통해 컨슈머 서버에서 메시지 소비에 문제가 생겼을 때 안정성 확보&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;카프카의 &lt;b&gt;OffSet&lt;/b&gt; : 컨슈머 서버가 데이터를 소비한 위치를 기록하는 기능&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 오프셋 정보는 컨슈머 서버가 결정, 저장은 카프카 브로커&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;컨슈머 오프셋 : 컨슈머 서버가 가진 책갈피&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;메시지 오프셋 : 카프카 브로커가 각 메시지마다 부여하는 시퀀스 번호&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;카프카에는 오프셋 개념이 있어 데이터 복구, 재처리 등에 강력하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;금융권 서버 개발 시 RabbitMQ보다 카프카를 선호하는 이유 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;카프카의 구조&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;클러스터 (Cluster)&lt;/b&gt; : 브로커들이 여러 개 모인 시스템&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,1,0&quot;&gt;브로커 (Broker):&lt;/b&gt; 카프카가 설치된 &lt;b data-index-in-node=&quot;23&quot; data-path-to-node=&quot;3,1,0&quot;&gt;개별 서버,&lt;/b&gt;&amp;nbsp;하나의 브로커 안에는 &lt;b data-index-in-node=&quot;42&quot; data-path-to-node=&quot;3,1,0&quot;&gt;여러 개의 토픽&lt;/b&gt;이 존재할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 프로듀셔, 컨슈머 서버의 요청 처리 및 메시지 저장 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,2,0&quot;&gt;토픽 (Topic):&lt;/b&gt; 데이터를 구분하는 &lt;b data-index-in-node=&quot;22&quot; data-path-to-node=&quot;3,2,0&quot;&gt;논리적인 단위&lt;/b&gt;(방), 하나의 토픽은 성능과 분산을 위해 &lt;b data-index-in-node=&quot;53&quot; data-path-to-node=&quot;3,2,0&quot;&gt;여러 개의 파티션&lt;/b&gt;으로 나뉜다.&lt;b&gt; (논리적 개념)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 메시지의 카테고리처럼 동작, 프로듀서와 컨슈머 서버는 토픽 단위에서 발행, 소비&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- order-topic 에 주문 요청, 주문 완료 메시지 등 주문과 관련된 메시지 발행&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,3,0&quot;&gt;파티션 (Partition):&lt;/b&gt; 실제로 데이터가 순서대로 쌓이는 &lt;b data-index-in-node=&quot;35&quot; data-path-to-node=&quot;3,3,0&quot;&gt;물리적인 칸막이 (물리적 개념)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,4,0&quot;&gt;메시지 (Message):&lt;/b&gt; 파티션 안에 저장되는 &lt;b data-index-in-node=&quot;27&quot; data-path-to-node=&quot;3,4,0&quot;&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 data-index-in-node=&quot;27&quot; data-path-to-node=&quot;3,4,0&quot;&gt;파티션 내부에서는 순서가 보장되지만 토픽의 입장에서 보면 시간 순서가 보장되지 못한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;토픽 1에서 꺼낸 '주문 요청 메시지'와 토픽 2에서 꺼낸 '회원 가입 백업 저장 메시지'의 시간 순서를 알 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;=&amp;gt; &lt;b&gt;Key&lt;/b&gt;의 개념을 이용하여 토픽에서 꺼낸 메시지들 간의 시간 순서를 보장하도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (7).png&quot; data-origin-width=&quot;3906&quot; data-origin-height=&quot;1582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH9ln4/dJMcahdfoqf/Y7shWmOtgJHDOXOBKxaMIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH9ln4/dJMcahdfoqf/Y7shWmOtgJHDOXOBKxaMIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH9ln4/dJMcahdfoqf/Y7shWmOtgJHDOXOBKxaMIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH9ln4%2FdJMcahdfoqf%2FY7shWmOtgJHDOXOBKxaMIK%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;3906&quot; height=&quot;1582&quot; data-filename=&quot;image (7).png&quot; data-origin-width=&quot;3906&quot; data-origin-height=&quot;1582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Key로 서로 다른 토픽 간 메시지 시간 순서를 보장하는 원리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;프로듀서 서버가 메시지를 카프카에 저장할 때 메시지에 key를 부여하면 카프카 브로커는 이 키에 대한 해시값을 구한 뒤 해시값을 파티션의 수로 나누어서 매핑할 파티션 번호를 추출하여 저장한다. (해시의 특징 : 같은 값을 여러 번 해싱해도 결과로 가지는 해시값은 항상 같다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;예시 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;stock-topic 이라는 주식 주문 토픽에 '종목별 주문 요청'을 시간 순서대로 저장해야할 경우 '종목명' 을 키 값으로 주문 요청 메시지 발행&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;'samsung'을 키 값으로 메시지 발행 -&amp;gt; 종목별 주문 내역의 시간 순서가 보장이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;예시 2&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;chat-topic 이라는 채팅 토픽에 채팅방 별로 시간 순서대로 메시지를 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;=&amp;gt; 채팅방 ID값으로 메시지를 발행하면 채팅방 별 채팅 메시지의 시간 순서가 보장이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;파티션 적정 개수 설정 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 시스템 규모 고려&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;시스템 규모가 크다면 단일 파티션으로는 메시지 처리 성능이 저하될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;시스템 규모에 따라 많은 파티션과 많은 컨슈머를 두고 메시지 병렬 처리 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- Key의 종류 고려&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Key의 종류에 따라 파티션 개수를 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Key의 종류가 매우 적다면 파티션이 많다 하더라도 몇몇 타티션에만 메시지가 쌓일 것이다. (유휴 자원)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 추후 확장 계획의 여부&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;미래 확장 여부를 고려하여 초기에 여유있는 파티션 세팅이 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;key를 사용하는 토픽일 경우 추후 파티션 개수 변경이 구조적으로 어렵다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;카프카의 사용 사례&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- msa에서의 통신&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;message bus : 주문 서버에서 주문 발생 시 카프카에 주문 데이터 메시지를 카프카에게 json으로 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;체결 서버 뿐만 아니라 로그 관리 서버, 실시간 서버 등 여러 다른 서버에서 카프카 메시지를 pull (소비) 가능&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;각 서버 별 주문 요청 메시지가 별도로 하나씩 발행되는 것이 아니라 주문 서버 메시지는 하나만 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;대신 각 서버들이 어디까지 소비했는지 확인 가능하도록 컨슈머 서버 별로 오프셋을 각각 가지게 된다. (컨슈머 그룹별 오프셋 설정)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 로그, 이벤트 수집 : 각 서버들에서 로그 발생 -&amp;gt; kafka에 전달하면 로그 관리 서버 (주로 logstash)가 consume&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;ELK 파이프라인 : 여러 서브들에서 로그 발생 (produce) -&amp;gt; Kafka -&amp;gt; Logstash 서버가 consume -&amp;gt; ElasticSearch에 정제하여 전달 -&amp;gt; Kibana&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- kafka connect : 데이터 파이프라인&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;카프카를 사용하기 위해서는 매번 스프링 서버에서 프로듀서와 컨슈머 설정 코드를 넣어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Kafka Connect를 사용하면 코드로 프로듀서, 컨슈머를 짜지 않아도 설정만으로 DB나 파일 시스템의 데이터를 카프카로 가져오거나 보낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;프로듀서 서버 역할 자동화 - 소스 커넥터 : 외부 시스템 (mysql, AWS S3)의 데이터를 카프카의 토픽으로 가져오는 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;컨슈머 역할 자동화 - 싱크 커넥터 : 카프카 토픽에 쌓인 데이터를 외부 시스템(몽고DB, ElasticSearch)에 전달&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;카프카 커넥트 사용 예시 - 회원 가입 시 회원 DB와 백업 DB에 두 번 insert&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;카프카 미사용 시 : 회원 가입 요청 -&amp;gt; 회원 DB에 저장, 백업 DB에 저장 후 사용자에게 응답 리턴 (병목 가능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;카프카를 사용하되 커넥트를 사용하지 않을 경우 : 회원 가입 요청 -&amp;gt; 회원 DB에 저장 -&amp;gt; 카프카 push -&amp;gt; 백업 처리 서버가 소비 -&amp;gt; 백업 DB 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;카프카 커넥트 사용 : 회원 가입 요청 -&amp;gt; 회원 테이블에 저장, 카프카에 데이터 push(produce) : 카프카에서 백업 DB에 데이터 바로 저장 (싱크 커넥트)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- kafka streams&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;프로듀서 서버에서 받은 메시지를 카프카 streams를 이용해 재가공 (통계 데이터 집계) 후 카프카의 또 다른 토픽에 메시지를 새로 발행 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;프도듀서 서버에서 메시지 수신, 재가공, 재발행에서 발생되는 트랜잭션 보장에 대한 편의를 제공 (exactly once)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&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;&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;</description>
      <category>Infra/Kafka</category>
      <author>nippycloud</author>
      <guid isPermaLink="true">https://nippyclouding.tistory.com/91</guid>
      <comments>https://nippyclouding.tistory.com/91#entry91comment</comments>
      <pubDate>Thu, 30 Apr 2026 18:31:31 +0900</pubDate>
    </item>
    <item>
      <title>QueryDSL</title>
      <link>https://nippyclouding.tistory.com/90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;환경 : Spring 4.x, Java 21, H2 DB&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;QueryDSL : JPA 환경에서 동적 쿼리 작성에 강한 라이브러리 이름이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Spring Boot가 아닌 일반 Spring 환경(MVC, Legacy..) 에서는 MyBatis가 동적 쿼리에 강한 장점을 가지고 있어 별도 라이브러리 추가가 필요하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Spring Boot 환경에서는 JPA Criteria 라는 것을 기반으로 동적 쿼리를 작성할 수 있지만 Criteria 자체가 가독성이 떨어지기 때문에 별도 외부 라이브러리로 QueryDSL을 사용하는 추세이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QueryDSL 의존성 (build.gradle)&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;dependencies {
    ...
    
    // QueryDSL dependency
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor &quot;com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta&quot;
    annotationProcessor &quot;jakarta.annotation:jakarta.annotation-api&quot;
    annotationProcessor &quot;jakarta.persistence:jakarta.persistence-api&quot;
}

clean {
	delete file('src/main/generated')
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;QueryDSL은 JPA 엔티티에 대해 먼저 Q 타입을 추출하여 Q 타입 엔티티로 쿼리를 작성하고 동작한다.&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Q 타입 엔티티 : QueryDSL에서 사용하는 메타 데이터 클래스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;실제 JPA 엔티티를 기반으로 자동 생성되며, 편리한 동적 쿼리 작성과 타입 안전성이 높다는 장점을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clean&amp;nbsp;{&lt;br /&gt;&amp;nbsp; &amp;nbsp; delete file('src/main/generated')&lt;br /&gt;}&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 스프링 구 버전에서는 빌드 도구가 gradle이 아닌 intelliJ로 설정되어있을 경우 Q 타입 엔티티가 src/main/generated 하위에 생성될 때도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 일반적으로 최신 스프링 부트는 Q 타입 엔티티를 build - generated - sources - annotationProcessor - java - main 하위에서 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- ./gradlew clean 명령어 입력 시 기본적으로 build와 그 하위의 파일들을 모두 삭제하는데, clean { delete file ('src/main/generated')} 추가 시 build 뿐만이 아니라 src - main - generated 하위의 Q 타입 엔티티 파일까지 모두 삭제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- JPA 엔티티 구조가 변경되거나 Spring Boot 실행 시 원인을 알 수 없는 오류들이 발생할 경우 ./gradlew clean을 입력하면 빌드 결과가 모두 삭제된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 스프링 부트를 재실행하거나 &lt;span data-path-to-node=&quot;4,3,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;4,3,1,0&quot;&gt;./gradlew compileJava 실행 시&lt;/span&gt; build 결과가 현재 프로그램에 맞게 정확한 build 결과물을 산출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;gradlew : gradle 빌드 도구를 wrapping한 실행 파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Q 타입 엔티티는 컴파일 시점에 자동으로 생성되기 때문에 Git에 포함하지 않도록 .gitignore에서 관리하는 것이 일반적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;.gitignore에는 기본적으로 build/가 작성되어 있어서 build 디렉토리와 그 하위의 모든 것들을 git의 관리 대상으로 두지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;만약 구 버전 스프링 사용 시 Q 타입 엔티티가 src 하위에 들어가고 있다면 별도로 .gitignore에 등록이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootTest
@Transactional
class QueryDslApplicationTests {

    @Autowired
    EntityManager em;

    // contextLoads : 해당 스프링 부트 프로젝트가 오류 없이 제대로 실행(로딩)되는지를 확인하는 가장 기초적인 테스트
    @Test
    void contextLoads() {

       TestEntity t = new TestEntity();
       em.persist(t); // 쓰기 지연 저장소에 insert 쿼리 저장, 영속화 (1차 캐시에 저장)

       JPAQueryFactory queryFactory = new JPAQueryFactory(em);

       // Q 타입 엔티티는 기존 변수 이름과 동일한 Q 타입 엔티티를 가진다.
       // public static final QTestEntity testEntity = new QTestEntity(&quot;testEntity&quot;);
       QTestEntity qTestEntity = QTestEntity.testEntity;


       // qTestEntity 를 이용하여 TestEntity 테이블에 있는 데이터를 select (모든 데이터 select : fetch())
       // select 쿼리가 동작하기 전 먼저 쓰기 지연 저장소의 insert 쿼리가 flush 된 후 select 동작
       TestEntity result = queryFactory
             .selectFrom(qTestEntity)
             .fetchOne();
       // fetch() : 리스트 조회, fetchOne() : 결과가 반드시 하나 (2개 이상이라면 NonUniqueResultException)

       // 하나의 트랜잭션 안에서는 JPA 영속성 컨텍스트에서 동일성(== 비교)을 보장한다. (1차 캐시의 주솟값을 같이 공유) 
       Assertions.assertThat(result).isEqualTo(t);
       Assertions.assertThat(result.getId()).isEqualTo(t.getId());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TestEntity result = queryFactory&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;.selectFrom(qTestEntity)&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;.fetchOne();&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Q 타입 엔티티 (qTestEntity) 를 이용하여 TestEntity 테이블의 데이터를 조회한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;fetch()&lt;/b&gt; 로 조회 시 List&amp;lt;TestEntity&amp;gt; 로 TestEntity 테이블의 모든 데이터를 조회하고, &lt;b&gt;fetchOne()&lt;/b&gt; 으로 조회 시 반드시 결과값이 하나라는 것을 가정하고 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777527956090&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// fetch() : 리스트 조회, 데이터가 없을 경우 빈 리스트 반환
List&amp;lt;Member&amp;gt; members = queryFactory
    .selectFrom(qMember)
    .fetch();

// fetchOne() : 단 건 조회
Member member = queryFactory
    .selectFrom(qMember)
    .fetch();

/* 
하나도 없을 경우 : null
1개일 경우 : 정상 조회 
2개 이상일 경우 : NonUniqueResultException 예외 발생
*/

// fetchFirst() : 단 건 조회, limit 조건 추가, limit(1).fetchOne()
Member memberFirst = queryFactory
    .selectFrom(qMember)
    .fetchFirst();

// fetchResults() : 페이징 정보 + 리스트 조회 (total count 쿼리를 추가적으로 실행 =&amp;gt; 2번의 쿼리가 실행)
QueryResults&amp;lt;Member&amp;gt; results = queryFactory
    .selectFrom(qMember)
    .fetchResults();

// fetchCount() : count 쿼리
long count = queryFactory
    .selectFrom(qMember)
    .fetchCount();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&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; &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; &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; &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; &amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;QueryDSL은 &lt;u&gt;JPQL의 빌더 역할&lt;/u&gt;을 수행하며 실행 시 JPQL로 변환되어 동작한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;JPQL을 예시로 @Query(&quot;select m from member m where m.name = :name&quot;) 에서 m.name은 varchar 타입이지만 파라미터 name이 int 타입일 경우 타입 불일치 문제가 생기는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;JPQL은 String 문자열로 작성되기 때문에 컴파일 시점에서 타입 불일치 문제를 잡지 않고, 런타임 환경에서 실제 JPQL 쿼리를 실행할 때 런타임 오류가 발생&lt;/u&gt;한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;QueryDSL은 JPQL에 비해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;Q 타입 엔티티가 컴파일 시점에 타입 오류를 검증&lt;/u&gt;하기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;타입 안정성이 높다는 장점&lt;/b&gt;을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;@BeforeEach&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @Autowired
    EntityManager em;
    JPAQueryFactory queryFactory;

    @BeforeEach
    void before() {
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team(&quot;teamA&quot;);
        Team teamB = new Team(&quot;teamB&quot;);

        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member(10, &quot;member1&quot;, teamA);
        Member member2 = new Member(20, &quot;member2&quot;, teamA);
        Member member3 = new Member(30, &quot;member3&quot;, teamB);
        Member member4 = new Member(40, &quot;member4&quot;, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        // 쓰기 지연 저장소에 insert 쿼리 저장 (team A, B, member 1, 2, 3, 4) 이후 flush
        }
        
        @Test
        void JPQL() { ... }
        
        @Test
        void queryDsl() { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL 동작 방식 : &lt;b&gt;문자열로 직접 작성, 오타 또는 타입 검증에 취약하다.&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void JPQL() {
    // given
    String qlString = &quot;select m from Member m where m.username = :username&quot;;

    // when
    Member findMember = em.createQuery(qlString, Member.class)
            .setParameter(&quot;username&quot;, &quot;member1&quot;)
            .getSingleResult();

    // then
    Assertions.assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QueryDSL 동작 방식 : 빌더 방식, 런타임에 JPQL로 변경되어 실행된다, &lt;b&gt;타입 안정성, 오타 대비 가능&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void queryDsl() {
    // given
    QMember qMember = QMember.member; 
    // 기본 인스턴스 사용 방식
    // 셀프 조인이 필요하지 않다면 기본 인스턴스 방식을 사용하는 것을 권장
    // QMember qMember = new QMember(&quot;m&quot;) 처럼 alias를 직접 지정해서 사용할 수도 있다.

    // when
    Member findMember = queryFactory
            .select(qMember)
            .from(qMember) // selectFrom(qMember) 로 합칠 수도 있다.
            .where(qMember.username.eq(&quot;member1&quot;)
                    .and(qMember.age.eq(10))) // .and(), .or() 로 메서드 체이닝 가능
            .fetchOne(); // 결과값이 반드시 하나일 때 fetchOne() 사용

    // then
    Assertions.assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;.where(...) , ... : and 조건과 동일 (,)&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void searchByQueryDsl() {
    QMember q = QMember.member;
    List&amp;lt;Member&amp;gt; members = queryFactory
            .selectFrom(q)
            .where(q.username.eq(&quot;member1&quot;), q.age.eq(10))
            // .where(조건1, 조건 2); 로 and 생략 가능
            .fetch();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;QueryDSL에서의&amp;nbsp;where&amp;nbsp;조건&amp;nbsp;처리&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777527234290&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.where(qMember.username.eq(&quot;member1&quot;) : where m.username = 'member1', eq : SQL의 = 로 치환
.where(qMember.username.ne(&quot;member1&quot;)
.where(qMember.username.eq(&quot;member1&quot;).not
 =&amp;gt; where m.username != 'member1', ne &amp;amp; eq().not : SQL의 != 로 치환

.where(qMember.username.isNotNull()) : IS NOT NULL 조건

.where (qMember.age.in(10, 20))
.where (qMember.notIn(10, 20))
.where (age.between(10, 30))

.where (qMember.age.goe(30)) : age &amp;gt;= 30, greater or equal
.where (qMember.age.gt(30)) : age &amp;gt; 30, greater then
.where (qMember.age.loe(30)) : age &amp;lt;= 30, less or equal
.where (qMember.age.lt(30)) : age &amp;lt; 30, less then

.where (qMember.username.like(&quot;member%&quot;)
.where (qMember.username.contains(&quot;member&quot;) : LIKE '%member%'
.where (qMember.username.startWith(&quot;member&quot;) : LIKE 'member%'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;QueryDSL에서의&amp;nbsp; 조건절 - NULL일 경우 무시 (pass) : 동적 쿼리 작성에서 큰 장점&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777527679849&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.where(조건 1), (조건2), (조건3) ...

// 조건 1이 true라면 where절에 추가
// 조건 2가 true라면 where절에 and로 추가
// 조건 3이 true라면 where절에 and로 추가

조건 1, 2, 3 중 false인 조건절이 있다면 where문에 false인 조건절은 추가하지 않는다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;QueryDSL에서의&amp;nbsp; 정렬, 페이징&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void sortAndPaging() {
    em.persist(new Member(100, null)); // username = null
    em.persist(new Member(100, &quot;memberA&quot;)); // username = &quot;memberA&quot;
    em.persist(new Member(100, &quot;memberB&quot;)); // username = &quot;memberB&quot;

    QMember qMember = QMember.member;

    // select 쿼리 전달 =&amp;gt; 쓰기 지연 저장소의 insert가 먼저 flush된 뒤 select 쿼리 동작
    // 정렬
    List&amp;lt;Member&amp;gt; members = queryFactory
            .selectFrom(qMember)
            .where(qMember.age.eq(100))
            .orderBy(qMember.age.desc(), qMember.username.asc().nullsFirst())
            // ORDER BY m.age DESC, m.username ASC
            // .nullsLast() : 회원 이름이 null 이라면 마지막에 출력
            // .nullsFirst() : 회원 이름이 null 이라면 처음에 먼저 출력
            .fetch();
    Assertions.assertThat(members.get(2).getUsername()).isEqualTo(&quot;memberB&quot;);

    // 페이징
    // 데이터 자체만 조회하는 방식, 실무 권장
    List&amp;lt;Member&amp;gt; memberList = queryFactory
            .selectFrom(qMember)
            .orderBy(qMember.username.desc())
            .offset(1)  // 두 번째 데이터부터 시작 (인덱스 기준 1 = 실제 두 번째 데이터)
            .limit(2)   // 데이터 2개 조회
            .fetch();
    Assertions.assertThat(memberList.size()).isEqualTo(2);

    // 데이터 전체 개수 조회 쿼리
    long totalCount = queryFactory
            .select(qMember.count())
            .from(qMember)
            .where(qMember.age.eq(100))
            .fetchOne();
    Assertions.assertThat(totalCount).isEqualTo(3);

    // 데이터 &amp;amp; 데이터 전체 개수 함께 조회, 매 번 count 쿼리 비효율적으로 동작 =&amp;gt; 권장하지 X
    QueryResults&amp;lt;Member&amp;gt; queryResults = queryFactory
            .selectFrom(qMember)
            .orderBy(qMember.username.desc())
            .offset(1)
            .limit(2)
            .fetchResults();
    Assertions.assertThat(queryResults.getResults().size()).isEqualTo(2);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;QueryDSL에서의&amp;nbsp; 집계 함수, GroupBy와 간단한 inner join&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void aggregationAndGroupBy() throws Exception {
    QMember qMember = QMember.member;
    QTeam qTeam = QTeam.team;

    List&amp;lt;Tuple&amp;gt; aggregationResult = queryFactory
            .select(qMember.count(), // count 쿼리에서도 사용
                    qMember.age.sum(),
                    qMember.age.avg(),
                    qMember.age.max(),
                    qMember.age.min())
            .from(qMember)
            .fetch();

    // Tuple : 여러 다른 타입의 조회 결과를 하나로 담아내기 위한 QueryDSL 전용 객체, Long, Integer, Double ..
    Tuple tuple = aggregationResult.get(0);
    Assertions.assertThat(tuple.get(qMember.count())).isEqualTo(4);

    List&amp;lt;Tuple&amp;gt; groupByResult = queryFactory
            .select(qTeam.name, qMember.age.avg())
            .from(qMember)
            .join(qMember.team, qTeam) // inner join, qMember의 team 필드와 qTeam
            .groupBy(qTeam.name)
            .fetch();

    Tuple teamA = groupByResult.get(0);
    Tuple teamB = groupByResult.get(1);

    Assertions.assertThat(teamA.get(qTeam.name)).isEqualTo(&quot;teamA&quot;);
    Assertions.assertThat(teamB.get(qTeam.name)).isEqualTo(&quot;teamB&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;QueryDSL에서의 Join (내부 조인, 외부 조인, 세타 조인, On 절, fetch Join)&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootTest
@Transactional
public class QuerydslJoinTest {
    @Autowired
    EntityManager em;
    JPAQueryFactory queryFactory;

    @BeforeEach
    void before() {
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team(&quot;teamA&quot;);
        Team teamB = new Team(&quot;teamB&quot;);

        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member(10, &quot;member1&quot;, teamA);
        Member member2 = new Member(20, &quot;member2&quot;, teamA);
        Member member3 = new Member(30, &quot;member3&quot;, teamB);
        Member member4 = new Member(40, &quot;member4&quot;, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        // 쓰기 지연 저장소에 insert 쿼리 저장 (team A, B, member 1, 2, 3, 4), flush
    }

    // inner join
    // select m from member m join team t on m.team_id = t.id where t.name = 'teamA';
    @Test
    void innerJoin() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        List&amp;lt;Member&amp;gt; result = queryFactory
                .selectFrom(qMember)
                .join(qMember.team, qTeam) // join (Q 타입 엔티티의 조인 대상 필드, join할 Q 타입 엔티티), 기본적으로 Team 필드는 가지고 오지 않는다 (n + 1 발생 가능)
                // .leftjoin, .rightjoin 사용 시 left, right join 동작
                .where(qTeam.name.eq(&quot;teamA&quot;))
                .fetch();

        Assertions.assertThat(result).extracting(&quot;username&quot;).containsExactly(&quot;member1&quot;, &quot;member2&quot;);
    }

    // 세타 조인 - SELECT m.* FROM member m, team t WHERE m.username = t.name;
    // 세타 조인 : 연관관계가 없는 데이터끼리 특정 조건으로 연관지어 join
    @Test
    void thetaJoin() throws Exception {
        em.persist(new Member(&quot;teamA&quot;));
        em.persist(new Member(&quot;teamB&quot;));

        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        // on 절을 사용하지 않으면 세타 조인 시 외부 조인이 불가능하다.
        // join 조건 : 회원 이름이 팀 이름과 같은 회원 조회
        List&amp;lt;Member&amp;gt; result = queryFactory
                .select(qMember)
                .from(qMember, qTeam) // from 절에 Q 타입 엔티티를 2개 둔다.
                .where(qMember.username.eq(qTeam.name))
                .fetch();

        Assertions.assertThat(result)
                .extracting(&quot;username&quot;)
                .containsExactly(&quot;teamA&quot;, &quot;teamB&quot;);
    }

    // SELECT m.*, t.* FROM member m LEFT OUTER JOIN team t
    // ON m.team_id = t.id AND t.name = 'teamA';
    // left outer join - member은 모두 조회, team 에만 조건
    @Test
    void on_filtering() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        // join 조건 : 회원, 팀을 join 하며 team 이름이 teamA인 팀만 필터링하여 조회, 회원은 전부 조회
        List&amp;lt;Tuple&amp;gt; result = queryFactory
                .select(qMember, qTeam)
                .from(qMember)
                .leftJoin(qMember.team, qTeam)
                .on(qTeam.name.eq(&quot;teamA&quot;)) // on 절 : join 대상 필터링 가능, 연관 관계가 없는 엔티티에 대해 외부 조인 가능
                .fetch();

        for (Tuple t : result)
            System.out.println(&quot;tuple = &quot; + t);
    }

    // SELECT m.*, t.* FROM member m LEFT OUTER JOIN team t
    // ON m.username = t.name
    // left outer join - member은 모두 조회, team 에만 조건
    @Test
    void on_no_relation() throws Exception {
        em.persist(new Member(&quot;teamA&quot;));
        em.persist(new Member(&quot;teamB&quot;));

        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        // join 조건 : 연관관계가 없는 엔티티 외부 조인 (회원의 이름과 팀 이름이 같은 대상 외부 조인)
        // 세타 조인 - on 절을 쓰지 않고 where 절을 쓸 경우 내부 조인만 가능
        // 세타 조인 - 외부 조인이 필요할 경우 on 절 사용
        List&amp;lt;Tuple&amp;gt; result = queryFactory
                .select(qMember, qTeam)
                .from(qMember)
                .leftJoin(qTeam)
                .on(qMember.username.eq(qTeam.name)) // on 절 : 연관 관계가 없는 엔티티에 대해 외부 조인 가능
                .fetch();

        for (Tuple t : result)
            System.out.println(&quot;tuple = &quot; + t);
    }

    @Autowired
    EntityManagerFactory emf;

    // Join 시 fetch join 으로 연관관계 필드도 즉시로딩
    @Test
    void fetchJoin() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        em.flush();
        em.clear();
        // 영속성 컨텍스트 먼저 초기화

        Member findMember = queryFactory
                .selectFrom(qMember)
                // .join(qMember.team, qTeam).fetchJoin() : 생략 시 내부 조인, n + 1 지연 로딩
                .join(qMember.team, qTeam).fetchJoin() // join 절 추가 시 fetch join, 즉시 로딩
                .where(qMember.username.eq(&quot;member1&quot;))
                .fetchOne();

        boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
        Assertions.assertThat(isLoaded).as(&quot;fetch join success&quot;).isTrue();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;QueryDSL에서의 서브쿼리 (where절, select절) // from절 서브쿼리(인라인뷰)는 지원하지 않는다. (JPQL 자체가 지원 x)&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootTest
@Transactional
public class QuerydslSubQueryTest {
    @Autowired
    EntityManager em;
    JPAQueryFactory queryFactory;

    @BeforeEach
    void before() {
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team(&quot;teamA&quot;);
        Team teamB = new Team(&quot;teamB&quot;);

        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member(10, &quot;member1&quot;, teamA);
        Member member2 = new Member(20, &quot;member2&quot;, teamA);
        Member member3 = new Member(30, &quot;member3&quot;, teamB);
        Member member4 = new Member(40, &quot;member4&quot;, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        // 쓰기 지연 저장소에 insert 쿼리 저장 (team A, B, member 1, 2, 3, 4), flush
    }

    // 서브 쿼리 : JPAExpressions 사용
    @Test
    void subQuery() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        QMember qMemberSub = new QMember(&quot;memberSub&quot;);

        // age 가 가장 많은 회원 조회
        // select m.* from Member m where m.age = (select Max(age) from Member)
        List&amp;lt;Member&amp;gt; result = queryFactory
                .selectFrom(qMember)
                .where(qMember.age.eq(  // qMember.age.goe( ... ) 도 가능,  goe : great or equal &amp;gt;=
                        JPAExpressions
                        .select(qMemberSub.age.max())
                        .from(qMemberSub))
                        // 서브 쿼리로 나이가 가장 많은 회원의 나이 조회
                )
                .fetch();

        Assertions.assertThat(result).extracting(&quot;age&quot;).containsExactly(40);
    }

    // 서브 쿼리 : IN 절 사용
    @Test
    void subQuery_IN() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        QMember qMemberSub = new QMember(&quot;memberSub&quot;);

        // age 가 가장 많은 회원 조회
        // select m.* from Member m where m.age in (select age from Member where age &amp;gt; 10)
        List&amp;lt;Member&amp;gt; result = queryFactory
                .selectFrom(qMember)
                .where(qMember.age.in(  // in 조건절 - 서브쿼리에서 10보다 큰 나이를 모두 조회
                                JPAExpressions
                                    .select(qMemberSub.age)
                                    .from(qMemberSub)
                                    .where(qMemberSub.age.gt(10))
                ))
                .fetch();

        Assertions.assertThat(result).extracting(&quot;age&quot;).containsExactly(20, 30, 40);
    }

    // select 절에 사용하는 서브쿼리
    @Test
    void subQuery_Select() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        QMember qMemberSub = new QMember(&quot;memberSub&quot;);

       // select m.username, (select avg(age) from Member) from Member m
        List&amp;lt;Tuple&amp;gt; result = queryFactory
                .select(qMember.username, JPAExpressions.select(qMemberSub.age.avg()).from(qMemberSub))
                .from(qMember)
                .fetch();

        for (Tuple t : result) {
            System.out.println(&quot;username = &quot; + t.get(qMember.username));
            System.out.println(&quot;age = &quot; + t.get(JPAExpressions.select(qMemberSub.age.avg()).from(qMemberSub)));
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;QueryDSL - Repository에서 DTO 바로 조회 : Projections.constructor&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777873031567&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;MemberDto&amp;gt; result = queryFactory
    .select(Projections.constructor(MemberDto.class, qMember.username, qMember.age))
    .from(qMember)
    .fetch();
    
@Data
@AllArgsConstructor
@NoArgsConstructor
class MemberDto {
    private String username;
    private int age;
}&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;size14&quot;&gt;&lt;b&gt;DB에서 데이터 조회 후 DTO로 리턴하는 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 일반 데이터 조회 후 DTO 객체로 조립 (단일 타입) : List&amp;lt;String&amp;gt; result = queryFactory.select(qMember.username) ..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 일반 데이터 조회 후 DTO 객체로 조립(복수 타입) : List&amp;lt;Tuple&amp;gt; result = ueryFactory.select(qMember.username, qMember.age) ..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- DTO 객체로 바로 조회 : Projections.constructor, Projections.bean, Projections.field 3가지 방식이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그러나 Projections.constructor 같은 방식은 실제 코드가 동작하는 시점에서 런타임 오류가 발생할 수 있기에 좋은 코드는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;실무에서는 DTO를 바로 조회 시 @QueryProjection 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777873879058&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Data
public class MemberDto {
    private String username;
    private int age;

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}


// 사용
List&amp;lt;MemberDto&amp;gt; result = queryFactory
    .select(new QMemberDto(qMemberDto.username, qMemberDto.age))
    .from(qMember)
    .fetch();&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;size14&quot;&gt;./gradlew clean compileJava 명령어 터미널 입력 : QMemberDto 가 빌드 결과로 생성 (Dto에 대한 Q 타입 엔티티가 생성된다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Projections.constructor 방식들처럼 런타임에 오류가 발생하지 않고 컴파일 시점에 오류 발생 - 안정적&amp;nbsp;&lt;br /&gt;DTO가 QueryDSL annotation 의존, DTO Q 타입 엔티티까지 Q 파일을 생성한다는 단점이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjeiAO/dJMcagSVHMu/AdPujukmyQCkSKmHri5BnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjeiAO/dJMcagSVHMu/AdPujukmyQCkSKmHri5BnK/img.png&quot; data-alt=&quot;DTO Q 타입 엔티티&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjeiAO/dJMcagSVHMu/AdPujukmyQCkSKmHri5BnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjeiAO%2FdJMcagSVHMu%2FAdPujukmyQCkSKmHri5BnK%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;1656&quot; height=&quot;547&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DTO Q 타입 엔티티&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;b&gt;QueryDSL - 동적 쿼리 (Boolean Builder, Where 다중 파라미터)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;1. Boolean Builder : where 절에 and, or 조건을 동적으로 추가 가능&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void 동적쿼리_BooleanBuilder() throws Exception {
    QMember qMember = QMember.member;

    // select 조건 (condition)
    String usernameCond = &quot;member1&quot;;
    int ageCond = 10;

    List&amp;lt;Member&amp;gt; result = searchMemberByBooleanBuilder(usernameCond, ageCond, qMember); // member1, 10인 회원 조회
    Assertions.assertThat(result.size()).isEqualTo(1);
}

private List&amp;lt;Member&amp;gt; searchMemberByBooleanBuilder(String usernameCond, Integer ageCond, QMember qMember) {
    BooleanBuilder booleanBuilder = new BooleanBuilder();
    if (usernameCond != null) booleanBuilder.and(qMember.username.eq(usernameCond)); // and qMember.username = usernameCond 조건 추가
    if (ageCond != null) booleanBuilder.and(qMember.age.eq(ageCond)); // and qMember.age = ageCond 조건 추가

    return queryFactory.selectFrom(qMember)
            .where(booleanBuilder)
            .fetch();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;2. Where 다중 파라미터 (실무 권장)&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private List&amp;lt;Member&amp;gt; searchMemberByWhere(String usernameCond, int ageCond, QMember qMember) {

    // BooleanExpression : Java 코드로 쓴 SQL의 WHERE 절 조건 
    BooleanExpression userNameEq = usernameCond != null ? qMember.username.eq(usernameCond) : null;
    BooleanExpression ageEq = usernameCond != null ? qMember.age.eq(ageCond) : null;

    return queryFactory.selectFrom(qMember)
            .where(userNameEq, ageEq) // where 조건에 null이 있다면 무시
            .fetch();

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;QueryDSL - 수정, 삭제 벌크 연산&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 영속성 컨텍스트 (1차 캐시)를 거치지 않고 DB에 직접 쿼리를 전달하는 방식 (where id In (1, 2, 3 ... ))&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 단 하나의 쿼리로 수 만 건의 데이터를 동시에 수정 =&amp;gt; 변경 감지로 여러 번 update 쿼리를 DB에 전달하는 것보다 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 벌크 연산은 영속성 컨텍스트를 거치지 않기 때문에 벌크 연산 수행 후 바로 em.flush(), em.clear()를 호출해서 영속성 컨텍스트를 비워야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777875817386&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;long count = queryFactory
    .update(qMember)
    .set(qMember.username, &quot;비회원&quot;)
    .where(qMember.age.lt(28)) // lt(28) : less then, 28살 미만일 경우 비회원으로 변경
    .execute(); // fetch가 아니라 벌크 연산 시에는 execute
    
long count = queryFactory
    .delete(qMember)
    .where(qMember.age.gt(18))
    .execute();&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;size14&quot;&gt;.set(qMember.username, &quot;비회원&quot;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;.set(qMember.age, qMember.age.add(1)) : age에 1을 더한 값으로 update&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;.set(qMember.age, qMember.age.multiply(2)) : age에 2를 곱한 값으로 update&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Spring</category>
      <author>nippycloud</author>
      <guid isPermaLink="true">https://nippyclouding.tistory.com/90</guid>
      <comments>https://nippyclouding.tistory.com/90#entry90comment</comments>
      <pubDate>Thu, 30 Apr 2026 13:09:08 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Data Jpa] 스프링 부트 페이징 처리</title>
      <link>https://nippyclouding.tistory.com/89</link>
      <description>&lt;p data-ke-size=&quot;size18&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;size14&quot;&gt;Pageable : 사용자의 Http 페이징 요청 정보를 담는 인터페이스 (페이징 파라미터 정보)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;PageRequest : Pageable 구현체, 스프링 부트는 Http 요청에서 페이징 처리 관련 정보가 있을 때 PageRequest 객체를 생성하여 Pageable 타입으로 메서드에 바인딩한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Page : Pageable 페이징 요청 정보를 통해 얻은 페이징 처리 결과물&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777121295301&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. Pageable 바인딩, Entity 반환
@GetMapping(&quot;/members&quot;)
public Page&amp;lt;Member&amp;gt; list(Pageable pageable) {
    Page&amp;lt;Member&amp;gt; page = memberRepository.findAll(pageable);
    return page;
}&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;size14&quot;&gt;/members?page=1 로 서버에 요청 시 page = 1 정보가 Pageable 타입으로 바인딩되어 데이터베이스에서 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;findAll(pageable)의 결과 타입은 Page&amp;lt;?&amp;gt; 이고, 페이징 처리 조건으로 조회한 결과를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;페이징 처리 요청 파라미터&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- page : 현재 페이지, 0부터 시작 (0페이지 : 1번 데이터부터 20번 데이터까지)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- size : 페이징 처리 한 번에 전달할 데이터 개수 (별도 설정이 없을 경우 기본 20개 조회)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- sort : 페이징 처리 시 정렬할 조건, 기본값 ASC&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;ex : localhost:8080/members?page=0&amp;amp;size=3&amp;amp;sort=id,desc&amp;amp;sort=username&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;=&amp;gt; 0페이지, 데이터 3개 조회, id 필드로 DESC 정렬하되 id가 같다면 username 으로 ASC 정렬&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;2243&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/43QF9/dJMcaarymGB/bX5EjHmwCMXaaHSn9fBrE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/43QF9/dJMcaarymGB/bX5EjHmwCMXaaHSn9fBrE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/43QF9/dJMcaarymGB/bX5EjHmwCMXaaHSn9fBrE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F43QF9%2FdJMcaarymGB%2FbX5EjHmwCMXaaHSn9fBrE0%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;2243&quot; height=&quot;659&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;2243&quot; data-origin-height=&quot;659&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;pre id=&quot;code_1777121507096&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/members/dto&quot;)
public Page&amp;lt;Member.MemberDto&amp;gt; dtoList(Pageable pageable) {
    Page&amp;lt;Member&amp;gt; page = memberRepository.findAll(pageable);
    Page&amp;lt;Member.MemberDto&amp;gt; dto = page.map(Member.MemberDto::new);
    return dto;

    // return memberRepository.findAll(pageable).map(MemberDto::new);
}&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;size14&quot;&gt;Entity를 화면에 전달하는 것은 좋은 구조가 아니기에 Dto로 변환하여 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Page&amp;lt;?&amp;gt; 객체는 .map 메서드를 지원하여서 dto로 쉽게 변경이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;페이징 처리 글로벌 설정 : application.yaml 에서 글로벌 설정이 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1777121993537&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  data:
    web:
      pageable:
        default-page-size:20 // 한 번의 페이징 요청 시 가져오는 데이터
        max-page-size:2000 // 한 번의 페이징 요청 시 제한하는 최대 데이터 개수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;페이징 처리 개별 설정 : @PageableDefault(size = 12, sort = &quot;username&quot;, direction = Sort.Direction.DESC) Pageable pageable&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;@PageableDefault로 컨트롤러의 메서드에 페이징 처리 설정이 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1777121588650&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/members/pageableDefault&quot;)
public Page&amp;lt;Member.MemberDto&amp;gt; listPageableDefault(
        @PageableDefault(size = 12, sort = &quot;username&quot;, 
        direction = Sort.Direction.DESC) Pageable pageable
        ) {
    Page&amp;lt;Member&amp;gt; page = memberRepository.findAll(pageable);
    Page&amp;lt;Member.MemberDto&amp;gt; dto = page.map(Member.MemberDto::new);
    return dto;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 전체 코드

@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;

    /*
    pageable : 사용자의 http 요청 속 페이징 처리와 관련된 데이터를 담아 만든다, 인터페이스
    pageRequest : pageable 인터페이스의 구현체, http 요청을 토대로 스프링 부트가 생성하는 페이징 요청 정보 객체
    page : pageable 페이징 처리 요청 정보를 토대로 만들어내는 페이징 처리 결과물
     */

    @GetMapping(&quot;/members&quot;)
    public Page&amp;lt;Member&amp;gt; list(Pageable pageable) {
        Page&amp;lt;Member&amp;gt; page = memberRepository.findAll(pageable);
        return page;
    }

    @GetMapping(&quot;/members/dto&quot;)
    public Page&amp;lt;Member.MemberDto&amp;gt; dtoList(Pageable pageable) {
        Page&amp;lt;Member&amp;gt; page = memberRepository.findAll(pageable);
        Page&amp;lt;Member.MemberDto&amp;gt; dto = page.map(Member.MemberDto::new);
        return dto;

        // return memberRepository.findAll(pageable).map(MemberDto::new);
    }
    
    @GetMapping(&quot;/members/pageableDefault&quot;)
    public Page&amp;lt;Member.MemberDto&amp;gt; listPageableDefault(
            @PageableDefault(size = 12, sort = &quot;username&quot;, direction = Sort.Direction.DESC) Pageable pageable) {
        Page&amp;lt;Member&amp;gt; page = memberRepository.findAll(pageable);
        Page&amp;lt;Member.MemberDto&amp;gt; dto = page.map(Member.MemberDto::new);
        return dto;
    }

    @PostConstruct
    public void setData(){
        for (int i = 0; i &amp;lt; 100; i++) {
            memberRepository.save(new Member(i, &quot;member&quot; + i));
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>Java &amp;amp; Spring/JPA</category>
      <author>nippycloud</author>
      <guid isPermaLink="true">https://nippyclouding.tistory.com/89</guid>
      <comments>https://nippyclouding.tistory.com/89#entry89comment</comments>
      <pubDate>Sat, 25 Apr 2026 22:02:52 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Batch 입문 : 3시간 만에 끝내는 대용량 처리의 기초</title>
      <link>https://nippyclouding.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/spring-batch-%EC%9E%85%EB%AC%B8-3%EC%8B%9C%EA%B0%84/dashboard?cid=340815&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/course/spring-batch-%EC%9E%85%EB%AC%B8-3%EC%8B%9C%EA%B0%84/dashboard?cid=340815&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776572514419&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring Batch 입문: 3시간 만에 끝내는 대용량 처리의 기초| JSCODE 시니 - 인프런 강의&quot; data-og-description=&quot;현재 평점 5.0점 수강생 380명인 강의를 만나보세요. 스프링 배치를 처음 접하는 취준생과 현업 개발자를 위한, 실전에서 바로 써먹을 수 있는 '스프링 배치' 강의 입니다! '단순 삭제'부터 '대량 &quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/spring-batch-%EC%9E%85%EB%AC%B8-3%EC%8B%9C%EA%B0%84/dashboard?cid=340815&quot; data-og-url=&quot;https://www.inflearn.com/course/spring-batch-%EC%9E%85%EB%AC%B8-3%EC%8B%9C%EA%B0%84?cid=340815&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/6klK0/dJMb8XR7Aen/L6pgcGkkNwbz0nHkvUP00k/img.png?width=3989&amp;amp;height=2598&amp;amp;face=0_0_3989_2598,https://scrap.kakaocdn.net/dn/h0XKL/dJMb83SkK48/XkkRG5IsnLqnIbNshExdMk/img.png?width=3989&amp;amp;height=2598&amp;amp;face=0_0_3989_2598,https://scrap.kakaocdn.net/dn/b3tBWF/dJMb82eOT5n/41RzBsZb0nmJLvfAKhj7p0/img.png?width=736&amp;amp;height=479&amp;amp;face=0_0_736_479&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/spring-batch-%EC%9E%85%EB%AC%B8-3%EC%8B%9C%EA%B0%84/dashboard?cid=340815&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/spring-batch-%EC%9E%85%EB%AC%B8-3%EC%8B%9C%EA%B0%84/dashboard?cid=340815&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/6klK0/dJMb8XR7Aen/L6pgcGkkNwbz0nHkvUP00k/img.png?width=3989&amp;amp;height=2598&amp;amp;face=0_0_3989_2598,https://scrap.kakaocdn.net/dn/h0XKL/dJMb83SkK48/XkkRG5IsnLqnIbNshExdMk/img.png?width=3989&amp;amp;height=2598&amp;amp;face=0_0_3989_2598,https://scrap.kakaocdn.net/dn/b3tBWF/dJMb82eOT5n/41RzBsZb0nmJLvfAKhj7p0/img.png?width=736&amp;amp;height=479&amp;amp;face=0_0_736_479');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Batch 입문: 3시간 만에 끝내는 대용량 처리의 기초| JSCODE 시니 - 인프런 강의&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;현재 평점 5.0점 수강생 380명인 강의를 만나보세요. 스프링 배치를 처음 접하는 취준생과 현업 개발자를 위한, 실전에서 바로 써먹을 수 있는 '스프링 배치' 강의 입니다! '단순 삭제'부터 '대량&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;해당 게시글은 위 강의를 수강한 뒤 작성하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;STO 토큰 증권 프로젝트를 진행하며 배치 개념이 필요할 것 같아 위 강의를 수강했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 시간이 긴 편이 아니라서 하루 ~ 이틀만에 배치에 대한 기본적인 개념을 익힐 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 해당 강의를 수강하며 공부한 내용을 복습, 응용한 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nippyclouding.tistory.com/category/Java%20%26%20Spring/Batch&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nippyclouding.tistory.com/category/Java%20%26%20Spring/Batch&lt;/a&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;스프링에서 동작하는 기술이기 때문에 @Configuration, @Bean, @Transactional 등 기본적인 스프링 빈 동작은 이해하고 있어야 해당 강의를 수강할 때 편할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;섹션 3에서는 Spring Batch의 핵심 컴포넌트들을 학습한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다이어그램 기반으로 한 눈에 이해하기 쉽게 설명해주셔서 스프링 배치가 어떤 방식으로 돌아가는지 거시적으로 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;섹션 4에서는 Spring Batch의 처리 방식 중 하나인 Tasklet에 대해 학습하고, 섹션 5에서는 또 다른 방식 중 하나인 Chunk에 대해 학습한다.&lt;/b&gt; 아래는 최근 진행 중인 증권 프로젝트에서 사용한 Chunk 방식의 Batch 이다. JS Code 멘토님들의 강의는 항상 느끼는 것인데, 공부를 하면서 프로젝트까지 진행하고, 응용하는 단계에서 정말 많이 얻어간다고 생각한다. 프로젝트를 진행하면서 배우고 학습한 기술들을 최대한 적용하려고 하는 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nippyclouding.tistory.com/83&quot;&gt;https://nippyclouding.tistory.com/83&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776573103326&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring] Batch : STO 토큰 상세 페이지 캔들 차트&quot; data-og-description=&quot;프로젝트 진행 중 STO 토큰 상세 페이지에서 보여지는 '캔들 차트' 를 구현하는 역할을 담당하게 되었다. (주식과 유사하게 동작) 사용 기술 : Spring Boot 3, Spring Batch 5 아래는 상세 페이지의 목업이&quot; data-og-host=&quot;nippyclouding.tistory.com&quot; data-og-source-url=&quot;https://nippyclouding.tistory.com/83&quot; data-og-url=&quot;https://nippyclouding.tistory.com/83&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/8TK9K/dJMb9kT4RwG/ake6FzjbD8ZkeqfNitYYP0/img.png?width=703&amp;amp;height=488&amp;amp;face=0_0_703_488,https://scrap.kakaocdn.net/dn/eyv6i/dJMb9fZxdvs/9Ll4syIDpzNCOfsaRRIa0K/img.png?width=703&amp;amp;height=488&amp;amp;face=0_0_703_488,https://scrap.kakaocdn.net/dn/dzntJL/dJMb9kmeExl/ujaBiHdCcj0qQdcmEqtvb0/img.png?width=1880&amp;amp;height=907&amp;amp;face=0_0_1880_907&quot;&gt;&lt;a href=&quot;https://nippyclouding.tistory.com/83&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nippyclouding.tistory.com/83&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/8TK9K/dJMb9kT4RwG/ake6FzjbD8ZkeqfNitYYP0/img.png?width=703&amp;amp;height=488&amp;amp;face=0_0_703_488,https://scrap.kakaocdn.net/dn/eyv6i/dJMb9fZxdvs/9Ll4syIDpzNCOfsaRRIa0K/img.png?width=703&amp;amp;height=488&amp;amp;face=0_0_703_488,https://scrap.kakaocdn.net/dn/dzntJL/dJMb9kmeExl/ujaBiHdCcj0qQdcmEqtvb0/img.png?width=1880&amp;amp;height=907&amp;amp;face=0_0_1880_907');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring] Batch : STO 토큰 상세 페이지 캔들 차트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 진행 중 STO 토큰 상세 페이지에서 보여지는 '캔들 차트' 를 구현하는 역할을 담당하게 되었다. (주식과 유사하게 동작) 사용 기술 : Spring Boot 3, Spring Batch 5 아래는 상세 페이지의 목업이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nippyclouding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;섹션 6, 7에서는 JobParameter, Step Scope와 Listener에 대해 학습한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 Listener도 Stomp 공부할 때 익혔던 EventListener과 유사한 개념이어서 리스너라는 것 자체가 '감시자' 역할을 한다는 것을 이해했다.&lt;/p&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;섹션 8에서는 전체적인 배치 개념을 '배달 플랫폼' 예시와 함께 복습한다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치에 대해 깊게 학습하고 싶다면 섹션 3 ~ 7 부분을 한 번 더 집중적으로 복습 후 섹션 8을 들어가도 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 것은 역시 섹션 8까지 익힌 뒤 실제 프로젝트에서 적용해보는 것일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;섹션 9에서는 젠킨스와 배치를 연동하는 내용이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젠킨스에 대한 내용을 자세히 알고 있지는 않아서 찍먹 느낌으로 빠르게 보고 넘어갔는데, 이후 젠킨스에 대해 공부할 기회가 생기면 해당 부분도 다시 짚고 넘어가야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;해당 강의를 빠르게 수강 후 프로젝트에서 배치를 적용하며 새로 익힌 점은, ItemWriter, ItemProcessor, ItemReader을 Config.java에서 Bean으로 return해도 되지만 별도 @Component 클래스로 생성하여 implements ItemProcessor&amp;lt;입력 타입, 출력 타입&amp;gt; 처럼 사용할 수도 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 아직은 정확히 감이 잡히지 않지만 대략 언제 tasklet을 사용하고 언제 chunk를 사용해야 하는지도 여러 경험들을 쌓으며 몸으로 체감할 수 있을 것 같다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 진행하는 프로젝트에 Batch가 필요하다는 생각이 들면 여러모로 추천하는 강의이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Spring/Batch</category>
      <author>nippycloud</author>
      <guid isPermaLink="true">https://nippyclouding.tistory.com/88</guid>
      <comments>https://nippyclouding.tistory.com/88#entry88comment</comments>
      <pubDate>Sun, 19 Apr 2026 13:47:49 +0900</pubDate>
    </item>
    <item>
      <title>NginX 2</title>
      <link>https://nippyclouding.tistory.com/87</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;nginx.conf &amp;amp; default.conf&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;nginx.conf : NginX 글로벌 설정, 최초 1회만 설정 후 변경이 자주 일어나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;default.conf : 특정 도메인/포트 (개별 사이트)에 대한 설정, default.conf를 사용해도 되지만 개발자가 직접 conf 파일을 생성해도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;/etc/nginx 경로로 이동&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;cat nginx.conf&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;nginx.conf&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776568874559&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] &quot;$request&quot; '
                      '$status $body_bytes_sent &quot;$http_referer&quot; '
                      '&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;';

    access_log  /var/log/nginx/access.log  main;
    # 로그 저장 위치

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
    # conf.d 속 .conf 파일을 모두 읽어오는 역할
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;/etc/nginx 에서 하위 경로 conf.d 로 이동&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;cat default.conf&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;NginX 설치 시 기본적으로 설치되는 파일이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;실무에서는 이 파일을 삭제하고 주로&lt;b data-path-to-node=&quot;9&quot; data-index-in-node=&quot;68&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;새로운 설정 파일(ex: myservice.conf)을 새로 만들어서 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;default.conf&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776568550704&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

 
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

}&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;size14&quot;&gt;sever { ... } : 서버 블록 선언&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;listen 80 : 외부에서 80번 포트(http://)로 들어오는 요청을 해당 서버 블록이 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;server_name localhost : 해당 웹사이트의 도메인 이름 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;location / { ... } : 사용자가 루트 경로로 접근할 경우에 대한 규칙 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;root /usr/share/nginx/html : 루트 경로로 접근했을 때 사용자에게 보여줄 자원이 있는 경로&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;index index.html index.htm : 사용자가 루트 경로로 접근 (특정 자원을 요청하지 않았을 경우) 기본적으로 index.html을 위 경로에서 보여주고, html 파일이 없다면 index.htm 파일을 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;error_page 500 502 503 504 /50x.html : 서버 오류 시 50x.html 파일을 사용자에게 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;root /usr/share/nginx/html : 50x.html 파일이 존재하는 경로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;703&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sXn8e/dJMcaaE1iG0/PX9t8p8j4S35sv1nIMzfVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sXn8e/dJMcaaE1iG0/PX9t8p8j4S35sv1nIMzfVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sXn8e/dJMcaaE1iG0/PX9t8p8j4S35sv1nIMzfVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsXn8e%2FdJMcaaE1iG0%2FPX9t8p8j4S35sv1nIMzfVk%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;943&quot; height=&quot;703&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;703&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;NginX 응용 - 80번 포트로 접근 시 NginX 기본 화면 대신 다른 화면을 출력하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. /etc/nginx/conf.d 이동 후 vi default.conf&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776571352478&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;location / {
    root   /usr/share/nginx/html;
    index  hello.html;		
    # 기존 index.html -&amp;gt; hello.html 로 변경
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1649&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1uEI3/dJMcagrFo95/BsbTSQPRZFP84zdtIGpxI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1uEI3/dJMcagrFo95/BsbTSQPRZFP84zdtIGpxI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1uEI3/dJMcagrFo95/BsbTSQPRZFP84zdtIGpxI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1uEI3%2FdJMcagrFo95%2FBsbTSQPRZFP84zdtIGpxI1%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;1649&quot; height=&quot;392&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1649&quot; data-origin-height=&quot;392&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;2. /usr/share/nginx/html로 이동 후 hello.html 작성&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;381&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbTt29/dJMcacCPmdR/RF5AWbmrgRA6VdjKKCvzN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbTt29/dJMcacCPmdR/RF5AWbmrgRA6VdjKKCvzN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbTt29/dJMcacCPmdR/RF5AWbmrgRA6VdjKKCvzN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbTt29%2FdJMcacCPmdR%2FRF5AWbmrgRA6VdjKKCvzN0%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;381&quot; height=&quot;82&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;381&quot; data-origin-height=&quot;82&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;&lt;b&gt;3. 변경한 nginx 테스트, 반영&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sudo nginx -t : 성공 시 successful, 실패 시 실패 위치를 알려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sudo nginx -s reload&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;171&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biXkT1/dJMcaiXhz3v/Qnbc3NhkHBNxR7c1r3yTsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biXkT1/dJMcaiXhz3v/Qnbc3NhkHBNxR7c1r3yTsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biXkT1/dJMcaiXhz3v/Qnbc3NhkHBNxR7c1r3yTsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiXkT1%2FdJMcaiXhz3v%2FQnbc3NhkHBNxR7c1r3yTsk%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;1151&quot; height=&quot;171&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;171&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nrXbv/dJMcaaZhDvf/6kpsamsFlQzlABGevNmwBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nrXbv/dJMcaaZhDvf/6kpsamsFlQzlABGevNmwBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nrXbv/dJMcaaZhDvf/6kpsamsFlQzlABGevNmwBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnrXbv%2FdJMcaaZhDvf%2F6kpsamsFlQzlABGevNmwBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;852&quot; height=&quot;197&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;197&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;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776595871974&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;위 방법과 마찬가지로 html css JS 파일, React &amp;amp; vite, NextJs 파일 등을 
/usr/share/nginx/ 경로에서 깃허브 clone 후 
/etc/nginx/conf.d 에서 default.conf 파일을 아래처럼 수정하면 
clone한 파일이 정상 동작한다. 
(sudo nginx -t, sudo nginx -s reload 필요)

location / {
    root   /usr/share/nginx/리눅스에서 클론한 경로;
    index  시작 파일;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NginX 디버깅 방법&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;1. sudo systemctl status nginx : 리눅스에서 Nginx 실행 상태를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;2. sudo nginx -t : 현재 리눅스에서 작성한 Nginx 에 대한 코드에서 문제가 있는지 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;3. cd /var/log/nginx 이후 tail access.log, tail error.log를 통해 어디에서 문제가 발생했는지 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra/NginX</category>
      <author>nippycloud</author>
      <guid isPermaLink="true">https://nippyclouding.tistory.com/87</guid>
      <comments>https://nippyclouding.tistory.com/87#entry87comment</comments>
      <pubDate>Sun, 19 Apr 2026 12:27:19 +0900</pubDate>
    </item>
    <item>
      <title>NginX 1</title>
      <link>https://nippyclouding.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NginX는 '미들웨어' 이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&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;NginX의 주요 역할&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;size14&quot;&gt;&lt;b&gt;- 웹 서버 Web Server&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;정적 파일을 클라이언트 (브라우저)에게 전달한다. (HTML ..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;웹 서버 : 사용자의 요청이 들어올 때마다 HTML, CSS, JS, 이미지와 같은 '정적' 파일들을 제공하는 컴퓨터&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;우분투 &amp;lt;-&amp;gt; NginX&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;=&amp;gt; 우분투는 OS의 한 종류이고 '서버 역할'을 주로 한다. NginX는 소프트웨어이기에 결국 OS 위에서 동작해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;- 리버스 프록시 Reverse Proxy&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;클라이언트는 모든 백엔드 서버를 알지 않아도 NginX에게 요청을 전달하면 NginX가 적절한 위치에 요청을 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;- 로드 밸런싱, 캐싱, 보안, SSL 처리 (Https) 등&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;로컬 VM(UTM)에서 우분투 설치 후 Mac에서 SSH로 우분투 접속&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;내 컴퓨터(Mac)에는 UTM이 설치되어있고 나는 이것으로 보통 Ubuntu 리눅스를 구동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Mac의 터미널로 우분투 SSH로 접속 방법&lt;/p&gt;
&lt;pre id=&quot;code_1776497299939&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Ubuntu 터미널
// SSH 서버 설치
sudo apt update
sudo apt install openssh-server

// SSH 서비스 실행 상태 확인 (처음 설치 시 inactive로 출력)
sudo systemctl status ssh

// ssh 접속을 우분투에서 허용
sudo ufw allow ssh

// 방화벽 활성화
sudo ufw enable

ifconfig
=&amp;gt; inet 뒤에 출력되는 내부 ip를 확인 

Mac 터미널에서 ssh 호스트네임@확인한 ip
lee@192.168. ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Mac 터미널로 우분투 SSH 접속 결과&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ㅅㄷㄴ.png&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;1193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mSlsQ/dJMcaduSzxA/ukjkJ75gAZZvOUWON0SURK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mSlsQ/dJMcaduSzxA/ukjkJ75gAZZvOUWON0SURK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mSlsQ/dJMcaduSzxA/ukjkJ75gAZZvOUWON0SURK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmSlsQ%2FdJMcaduSzxA%2FukjkJ75gAZZvOUWON0SURK%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;1288&quot; height=&quot;1193&quot; data-filename=&quot;ㅅㄷㄴ.png&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;1193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;맥 터미널에서 우분투에 접속한 뒤 NginX 설치&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1776495694152&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update // 컴퓨터에서 설치 가능한 패키지를 최신으로 적용

sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring
// nginx 설치 전 필요한 라이브러리 설치
// curl: 인터넷에서 파일을 다운로드하는 도구
// gnupg2: 암호화된 키를 해독하고 관리하는 도구
// ca-certificates: 웹사이트의 보안 인증서(HTTPS)를 확인하는 도구
// lsb-release: 내 리눅스 버전을 확인하는 도구

curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg &amp;gt;/dev/null
// =&amp;gt; Nginx 공식 서버에서 다운로드한 디지털 서명 열쇠를 관리자 권한으로 시스템 금고 경로에 조용히 저장 (화면 출력 x)

// curl https://nginx.org/keys/nginx_signing.key
// Nginx 공식 홈페이지에서 &quot;NginX 공식팀에서 만든 프로그램이 맞다&quot;는 증명서(Key 파일)를 인터넷으로 읽어온다.
// =&amp;gt; 리눅스가 올바른 프로그램인지 확인하기 위해서

// gpg --dearmor
// 텍스트로 읽어온 데이터를 바이너리 파일로 변환

// sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg &amp;gt;/dev/null
// tee : 파일에 저장하며 콘솔 화면에도 출력 (데이터 출력 흐름을 두 갈래로 나누는 명령어)
// &amp;gt;/dev/null : 실행 결과를 콘솔 화면에 출력하지 않고 /dev/null로 출력 (출력 경로를 변경)
// /dev/null : 사용하지 않는 파일을 삭제하기 위한 공간, 리눅스의 휴지통과 비슷한 개념

// =&amp;gt; key를 관리자 권한으로 /usr/share/keyrings 경로에 저장하고, 
// 화면에는 바이너리 코드로 출력하지 말고 휴지통에 전달

// key 유효성 확인 명령어 (key 내용 확인) 
gpg --dry-run --quiet --no-keyring --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg

// 리눅스가 Nginx에서 필요한 파일 설치 시 다운로드할 경로를 기록
echo &quot;deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/ubuntu `lsb_release -cs` nginx&quot; | sudo tee /etc/apt/sources.list.d/nginx.list

// 기본적으로 리눅스 (APT)는 /etc/apt/sources.list에 모든 프로그램 다운 주소를 기록해둔다.
// /etc/apt/sources.list.d/ : 각 프로그램 별 별도 프로그램 다운 주소 기록용

sudo apt update
sudo apt install nginx



전체 흐름
1. /usr/share/keyrings/ 경로에 key 저장

2. /etc/apt/sources.list.d/nginx.list 가 NginX에 필요한 데이터 설치에서 사용되는 경로, key를 이용해 검증

3. sudo apt update 
/etc/apt/sources.list 와 sources.list.d/ 에 적힌 목록들을 전부 최신으로 업데이트

4. sudo apt install nginx : apt가 nginx.list 목록을 확인 후 공식 서버로 가서 안전하게 NginX 설치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;NginX 설치 이후 Mac 의 chrome 브라우저에서 NginX 접속&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1776498452283&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 맥 터미널에서 SSH로 우분투에 연결한 뒤 아래 명령어 실행

// 우분투에 설치된 nginx가 잘 설치되었는지 버전 확인
nginx -v 

// 우분투에 설치된 nginx 시작, 성공적으로 시작되었는지 확인
sudo systemctl start nginx 
sudo systemctl status nginx 

// 리눅스가 종료 뒤 다시 실행될 때 자동으로 nginx도 재실행하는 명령어 
sudo systemctl enable nginx 

// 우분투 리눅스 방화벽 - 80포트 tcp 접근 허용
sudo ufw allow 80/tcp 

// 방화벽 적용
sudo ufw status


이후 http://192.168. .. (우분투 내부 ip) 로 맥의 chrome 브라우저에 입력 시 Welcome to NginX 정상 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u7cQY/dJMcagLZGE1/vkKdyuRrUFN42TF2pIcOb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u7cQY/dJMcagLZGE1/vkKdyuRrUFN42TF2pIcOb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u7cQY/dJMcagLZGE1/vkKdyuRrUFN42TF2pIcOb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu7cQY%2FdJMcagLZGE1%2FvkKdyuRrUFN42TF2pIcOb1%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;2048&quot; height=&quot;904&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;904&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;NginX 로그 파일&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- NginX를 리눅스에 설치하면 로그 파일이 /var/log/nginx 아래에서 기록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;- 해당 경로에는 access.log, error.log 파일이 있으며 각각 접근 기록, 오류 기록을 나타낸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;2545&quot; data-origin-height=&quot;1403&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgq1A1/dJMcabcNPvb/NqFuvCDN2UHDUQmIBcT7q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgq1A1/dJMcabcNPvb/NqFuvCDN2UHDUQmIBcT7q0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgq1A1/dJMcabcNPvb/NqFuvCDN2UHDUQmIBcT7q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgq1A1%2FdJMcabcNPvb%2FNqFuvCDN2UHDUQmIBcT7q0%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;2545&quot; height=&quot;1403&quot; data-filename=&quot;무제.png&quot; data-origin-width=&quot;2545&quot; data-origin-height=&quot;1403&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;pre id=&quot;code_1776566785245&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;리눅스 기본 명령어 정리

systemctl status 확인할 프로그램 : 프로그램이 실행되고 있는지 확인 

- systemctl status nginx, systemctl status ssh



systemctl start 시작할 프로그램 : 프로그램 시작

systemctl stop 종료할 프로그램 : 프로그램 종료

systemctl enable 프로그램 : 리눅스가 부팅될 때마다 해당 프로그램을 실행 



ufw allow 80/tcp : 리눅스 방화벽 인바운드 추가

ufw status : 방화벽 적용

ssh 호스트네임@IP =&amp;gt; ssh 접근 (서버 쪽에서 ssh를 열어줘야 한다)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra/NginX</category>
      <author>nippycloud</author>
      <guid isPermaLink="true">https://nippyclouding.tistory.com/85</guid>
      <comments>https://nippyclouding.tistory.com/85#entry85comment</comments>
      <pubDate>Sun, 12 Apr 2026 21:19:53 +0900</pubDate>
    </item>
  </channel>
</rss>