<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>noStress</title>
        <link>https://velog.io/</link>
        <description>가끔은 정신줄 놓고 멍 때리는 것도 필요하다.</description>
        <lastBuildDate>Wed, 08 Apr 2026 05:10:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>noStress</title>
            <url>https://velog.velcdn.com/images/half-phycho/profile/27a89796-59d4-481d-9593-e99deeb4a800/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. noStress. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/half-phycho" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[AI에 대한 나의 생각과 고민]]></title>
            <link>https://velog.io/@half-phycho/AI%EC%97%90-%EB%8C%80%ED%95%9C-%EB%82%98%EC%9D%98-%EC%83%9D%EA%B0%81%EA%B3%BC-%EA%B3%A0%EB%AF%BC</link>
            <guid>https://velog.io/@half-phycho/AI%EC%97%90-%EB%8C%80%ED%95%9C-%EB%82%98%EC%9D%98-%EC%83%9D%EA%B0%81%EA%B3%BC-%EA%B3%A0%EB%AF%BC</guid>
            <pubDate>Wed, 08 Apr 2026 05:10:15 GMT</pubDate>
            <description><![CDATA[<p>올해는 이제 1분기가 끝나가지만 너무 많은일들이 있었습니다. 잘다니던 회사를 한순간에 경영상 악화를 이유로 권고사직을 당했고 그래서 6개월만에 다시 구직자로 돌아갔습니다. 그 뒤로 여러가지를 해왔습니다.</p>
<ul>
<li>일본어 학원 다니며 일본어 습득하기</li>
<li>난생 처음으로 코딩테스트 보기</li>
<li>AI를 활용한 프로젝트 만들어보기</li>
</ul>
<p>크게 에피소드를 나누면 이렇게 해온거 같습니다. 그래서 요즘은 과연 개발자로써 벌어먹고 살 수 있을지가 가장큰 걱정 입니다.</p>
<p>저는 지금까지 여러가지 기술 블로그와 AI관련 영상 및 매체를 접근하면서 가장많이 들은말이 <code>xx 없으면 살아남을 수 없다.</code> , <code>개발자 및 기타 디지털 직종은 종말한다.</code> 이런말을 끊임없이 듣다보니 어느순간 부터 스스로 마음안쪽에는 공포심이 자라기 시작했습니다. 그래서 시대를 따라가고자 프로젝트를 만들었습니다.</p>
<br>

<h2 id="ai-프로젝트">AI 프로젝트</h2>
<p>프로젝트로 웹 HTML파싱 방식 크롤링을 하면 그 데이터를 AI로 분석 및 검증을 하고 만약 찾고자하는 데이터가 크롤러에 없으면 AI 데이터를 추가하는 그러한 프로젝트를 만들었습니다. <code>Claude Code</code>를 활용하여 금방 만들 수 있었습니다. 하지만 온전히 나만의 프로젝트라는 느낌은 없었습니다. 그저 나의 생각을 AI로 나타낸 것 뿐 주인의식이 없었습니다. 그래도 만들면서 바이브 코딩이라는 것을 해본 좋은 경험 이었습니다.</p>
<br>

<h2 id="ai에-대한-생각">AI에 대한 생각</h2>
<p>AI는 이제 우리 시대에 없어서는 안 될 필수적인 존재가 되었습니다. 누군가는 이를 &#39;전기의 발견&#39;과 같은 혁명이라 칭송하고, 누군가는 기술의 범람 속에서도 변하지 않는 인간만의 가치를 수호해야 한다고 말합니다. 솔직히 말해, 현재의 AI 열풍 속에는 과도한 <strong>&#39;공포 마케팅&#39;</strong>이 섞여 있다는 인상을 지우기 어렵습니다. AI의 능력이 뛰어난 것은 사실이나, &#39;도태&#39;나 &#39;생존&#39; 같은 자극적인 표현을 서슴지 않는 것은 분명 과장된 측면이 있습니다.</p>
<blockquote>
<p>PC의 등장 ----&gt; 인터넷의 등장 ---&gt; 스마트폰의 등장 --&gt; AI의 등장</p>
</blockquote>
<p>우리는 이미 PC, 인터넷, 스마트폰으로 이어지는 거대한 기술적 변곡점들을 거쳐왔습니다. 과거의 혁신들이 효율적인 <strong>&#39;도구&#39;</strong>나 무한한 <strong>&#39;공간&#39;</strong>의 확장에 머물렀다면, AI는 우리의 일상에 깊숙이 침투하여 <strong>&#39;노동과 생존&#39;</strong>이라는 근본적인 개념을 흔들고 있습니다. 우리가 일하고 가치를 창출하는 방식 자체를 재정의하고 있기에, 이전의 기술 혁명보다 훨씬 더 강력한 충격으로 다가오는 것입니다.</p>
<br>

<h2 id="ai와-자연에-대한-외면">AI와 자연에 대한 외면</h2>
<p>AI 기술이 등장하기 전, 우리 사회의 화두는 단연 기후와 환경 문제였습니다. 하지만 AI 시대가 도래하면서 기후 위기는 어느덧 뒷전으로 밀려난 듯합니다. 기존 데이터 센터보다 막대한 에너지를 소비하는 AI 데이터 센터가 우후죽순 들어서고 있음에도, 이에 따른 환경적 책임에 대한 목소리는 점차 희미해지고 있습니다. 불과 3년 전만 해도 쉽게 접할 수 있었던 ‘기후 위기 시계’에 대한 소식조차 이제는 찾아보기 어렵습니다. 눈부신 기술 발전의 대가로 우리가 과연 무엇을 희생하고 있는지 깊이 고민해 보게 되는 시점입니다.</p>
<br>

<h2 id="정리하기">정리하기</h2>
<p>생각보다 길다면 길고 짧다면 짧은 글이 되었습니다. 제가 결론적으로 생각하는 건 하나 입니다. AI 시대가 와도 우리가 살아가는건 변함이 없고 앞으로도 변함이 없다는 것 입니다. 우리의 일하는 방식이 달라질 뿐 근본적인것은 달라지지 않을겁니다. 숨을쉬어야 살 수 있고 밥을먹어야 에너지를 쓸 수 있는것은 당연하지만 너무 당연한 나머지 우리는 잊어버리고 있는게 아닌가 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Jenkins] 속도 지연 해결법]]></title>
            <link>https://velog.io/@half-phycho/Jenkins-%EC%86%8D%EB%8F%84-%EC%A7%80%EC%97%B0-%ED%95%B4%EA%B2%B0%EB%B2%95</link>
            <guid>https://velog.io/@half-phycho/Jenkins-%EC%86%8D%EB%8F%84-%EC%A7%80%EC%97%B0-%ED%95%B4%EA%B2%B0%EB%B2%95</guid>
            <pubDate>Fri, 06 Mar 2026 04:26:28 GMT</pubDate>
            <description><![CDATA[<h2 id="❓작성계기">❓작성계기</h2>
<p>개인적으로 진행하는 프로젝트에 기존에는 없었던 CI/CD를 구축하던 와중에 발생한 문제였습니다. 젠킨스를 설치하고 스크립트를 작성도 되었고 다좋았지만 서버오류 때문에 서버를 재부팅하였습니다. 그러고 다시 젠킨스에 접속해보니 눈에띄게 느려지는 현상이 발생했습니다. 이번에는 해당 상황이 어째서 생겼고 어떻게 해결했는지 그리고 어째서 일어난 현상인지 적어보겠습니다.</p>
<br>

<h2 id="📘-젠킨스-실행-환경">📘 젠킨스 실행 환경</h2>
<p>젠킨스는 일단 AWS EC2환경에서 실행하고 있습니다. 별도로 고정 IP는 주지 않았습니다. 그래서 서버를 재부팅하면 IP가 재할당됩니다.</p>
<br>

<h2 id="📗-문제원인-및-해결방법">📗 문제원인 및 해결방법</h2>
<p>결론부터 말하면 문제는 등록된 IP가 문제였습니다. 그래서 환경설정에서 세팅되어있는 IP를 수정하니까 정상적으로 동작하게 되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/909a372a-97d6-40a1-bbc1-96f6f41796aa/image.png" alt=""></p>
<ol>
<li>먼저 젠킨스 설정의 System으로 접속합니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/a0dd7dee-ad96-4441-aab7-32bd548aa37f/image.png" alt=""></p>
<ol start="2">
<li>Jenkins URL에 <code>http://[서버주소]:[젠킨스 포트번호]/</code> 이렇게 넣어주고 저장버튼을 클릭하면 다시 정상적으로 빠르게 돌아가는걸 볼 수 있습니다.</li>
</ol>
<h3 id="문제원인">문제원인</h3>
<p>그래서 원인은 무엇인가 생각해보니 아무래도 Jenkins Location은 젠킨스가 위치한곳이 어디인지 알려주는 설정이라고 생각됩니다. 그래서 제 예상으로는 내부적으로는 설정된 URL을 찾는중 timeout이 되었고 외부로 나가서 다시 찾는 작업을 한 번더 작업을 하니까 생기는 현상으로 판단됩니다. 정리하자면 한번만 찾으면 되는 행위를 2번 하니까 발생한 현상으로 판단됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Claude Code] 바이브 코딩으로 프로젝트 개발 ]]></title>
            <link>https://velog.io/@half-phycho/Claude-Code-%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9%EC%9C%BC%EB%A1%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@half-phycho/Claude-Code-%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9%EC%9C%BC%EB%A1%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Thu, 19 Feb 2026 03:27:29 GMT</pubDate>
            <description><![CDATA[<h2 id="❓작성계기">❓작성계기</h2>
<p>IT업계 뿐만아니라 거의 모든 분야에서 AI 열풍이 불고 있고, 관련해서 여기저기서 사람들을 정리해고한다는 이야기는 이제 이상에서 일상으로 변한 지 오래라고 생각합니다. 그래서 얼마나 대단한지 실제로 사용해 보니 제 생각 이상으로 대단했습니다. 왜 개발자들이 이렇게 실직을 많이 하고 있는지 알게 되었습니다. 그래서 얼마나 대단했는지에 대해 공유하고자 이렇게 게시글로 남겨보기로 했습니다.</p>
<br>

<h2 id="❓claude-code-선택이유">❓Claude Code 선택이유</h2>
<p>지금은 수많은 바이브 코딩 도구가 존재합니다. 커서 AI, 이번에 사용한 Claude Code, 구글 제미나이 코드 어시스턴트 등등 정말 많습니다. 그중에서도 Claude Code를 선택한 이유는 어이없게도 가장 사고를 늦게 쳐서라고 말하고 다닙니다. 이게 무슨 말이냐면 제미나이와 Claude 중에 어떤 거를 쓸지 고민이었습니다. 그러다가 두 AI 중에 사고를 늦게 친 쪽을 구매해서 쓰기로 마음을 먹고 얼마 안 있고 제미나이에서 이슈를 냈습니다. 그래서 Claude를 사용하게 되었습니다. 지금 생각하면 Claude를 사용하길 잘한 거 같습니다.</p>
<br>

<h2 id="📕-개발한-프로젝트">📕 개발한 프로젝트</h2>
<p>이번에 개발한 프로젝트는 해외 및 아티스트 들의 내한 콘서트 일정 정보를 수집하는 프로그램입니다. 지금은 폐쇄한 상태지만 그 당시 일본 가수 내한 일정 사이트를 운영하던 불편한 점이 있었습니다. 그것은 콘서트 일정을 하나하나 수기로 입력해야 했다는 점과 심지어 일정이 발표되는 것도 갑자기 나오는 경우가 많기에 일정을 실시간으로 확인하는 게 문제였습니다. 그래서 다음과 같은 문제를 해결하고자 했습니다.</p>
<ul>
<li>Docker로 동작하도록 만들기</li>
<li>수기작성에서 매일 오후 12시마다 자동으로 데이터가 입력되도록 하기</li>
</ul>
<br>

<h2 id="📗-프로젝트-실행">📗 프로젝트 실행</h2>
<p>처음에는 모든게 막막했습니다. 파이썬도 조금다룰줄만 알았지 전문적으로 할줄은 몰랐으니까요 그래도 Claude Code 덕분에 그부분은 해결이되어서 올바르게 AI에게 명령을 내리는 부분에 집중했습니다. Claude Code는 실로 놀라웠습니다. 코드작성은 물론 Git의 Repository와 연결하면 알아서 PR도 만들어줄거라곤 생각도 못했습니다. 만약 저 혼자 했다면 이 글을 쓸시간에도 파이썬 크롤링부분에서 애를 먹고 있었을 것 입니다. </p>
<br>

<h3 id="프로젝트-repository">프로젝트 Repository</h3>
<p><a href="https://github.com/delight-HK3/gemini_concert_search">https://github.com/delight-HK3/gemini_concert_search</a></p>
<br>

<h2 id="😊-바이브코딩-후기">😊 바이브코딩 후기</h2>
<p>프로젝트는 결과적으로 1주일 정도 걸렸던거 같습니다. 생산력이 말이 안되는 수준이었습니다. 테스트 해본결과 정상적으로 동작하는 것도 확인했습니다. 정말로 IT업계에서 개발자라는 직업이 사라질거라는 말이 나오는 것도 무리는 아니겠구나 싶었습니다. 하지만 한편으로는 좀 섬뜩했습니다. 어디서 들었는데 기술의 특이점이 2055년에 온다고 했는데 그것보다 빠르게 올거 같다고 생각했습니다. 그리고 이러한 편안함속에 우리는 무엇을 잃었는지에 대해 생각해보는 계기가 되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD] DockerHub에서 AWS ECR 전환하기]]></title>
            <link>https://velog.io/@half-phycho/CICD-Docker-Hub%EC%97%90%EC%84%9C-AWS-ECR-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@half-phycho/CICD-Docker-Hub%EC%97%90%EC%84%9C-AWS-ECR-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 11 Feb 2026 06:28:36 GMT</pubDate>
            <description><![CDATA[<h2 id="❓작성계기">❓작성계기</h2>
<p>예전에 DockerHub로 배포를 하려고 했는데 DockerHub에 접속이 안되는 이슈가 있었습니다. 그래서 확인해보니 DockerHub자체가 마비되서 일어난 이슈였고 그래서 현재 서비스가 AWS EC2위에서 동작하는점이 반영되어 이미지 업로드 저장소를 소프트웨어 이미지를 DockerHub에서 AWS ECR로 변경하기로 결정했고 그 역할을 제가 맏기로 했습니다. 해당 작업을 마치고 나중에 해당 에피소드를 써야지 했다가 여러가지 이슈로 인해 미루어졌다가 이제와서야 쓰게 됩니다.</p>
<br>

<h2 id="🗒️-기존-cicd-구조">🗒️ 기존 CI/CD 구조</h2>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/44921ee7-573c-49a7-bc36-e20fa7d0121a/image.png" alt=""></p>
<p>이미지로 그리자면 대충 이러한 형태가 됩니다. 작업자가 GitHub에 Push 및 Merge를 하면 Jenkins에서는 해당 GitHub계정에 접근해 해당하는 Repository를 이미지로 만들고 그거를 DockerHub에 올린 후 지정된 서버로 업로드 하는 그런 형식입니다.</p>
<br>

<h2 id="🗒️-개선한-cicd-구조">🗒️ 개선한 CI/CD 구조</h2>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/903e4037-e6f4-40d9-bdd3-640b779b475e/image.png" alt=""></p>
<p>구조는 단순히 DockerHub에 올리는 방식에서 AWS ECR에 업로드하는 방식으로 변경한 것 뿐이었습니다만 의외로 괜찮은 결과를 얻을 수 있었습니다.</p>
<ul>
<li>얻은 것들<ul>
<li>개발서버 배포시간 51% 감소</li>
<li>스크립트 과정 간결화</li>
<li>기존 DockerHub 인증서 노출 보안 이슈 해결</li>
</ul>
</li>
</ul>
<br>

<h2 id="🔗-젠킨스-인증서-등록">🔗 젠킨스 인증서 등록</h2>
<p>해당 작업을 하기전에 인증서를 등록해야하는데 그거는 하단의 주소를 참고하면 됩니다.
<a href="https://fwani.tistory.com/29">https://fwani.tistory.com/29</a></p>
<br>

<h2 id="🧑💻-기존-젠킨스-스크립트">🧑‍💻 기존 젠킨스 스크립트</h2>
<pre><code class="language-java">pipeline {
    agent any

    environment {
        APP_NAME = &#39;[프로젝트 명]&#39; // 저장소 명
        DOCKER_CREDENTIALS = credentials(&#39;[Jenkins에 등록된 아이디와 비밀번호가 DockerHub 로그인 정보와 같은 인증키 이름]&#39;)
        DOCKER_IMAGE = &#39;[이미지 명]&#39; // Docker Image 이름
        SPRING_PROFILE = &#39;[운영설정]&#39;  // 운영 환경 프로필
        REMOTE_HOST = &#39;[배포서버 IP]&#39;  // 운영 서버 IP 또는 호스트명
        REMOTE_USER = &#39;[접속 계정 명]&#39; // SSH 접속 계정
        REMOTE_CREDENTIALS = credentials(&#39;[Jenkins에서 발급한 인증키 이름]&#39;)  // 비밀번호 자격증명 ID
    }

    stages {
        stage(&#39;Git Clone&#39;) {
            steps {
                git branch: &#39;[브랜치 이름]&#39;,  // 운영 환경은 main 브랜치 사용
                    credentialsId: &#39;[Jenkins에서 발급한 토큰 Key]&#39;,
                    url: &quot;[깃허브 주소]/${APP_NAME}.git&quot;
            }
        }

        // 가져온 chat-Repository에서 gradle에 등록된 application version 가져오기
        stage(&#39;get application version&#39;) {
            steps {
                script {
                    // Gradle 실행권한 부여
                    sh &#39;chmod +x ./gradlew&#39;

                    // Gradle의 callVersion 태스크를 실행하여 버전 값 가져오기
                    def appVersion = sh(returnStdout: true, script: &#39;./gradlew -q callVersion&#39;).trim()

                    // 가져온 버전을 환경 변수로 설정합니다.
                    env.DOCKER_TAG = appVersion
                }
            }
        }

        stage(&#39;Build and Push Docker Image&#39;) {
            steps {
                // Docker Hub 로그인
                sh &#39;&#39;&#39;
                    echo $DOCKER_CREDENTIALS_PSW | docker login -u $DOCKER_CREDENTIALS_USR --password-stdin
                &#39;&#39;&#39;

                // Jib을 사용하여 도커 이미지 빌드 및 푸시 (태그와 프로필 전달)
                sh &quot;&quot;&quot;
                    ./gradlew jib \
                        -Djib.to.tags=${DOCKER_TAG} \
                        -Djib.container.environment.SPRING_PROFILES_ACTIVE=${SPRING_PROFILE}
                &quot;&quot;&quot;
            }
        }

        stage(&#39;Deploy to Production&#39;) {
            steps {
                withCredentials([
                    sshUserPrivateKey(credentialsId: &#39;[Jenkins에서 발급한 인증키]&#39;, keyFileVariable: &#39;KEY_FILE&#39;),
                    usernamePassword(credentialsId: &#39;[Jenkins에 등록된 아이디와 비밀번호가 DockerHub 로그인 정보와 같은 인증키 이름]&#39;, passwordVariable: &#39;DOCKER_PASSWORD&#39;, usernameVariable: &#39;DOCKER_USERNAME&#39;)
                ]) {
                    sh &#39;&#39;&#39;
                        # 배포 스크립트 생성
                        cat &gt; deploy.sh &lt;&lt; EOF
#!/bin/bash
# Docker Hub 로그인
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
# 기존 컨테이너가 있으면 중지 및 삭제
if docker ps -a | grep -q ${APP_NAME}; then
    docker stop ${APP_NAME}
    docker rm ${APP_NAME}
fi
# 이미지 풀링
docker pull ${DOCKER_IMAGE}:${DOCKER_TAG}

# 컨테이너 실행
[컨테이너 실행 설정 및 명령어]

# Docker Hub 로그아웃
docker logout
EOF

                        # 스크립트에 실행 권한 부여
                        chmod +x deploy.sh

                        # 원격 서버로 스크립트 전송
                        scp -i $KEY_FILE -o StrictHostKeyChecking=no deploy.sh ${REMOTE_USER}@${REMOTE_HOST}:~/

                        # 원격 서버에서 스크립트 실행
                        ssh -i $KEY_FILE -o StrictHostKeyChecking=no ${REMOTE_USER}@${REMOTE_HOST} &quot;chmod +x ~/deploy.sh &amp;&amp; ~/deploy.sh&quot;
                    &#39;&#39;&#39;
                }
            }
        }

        // 클린업을 위한 별도 스테이지 추가
        stage(&#39;Cleanup&#39;) {
            steps {
                sh &#39;docker logout || true&#39;  // Docker Hub 로그아웃
                cleanWs()  // 워크스페이스 정리
            }
        }
    }

    post {
        success {
            echo &#39;Production deployment succeeded!&#39;
        }
        failure {
            echo &#39;Production deployment failed!&#39;
        }
    }
}
</code></pre>
<p>기존에는 DockerHub로그인 및 로그아웃 과정이 있어서 시간이 약간 오래걸렸던거 같습니다. 이 글을 쓰면서 새삼 보니까 어째서 DockerHub로그아웃을 2번 요청하는지는 모르겠습니다.</p>
<br>

<h2 id="🧑💻-개선한-젠킨스-스크립트">🧑‍💻 개선한 젠킨스 스크립트</h2>
<pre><code class="language-java">pipeline {
    agent any

    environment {
        AWS_REGION = &#39;[현재 서비스를 실행하는 리전]&#39;
        ECR_REGISTRY = &quot;[AWS 계정 12자리 일련번호].[ECR Repository 주소]&quot;
        APP_NAME = &#39;&#39;

        CONTAINER_NAME = &#39;[등록될 컨테이너 이름]&#39;
        DOCKER_IMAGE = &quot;${ECR_REGISTRY}/[ECR_Repository 이름]&quot; // ECR에 등록된 Repository
        SPRING_PROFILE = &#39;[운영설정]&#39;  // 운영 환경 프로필
        REMOTE_HOST = &#39;[배포서버 IP]&#39;  // 운영 서버 IP 또는 호스트명
        REMOTE_USER = &#39;[접속 계정 명]&#39; // SSH 접속 계정
        SSH_CREDENTIALS_ID = &#39;[Jenkins에서 발급한 인증키 이름]&#39; // 비밀번호 자격증명 ID
    }

    stages {
        stage(&#39;Git Clone&#39;) {
            steps {
                git branch: &#39;develop&#39;,
                    credentialsId: &#39;github-lch-token1&#39;,
                    url: &quot;https://github.com/store-labs/${APP_NAME}.git&quot;
            }
        }

        stage(&#39;Get Version&#39;) {
            steps {
                script {
                    sh &#39;chmod +x ./gradlew&#39;
                    def tag = sh(script: &#39;./gradlew -q callVersion&#39;, returnStdout: true).trim()
                    env.DOCKER_TAG = tag
                }
            }
        }

        stage(&#39;Build &amp; Push to ECR&#39;) {
            steps {
                // 문자열 보간법을 사용하여 docker 명령어가 젠킨스 변수로 오해받지 않게 함
                sh &quot;aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}&quot;
                sh &quot;./gradlew jib -Djib.to.image=${DOCKER_IMAGE} -Djib.to.tags=${env.DOCKER_TAG} -Djib.container.environment.SPRING_PROFILES_ACTIVE=${SPRING_PROFILE}&quot;
            }
        }

        stage(&#39;Deploy to Production&#39;) {
                    steps {
                        withCredentials([
                            sshUserPrivateKey(credentialsId: &quot;${SSH_CREDENTIALS_ID}&quot;, keyFileVariable: &#39;KEY_FILE&#39;)
                        ]) {
                            sh &quot;&quot;&quot;
                            cat &gt; deploy.sh &lt;&lt; EOF
#!/bin/bash
# 1. ECR 로그인
aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}

# 2. 기존 컨테이너가 있다면 무조건 삭제
if docker ps -a | grep -q &quot;${APP_NAME}&quot;; then
    docker stop ${APP_NAME} || true
    docker rm ${APP_NAME} || true
fi
# 3. 이미지 작업
docker pull ${DOCKER_IMAGE}:${env.DOCKER_TAG}
# 4. 컨테이너 실행
[컨테이너 실행 명령어 및 설정]
EOF
                        chmod +x deploy.sh
                        scp -i \$KEY_FILE -o StrictHostKeyChecking=no deploy.sh ${REMOTE_USER}@${REMOTE_HOST}:~/
                        ssh -i \$KEY_FILE -o StrictHostKeyChecking=no ${REMOTE_USER}@${REMOTE_HOST} &quot;bash ~/deploy.sh&quot;
                    &quot;&quot;&quot;
                }
            }
        }

        // 클린업을 위한 별도 스테이지 추가
        stage(&#39;Cleanup&#39;) {
            steps {
                cleanWs()  // 워크스페이스 정리
            }
        }
    }

    post {
        success {
            echo &#39;Production deployment succeeded!&#39;
        }
        failure {
            echo &#39;Production deployment failed!&#39;
        }
    }
}</code></pre>
<p>전체적으로 스크립트가 간결해졌고 추가적인 로그인 및 로그아웃 과정이 생략되었습니다.</p>
<br>

<h2 id="🖼️-필요한-추가작업">🖼️ 필요한 추가작업</h2>
<p>단순히 스크립트만 바꾸면 안되고 AWS에서 추가작업이 필요합니다.</p>
<ul>
<li>배포하는 서버에 AWS CLI설치</li>
<li>ECR Repository 등록</li>
<li>ECR이 접근가능하도록 IAM Role 생성</li>
<li>EC2에 IAM 역할 연결</li>
</ul>
<p>(AWS CLI 설치방법은 구글링으로 찾으면 방법은 쉽게 알 수 있습니다.)</p>
<h3 id="ecr-repository-등록">ECR Repository 등록</h3>
<ol>
<li><p>리포지토리 생성을 클릭합니다.
<img src="https://velog.velcdn.com/images/half-phycho/post/cd68b116-f782-43b2-a129-ff491257d352/image.png" alt=""></p>
</li>
<li><p>ECR Repository이름을 입력하고 생성합니다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/f1dd69d5-ad05-47f5-ab84-05e9f713a959/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/ce014e7b-c86e-45ce-868a-d925028228cd/image.png" alt=""></p>
<p>(저의 경우에는 임시로 tester라고 이름을 지었습니다.)</p>
<br>

<h3 id="ecr이-접근가능하도록-iam-role-생성">ECR이 접근가능하도록 IAM Role 생성</h3>
<ol>
<li><p>IAM 역할을 추가하는 방법은 일단 IAM &gt; 역할 &gt; 역할 생성 이렇게 진행합니다.
<img src="https://velog.velcdn.com/images/half-phycho/post/bb067322-2190-4ba0-9911-406487589a50/image.png" alt=""></p>
</li>
<li><p>역할 &gt; 권한 추가 에서는 2가지만 필요합니다.</p>
<ul>
<li>AmazonEC2ContainerRegistryReadOnly (ECR Pull)</li>
<li>AmazonEC2ContainerRegistryPowerUser (ECR Push/Pull)
<img src="https://velog.velcdn.com/images/half-phycho/post/e8319ed6-e83c-44e2-997e-d5e159577b13/image.png" alt=""></li>
</ul>
</li>
</ol>
<p>(하나의 역할에 2개의 권한을 줘서 배포서버, 젠킨스 서버에 동시에 지정해도 되고 배포 서버는 AmazonEC2ContainerRegistryReadOnly 이것 젠킨스 서버는 AmazonEC2ContainerRegistryPowerUser를 가진 역할을 주면 됩니다.)</p>
<ol start="3">
<li>역할 이름을 지정하고 역할 생성 합니다.
<img src="https://velog.velcdn.com/images/half-phycho/post/78b3238b-3e4e-42b6-99c6-23cef3289a56/image.png" alt=""></li>
</ol>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/1ff1131e-7a7e-4de9-9ce7-894ff3a8b126/image.png" alt=""></p>
<p>(이렇게 ECR_Test 이름의 역할이 만들어집니다.)</p>
<h3 id="ec2에-iam-역할-연결">EC2에 IAM 역할 연결</h3>
<p>EC2 &gt; 인스턴스 &gt; [인스턴스 아이디] &gt; IAM 역할 수정</p>
<p>이렇게 넘어가면 방금 만든 ECR_Test라는 역할을 확인할 수 있고 지정한 후 IAM 역할 업데이트를 클릭하여 등록하면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/fe02fa1b-cdfd-43b5-803d-8de62e66cf0a/image.png" alt=""></p>
<br>


<h2 id="📋-참고-사이트">📋 참고 사이트</h2>
<p><a href="https://fwani.tistory.com/29">https://fwani.tistory.com/29</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RabbitMQ] TTL / 데드 레터링 사용해보기]]></title>
            <link>https://velog.io/@half-phycho/RabbitMQ-TTL-%EB%8D%B0%EB%93%9C-%EB%A0%88%ED%84%B0%EB%A7%81-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@half-phycho/RabbitMQ-TTL-%EB%8D%B0%EB%93%9C-%EB%A0%88%ED%84%B0%EB%A7%81-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 22 Jan 2026 05:27:40 GMT</pubDate>
            <description><![CDATA[<h2 id="❓-작성계기">❓ 작성계기</h2>
<p>이전에 Exchange를 종류대로 사용하는법을 설명하고 공부하면서 RabbitMQ와 같이 실시간 메세지 브로커에서 과연 연결이 안되는 상황에는 어떤걸 사용해야 하는가에 대해 생각해본 적이 있습니다.  다행히 관련해서 자료를 찾아보니 메세지가 안보내지는 상황에 어떻게 해야하는지 방법이 있었고 그부분을 공부하고 공유하고자 이렇게 글을 작성하게 되었습니다. 그리고 겸사겸사 TTL이 무엇이고 RabbitMQ에서는 이걸 어떻게 사용하는지 알아보는 시간을 가져보겠습니다.</p>
<blockquote>
<p><a href="https://velog.io/@half-phycho/RabbitMQ-Exchange-%EC%A2%85%EB%A5%98%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0">https://velog.io/@half-phycho/RabbitMQ-Exchange-종류대로-사용해보기</a>
이전에 작성한 게시글 입니다.</p>
</blockquote>
<br>

<h2 id="📕-ttl-time-to-live">📕 TTL (Time-To-Live)</h2>
<p>TTL은 간단히 말하면 유효시간 입니다. 의외로 TTL은 IT 여러분야에서 볼 수 있다고 생각하는데 대표적으로 네트워크, 데이터베이스가 있습니다. 그리고 RabbitMQ에서도 사용가능한데 메세지 자체에 TTL을 설정하여 시간을 다르게 하거나 아니면 Queue자체에 TTL을 설정해 들어오는 모든 메세지에 일관된 유효시간을 부여할 수 있습니다.</p>
<blockquote>
<p>메세지에서 TTL 설정하고 Queue에서도 TTL 설정을 한다면 어떤결과가 나오는지 찾아보니 메세지 TTL이 짧은쪽을 기준으로 유효시간을 가진다고 합니다. 결국 Queue TTL보다 메세지 TTL이 짧게 설정되면 해당하는 메세지만 삭제되고 메세지의 TTL이 Queue TTL보다 길면 Queue TTL 수명이 다되면 Queue에 남아있는 모든 메세지가 삭제 됩니다.</p>
</blockquote>
<br>

<h3 id="🔎-메세지-ttl-사용하기">🔎 메세지 TTL 사용하기</h3>
<p>앞에서 말한대로 메시지 자체에 TTL을 설정할 수 있고 Queue에서도 TTL설정이 가능합니다. </p>
<p><strong>메세지 레벨에서 TTL 설정하기</strong></p>
<pre><code class="language-java">public String sendDirectTTLMessage(MessageDTO messageDTO) {
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            // DTO -&gt; String 직렬화 수행
            String objectToJSON = objectMapper.writeValueAsString(messageDTO);

            // 라우터를 기반으로 큐에 메시지 전송
            // ttl 시간은 5초 (5000) 으로 설정
            rabbitTemplate.convertAndSend(rabbitmqExchangeInfo.get_DIRECT_EXCHANGE_NAME()
                                        , rabbitmqExchangeInfo.get_DIRECT_EXCHANGE_KEY()
                                        , objectToJSON
                                        , message -&gt; {
                                            message.getMessageProperties().setExpiration(&quot;5000&quot;);
                                            return message;
                                        });
        } catch (JsonProcessingException ex) {
            log.error(&quot;parsing error : {}&quot;, ex.getMessage(), ex);
        }

        return &quot;success_ttl_direct&quot;;
    }</code></pre>
<p><strong>Queue 레벨에서 TTL 설정</strong></p>
<pre><code class="language-java">@Bean
public Queue directQueue() {
        // TTL 설정이 미포함된 Queue
        //return new Queue(rabbitmqExchangeInfo.get_DIRECT_QUEUE_NAME(), true);

        // TTL 설정이 포함된 Queue
        // QueueBuilder에 durable 메서드 안에 Queue이름을 넣으면
        // RabbitMQ가 재부팅되도 Queue 대기열에 남는다.
        return QueueBuilder.durable(rabbitmqExchangeInfo.get_DIRECT_QUEUE_NAME())
                .withArgument(&quot;x-message-ttl&quot;, 3000)
                .build();
}</code></pre>
<br>

<h2 id="📗-데드-레터링-dead-lettering">📗 데드 레터링 (Dead Lettering)</h2>
<p>메세지가 정상적으로 처리되지 못하고 RabbitMQ의 큐 레벨에서 제거될 때 발생하는 처리 방식을 말합니다. 메세지가 정상적으로 처리되지 못하면 <strong>데드 레터 큐</strong>로 보내집니다.  </p>
<p><strong>데드 레터링 발생 경우</strong></p>
<blockquote>
<ul>
<li>메세지가 큐에서 거부(Reject)되거나 NACK 상태 혹은 다시 큐(requeue)로 돌아오는 경우</li>
</ul>
</blockquote>
<ul>
<li>메세지가 큐에 있었던 시간이 메세지의 TTL을 초과하여 만료된 경우</li>
<li>메세지가 큐의 최대 길이를 초과하여 메시지를 추가할 수 없는 경우</li>
<li>큐가 가득 차서 메시지를 더 이상 저장할 수 없는 경우</li>
</ul>
<p>(큐 전체가 만료되어도 큐에 있는 메세지는 데드 레터링 되지 않습니다.)</p>
<blockquote>
<p><strong>NACK(Negative Acknowledgement)</strong>
네트워크에서 사용하는 용어로 네트워크 기기 간에 데이터를 주고 받았을 때, 수신 장비에 데이터가 도착하지 않았을 때 보내는 신호 그 반대로 ACK(Acknowledgement)는 장비에 데이터가 도착했을때 보내는 신호이다.</p>
</blockquote>
<br>

<h3 id="데드-레터-exchange">데드 레터 Exchange</h3>
<p>RabbitMQ에서 제공하는 익스체인지 유형으로 메세지가 큐에서 제거되는 경우에 사용됩니다. 기존 Exchange를 설정한다면 DirectExchange, FanoutExchange 이런 식으로 특정 Exchange를 설정했는데 데드 레터 Exchange를 설정하려면 Queue에 &quot;x-dead-letter-exchange&quot;라는 매개변수를 설정하고 메시지가 큐에서 제거될 때 메시지를 받을 익스체인지의 이름을 파라미터로 설정해주면 됩니다.</p>
<br>

<h3 id="데드-레터-exchange-처리과정">데드 레터 Exchange 처리과정</h3>
<ol>
<li>Producer가 생성한 메세지가 Exchange로 전달</li>
<li>Exchange는 처리방식별로 Queue에 메세지가 적재됩니다.</li>
<li>Queue를 수신하고 있는 Consumer에게 전달되는데 메세지 전송에 실패하면 데드 레터 Exchange를 거쳐 데드 레터 큐로 라우팅 합니다.</li>
<li>데드 레터 큐에서는 전송 실패 메세지를 적재하여 관리합니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/3976ea34-caa1-4378-8f82-2fe1ef7879c6/image.png" alt=""></p>
<h3 id="데드-레터-exchange-구성하기">데드 레터 Exchange 구성하기</h3>
<p>원래는 참고한 게시글을 기반으로 대부분 작업하려고 했으나 생각보다 잘 되지않았습니다. 그래서 방향을 틀어서 ttl 시간이 만료되면 dead Queue로 이동하는게 아닌 Exception을 의도적으로 발생시켜 dead Queue로 보내는 방법을 생각했습니다.</p>
<pre><code class="language-java">// RabbitMQ 설정

import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 설정 파일
 */
@Configuration
@RequiredArgsConstructor
public class RabbitmqConfig {

    private final RabbitmqExchangeInfo rabbitmqExchangeInfo;

    /**
     * 메세지 성공을 못하는 경우 direct.queue로 라우팅하고 dead.queue로 이동
     */
    @Bean
    public DirectExchange deadExchange(){
        return ExchangeBuilder
                .directExchange(rabbitmqExchangeInfo.get_DEAD_EXCHANGE_NAME())
                .build();
    }

    /**
     * Direct Exchange 구성 : direct.queue를 라우팅 하는데 사용
     */
    @Bean
    public DirectExchange directExchange(){
        return ExchangeBuilder
                .directExchange(rabbitmqExchangeInfo.get_DIRECT_EXCHANGE_NAME())
                .build();
    }


    /**
     * deadQueue와 라우팅 키(Routing key)를 기반으로 바인딩 수행.
     *
     * @param deadQueue    성공적으로 처리하지 못한 메시지를 담는 공간
     * @param deadExchange 성공적으로 처리하지 못한 메시지를 라우팅
     */
    @Bean
    public Binding deadBinding(Queue deadQueue, DirectExchange deadExchange){
        return BindingBuilder
                .bind(deadQueue)
                .to(deadExchange)
                .with(rabbitmqExchangeInfo.get_DEAD_ROUTING_KEY());
    }

    /**
     * Direct Exchange 와 direct Queue 간의 바인딩을 수행합니다.
     *
     * @param directQueue    메시지를 담을 큐
     * @param directExchange 메시지를 담기 위한 라우팅
     */
    @Bean
    public Binding directBinding(Queue directQueue, DirectExchange directExchange){
        return BindingBuilder
                .bind(directQueue)
                .to(directExchange)
                .with(rabbitmqExchangeInfo.get_DIRECT_ROUTING_KEY());
    }


    /**
     * 만약 메세지 전송에 실패하면 해당 Queue(dead.queue) 로 이동
     *
     * @return dead Queue
     */
    @Bean
    public Queue deadQueue(){
        return new Queue(rabbitmqExchangeInfo.get_DEAD_QUEUE_NAME(), true);
    }

    /**
     * directQueue 이름의 큐를 구성
     * - 해당 큐에서는 속성 값으로 x-dead-letter-exchange가 발생시 rabbit.dead 로 라우팅
     * - 해당 큐에서는 속성 값으로 x-dead-letter-routing-key를 통해 Direct Queue의 라우팅 키를 전달하여 라우팅
     *
     * @return direct Queue
     */
    @Bean
    public Queue directQueue(){
        return QueueBuilder.durable(rabbitmqExchangeInfo.get_DIRECT_QUEUE_NAME())
                .withArgument(&quot;x-dead-letter-exchange&quot;, rabbitmqExchangeInfo.get_DEAD_EXCHANGE_NAME())
                .withArgument(&quot;x-dead-letter-routing-key&quot;, rabbitmqExchangeInfo.get_DEAD_ROUTING_KEY())
                .build();
    }

}</code></pre>
<pre><code class="language-java">// 주요 리스너 설정

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.AmqpRejectAndDontRequeueException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

/**
 * 큐에 등록된 메세지 리턴 component
 */
@Slf4j
@Component
public class RabbitmqMessage {

    // direct Queue 메세지
    @RabbitListener(queues = &quot;direct.queue&quot;)
    public void directMessage(Message message){
        String body = new String(message.getBody(), StandardCharsets.UTF_8);
        log.info(&quot;direct.queue 내의 메시지 반환 : {}&quot;, body);
        // 해당 Exception은 바로 해당 Queue에 등록된 Dead Queue로 이동시키는 Exception 입니다.
        throw new AmqpRejectAndDontRequeueException(&quot;Dead Queue 테스트&quot;);
    }

    // ttl이 적용된 direct Queue 메세지
    @RabbitListener(queues = &quot;direct.queue.ttl&quot;)
    public void directTTLMessage(Message message){
        String body = new String(message.getBody(), StandardCharsets.UTF_8);
        log.info(&quot;direct.queue.ttl 내의 메시지 반환 : {}&quot;, body);
    }

    // dead Queue 메세지
    @RabbitListener(queues = &quot;dead.queue&quot;)
    public void deadDirectMessage(Message message){
        String body = new String(message.getBody(), StandardCharsets.UTF_8);
        log.info(&quot;dead.queue 내의 메시지 반환 : {}&quot;, body);
    }

}
</code></pre>
<p>Service 혹은 Controller 파일은 기존과 동일하여 게시글에는 넣지는 않았습니다.</p>
<blockquote>
<p><code>AmqpRejectAndDontRequeueException</code>
이번에 새롭게 알게된 Exception 입니다. 해당 Exception은 다른 일반적인 Exception과는 다르게 만약 리스너에 해당하는 Queue에 등록된 dead queue로 이동 시킵니다.</p>
</blockquote>
<blockquote>
<p><code>spring.rabbitmq.listener.simple.default-requeue-rejected=false</code>
spring boot의 .properties 파일의 설정중 하나로 해당 설정을 추가하면 리스너에서 자동으로 예외 혹은 에러가 발생하면 자동으로 해당 queue에 둥록된 dead.queue로 이동 하도록 해줍니다. 그리고 해당 설정을 해주면 의도적으로 AmqpRejectAndDontRequeueException 을 따로 안해주어도 됩니다.</p>
</blockquote>
<h3 id="실행결과">실행결과</h3>
<img src="https://velog.velcdn.com/images/half-phycho/post/c7a96441-1d08-4553-9c60-39bffe959700/image.png">

<p>만약 Exception을 해제한다면 direct.queue 리스너로 정상적으로 넘어가 다음과 같이 로그가 출력되는 것을 확인할 수 있습니다.</p>
<img src="https://velog.velcdn.com/images/half-phycho/post/69043afe-249b-4a33-9b79-8c080625d130/image.png">

<p>만약 의도적으로 Exception을 날렸다면 처음에는 direct.queue가 나오고 그 다음에는 의도한대로 Exception이 동작하고 dead.queue로 이동하는 것을 확인 할 수 있습니다.</p>
<br>

<h2 id="📘-ttl-및-데드-레터링-queue-설정-결과">📘 TTL 및 데드 레터링 Queue 설정 결과</h2>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/737ebb92-fe5e-490e-94e3-08e962fdd89b/image.png" alt=""></p>
<p>만약 설정이 잘되었다면 Queue에는 다음과 같이 설정이 나오게 됩니다.</p>
<ul>
<li>DLX : 이동할 데드 레터링 Exchange 이름 입니다.</li>
<li>DLK : 이동할 데드 레터링의 라우팅 키 입니다.</li>
<li>TTL : 앞에서 설명한 Time To Live 약자 입니다.</li>
</ul>
<br>

<h2 id="😊-새롭게-알게-된-점">😊 새롭게 알게 된 점</h2>
<p>오히려 예시대로 안되었기에 <code>AmqpRejectAndDontRequeueException</code> 라는 새로운 Exception에 대해 알게 되었고 RabbitMQ의 추가 설정에 대해 알 수 있었습니다. 가끔은 실패해도 거기 안에서 본인이 몰랐던 새로운걸 알게되는 거는 더더욱 의미 있는거 같다고 생각합니다.</p>
<br>

<h2 id="💾-github-저장소">💾 Github 저장소</h2>
<p><a href="https://github.com/delight-HK3/rabbitmq-test">https://github.com/delight-HK3/rabbitmq-test</a></p>
<br>

<h2 id="📋-참고-사이트">📋 참고 사이트</h2>
<p><a href="https://www.rabbitmq.com/docs/dlx">https://www.rabbitmq.com/docs/dlx</a></p>
<p><a href="https://ko.wikipedia.org/wiki/%EB%B6%80%EC%A0%95_%EC%9D%91%EB%8B%B5">https://ko.wikipedia.org/wiki/%EB%B6%80%EC%A0%95_%EC%9D%91%EB%8B%B5</a></p>
<p><a href="https://adjh54.tistory.com/501?category=1187853">https://adjh54.tistory.com/501?category=1187853</a></p>
<p><a href="https://www.cloudamqp.com/blog/when-and-how-to-use-the-rabbitmq-dead-letter-exchange.html">https://www.cloudamqp.com/blog/when-and-how-to-use-the-rabbitmq-dead-letter-exchange.html</a></p>
<p>(항상 감사합니다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RabbitMQ] Queue에 존재하는 메세지 조회]]></title>
            <link>https://velog.io/@half-phycho/RabbitMQ-Queue%EC%97%90-%EC%A1%B4%EC%9E%AC%ED%95%98%EB%8A%94-%EB%A9%94%EC%84%B8%EC%A7%80-%EC%A1%B0%ED%9A%8C</link>
            <guid>https://velog.io/@half-phycho/RabbitMQ-Queue%EC%97%90-%EC%A1%B4%EC%9E%AC%ED%95%98%EB%8A%94-%EB%A9%94%EC%84%B8%EC%A7%80-%EC%A1%B0%ED%9A%8C</guid>
            <pubDate>Sun, 28 Dec 2025 06:25:17 GMT</pubDate>
            <description><![CDATA[<h2 id="❓작성계기">❓작성계기</h2>
<p>RabbitMQ를 공부하면서 현재 Queue에 어떤 메세지가 있는지 조회하고 싶었는데 이상하게 조회가 안되었습니다. 다행히 해결했고 이 에피소드를 기록으로 남기고 공유하면 좋을거 같아서 글을 써봅니다. </p>
<br>

<h2 id="🔎-문제-및-해결">🔎 문제 및 해결</h2>
<p>문제는 다음과 같았습니다. RabbitMQ로 메세지를 보내는건 성공했으나 이상하게 보낸메세지를 조회하려고 하니까 조회가 안되는 현상이었습니다. 
하단에 작성한 코드는 RabbitMQ에서 조회하는 코드 입니다.</p>
<pre><code class="language-java">/**
 * Direct Queue에 등록되어 있는 메세지 Queue 불러오기
 */
public void receiveDirectMessage() {

    Object message = rabbitTemplate.receiveAndConvert(rabbitmqExchangeInfo.get_DIRECT_QUEUE_NAME());
    if (message != null) {
        System.out.println(&quot;Received: &quot; + message);
    }

}</code></pre>
<p>이처럼 Direct Exchange 방식으로 보낸 Queue를 조회하려고 하니 null 값이 리턴되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/4acaef70-9609-4f26-b1c1-be2cb71d7457/image.png" alt=""></p>
<p>그런데 생각해보니 Message Queue에 어떤메세지를 보냈는지 확인하고 싶어서 <code>@RabbitListener</code> 를 설정한 클래스가 있는게 생각이 났습니다.</p>
<pre><code class="language-java">@RabbitListener(queues = &quot;direct.queue&quot;)
public void directMessage(String message){
    log.info(&quot;direct.queue 내의 메시지 반환 : {}&quot;, message);
}</code></pre>
<p>혹시 이부분 때문에 에러가 나는게 아닌가 싶어서 해당 리스너를 주석처리하고 실행시켜본 결과</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/68d881d6-f609-493a-aa5c-b8a75e223a53/image.png" alt=""></p>
<p>이렇게 보낸 메세지를 조회할 수 있었습니다, 저는 지금까지 이렇게 진행된다고 생각했습니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/808a3e43-2796-4a5f-a8a3-938d2ea80d1f/image.png" alt=""></p>
<p>하지만 실제로는 하단의 이미지에 작성한 순서대로 실행되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/9d8922e4-fe28-43a2-bd8b-8bb4763d94ce/image.png" alt=""></p>
<h2 id="❗깨달은-점">❗깨달은 점</h2>
<p>RabbitMQ에서 메세지를 보내고 해당하는 메세지를 Queue에 차곡차곡 저장한다고 생각했습니다. 하지만 RabbitMQ는 결국 단순 메세지 전달 브로커일뿐 자체적으로 저장하는 기능은 Delay Queue라도 사용하지 않는한 힘들다는걸 다시한번 깨달았습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RabbitMQ] Exchange 종류대로 사용해보기]]></title>
            <link>https://velog.io/@half-phycho/RabbitMQ-Exchange-%EC%A2%85%EB%A5%98%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@half-phycho/RabbitMQ-Exchange-%EC%A2%85%EB%A5%98%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 06 Dec 2025 07:37:14 GMT</pubDate>
            <description><![CDATA[<h2 id="❓-들어가기에-앞서">❓ 들어가기에 앞서</h2>
<p>늘 그렇듯이 회사 업무가 바빠서 공부는 고사하고 새벽까지 일하는 경우가 많아져 공부를 소홀히 하게되었습니다. 하지만 틈틈히 조금씩이라도 자료를 수집하고 모으다보니 그래도 조금만 작업하면 되었기에 과거의 저에게 감사하다고 전하고 싶었습니다.
<br></p>
<h2 id="📚-사전준비-및-읽으면-도움되는-글">📚 사전준비 및 읽으면 도움되는 글</h2>
<blockquote>
<p><a href="https://velog.io/@half-phycho/RabbitMQ-SpringBoot%EB%A1%9C-RabbitMQ-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0">https://velog.io/@half-phycho/RabbitMQ-SpringBoot로-RabbitMQ-사용해보기</a>
Spring Boot로 RabbitMQ기본 세팅하는 방법 입니다. </p>
</blockquote>
<blockquote>
<p><a href="https://velog.io/@half-phycho/RabbitMQ-AMQP%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80">https://velog.io/@half-phycho/RabbitMQ-AMQP란-무엇인가</a>
AMQP에 대해 정리한 글 입니다. 이번에는 Exchange를 다양하게 써볼 예정이라 도움이 되겠습니다.</p>
</blockquote>
<br>

<h2 id="💡rabbitmq로-다양한-exchange-써보기">💡RabbitMQ로 다양한 Exchange 써보기</h2>
<p>지난번에는 RabbitMQ로 기본적인 Direct Exchange를 보냈습니다. 이번에는 Direct Exchange를 포함한 여러 Exchange를 보내보면서 비교해보는 시간을 가져보도록 하겠습니다.</p>
<br>

<h3 id="❗-변경된-프로젝트-구조">❗ 변경된 프로젝트 구조</h3>
<p><a href="https://velog.io/@half-phycho/RabbitMQ-SpringBoot%EB%A1%9C-RabbitMQ-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0">이전 게시글</a> 에서는 RabbitMQ 연결세팅과 Queue, Binding을 모두 하나의 클래스에서 만들었지만 이번에는 역할을 분리하는 등 여러가지 부분이 변경되었습니다.</p>
<ul>
<li>RabbitMQ Connection 파일 별도로 관리</li>
<li>Queue, Binding, Exchange에 사용될 이름을 오타 방지를 위해 properties에 등록하고 가져다 사용하는 형식으로 변경</li>
<li>기존 RabbitmqConfig.java 에는 Queue, Binding, Exchange 선언 및 생성을 집중으로 관리 </li>
<li>RabbitMQ에 Exchange별로 보낸 메세지를 출력하는 코드추가</li>
<li>자체적인 Exception Handler, ApiResponse 구축</li>
</ul>
<br>

<p><strong>RabbitmqConnConfig</strong></p>
<pre><code class="language-java">import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 연결 설정
 */
@Configuration
public class RabbitmqConnConfig {

    @Value(&quot;${spring.rabbitmq.host}&quot;)
    private String host; // 접속 호스트

    @Value(&quot;${spring.rabbitmq.port}&quot;)
    private Integer port; // 접속 포트번호

    @Value(&quot;${spring.rabbitmq.username}&quot;)
    private String username; // 접속 아이디

    @Value(&quot;${spring.rabbitmq.password}&quot;)
    private String password; // 접속 비밀번호

    /**
     * RabbitMQ와 메시지 통신을 담당하는 클래스
     */
    @Bean
    public RabbitTemplate rabbitTemplate(){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
        rabbitTemplate.setMessageConverter(messageConverter());

        return rabbitTemplate;
    }

    /**
     * RabbitMQ와 연결을 관리하는 클래스
     */
    @Bean
    public ConnectionFactory connectionFactory(){
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
        connectionFactory.setHost(host);
        connectionFactory.setPort(port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);

        return connectionFactory;
    }

    /**
     * RabbitMQ 메시지를 JSON형식으로 보내고 받을 수 있다.
     */
    @Bean
    public Jackson2JsonMessageConverter messageConverter() {

        ObjectMapper objectMapper = new ObjectMapper()
                // 날짜 관련 타임스탬프 직렬화를 막고 ISO-8601 형태로 포맷된다.
                .configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, true)
                .registerModule(dateTimeModule()); // Java에서 시간을 처리하기위한 모듈

        return new Jackson2JsonMessageConverter(objectMapper);
    }

    /**
     * 자바 시간 모듈 등록
     */
    @Bean
    public JavaTimeModule dateTimeModule() {
        return new JavaTimeModule();
    }

}
</code></pre>
<p><strong>RabbitmqExchangeInfo</strong></p>
<pre><code class="language-java">import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * RabbitMQ에서 사용하는 exchange이름과 Queue정보 모아둔 Component
 */
@Component
public class RabbitmqExchangeInfo {

    @Value(&quot;${rabbitmq.direct.exchange.name}&quot;)
    private String DIRECT_EXCHANGE_NAME; // direct Exchange

    @Value(&quot;${rabbitmq.direct.exchange.key}&quot;)
    private String DIRECT_EXCHANGE_KEY; // direct Exchange Key

    @Value(&quot;${rabbitmq.direct.queue.name}&quot;)
    private String DIRECT_QUEUE_NAME;   // direct Queue name

    public String get_DIRECT_EXCHANGE_NAME() {
        return DIRECT_EXCHANGE_NAME;
    }

    public String get_DIRECT_EXCHANGE_KEY() {
        return DIRECT_EXCHANGE_KEY;
    }

    public String get_DIRECT_QUEUE_NAME() {
        return DIRECT_QUEUE_NAME;
    }

}</code></pre>
<p><strong>RabbitmqConfig</strong></p>
<pre><code class="language-java">import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 설정 파일
 */
@Configuration
@RequiredArgsConstructor
public class RabbitmqConfig {

    private final RabbitmqExchangeInfo rabbitmqExchangeInfo;

    // 공통적으로 RabbitMQ가 재부팅되도 Exchange가 삭제안되고 동시에 대기열에 남도록 설정
    @Bean
    public DirectExchange directExchange() {
        // DIRECT_EXCHANGE_NAME 이름의 direct Exchange 구성
        return ExchangeBuilder
                .directExchange(rabbitmqExchangeInfo.get_DIRECT_EXCHANGE_NAME())
                .build();
    }

    // 공통적으로 RabbitMQ가 재부팅되도 Queue 대기열에 남도록 설정
    @Bean
    public Queue directQueue() {
        return new Queue(rabbitmqExchangeInfo.get_DIRECT_QUEUE_NAME(), true);
    }

    /**
     * Queue와 DirectExchange를 바인딩
     */
    @Bean
    public Binding directBinding(DirectExchange directExchange, Queue directQueue) {
        // queue까지 가는 바인딩 Exchange 타입을 directExchange로 지정하고 test.key 이름으로 바인딩 구성
        return BindingBuilder
                .bind(directQueue)
                .to(directExchange)
                .with(rabbitmqExchangeInfo.get_DIRECT_EXCHANGE_KEY());
    }

}</code></pre>
<p><strong>RabbitmqMessage</strong></p>
<pre><code class="language-java">import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 큐에 등록된 메세지 리턴 component
 */
@Slf4j
@Component
public class RabbitmqMessage {

    @RabbitListener(queues = &quot;direct.queue&quot;)
    public void directMessage(String message){
        log.info(&quot;direct.queue 내의 메시지 반환 : {}&quot;, message);
    }

}</code></pre>
<p>(기타 Fanout, Topic, Header 방식은 업로드한 GitHub Repository를 참고하시기 바랍니다. 위의 코드는 게시글이 길어지는 것을 방지하고자 일부러 Direct Exchange관련만 코드에 남겼습니다.)</p>
<blockquote>
<p>해당 프로젝트가 있는 Github 주소 입니다.
<a href="https://github.com/delight-HK3/rabbitmq-test">https://github.com/delight-HK3/rabbitmq-test</a></p>
</blockquote>
<br>

<h3 id="▶️-해당-프로젝트-실행">▶️ 해당 프로젝트 실행</h3>
<p>만약 해당 프로젝트를 실행시키면 자동으로 관련 Exchange와 Queue가 생성되는 것을 확인할 수 있습니다.</p>
<blockquote>
<p>❗<strong>여기서 잠깐! 어째서 자동으로 Binding, Queue, Exchange가 생성되는 것 인가?</strong>
Spring Boot에서 프로젝트를 만들때 Gradle에서 <code>pring-boot-starter-amqp</code>가 프로젝트에 있는경우 자동으로 <code>AmqpAdmin</code> 이라는 Bean을 구성하고 등록합니다. 그리고 <code>@Configuration</code> 클래스 내에서 <code>@Bean</code>으로 등록된 Queue, Exchange, Binding 설정들이 자동으로 <code>AmqpAdmin</code>의 자동 선언 대상이 됩니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/6cbe5977-8e8f-49cb-acad-16848fa2abb6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/a2299af2-be95-4a90-a577-5507a8fc5d87/image.png" alt=""></p>
<h3 id="📕-direct-exchange">📕 Direct Exchange</h3>
<p>Direct Exchange는 이전에 간단하게 RabbitMQ에 대해 시연해본 방법으로 요청시 Exchange의 이름과 Exchange에 등록되어 있는 라우팅키가 일치하면 해당 Exchange와 연결된 Queue로 보내는 방식입니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/fa8f6241-8a5b-4dd6-a739-81edc923ddf8/image.png" alt=""></p>
<h4 id="exchange-queue-binding-생성-과정">Exchange, Queue, Binding 생성 과정</h4>
<blockquote>
<p><strong>1. Exchange 생성</strong> : Direct 타입의 Exchange를 생성합니다.
<strong>2. Queue 생성</strong> : Direct Exchange로 받은 데이터를 적재할 Queue를 생성합니다.
<strong>3. Binding 생성</strong> : 앞에서 생성한 Exchange와 Queue간 바인딩을 설정하고 Routing Key 기반으로 바인딩을 수행합니다.</p>
</blockquote>
<p><strong>코드예시</strong></p>
<pre><code class="language-java">private final RabbitmqExchangeInfo rabbitmqExchangeInfo;

@Bean
public DirectExchange directExchange() {
    // DIRECT_EXCHANGE_NAME 이름의 direct Exchange 구성
    return ExchangeBuilder
                .directExchange(rabbitmqExchangeInfo.get_DIRECT_EXCHANGE_NAME())
                .build();
}

@Bean
public Queue directQueue() {
    // 여기서 true는 RabbitMQ가 재부팅되도 Queue 대기열에 남도록 하는 설정입니다.
    return new Queue(rabbitmqExchangeInfo.get_DIRECT_QUEUE_NAME(), true);
}

@Bean
public Binding directBinding(DirectExchange directExchange, Queue directQueue) {
    // queue까지 가는 바인딩 Exchange 타입을 directExchange로 지정하고 test.key 이름으로 바인딩 구성
    return BindingBuilder
                .bind(directQueue)
                .to(directExchange)
                .with(rabbitmqExchangeInfo.get_DIRECT_EXCHANGE_KEY());
}</code></pre>
<br>

<h4 id="요청-및-결과">요청 및 결과</h4>
<p><strong>Controller</strong></p>
<pre><code class="language-java">@PostMapping(&quot;/direct&quot;)
public ResponseEntity&lt;ApiResponse&gt; sendDirectMessage(@RequestBody MessageDTO messageDTO) {
    String resultMessage = messsageService.sendDirectMessage(messageDTO);

    return ApiResponse.success(resultMessage,200);
}</code></pre>
<p><strong>Service</strong></p>
<pre><code class="language-java">/**
  * Direct Exchange 방식 메세지 전송
  *
  * @param messageDTO 메세지 DTO
  * @return 성공 시 &quot;success_direct&quot; 리턴
  */
public String sendDirectMessage(MessageDTO messageDTO) {
    try{
        ObjectMapper objectMapper = new ObjectMapper();
        String objectToJson = objectMapper.writeValueAsString(messageDTO);

        rabbitTemplate.convertAndSend(rabbitmqExchangeInfo.get_DIRECT_EXCHANGE_NAME()
                                        , rabbitmqExchangeInfo.get_DIRECT_EXCHANGE_KEY()
                                        , objectToJson);
    } catch (JsonProcessingException ex) {
        log.error(&quot;parsing error : {}&quot;, ex.getMessage(), ex);
    }

    return &quot;success_direct&quot;;
}</code></pre>
<p><strong>요청결과</strong></p>
<img src="https://velog.velcdn.com/images/half-phycho/post/ae722ddb-2004-4edf-9237-b4531a36231e/image.png">

<ol>
<li>먼저 API 요청결과를 리턴 받습니다.</li>
</ol>
<img src="https://velog.velcdn.com/images/half-phycho/post/46b19128-28fd-47b1-9c7e-5149b4397eef/image.png">

<ol start="2">
<li>해당 로그는 사전에 <code>@RabbitListener</code>로 Direct Exchange를 통해 <code>direct.queue</code>에 데이터가 들어오면 발동하는 이벤트인데 해당 로그가 나왔다는 의미는 데이터가 Queue 까지 전달이 잘되었다는 의미입니다.</li>
</ol>
<br>

<h3 id="📙-fanout-exchange">📙 Fanout Exchange</h3>
<p>Fanout Exchange는 Exchange에 라우팅 키와 관계없이 Exchange에 연결된 모든 Queue에 메세지를 보내는 방식입니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/668b1b57-8234-48b3-86c6-fbf81591eaf8/image.png" alt=""></p>
<h4 id="exchange-queue-binding-생성-과정-1">Exchange, Queue, Binding 생성 과정</h4>
<blockquote>
<p><strong>1. Exchange 생성</strong> : Fanout 타입의 Exchange를 생성합니다.
<strong>2. Queue 생성</strong> : Fanout Exchange로 받은 데이터를 적재할 Queue를 2개 생성합니다.
<strong>3. Binding 생성</strong> : 앞에서 생성한 Exchange와 Queue간 바인딩을 설정하고 Exchange는 연결설정된 Queue에게만 메세지를 보냅니다.</p>
</blockquote>
<p><strong>코드예시</strong></p>
<pre><code class="language-java">private final RabbitmqExchangeInfo rabbitmqExchangeInfo;

@Bean
public FanoutExchange fanoutExchange() {
    // FANOUT_EXCHANGE_NAME 이름의 fanout Exchange 구성
    return ExchangeBuilder
                .fanoutExchange(rabbitmqExchangeInfo.get_FANOUT_EXCHANGE_NAME())
                .build();
}


@Bean
public Queue fanoutQueueOne() {
    return new Queue(rabbitmqExchangeInfo.get_FANOUT_QUEUE_NAME_ONE(), true);
}

@Bean
public Queue fanoutQueueTwo() {
    return new Queue(rabbitmqExchangeInfo.get_FANOUT_QUEUE_NAME_TWO(), true);
}


/**
  * Queue(fanoutQueueOne)와 FanoutExchange 바인딩
  * Fanout 방식은 Exchange와 연결된 모든 Queue에 보내는 방식으로
  * FanoutExchange와 연결된 fanoutQueueOne, fanoutQueueTwo에게 메세지를 보낸다.
  */
@Bean
public Binding fanoutBindingOne(FanoutExchange fanoutExchange, Queue fanoutQueueOne) {
    return BindingBuilder
                .bind(fanoutQueueOne)
                .to(fanoutExchange);
}

/**
  * Queue(fanoutQueueTwo)와 FanoutExchange 바인딩
  * Fanout 방식은 Exchange와 연결된 모든 Queue에 보내는 방식으로
  * FanoutExchange와 연결된 fanoutQueueOne, fanoutQueueTwo에게 메세지를 보낸다.
  */
@Bean
public Binding fanoutBindingTwo(FanoutExchange fanoutExchange, Queue fanoutQueueTwo) {
    return BindingBuilder
                .bind(fanoutQueueTwo)
                .to(fanoutExchange);
}</code></pre>
<br>

<h4 id="요청-및-결과-1">요청 및 결과</h4>
<p><strong>Controller</strong></p>
<pre><code class="language-java">@PostMapping(&quot;/fanout&quot;)
public ResponseEntity&lt;ApiResponse&gt; sendFanoutMessage(@RequestBody MessageDTO messageDTO){
    String resultMessage = messsageService.sendFanoutMessage(messageDTO);

    return ApiResponse.success(resultMessage, 200);
}</code></pre>
<p><strong>Service</strong></p>
<pre><code class="language-java">/**
  * Fanout Exchange 방식 메세지 전송
  *
  * @param messageDTO 메세지 DTO
  * @return 성공 시 &quot;success_fanout&quot; 리턴
  */
public String sendFanoutMessage(MessageDTO messageDTO) {
    try{
        ObjectMapper objectMapper = new ObjectMapper();
        String objectToJson = objectMapper.writeValueAsString(messageDTO);

        rabbitTemplate.convertAndSend(rabbitmqExchangeInfo.get_FANOUT_EXCHANGE_NAME()
                                        , &quot;&quot;
                                        , objectToJson);
    } catch (JsonProcessingException ex) {
        log.error(&quot;parsing error : {}&quot;, ex.getMessage(), ex);
    }

    return &quot;success_fanout&quot;;
}</code></pre>
<p><strong>요청결과</strong></p>
<img src="https://velog.velcdn.com/images/half-phycho/post/3e4a87d7-2292-470e-93a7-221075f55ff0/image.png">

<ol>
<li>먼저 API 요청결과를 리턴 받습니다.</li>
</ol>
<img src="https://velog.velcdn.com/images/half-phycho/post/da33d083-0d6d-4aba-9a81-a2239b5e09b1/image.png">

<ol start="2">
<li>마찬가지로 해당 로그도 해당 Queue에게 전달 되었기 때문에 출력되는 로그 입니다. 다른점은 Direct Exchange와 다르게 Exchange와 바인딩 된 Queue에게 일괄적으로 메세지를 전송했다는 점 입니다.</li>
</ol>
<br>

<h3 id="📗-topic-exchange">📗 Topic Exchange</h3>
<p>Topic Exchange 방식은 라우팅 키에 등록된 패턴에 따라 메세지를 보내는 방식으로 보통 키의 구분자를 <code>*</code>를 주로 사용하는 것 같은데 경우에 따라 <code>!</code>,<code>#</code>등 방법은 다양합니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/4cc9ee69-fd21-4e5d-8926-cf54f675b6c8/image.png" alt=""></p>
<h4 id="exchange-queue-binding-생성-과정-2">Exchange, Queue, Binding 생성 과정</h4>
<blockquote>
<p><strong>1. Exchange 생성</strong> : Topic 타입의 Exchange를 생성합니다.
<strong>2. Queue 생성</strong> : Topic Exchange로 받은 데이터를 적재할 Queue를 2개 생성합니다.
<strong>3. Binding 생성</strong> : 앞에서 생성한 Exchange와 Queue간 바인딩을 설정하고 Exchange는 라우팅키 규칙과 맞는 Queue에게만 메세지를 보냅니다.</p>
</blockquote>
<p><strong>코드예시</strong></p>
<pre><code class="language-java">private final RabbitmqExchangeInfo rabbitmqExchangeInfo;

@Bean
public TopicExchange topicExchange() {
    // TOPIC_EXCHANGE_NAME 이름의 topic Exchange 구성
    return ExchangeBuilder
                .topicExchange(rabbitmqExchangeInfo.get_TOPIC_EXCHANGE_NAME())
                .build();
}


@Bean
public Queue topicQueue() {
    return new Queue(rabbitmqExchangeInfo.get_TOPIC_QUEUE_NAME(), true);
}

// 편의상 fanout 전용으로 사용한 Queue를 사용하겠습니다
@Bean
public Queue fanoutQueueTwo() {
    return new Queue(rabbitmqExchangeInfo.get_FANOUT_QUEUE_NAME_TWO(), true);
}


/**
  * topic Exchange 와 topicQueue간 바인딩
  * producer에서 topic.send. 으로 시작하는 라우팅 키를 보내주면 라우팅 키 규칙과 같은 Exchange와 연결
  */
@Bean
public Binding topicBinding(TopicExchange topicExchange, Queue topicQueue){
        return BindingBuilder
                .bind(topicQueue)
                .to(topicExchange)
                .with(&quot;topic.send.*&quot;);
}

@Bean
public Binding topicBinding2(TopicExchange topicExchange, Queue fanoutQueueTwo){
        return BindingBuilder
                .bind(fanoutQueueTwo)
                .to(topicExchange)
                .with(&quot;topic.send.tester&quot;);
}</code></pre>
<br>

<h4 id="요청-및-결과-2">요청 및 결과</h4>
<p><strong>Controller</strong></p>
<pre><code class="language-java">@PostMapping(&quot;/topic&quot;)
public ResponseEntity&lt;ApiResponse&gt; sendTopicMessage(@RequestBody MessageDTO messageDTO){
    String resultMessage = messsageService.sendTopicMessage(messageDTO);

    return ApiResponse.success(resultMessage, 200);
}</code></pre>
<p><strong>Service</strong></p>
<pre><code class="language-java">/**
  * Topic Exchange 방식 메세지 전송
  *
  * @param messageDTO 메세지 DTO
  * @return 성공 시 &quot;success_topic&quot; 리턴
  */
public String sendTopicMessage(MessageDTO messageDTO) {
    try{
        ObjectMapper objectMapper = new ObjectMapper();
        String objectToJson = objectMapper.writeValueAsString(messageDTO);

        rabbitTemplate.convertAndSend(rabbitmqExchangeInfo.get_TOPIC_EXCHANGE_NAME()
                                        , &quot;topic.send.test&quot;
                                        , objectToJson);
    } catch (JsonProcessingException ex) {
        log.error(&quot;parsing error : {}&quot;, ex.getMessage(), ex);
    }

    return &quot;success_topic&quot;;
}</code></pre>
<p><strong>요청결과</strong></p>
<img src="https://velog.velcdn.com/images/half-phycho/post/cecfb675-c5b2-417e-9649-3d8e622f84df/image.png">

<ol>
<li>먼저 API 요청결과를 리턴 받습니다.</li>
</ol>
<img src="https://velog.velcdn.com/images/half-phycho/post/7d4b19a9-e971-46e4-ada4-4a9b2e8a90fc/image.png">

<ol start="2">
<li>마찬가지로 해당 로그도 해당 Queue에게 전달되었기 때문에 출력되는 로그입니다. 다른 점은 분명 2개의 Queue를 만들고 바인딩까지 해주었는데 topic.queue에만 메세지가 들어왔고 fanout.queue.two 에는 데이터가 안 들어왔습니다. 그 이유는 Service에서 설정한 라우팅 키 때문인데 <code>sendTopicMessage</code>에는 routingKey를 <code>topic.send.test</code>로 설정했습니다. Topic Exchange는 특성상 라우팅 키의 규칙을 따르거나 라우팅 키가 정확히 일치해야 통신이 됩니다. 그런데 <code>fanoutQueueTwo</code>에 설정된 라우팅키는 <code>topic.send.tester</code>입니다. 그래서 <code>topicBinding2</code>으로는 메세지가 전송이 안 된 것입니다.</li>
</ol>
<br>

<h3 id="📘-header-exchange">📘 Header Exchange</h3>
<p>Header Exchange는 요청 시 HTTP Header에 지정된 value와 키가 있으면 메세지를 보내는 Exchange 방식입니다. 지금까지는 Routing Key가 해왔던 역할을 Header가 대신해준다고 생각하면 됩니다. 그래서 RabbitMQ 관리자 페이지에서 Header Exchange를 조회하면 다음과 같이 조회됩니다.
<img src="https://velog.velcdn.com/images/half-phycho/post/38e63ded-c4a9-4223-8540-d136bf8c707c/image.png" alt=""></p>
<blockquote>
<p>여기서 (key):true 이렇게 되어있는게 이건 해당 키가 있으면 어떤 value여도 통과 시켜준다는 의미입니다.</p>
</blockquote>
<h4 id="exchange-queue-binding-생성-과정-3">Exchange, Queue, Binding 생성 과정</h4>
<blockquote>
<p><strong>1. Exchange 생성</strong> : Header 타입의 Exchange를 생성합니다.
<strong>2. Queue 생성</strong> : Header Exchange로 받은 데이터를 적재할 Queue를 1개 생성합니다.
<strong>3. Binding 생성</strong> : 앞에서 생성한 Exchange와 Queue 간 바인딩을 설정하고 Header Key를 지정해 줍니다, 만약 요청 시해당하는 key와 value 값이 있으면 연결된 Queue로 메시지를 전송합니다.</p>
</blockquote>
<p><strong>코드예시</strong></p>
<pre><code class="language-java">private final RabbitmqExchangeInfo rabbitmqExchangeInfo;

@Bean
public HeadersExchange headersExchange() {
    // HEADER_EXCHANGE_NAME 이름의 header Exchange 구성
    return ExchangeBuilder
                .headersExchange(rabbitmqExchangeInfo.get_HEADER_EXCHANGE_NAME())
                .build();
}


@Bean
public Queue headersQueue() {
    return new Queue(rabbitmqExchangeInfo.get_HEADER_QUEUE_NAME(), true);
}


/**
  * headers Exchange 와 headersQueue간 바인딩
  * headersExchange 방식으로 headersQueue와 Header값을 조건으로 바인딩 수행
  */
@Bean
public Binding headerBinding(HeadersExchange headersExchange, Queue headersQueue){
        return BindingBuilder
                .bind(headersQueue)
                .to(headersExchange)
                .where(&quot;x-execute-key&quot;).matches(true);
                // x-execute-key에 들어온 모든 값을 허용한다는 의미
}
</code></pre>
<br>

<h4 id="요청-및-결과-3">요청 및 결과</h4>
<p><strong>Controller</strong></p>
<pre><code class="language-java">@PostMapping(&quot;/header&quot;)
public ResponseEntity&lt;ApiResponse&gt; sendHeaderMessage(@RequestBody MessageDTO messageDTO){
    String resultMessage = messsageService.sendHeaderMessage(messageDTO);

    return ApiResponse.success(resultMessage, 200);
}</code></pre>
<p><strong>Service</strong></p>
<pre><code class="language-java">/**
  * Header Exchange 방식 메세지 전송
  *
  * @param messageDTO 메세지 DTO
  * @return 성공 시 &quot;success_header&quot; 리턴
  */
public String sendHeaderMessage(MessageDTO messageDTO){
    try{
        ObjectMapper objectMapper = new ObjectMapper();
        String objectToJson = objectMapper.writeValueAsString(messageDTO);

        rabbitTemplate.convertAndSend(rabbitmqExchangeInfo.get_HEADER_EXCHANGE_NAME()
                    ,&quot;&quot;
                    ,objectToJson);
    } catch (JsonProcessingException ex) {
        log.error(&quot;parsing error : {}&quot;, ex.getMessage(), ex);
    }

    return &quot;success_header&quot;;
}</code></pre>
<p><strong>요청결과</strong></p>
<img src="https://velog.velcdn.com/images/half-phycho/post/361a57d3-a289-4887-990b-33de835fdda1/image.png">

<p>(Header Exchange는 Header Key 값이 들어가야 해서 요청형태가 다릅니다.)</p>
<img src="https://velog.velcdn.com/images/half-phycho/post/f957d4d3-f1de-4a05-8247-cdeec93f7182/image.png">

<ol>
<li>먼저 API 요청결과를 리턴 받습니다.</li>
</ol>
<img src="https://velog.velcdn.com/images/half-phycho/post/e4904917-9f20-4292-b829-96fe54adfd8b/image.png">

<ol start="2">
<li>마찬가지로 해당 로그도 해당 Queue에게 전달 되었기 때문에 출력되는 로그 입니다.</li>
</ol>
<br>

<h2 id="😊-직접-경험해보고-느낀점">😊 직접 경험해보고 느낀점</h2>
<p>이 글을 쓰기까지 비록 긴 시간이 걸렸지만 도움은 많이 되었습니다. RabbitMQ를 간단하게나마 써보니 왜 대용량 트래픽 제어에 사용하는지 알 수 있었습니다, 아직도 생소하지만, 이론적 지식과 실제로 프로젝트를 만들어 보면서 여러 가지 프로세스를 생각할 수 있게 해주었던 시간이었습니다, 만약 여러 요청이 들어와도 각기 다른 서버에서 병렬로 데이터를 처리할 수만 있다면 처리시간이 대폭 줄일 수 있겠다고 생각했습니다.</p>
<br>

<h2 id="📋-참고-사이트">📋 참고 사이트</h2>
<p><a href="https://hoestory.tistory.com/85">https://hoestory.tistory.com/85</a></p>
<p><a href="https://adjh54.tistory.com/497?category=1187853#3">https://adjh54.tistory.com/497?category=1187853#3</a></p>
<p>(항상 감사합니다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RabbitMQ] SpringBoot로 RabbitMQ 사용해보기 (기본)]]></title>
            <link>https://velog.io/@half-phycho/RabbitMQ-SpringBoot%EB%A1%9C-RabbitMQ-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@half-phycho/RabbitMQ-SpringBoot%EB%A1%9C-RabbitMQ-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 11 Nov 2025 10:12:05 GMT</pubDate>
            <description><![CDATA[<h2 id="💡시작하기에-앞서">💡시작하기에 앞서</h2>
<p>RabbitMQ로 메세지 큐를 구현하기에 앞서 이전에 작성한 RabbitMQ의 이론지식과 AMQP가 어떤의미인지 읽어보는걸 추천드립니다.</p>
<blockquote>
<p><a href="https://velog.io/@half-phycho/RabbitMQ-AMQP%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80">https://velog.io/@half-phycho/RabbitMQ-AMQP란-무엇인가</a></p>
</blockquote>
<br>

<h2 id="📒-예제-환경설정">📒 예제 환경설정</h2>
<p>RabbitMQ를 사용하기에 앞서 해당 예제에서 사용한 프레임워크 및 서버입니다.</p>
<pre><code>IDE : intellij Ultimate
Framework : Spring Boot
Language : Java 17
Server : AWS EC2</code></pre><p>이번에 만드는 예제는 Github에 등록해서 얼마든지 가져오실 수 있습니다.</p>
<blockquote>
<p><a href="https://github.com/delight-HK3/rabbitmq-test">https://github.com/delight-HK3/rabbitmq-test</a></p>
</blockquote>
<br>

<h2 id="❗주의사항">❗주의사항</h2>
<p>만약 본인 PC에 직접 RabbitMQ를 설치하셨으면 상관이 없지만 지금까지 게시글을 따라오셨다면 EC2의 보안 그룹에서 <code>5672</code>포트를 반드시 인바운드 규칙에 포함시켜야 합니다. 처음에 이걸 생각못해서 시간을 많이 낭비했습니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/0a221442-44cb-46f8-ac00-f7bf98152787/image.png" alt=""></p>
<p>(해당 이미지에 나온 인바운드 규칙처럼 다른 포트는 몰라도 <code>5672</code>포트는 추가해야 합니다.)</p>
<br>

<h2 id="📚-목차">📚 목차</h2>
<ul>
<li>Spring Boot Gradle 설정</li>
<li>properties 설정</li>
<li>RabbitMQ config 파일 작성</li>
<li>Mapping 설정</li>
<li>실제 Request 요청 보내기</li>
</ul>
<br>

<h3 id="1-spring-boot-gradle-설정">1. Spring Boot Gradle 설정</h3>
<pre><code class="language-java">dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-amqp&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-actuator&#39;

    compileOnly &#39;org.projectlombok:lombok&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;
    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
    testImplementation &#39;org.springframework.amqp:spring-rabbit-test&#39;
    testRuntimeOnly &#39;org.junit.platform:junit-platform-launcher&#39;
}</code></pre>
<ol>
<li><strong>amqp</strong> : Spring Boot에서 RabbitMQ를 사용하려면 amqp를 반드시 추가해야 합니다.</li>
<li><strong>web</strong> : Request 요청하기위해 추가했습니다.</li>
<li><strong>actuator</strong> : RabbitMQ의 연결상태를 확인하기 위해 추가 했습니다.</li>
<li><strong>lombok</strong> : Getter, Setter를 편리하게 만들기위해 추가했습니다.</li>
</ol>
<br>

<h3 id="2-properties-설정">2. properties 설정</h3>
<pre><code class="language-java"># spring boot port
server.port=8088

spring.application.name=rabbitmq-test

logging.level.root=info

# RabbitMQ setting
spring.rabbitmq.host=&lt;localhost / 서버주소&gt;
spring.rabbitmq.port=5672
spring.rabbitmq.username=&lt;설정한 사용자 이름 / admin&gt;
spring.rabbitmq.password=&lt;설정한 비밀번호 / admin&gt;
spring.rabbitmq.virtual-host=/

# application status check
management.endpoints.web.exposure.include=health,info</code></pre>
<p>여기에서 중요한 부분은 RabbitMQ setting 부분인데 하나하나 해석하면 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>설정</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>spring.rabbitmq.host</td>
<td>RabbitMQ를 실행시키고 있는 호스트 IP 주소</td>
</tr>
<tr>
<td>spring.rabbitmq.port</td>
<td>RabbitMQ에서 사용하는 포트</td>
</tr>
<tr>
<td>spring.rabbitmq.username</td>
<td>RabbitMQ의 사용자 이름</td>
</tr>
<tr>
<td>spring.rabbitmq.password</td>
<td>RabbitMQ에 설정한 패스워드</td>
</tr>
<tr>
<td>spring.rabbitmq.virtual-host</td>
<td>RabbitMQ에서 설정한 가상 호스트 기본 URL <br> (만약 가상 호스트를 추가안해도 기본 주소는 &#39;/&#39; 입니다.)</td>
</tr>
</tbody></table>
<br>

<h3 id="3-rabbitmq-config-파일">3. RabbitMQ config 파일</h3>
<p>RabbitMQ 설정은 별도의 설명없이 주석으로 작성해놓았습니다.</p>
<pre><code class="language-java">
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 설정 파일
 */
@EnableRabbit // RabbitMQ의 설정을 활성화 하기위해 필요
@Configuration
@RequiredArgsConstructor
public class RabbitmqConfig {

    @Value(&quot;${spring.rabbitmq.host}&quot;)
    private String host;     // 접속 호스트

    @Value(&quot;${spring.rabbitmq.port}&quot;)
    private Integer port;     // 접속 포트번호

    @Value(&quot;${spring.rabbitmq.username}&quot;)
    private String username; // 접속 아이디

    @Value(&quot;${spring.rabbitmq.password}&quot;)
    private String password; // 접속 비밀번호

    private static final String BINDING_KEY = &quot;test.key&quot;;            // 바인딩 키 name
    private static final String EXCHANGE_NAME = &quot;test.exchange&quot;;    // exchange name
    private static final String QUEUE_NAME = &quot;queue&quot;;                // 목적지 queue name

    /**
     * direct Exchange 구성
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange(EXCHANGE_NAME); // Exchange속성을 direct로 설정
    }

    /**
     * Queue 구성
     *
     * @return Queue
     */
    @Bean
    public Queue queue() {
        return new Queue(QUEUE_NAME);
    }

    /**
     * Queue와 DirectExchange를 바인딩
     * test.key라는 이름으로 바인딩 구성
     *
     * @param directExchange
     * @param queue
     * @return Binding
     */
    @Bean
    public Binding binding(DirectExchange directExchange, Queue queue) {
        // queue까지 가는 바인딩 Exchange 타입을 directExchange로 지정하고 test.key 이름으로 바인딩 구성
        return BindingBuilder
                .bind(queue)
                .to(directExchange)
                .with(BINDING_KEY);
    }

    /**
     * RabbitMQ와 메시지 통신을 담당하는 클래스
     */
    @Bean
    public RabbitTemplate rabbitTemplate(){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
        rabbitTemplate.setMessageConverter(messageConverter());

        return rabbitTemplate;
    }

    /**
     * RabbitMQ와 연결을 관리하는 클래스
     */
    @Bean
    public ConnectionFactory connectionFactory(){
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
        connectionFactory.setHost(host);
        connectionFactory.setPort(port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);

        return connectionFactory;
    }

    /**
     * RabbitMQ 메시지를 JSON형식으로 보내고 받을 수 있다.
     */
    @Bean
    public Jackson2JsonMessageConverter messageConverter() {

        ObjectMapper objectMapper = new ObjectMapper()
                // 날짜 관련 타임스탬프 직렬화를 막고 ISO-8601 형태로 포맷된다.
                .configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, true)
                .registerModule(dateTimeModule()); // Java에서 시간을 처리하기위한 모듈

        return new Jackson2JsonMessageConverter(objectMapper);
    }

    /**
     * 자바 시간 모듈 등록
     */
    @Bean
    public JavaTimeModule dateTimeModule() {
        return new JavaTimeModule();
    }
}</code></pre>
<br>

<h3 id="4-mapping-설정">4. Mapping 설정</h3>
<p>Mapping 설정은 postman혹은 intellij에 내장되어있는 클라이언트 요청 기능으로 직접 메세지를 보내는 용도로 만든 Mapping 입니다.</p>
<h4 id="controller-layer">controller Layer</h4>
<pre><code class="language-java">import com.example.rabbitmqtest.common.codes.SuccessCode;
import com.example.rabbitmqtest.common.response.ApiResponse;
import com.example.rabbitmqtest.dto.MessageDTO;
import com.example.rabbitmqtest.service.MesssageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping(&quot;/api/v1/publisher&quot;)
public class MessageController {

    private final MesssageService messsageService;

    public MessageController(MesssageService messsageService) {
        this.messsageService = messsageService;
    }

    @PostMapping(&quot;/send&quot;)
    public ResponseEntity&lt;?&gt; sendMessage(@RequestBody MessageDTO messageDTO) {

        messsageService.sendMessage(messageDTO);
        ApiResponse ar  = ApiResponse.builder()
                    .resultMsg(SuccessCode.SELECT.getMessage())
                    .resultCode(SuccessCode.SELECT.getStatus())
                .build();

        return new ResponseEntity&lt;&gt;(ar, HttpStatus.OK);
    }
}</code></pre>
<h4 id="service-layer">Service Layer</h4>
<pre><code class="language-java">import com.example.rabbitmqtest.dto.MessageDTO;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;

/**
 * MessageService 서비스 Layer
 */
@Slf4j
@Service
public class MesssageService {

    private final RabbitTemplate rabbitTemplate;

    public MesssageService(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    private static final String BINDING_KEY = &quot;test.key&quot;;
    private static final String EXCHANGE_NAME = &quot;test.exchange&quot;;

    /**
     * 메세지 전송
     *
     * @param messageDTO 메세지 DTO
     */
    public void sendMessage(MessageDTO messageDTO) {
        try{
            ObjectMapper objectMapper = new ObjectMapper();
            String objectToJson = objectMapper.writeValueAsString(messageDTO);
            rabbitTemplate.convertAndSend(EXCHANGE_NAME, BINDING_KEY, objectToJson);
        } catch (JsonProcessingException ex) {
            log.error(&quot;parsing error : {}&quot;, ex.getMessage(), ex);
        }

    }

}</code></pre>
<h4 id="successcode-enum">SuccessCode (Enum)</h4>
<pre><code class="language-java">import lombok.Getter;

@Getter
public enum SuccessCode {

    SELECT(200,&quot;SELECT SUCCESS&quot;);

    private final Integer status;
    private final String message;

    SuccessCode(Integer status, String message){
        this.status = status;
        this.message = message;
    }
}</code></pre>
<h4 id="messagedto-request-dto">MessageDTO (Request DTO)</h4>
<pre><code class="language-java">import lombok.*;

/**
 * 메세지 전송하는 DTO
 */
@Getter
public class MessageDTO {
    private String title;
    private String content;
}
</code></pre>
<h4 id="apiresponse-response-dto">ApiResponse (Response DTO)</h4>
<pre><code class="language-java">import lombok.Builder;
import lombok.Getter;

/**
 * API 결과를 저장하는 Response 객체
 */
@Getter
public class ApiResponse {

    private final String resultMsg;
    private final Integer resultCode;

    @Builder
    public ApiResponse(String resultMsg, Integer resultCode) {
        this.resultMsg = resultMsg;
        this.resultCode = resultCode;
    }

}</code></pre>
<br>

<h3 id="5-실제-request-요청-보내기">5. 실제 Request 요청 보내기</h3>
<p>위와 같이 설정했으면 대부분의 경우 요청이 보내지고 RabbitMQ에 바인딩이 쌓이게 됩니다. 하지만 만일에 대비해 현재 RabbitMQ와의 연결상태를 확인해보겠습니다. 
(저의 경우에는 intellij Ultimate에서 제공하는 기능을 사용했습니다.)</p>
<blockquote>
<p>GET <a href="http://localhost:8088/actuator/health">http://localhost:8088/actuator/health</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/8d9b6e81-a449-45f3-9130-3c4e0bf105f9/image.png" alt=""></p>
<p>해당 주소로 GET요청을 보내면 현재 RabbitMQ가 동작하는지 확인할 수 있습니다, 만약 RabbitMQ가 정상적으로 올라왔으면 UP으로 나오고 아니면 DOWN으로 나옵니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/d9fb5387-e31e-4acd-8490-8a6c4b09d881/image.png" alt=""></p>
<blockquote>
<p>POST <a href="http://localhost:8088/api/v1/publisher/send">http://localhost:8088/api/v1/publisher/send</a>
Content-Type: application/json
{
  &quot;title&quot;: &quot;test&quot;,
  &quot;content&quot;: &quot;test&quot;
}</p>
</blockquote>
<p>다음과 같이 주소를 작성하고 파라미터를 입력하여 보내면 </p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/3a86bc38-89d2-4d90-a2af-0c03ac8f58b3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/762d45eb-fdb2-40e4-ba6e-06d94e6059ec/image.png" alt=""></p>
<p>요청이 성공하고 RabbitMQ에 Direct속성으로 <code>test.exchange</code>이름의 바인딩이 등록된 것을 확인할 수 있습니다.</p>
<br>

<h2 id="😎-작성후기">😎 작성후기</h2>
<p>회사업무도 많고 예제 프로젝트를 만드느라 시간이 많이 걸렸습니다. 하지만 처음으로 RabbitMQ를 실행시키고 나아가 SpringBoot와 처음 연결해보니 새삼 대학교에서 처음 배우던 시절이 떠올랐습니다. 이 게시글에는 direct 타입 Exchange만 전송했지만 다음 게시글에서는 topic이나 Fanout Exchange를 보내보고 비교해보는 게시글을 작성할 계획입니다.</p>
<br>

<h2 id="참고-게시글">참고 게시글</h2>
<p><a href="https://adjh54.tistory.com/292">https://adjh54.tistory.com/292</a></p>
<p>(항상 감사합니다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RabbitMQ] EC2에 RabbitMQ 설치/설정/접속]]></title>
            <link>https://velog.io/@half-phycho/RabbitMQ-EC2%EC%97%90-RabbitMQ-%EC%84%A4%EC%B9%98%EC%84%A4%EC%A0%95%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@half-phycho/RabbitMQ-EC2%EC%97%90-RabbitMQ-%EC%84%A4%EC%B9%98%EC%84%A4%EC%A0%95%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Thu, 23 Oct 2025 10:39:27 GMT</pubDate>
            <description><![CDATA[<h2 id="📒-사전설명">📒 사전설명</h2>
<p>원래는 RabbitMQ를 설치하는 방법만 설명하려고 했으나 제가 근무하고 있는 회사도 그렇고 타 기업들도 보면 RabbitMQ를 사용하는 방법이 다양했습니다. EC2에 docker 컨테이너로 운영하는 회사, AWS자체에 존재하는 Amazon MQ 라는 메시지 브로커를 사용하는 등 다양한 방법으로 사용하고 있었습니다. 이번 게시글에서는 EC2기반에 docker 컨테이너로 RabbitMQ를 운영하고 RabbitMQ 관리 페이지에 접속하는 시간을 가져보겠습니다.
(docker 설치는 이전에 작성한 게시글이 있어서 그걸로 대체 하겠습니다.)</p>
<br>

<h2 id="📚-목차">📚 목차</h2>
<ul>
<li>Amazon EC2 서버 등록</li>
<li>EC2에 Docker 설치</li>
<li>Docker에 RabbitMQ 설치 및 설정</li>
<li>RabbitMQ 테스트 해보기</li>
</ul>
<br>

<h3 id="1-amazon-ec2-서버-등록">1. Amazon EC2 서버 등록</h3>
<p>해당 내용은 이전에 작성한 게시글을 사용하겠습니다.</p>
<blockquote>
<p><a href="https://velog.io/@half-phycho/AWS-Amazon-EC2-%EC%84%B8%ED%8C%85">https://velog.io/@half-phycho/AWS-Amazon-EC2-세팅</a></p>
</blockquote>
<br>

<h3 id="2-ec2에-docker-설치">2. EC2에 Docker 설치</h3>
<p>해당 내용 또한 이전에 작성한 게시글을 사용하겠습니다.</p>
<blockquote>
<p><a href="https://velog.io/@half-phycho/Docker-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95">https://velog.io/@half-phycho/Docker-설치-및-사용법</a></p>
</blockquote>
<br>

<h3 id="3-docker에-rabbitmq-설치-및-실행">3. Docker에 RabbitMQ 설치 및 실행</h3>
<p>Docker 환경에서 RabbitMQ를 실행시키는 방법은 2가지가 있습니다. </p>
<ul>
<li>docker 명령어로 운영하는 방법</li>
<li>Dockerfile을 사용하여 운영하는 방법</li>
</ul>
<br>

<p>📗 <strong>docker 명령어로 운영</strong>
해당 방법은 명령어를 통해 RabbitMQ이미지 다운로드, 컨테이너 실행하는 방법입니다.</p>
<pre><code># latest RabbitMQ 4.x
docker run -d -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4-management
# 기존 명령어에는 -d 없습니다, 백그라운드로 로딩시키기 위해 추가했습니다.</code></pre><p>만약 RabbitMQ 컨테이너를 생성한다면 5672, 15672, 25672 포트와 함께 등록되는데 포트별로 의미는 다음과 같습니다.</p>
<ul>
<li>5672 : AMQP 프로토콜 포트번호</li>
<li>15672 : RabbitMQ를 웹 UI로 접근하기 위한 포트번호</li>
<li>25672 : 클러스터기능 사용을 위한 포트번호</li>
</ul>
<br>

<p>🖼️ <strong>실행 이미지</strong></p>
<img src="https://velog.velcdn.com/images/half-phycho/post/f841f687-98f0-4d2a-ac70-2dc95b5e66a3/image.png">
명령어를 실행하면 RabbitMQ의 이미지를 다운로드하고 백그라운드 상태로 컨테이너를 실행합니다.

<img src="https://velog.velcdn.com/images/half-phycho/post/81e433b1-b5b6-463a-bc9e-75e3b44f2cd2/image.png">

<p><code>docker ps -a</code> 명령어를 실행시키면 다음과 같이 RabbitMQ가 실행되는 것을 확인할 수 있습니다.</p>
<br>

<h4 id="dockerfile을-사용하여-운영">Dockerfile을 사용하여 운영</h4>
<p>직접 docker 명령어를 사용하면 빠르게 이미지를 다운로드하고 바로 실행시킬 수 있지만 상세 설정이 불편하다는 단점이 있습니다. 그렇기에 필요한 자잘한 설정들을 Dockerfile에 작성한 후 이미지로 빌드하여 컨테이너로 실행시키는 방법이 있습니다.</p>
<p>(해당 방법은 adjh54님의 방법을 사용했습니다.)</p>
<p>폴더구조는 다음과 같습니다.
<img src="https://velog.velcdn.com/images/half-phycho/post/a2e333d9-e063-46e1-a87d-70954f2bdd57/image.png" alt=""></p>
<p><strong>rabbitmq.conf</strong> : 해당 파일에는 RabbitMQ 메세지 브로커의 설정을 담고 있습니다. 예를 들어 사용할 포트, 클러스터링 설정, 가상 호스트 설정 등이 있습니다.</p>
<p><strong>1. rabbitmq.conf 설정파일 작성</strong></p>
<pre><code># guest계정으로 접근 못하도록 막을 때 사용하는 설정입니다.
loopback_users.guest = false

# 해당 메세지 큐를 단일노드로 사용한다는 의미인데 경우에따라서 classic 혹은 milti로 설정이 가능합니다. (기본값은 single 입니다.)
distribution.mode = single

# 디스크 노드를 설정하는 의미 입니다.
# 만약 디스크와 메모리에 공간이 1GB만 남아있으면 메세지 발생을 중단시킵니다.
disk_free_limit.absolute = 1GB

# 디스크에 남은 여유 공간이 1.0배 정도면 메세지 발생을 중단시킨다는 의미입니다.
# 다른말로 하면 1.0으로 설정하면 여유 공간 한계를 두지않는다는 의미 입니다.
# 3.7.x 이전 버전은 0.5가 그런 이유 였지만 3.7.x 버전 이후 부터는 1.0이 비활성화를 의미합니다.
disk_free_limit.relative = 1.0

# 로깅설정 부분입니다.
log.dir = /var/log/rabbitmq # 로그 저장 경로
log.file = rabbitmq.log        # 저장할 로그 이름
log.file.rotation = daily    # 로그파일의 로테이션 (daily : 매일, weekly : 매주, size : 특정크기)
log.file.keep_count = 7        # 로테이션 된 로그파일의 최대 수량

# 클러스터 이름을 설정합니다.
cluster.name = rabbit@node1 

# TCP 리스너의 기본 포트를 지정합니다.
listeners.tcp.default = 5672

management.listener.port = 15672 # 관리 인터페이스 접근 HTTP API 포트 지정    
management.listener.ssl = false     # 관리 인터페이스 SSL 사용여부</code></pre><p>위의 설정을 반드시 따를 필요는 없고 용도에 맞게 설정하면 됩니다.</p>
<br>

<p><strong>2. Dockerfile 작성예시</strong></p>
<pre><code># 관리형 이미지를 설정합니다. 여기서는 RabbitMQ 이미지를 선택했습니다.
FROM rabbitmq:4-management

# rabbitmq.conf에서 구성한 RabbitMQ 설정 파일을 복사합니다.
COPY ./conf/rabbitmq.conf /etc/rabbitmq

# RabbitMQ 플러그인을 활성화합니다.
RUN rabbitmq-plugins enable --offline rabbitmq_management
# or 플러그인 추가 가능
# RUN rabbitmq-plugins enable --offline rabbitmq_mqtt rabbitmq_federation_management rabbitmq_stomp

# RabbitMQ 관리자 계정, 비밀번호를 지정합니다.
ENV RABBITMQ_DEFAULT_USER dev
ENV RABBITMQ_DEFAULT_PASS dev1234

# 컨테이너가 시작될 때 실행될 명령을 설정합니다.
CMD [&quot;rabbitmq-server&quot;]</code></pre><br>

<p><strong>3. Docker image 빌드</strong></p>
<blockquote>
<p><code>docker build -t rabbitmq-test .</code></p>
</blockquote>
<p>명령어를 실행하면 rabbitmq-test이름의 Docker 이미지가 생성되는 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/373b147f-2050-4cc0-86d3-384f8cbe2b77/image.png" alt=""></p>
<br>

<p><strong>4. Docker 컨테이너 생성 및 실행</strong></p>
<blockquote>
<p><code>docker run -d --name rabbitmq-dev-test -p 5672:5672 -p 15672:15672 rabbitmq-test</code></p>
</blockquote>
<p>명령어를 실행하면 다음과 같이 컨테이너가 생성되는 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/5ecf8167-4c5f-4c6e-92e6-b72c927c4608/image.png" alt=""></p>
<br>

<h3 id="4-rabbitmq-접속-해보기">4. RabbitMQ 접속 해보기</h3>
<p>컨테이너가 올라갔으면 RabbitMQ 관리 페이지에 접속이 가능해집니다.</p>
<blockquote>
<p>만약 본인 Local PC가 아니라 AWS EC2로 접근했으면 해당사항을 주의해야 합니다.</p>
</blockquote>
<ul>
<li>EC2 보안그룹에서 15672 포트를 인바운드 규칙추가</li>
<li>접속할 때에는 해당 서버주소 뒤에 포트 붙여서 접속하기</li>
</ul>
<table>
<thead>
<tr>
<th>localhost</th>
<th>AWS EC2</th>
</tr>
</thead>
<tbody><tr>
<td>localhost:15672</td>
<td>&lt;EC2 서버 포트&gt;:15672</td>
</tr>
</tbody></table>
<p>주의사항을 지켰다면 RabbitMQ 관리 페이지에 접속합니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/4079e718-d92b-49ac-920b-ec81d8953f51/image.png" alt=""></p>
<p>이렇게 위와 같은 페이지가 로딩되었고 Dockerfile에 작성한 내용도 같으면 dev/dev1234 계정으로 로그인합니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/cff6f6d9-f852-4125-b3f2-1dcfcdb4c90c/image.png" alt=""></p>
<p>접속하면 다음과 같은 페이지를 확인할 수 있겠습니다.</p>
<br>

<h2 id="😎-작성후기">😎 작성후기</h2>
<p>지난 글에서도 이야기했지만 처음 배운 기술이다 보니 익숙하지 않습니다. 하지만 이렇게 실제로 페이지를 구동시켜 보니 점점 익숙해지는 것 같은 기분이 듭니다, 다음 게시글에는 RabbitMQ 관리 페이지에서 메시지를 보내보는 실습을 진행해 보겠습니다.</p>
<br>

<h2 id="📋-참고-사이트">📋 참고 사이트</h2>
<p><a href="https://adjh54.tistory.com/496#1.%20Dockerfile%20%EC%88%98%ED%96%89%EA%B3%BC%EC%A0%95-1-2">https://adjh54.tistory.com/496#1.%20Dockerfile%20%EC%88%98%ED%96%89%EA%B3%BC%EC%A0%95-1-2</a></p>
<p><a href="https://psychoria.tistory.com/541">https://psychoria.tistory.com/541</a></p>
<p> <a href="https://printf-hellojudyworld.tistory.com/28">https://printf-hellojudyworld.tistory.com/28</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RabbitMQ] RabbitMQ 기본정리]]></title>
            <link>https://velog.io/@half-phycho/RabbitMQ-RabbitMQ-%EA%B8%B0%EB%B3%B8%ED%8A%B9%EC%A7%95</link>
            <guid>https://velog.io/@half-phycho/RabbitMQ-RabbitMQ-%EA%B8%B0%EB%B3%B8%ED%8A%B9%EC%A7%95</guid>
            <pubDate>Mon, 13 Oct 2025 09:44:56 GMT</pubDate>
            <description><![CDATA[<h2 id="❓작성계기">❓작성계기</h2>
<p>작성계기는 하단의 게시글에서 말했듯이 프로젝트에 현재 사용하고 있고 원리를 파악하고 실제로 적용해보고자 작성하게 되었습니다.</p>
<blockquote>
<p><a href="https://velog.io/@half-phycho/RabbitMQ-AMQP%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80">https://velog.io/@half-phycho/RabbitMQ-AMQP란-무엇인가</a></p>
</blockquote>
<p>이전 게시글에서는 RabbitMQ의 상위 개념인 AMQP에 대해 학습을 해보았으니 이제는 본격적으로 RabbitMQ에 대해 이론적으로 알아보고 다음 게시글에서는 실제로 RabbitMQ를 설치하고 운영해보도록 하겠습니다. 
<br></p>
<h2 id="📚-rabbitmq란-무엇인가">📚 RabbitMQ란 무엇인가?</h2>
<p>RabbitMQ는 간단히 이야기하면 AMQP의 구현 체중 하나로 AMQP 프로토콜 기반의 경량화된 메시지 브로커 시스템입니다. 주로 요청을 많은 사용자에게 전달하는 경우, 많은 작업이 요청되어 처리해야 할 경우, 요청에 대한 처리시간이 긴 경우에 주로 사용됩니다. 이러한 특징으로 마이크로 서비스, 분산 시스템, 서버리스 아키텍처 등 다양한 현대적인 애플리케이션에서 널리 사용되고 있습니다.</p>
<blockquote>
<p><strong>메시지 브로커</strong><br>서로 다른 언어로 작성되었거나 다른 플랫폼에서 구현된 애플리케이션, 시스템, 서비스 간의 통신과 정보 교환을 가능하게 하는 소프트웨어를 부르는 말 입니다.</p>
</blockquote>
<br> 

<h3 id="💡-amqp의-구현체들">💡 AMQP의 구현체들</h3>
<p>그렇다면 다른 AMQP의 다른 구현체도 있다는 이야기인데 AMQP의 구현체는 다음과 같습니다.</p>
<ul>
<li>RabbitMQ</li>
<li>ActiveMQ </li>
<li>IBM MQ</li>
<li>Azure Service Bus</li>
</ul>
<p>이거 외에 StormMQ, OpenAMQ등 다른 AMQP구현체들이 많습니다, 이중에서 가장 많이쓰고 익숙한거는 RabbitMQ라고 생각합니다.</p>
<br>

<h3 id="👍-rabbitmq의-장점">👍 RabbitMQ의 장점</h3>
<p>RabbitMQ의 장점에 대해서는 타 게시글을 인용했지만 제가 생각하는 장점만 적어보겠습니다.</p>
<p><strong>1. 다양한 통합 옵션 지원</strong> : 다양한 프로그래밍 언어와 프레임워크를 지원하며, REST API, WEB Socket등을 통한 다양한 통합이 가능합니다. (실제로 제가 다니는 회사에서는 RabbitMQ를 WEB Socket와 통합하여 사용합니다.)</p>
<p><strong>2. 다중 프로토콜 지원</strong> : 공부하면서 알게되었는데 RabbitMQ는 기본적으로 AMQP 프로토콜이지만 MQTT, STOMP와 같은 다양한 메시징 프로토콜을 지원합니다.</p>
<p><strong>3. 높은 메시징 처리량</strong> : 메모리와 디스크에 메세지 저장이 가능하여 높은 메시지 처리량과 함께 높은 가용성을 제공합니다. 이로 인해 RabbitMQ가 대규모 분산 시스템에서도 안정적으로 사용 될 수 있습니다.</p>
<p><strong>4. 트랜잭션 지원</strong> : 메시지 발생, 메시지 확인, 삭제 등의 작업을 하나의 트랜잭션으로 묶어서 처리가 가능하여 데이터의 일관성과 신뢰성을 높이는 데 도움이 됩니다.</p>
<br>

<h3 id="😕-rabbitmq의-단점">😕 RabbitMQ의 단점</h3>
<p>모든 기술이 그렇듯 장점만 있는 것이 아니라 반드시 단점도 존재합니다.</p>
<p><strong>러닝커브 상승</strong> : 아무래도 저는 학습 러닝 커브가 상승하는 것을 뽑을 거 같습니다. RabbitMQ는 다양하고 강력한 기능들이 있는 만큼 이해하고 사용하기까지 많은 시간과 노력이 필요합니다, 또한 메시지의 신뢰성을 보장하기 위한 다양한 설정(메시지 지속성, 메시지 확인 등)을 올바르게 구성하기 위해서도 공부가 필요하다고 느꼈습니다.</p>
<br>

<h2 id="😎-이론지식을-보고-느낀점">😎 이론지식을 보고 느낀점</h2>
<p>RabbitMQ를 들어가기 전에 AMQP를 먼저 공부를 하고 RabbitMQ를 보면 어느 정도 이해할 수 있을 줄 알았는데 솔직히 이야기하면 아직도 이해 못 하는 용어투성이입니다. 하지만 지금까지 이러한 경우들은 수없이 있었습니다. 대학생 때 처음으로 Spring Boot을 배울 때에도 첫 직장에서 처음으로 복잡한 쿼리를 작성할 때도 그리고 지금 회사에 들어오기 위해 AWS를 공부할 때도 어려운 것, 이해가 힘든 것투성이였습니다. 그렇기에 이 RabbitMQ도 똑같이 어려워하고 지속적으로 공부하여 저의 것으로 만들기 위해 노력하다 보면 어느 순간 익숙해질 것으로 생각합니다.</p>
<br>

<h2 id="📋-참고-사이트">📋 참고 사이트</h2>
<p><a href="https://www.ibm.com/kr-ko/think/topics/message-brokers">https://www.ibm.com/kr-ko/think/topics/message-brokers</a></p>
<p><a href="https://velog.io/@sdb016/RabbitMQ-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90">https://velog.io/@sdb016/RabbitMQ-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90</a></p>
<p><a href="https://yuna-ninano.tistory.com/entry/%EB%A9%94%EC%8B%9C%EC%A7%80-%ED%81%90-RabbitMQ%EB%9E%80-%EA%B0%9C%EB%85%90-%ED%8A%B9%EC%A7%95-%EC%9E%A5%EB%8B%A8%EC%A0%90-%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84">https://yuna-ninano.tistory.com/entry/%EB%A9%94%EC%8B%9C%EC%A7%80-%ED%81%90-RabbitMQ%EB%9E%80-%EA%B0%9C%EB%85%90-%ED%8A%B9%EC%A7%95-%EC%9E%A5%EB%8B%A8%EC%A0%90-%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84</a></p>
<p>(항상 감사합니다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RabbitMQ] AMQP란 무엇인가?]]></title>
            <link>https://velog.io/@half-phycho/RabbitMQ-AMQP%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@half-phycho/RabbitMQ-AMQP%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Fri, 03 Oct 2025 10:55:28 GMT</pubDate>
            <description><![CDATA[<h2 id="❓작성계기">❓작성계기</h2>
<p>제가 지금 하고 있는 프로젝트에서는 메시지를 보내고 비동기로 관리하기 위해 RabbitMQ를 사용하고 있습니다. 그런데 RabbitMQ는 말로만 들어봤지 실제로 어떻게 동작을 하는지 그리고 이걸 왜 그렇게 많은 기업에서 요구하는지 알고 싶었습니다. 그래서 RabbitMQ를 조금 알아봤는데 RabbitMQ이전에 상위개념인 AMQP를 먼저 알아보고자 이렇게 정리해 보고자 합니다.</p>
<br>

<h2 id="📚-amqp-advanced-message-queue-protocol란">📚 AMQP (Advanced Message Queue Protocol)란?</h2>
<p>AMQP는 <strong>메시지 지향 미들웨어 (MOM)을 위한 표준 응용 계층 프로토콜입니다.</strong> 풀어서 이야기하면 <strong>메시지 통신을 위한 규약</strong>이라고 보면 되겠습니다. 서로다른 특히 플랫폼에 종속적인 제품들 사이에서 채팅해야 하는 경우가 있습니다. 이럴 때 메시지 교환을 위해 메시지 포맷을 변환 목적으로 메시지 Bridge를 사용하거나 서로 다른 두 시스템을 통합할 필요가 있었습니다. </p>
<p>그래서, 서로 다른 시스템 간의 최대한 효율적으로 메시지를 교환하기 위해 AMQP 프로토콜이 등장했습니다. 이러한 규약 기반으로 만들어진 구현체 중에 하나가 <strong>RabbitMQ</strong>입니다.</p>
<br>

<h3 id="🖼️-amqp-주요-구성요소">🖼️ AMQP 주요 구성요소</h3>
<img src="https://velog.velcdn.com/images/half-phycho/post/39030c57-3dd7-4182-a2ed-5df6414af29a/image.png">
<p style="text-align:center">출처 : <a href="https://www.wallarm.com/what/what-is-amqp">https://www.wallarm.com/what/what-is-amqp</a></p>

<table>
<thead>
<tr>
<th>구성요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Exchange(교환기)</td>
<td>메시지를 수신하고 이를 라우팅 규칙에 따라 적절한 큐에 전달하는 역할을 합니다.</td>
</tr>
<tr>
<td>Binding(바인딩)</td>
<td>교환기와 큐를 연결하여 라우팅 규칙을 정의하는 설정입니다.</td>
</tr>
<tr>
<td>Queue(큐)</td>
<td>메시지를 저장하는 버퍼 역할을 하며, 소비자(Consumer)가 메시지를 읽을 때 까지 대기합니다.</td>
</tr>
<tr>
<td>Message(메시지)</td>
<td>전달되는 데이터의 기본단위로 HTTP와 비슷하게 헤더/본문으로 구성됩니다.</td>
</tr>
</tbody></table>
<br>

<h3 id="💡-amqp-특징">💡 AMQP 특징</h3>
<ul>
<li>플랫폼에 종속적이지 않다.</li>
<li>AMQP는 교환기(Exchange)와 큐(Queue)를 사용하여 메시지를 다양한 방법으로 라우팅이 가능</li>
<li>메시지를 확실히 전달하고 손실을 방지하기위해 메시지 전달보장 기능을 지원합니다.</li>
<li>메시지 순서보장 및 트랜잭션 처리로 안정적으로 메시징이 가능합니다.</li>
<li>Exchange를 사용해 여러가지 방식의 라우팅을 제공합니다.</li>
<li>SSL/TLS를 사용한 보안 및 사용자 인증을 지원하여 안전한 메세징이 가능합니다.</li>
</ul>
<br>

<h3 id="📗-exchange-타입">📗 Exchange 타입</h3>
<p>AMQP는 메시지를 적절한 Queue에게 바인딩하기위해 다양한 타입의 Exchange속성이 존재합니다. </p>
<blockquote>
<p>설명에 앞서 라우팅 키, 바인딩 키를 언급하는데 라우팅 키를 설정하는 주체는 Publisher고 바인딩 키를 설정하는 주체는 Consumer입니다. 
여기서 (라우팅 키 = 바인딩 키) 의미를 가집니다.</p>
</blockquote>
<br>

<h4 id="direct-exchange">Direct Exchange</h4>
<p>Publisher가 메시지를 보낼 때 보내야할 Exchange의 명칭과 라우팅 키를 함께 보내면 Exchange에서는 라우팅 키와 바인딩 키가 일치하는 Queue에 메시지를 보내는 방식입니다. 
<img src="https://velog.velcdn.com/images/half-phycho/post/27b096ff-f5ae-49dc-bfc1-2f3fbaf201ad/image.png" alt=""></p>
<p>다음과 같은 AMQP 구조가 있으면 Publisher가 Exchange E1과 라우팅 키 success를 보내면 먼저 Exchange E1으로 연결되고 라우팅 키 success와 일치하는 Q1 Queue로 메시지를 전달합니다.</p>
<br>

<h4 id="fanout-exchange">Fanout Exchange</h4>
<p>Publisher가 특정 Exchange에 라우팅 키와 상관없이 Exchange에 연결된 모든 Queue에게 메시지를 보내는 방식입니다. 이 Exchange 방식은 알림 시스템 등에 사용됩니다.
<img src="https://velog.velcdn.com/images/half-phycho/post/267c0d81-f0b5-44da-9728-34f15eca9c3b/image.png" alt=""></p>
<br>

<h4 id="topic-exchange">Topic Exchange</h4>
<p>Publisher에서 보내는 라우팅 키의 패턴에 따라 메시지를 라우팅 하는 방식입니다. 만약 라우팅 키의 구분을 &quot;/&quot; 으로 지정하고 바인딩 시 와일드 카드로 &quot;*&quot;,&quot;#&quot;을 지정하여 사용할 수 있습니다.
<img src="https://velog.velcdn.com/images/half-phycho/post/f57b83b8-6578-4541-a9ee-d0ec19bcb283/image.png" alt=""></p>
<p>위의 예시처럼 라우팅 키 요청을 check.* 로 설정하면 바운딩에서 check와 관계된 바인딩에만 메시지를 송신합니다.</p>
<br>

<h4 id="headers-exchange">headers Exchange</h4>
<p>메시지의 헤더 정보를 기반으로 라우팅을 하는 Exchange입니다. 다른 Exchange와 다르게 라우팅 키를 사용하지 않고 메시지 자체의 헤더와 바인딩 된 Queue의 헤더 조건과 일치할 때 Queue로 전달됩니다. 특정 헤더를 가진 메시지만 큐에 전달하도록 만들 수 있습니다.</p>
<br>

<h3 id="📘-amqp의-계층-구조">📘 AMQP의 계층 구조</h3>
<p>AMQP의 계층 구조는 OSI 7 Layer 구조와 비슷한 부분이 있어서 OSI 7 Layer 구조를 알고있으면 이해가 쉬울거라고 생각합니다.</p>
<table>
<thead>
<tr>
<th>계층이름</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>프레임 계층(Frame)</td>
<td>네트워크에서 교환되는 패킷의 전송을 담당</td>
</tr>
<tr>
<td>전송 계층(Transport)</td>
<td>연결과 흐름 제어를 관리하며 안정적인 데이터 전송을 보장</td>
</tr>
<tr>
<td>세션 계층(Session)</td>
<td>여러 개의 세션을 연결 위에서 관리하며, 각 세션마다 독립적인 흐름 제어 가능</td>
</tr>
<tr>
<td>애플리케이션 계층(Application)</td>
<td>메시지 큐, 교환기, 바인딩 등 메시징의 주요 개념을 관리하고 정의</td>
</tr>
</tbody></table>
<br>

<h3 id="📙-amqp의-프레임-종류">📙 AMQP의 프레임 종류</h3>
<p>AMQP는 프레임 기반 프로토콜 입니다. 프레임은 AMQP의 전송 단위이며, 각 프레임에는 특정 작업에 대한 명령 혹은 데이터가 포함됩니다.</p>
<table>
<thead>
<tr>
<th>프레임 이름</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>메서드 프레임(Method)</td>
<td>Exchange 생성, Queue 선어, 메세지 퍼블리싱 등의 AMQP 명령을 표현</td>
</tr>
<tr>
<td>헤더 프레임(Header)</td>
<td>메세지의 속성 정보</td>
</tr>
<tr>
<td>바디 프레임(Body)</td>
<td>메세지의 실제 데이터</td>
</tr>
<tr>
<td>(Heartbeat) 프레임</td>
<td>심장 박동처럼 클라이언트와 서버간 연결 상태 유지를 위해 주기적으로 전송</td>
</tr>
</tbody></table>
<br>

<h3 id="⛓️-amqp의-통신-흐름">⛓️ AMQP의 통신 흐름</h3>
<p><strong>1. 연결 및 채널 생성</strong><br>Publisher가 메시지 브로커에 연결을 요청하고 연결이 수립되면 여러개의 채널을 생성할 수 있습니다. </p>
<p><strong>2. Exchange 및 Queue 선언</strong>
이때 Publisher는 Exchange 선언 후 라우팅 규칙을 정하고 Queue 선언시에는 Exchange와 Queue사이의 바인딩 규칙을 설정합니다.</p>
<p><strong>3. 메시지 전송</strong>
Publisher는 특정 Exchange에 메시지를 전송합니다. 메시지에 라우팅 키가 포함할 수 있으며 Exchange는 라우팅 규칙에 따라 특정 Queue에 메시지를 전달합니다.</p>
<p><strong>4. 메시지 소비</strong>
Queue에서 Consumer에게 메시지를 전달합니다. Consumer가 메시지를 성공적으로 전달받으면 확인 응답(ACK) 혹은 부정 응답(NACK)을 보냅니다. </p>
<br>

<h2 id="❓-왜-많이-사용하는가">❓ 왜 많이 사용하는가?</h2>
<p>요즘 많은 소프트웨어가 확장 목적으로 마이크로서비스 아키텍처를 도입하게 되었습니다, 확장성은 좋아졌으나 모듈 간 안정적이고 효율적인 통신이 필요하게 되었고 이때 AMQP는 데이터의 손실이나 중복 없이 전달해야 하는 경우 그리고 라우팅이 복잡한 시스템에 적합한데 이러한 구조를 가진 소프트웨어는 대부분 비교적 최근에 나오는 소프트웨어들이고 그래서 기업들이 많이 요구하는 거 같다고 생각합니다.</p>
<br>

<h2 id="📋-참고-사이트">📋 참고 사이트</h2>
<p><a href="https://velog.io/@holicme7/%ED%91%9C%EC%A4%80-%EB%A9%94%EC%84%B8%EC%A7%95-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EC%A0%95%EB%A6%AC-AMQP-STOMP-MQTT">https://velog.io/@holicme7/표준-메세징-프로토콜-정리-AMQP-STOMP-MQTT</a></p>
<p><a href="https://jeongchul.tistory.com/812#AMQP%20%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-1">https://jeongchul.tistory.com/812#AMQP%20%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-1</a></p>
<p><a href="https://blog.naver.com/pjt3591oo/223363564670">https://blog.naver.com/pjt3591oo/223363564670</a></p>
<p>(항상 감사합니다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Jenkins] Spring Boot의 Gradle 버전으로 Docker 태그 반영하기]]></title>
            <link>https://velog.io/@half-phycho/Jenkins-Spring-Boot-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%8A%94%EB%8D%B0-Docker-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%B2%84%EC%A0%84-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@half-phycho/Jenkins-Spring-Boot-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%8A%94%EB%8D%B0-Docker-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%B2%84%EC%A0%84-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 26 Sep 2025 23:22:53 GMT</pubDate>
            <description><![CDATA[<h2 id="작성계기">작성계기</h2>
<p>작성 계기는 이직한 회사에 입사한 지 얼마 안 된 무렵이었습니다. 저희 회사는 CI/CD를 젠킨스로 운영하는데 기존에는 배포할 때 새롭게 반영이 되었는지 확인이 안 되는 문제와 기존 이미지가 업데이트되면 만일의 비상 상황에 이전 버전으로 롤백시킬 수도 없다는 단점이 있었습니다, 물론 새롭게 반영할 때마다 젠킨스 스크립트를 수정하면 되지만 작업도 결국 사람이 하기에 버전수정을 위해 매번 스크립트를 수정 및 반영하는게 매우 번거롭고 잊어먹는 경우가 발생했습니다. 그래서 저의 사수님은 이러한 문제를 해소하고자 기존 도커로 운영 중인 애플리케이션 버전관리 체제와 버전을 태그에 붙이는 방식을 설계하라는 업무를 받았습니다.</p>
<br>

<h2 id="버전체계-변경">버전체계 변경</h2>
<p>기존에는 단순히 X.Y 버전 체제로 배포버전과 수정버전만 확인 가능했습니다. 그래서 체제를 많은 소프트웨어 회사와 프로젝트들이 사용하는 X.Y.Z (시맨틱 버저닝)을 사용하기로 했습니다.
<img src="https://velog.velcdn.com/images/half-phycho/post/862dad15-62f5-4a91-b975-4a805da41904/image.png" alt=""></p>
<br>

<h2 id="버전관리-방식-변경">버전관리 방식 변경</h2>
<p>기존에는 방식은 API를 수정하고 반영하고 추가로 젠킨스 스크립트도 변경해야 비로소 새로운 버전으로 컨테이너가 생성되었습니다. 이렇게 되니 API 수정과 별개로 젠킨스 스크립트도 수정해야 해서 좀 번거로운 부분이 있었습니다. 그래서 생각해 낸 방법은 Spring Boot Gradle의 version속성을 이용하여 버전관리를 해보자고 생각했습니다. </p>
<ul>
<li><p>기존 버전관리 방식
<img src="https://velog.velcdn.com/images/half-phycho/post/b95b1044-a698-4547-ba42-319d0e33f6cc/image.png" alt=""></p>
</li>
<li><p>설계한 신규 버전관리 방식
<img src="https://velog.velcdn.com/images/half-phycho/post/e622d52a-e99f-49ba-b955-ce0bf4534cc0/image.png" alt=""></p>
</li>
</ul>
<br>

<h3 id="수정한-부분">수정한 부분</h3>
<ul>
<li><strong>애플리케이션(Spring Boot) 수정사항</strong><ol>
<li>버전을 젠킨스 스크립트에 직접입력하는 방식에서 Spring Boot API build.gradle파일의 version 변수를 사용</li>
<li>젠킨스에서 build.gradle에 설정되어있는 version 속성 값을 가져오는 태스크 함수 추가</li>
</ol>
</li>
</ul>
<pre><code class="language-java">// build.gradle
version = &#39;{버전정보 X.Y.Z}&#39;

// 중간생략

tasks.register(&#39;{태스크 함수 명}&#39;) {
    doLast {
        println version
    }
}
</code></pre>
<ul>
<li><strong>젠킨스 스크립트 수정사항</strong> <ol>
<li>애플리케이션의 버전을 가져오는 stage추가</li>
<li>stage 내부에서 Gradle 실행권한 부여</li>
<li>태스크 실행하여 버전값 가져오는 코드 추가</li>
<li>가져온 버전을 도커 태그에 반영</li>
</ol>
</li>
</ul>
<pre><code class="language-java">// 젠킨스 스크립트
stage(&#39;get version&#39;) {
    steps {
        script {
            // Gradle 실행권한 부여
            sh &#39;chmod +x ./gradlew&#39;

            // Gradle의 callVersion 태스크를 실행하여 버전 값 가져오기
            def appVersion = sh(returnStdout: true, script: &#39;./gradlew -q callVersion&#39;).trim()

            // 가져온 버전을 환경 변수로 설정
            env.DOCKER_TAG = appVersion
        }
    }
}</code></pre>
<br>

<h2 id="개선결과">개선결과</h2>
<p>이러한 세팅을 선임님과 CTO분께 리뷰를 하고 이 방식이 좋겠다는 피드백을 받아 이방법을 사용하기로 결정했습니다. 
결과적으로 애플리케이션의 API를 수정하고 build.gradle의 version만 수정하면 별다른 작업이 필요없어졌고 나아가 더이상 소프트웨어 버전수정을 위해 젠킨스 스크립트를 수정할 필요가 없어졌기에 API 배포과정을 많이 간소화 시킬 수 있었습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[기나긴 고난을 딛고 취업!!!]]></title>
            <link>https://velog.io/@half-phycho/%EA%B8%B0%EB%82%98%EA%B8%B4-%EA%B3%A0%EB%82%9C%EC%9D%84-%EB%94%9B%EA%B3%A0-%EC%B7%A8%EC%97%85</link>
            <guid>https://velog.io/@half-phycho/%EA%B8%B0%EB%82%98%EA%B8%B4-%EA%B3%A0%EB%82%9C%EC%9D%84-%EB%94%9B%EA%B3%A0-%EC%B7%A8%EC%97%85</guid>
            <pubDate>Wed, 10 Sep 2025 13:31:45 GMT</pubDate>
            <description><![CDATA[<p>지난 1년동안 취업까지 정말 보람차면서 힘든시간을 보냈습니다.</p>
<p>취업은 했지만 그동안 정말 많은 고난과 역경들이 있었습니다. 그 사이에 어떤일들이 있었는지는 장문으로 적기에는 너무 길어서 간략하게 나열하면서 그동안 1년동안 무엇을 했고 어떻게 힘들었는지 나아가 긴 시간동안 어떻게 버텼는지 정리하고자 이렇게 간만에 (거의 3개월) 블로그를 작성해 봅니다.</p>
<h2 id="고통과-고난들">고통과 고난들</h2>
<ul>
<li>모아둔 돈이 줄어들고 있다는 압박감을 항상 받음</li>
<li>취업이 안되어서 스스로 자책을 하면서 마음이 더욱더 망가짐</li>
<li>스트레스를 너무 받아서 응급실에 갈 정도로 흉통을 달고 살게 됨</li>
<li>주변에서는 괜찮다고 하지만 오히려 안심이 안되는 말할 수 없는 느낌</li>
<li><strong>서류광탈</strong>만 수 백번을 하면서 자존감이 바닥을 뚫고 지하실까지 내려감</li>
<li>마지막 면접에 떨어져서 절망과 분노를 한번에 느낌</li>
<li>아침부터 잠들기 전까지 좋지않은 생각이 머리속을 지배</li>
<li>세상을 향한 무의미한 분노</li>
<li>주변인들의 취업소식에 자연스럽게 비교하면서 스스로 자존감을 하락</li>
</ul>
<p>기타 등등 하나하나 나열하기도 힘들정도 였습니다. 특히 힘들었던거는 아무리 공부를해도 결과가 안나온다는 절망감과 포기하고 싶어지는 마음이 커졌고 스스로 노력해도 결과가 없으면 아무런 소용도 없다는 생각이 머릿속을 지배해서</p>
<p>과정을 종요시하는 나 VS 결과가 없으면 전부 소용없다라고 말하는 나</p>
<p>이렇게 자아가 서로 부딪히면서 매일마다 정신적으로 전쟁터에 있는 기분을 항상 느끼면서 살았던거 같습니다. </p>
<p>하지만 그럼에도 저라는 인간 성격상 절대로 포기하고 싶은 마음은 전혀없었기에 죽어라 노력을 했습니다. 만약 여전히 취업을 못했어도 여전히 저는 어떠한 방향이든 노력하고 있었을 것 입니다.</p>
<h2 id="취업을-위해-노력-한-것들">취업을 위해 노력 한 것들</h2>
<ul>
<li>항상 이력서 점검하고 주변 지인들 혹은 전문가에게 코칭받기</li>
<li>개인 포토폴리오 강화를 위해 velog 계정 개설 및 운영</li>
<li>정보처리기사 자격증 취득을 위해 학원수강</li>
<li>파이썬 기반 랭체인 신기술 공부</li>
<li>기술력을 강화하기 위해 개인 서비스를 구축하고 운영 (지금은 임시로 폐쇄했습니다.)</li>
<li>면접을 복기를 하면서 면접기술 강화</li>
</ul>
<p>이렇게 주말도 낮도 밤도 없이 죽도록 노력해서 결국 취업했지만, 이런 생활을 하도록 유지하는 게 정말 힘들었습니다. 그리고 취업 활동을 하면서 느낀 게 저의 주변의 저를 생각해 주는 사람이 정말 많았고, 그들에게 도움을 많이 받은 덕분에 힘들어도 앞을 바라보고 걸을 수 있었습니다, 항상 그들에게 감사하고 있습니다.</p>
<h2 id="취준생-시절-꾸준함을-유지할-수-있었던-비법">취준생 시절 꾸준함을 유지할 수 있었던 비법</h2>
<p><strong>1. 항상 몸을 바쁘게 해라!</strong></p>
<ul>
<li>이건 정말 중요하다고 생각합니다, 간단히 말하면 몸이 바쁘면 잡생각도 안나고 우울감에 사로잡히기 쉽지가 않습니다.</li>
</ul>
<p><strong>2. 일단 밖으로 나가라!!!</strong></p>
<ul>
<li>이건 1번과 연결되는데 마찬가지로 집에 있으면 자연스럽게 혼자 있으니까, 우울감에 잡히고 결국 스스로 고통만 줍니다. 설령 아무것도 안 하더라도 밖으로 나가서 걷기라도 하는 게 충분히 도움이 됩니다.</li>
</ul>
<p><strong>3. 용모를 항상 단정히 해라!!!</strong></p>
<ul>
<li>제가 좋아하는 말 중에 이런 말이 있습니다. &quot;복장의 흐트러짐은 마음의 흐트러짐이다.&quot; 저의 경우에는 해당한다고 생각하는 게 면바지를 입고 공부를 하는 것과 체육복을 입고 공부를 하는 거는 이상하게 긴장감의 무게가 다르고 공부하는 시간이 달랐습니다.</li>
</ul>
<p><strong>4. 쉴때는 반드시 휴식을 취해라!!!</strong></p>
<ul>
<li><p>정확히 말하자면 밖에서 활동적인 것을 하라는 이야기입니다. 한편으로 부끄럽지만 저는 이 취업 준비 기간 동안 틈틈히 신나게 놀았습니다.</p>
<ol>
<li>보고 싶었던 아티스트 콘서트 관람 </li>
<li>읽고 싶었던 책을 읽기 </li>
<li>그림 그리기 </li>
<li>가고 싶었던 게임 행사 관람</li>
<li>친구들과 처음으로 해외여행</li>
<li>서울에 있는 유명한 건축물이나 문화재 탐방</li>
</ol>
<p>이렇게 다양한 장소를 다니면서 스트레스 관리를 했고 서울에 있는 유명한 건축물을 탐방하면서 서울이라는 도시를 이해하는 시간을 가질 수 있었습니다. 이러한 경험들이 저의 취업활동을 지속하게 해준 연료가 되었습니다.</p>
</li>
</ul>
<p><strong>5. 힘들면 스스로 해결하지말고 주변에 도움을 요청해라!!!</strong></p>
<ul>
<li>앞에서 한 행동을 모두 하더라고 사람 마음이 그렇게 쉽게 치유되지는 않습니다. 저도 친구들과 해외여행을 다녀오고 한국에 도착한 직후에 우울감에 바로 휩싸였으니까요. 세상일은 스스로 해결할 수 있는 일은 별로 없다고 생각합니다. 그래서 저는 항상 주변인들에게 정말 힘들다고 내 이야기를 들어달라고 도움 요청을 많이 했습니다. 만약 본인의 이야기를 들어줄 상대가 없으면 자살 예방 전화든 뭐든 조금이나마 도움을 주는 게 있으면 도움을 받는 게 좋다고 생각합니다. 인생을 산 지 얼마 되지는 않았지만, 감정을 약간이라도 해소하는 게 얼마나 중요한지는 누구보다 잘 알고 있다고 말할 수 있습니다.</li>
</ul>
<p><strong>5. 조그마하고 사소한 일이라고 스스로 자랑스럽게 여겨라!!!</strong></p>
<ul>
<li>저는 취업 준비 기간 때 날마다 제가 해낸 일들을 사소한 것들이라도 적는 습관을 만들었습니다. 가령 매일 아침에 일찍 일어나고 이불 정리를 하고 독서를 하는 등 스스로 무언가를 했다면 전부 기록했고 반년 정도 하니까 다이어리의 내용이 꽉 차져 있었고 스스로 이렇게 많은 걸 했구나 라는걸 느낄 수 있었습니다.</li>
</ul>
<h2 id="과거의-나의-모습을-가진-사람들에게">과거의 나의 모습을 가진 사람들에게</h2>
<p>솔직히 언젠가는 나아진다는 희망적인 말은 도저히 못 하겠습니다. 인생은 실전이기 때문이죠 하지만 이것만은 말해주고 싶습니다. 어디서 어떤 모습으로 어떤 일을 하더라도 스스로를 원망하고 심하게 자책하지 말아주세요. 현재 취업이 잘 안되는거는 여러분들 본인 잘못이 아니고 본인의 노력이 부족한거는 더더욱 아닙니다. 그렇기에 본인을 자책하지 말아주세요 본인을 몰아세우면 망가지는건 결국 본인입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] Spring Boot S3 이미지 업로드 (2)]]></title>
            <link>https://velog.io/@half-phycho/AWS-Spring-Boot-S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-2</link>
            <guid>https://velog.io/@half-phycho/AWS-Spring-Boot-S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-2</guid>
            <pubDate>Fri, 20 Jun 2025 03:56:58 GMT</pubDate>
            <description><![CDATA[<h2 id="사전작업">사전작업</h2>
<p>해당 게시글에서는 본격적으로 SpringBoot에 코드를 작성하여 실질적으로 s3에 이미지 혹은 파일을 업로드하는데 사전작업이 필요합니다. </p>
<ul>
<li>s3 버킷 생성</li>
<li>IAM 사용자 생성</li>
</ul>
<p>사전작업방법은 하단의 링크를 참고하시기 바랍니다.</p>
<blockquote>
<p><a href="https://velog.io/@half-phycho/AWS-Spring-Boot-S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-1">https://velog.io/@half-phycho/AWS-Spring-Boot-S3-이미지-업로드-1</a></p>
</blockquote>
<br>

<h2 id="springboot-코드-작성">SpringBoot 코드 작성</h2>
<p>사전작업을 마치셨다면 이제 본격적으로 코드를 작성해보겠습니다, SpringBoot에서는 다음과 같은 설정을 거쳐야합니다.</p>
<ul>
<li>application.properties 파일에 사용자 및 버킷 등록 </li>
<li>gradle에 Amazon S3 의존성 추가</li>
<li>Amazon S3 config 파일 생성</li>
<li>Amazon S3에 파일을 등록하는 기능 클래스 생성</li>
<li>Spring Boot service layer에서 활용</li>
</ul>
<p>이 게시글에서는 다음과 같은 환경에서 제작했습니다.</p>
<pre><code>OS : window11
Framework : Spring Boot 3.4.5
gradle : 8.8</code></pre><br>

<h3 id="properties-파일-작성">properties 파일 작성</h3>
<p>제일먼저 application 파일에 사전에 만든 IAM사용자 정보와 S3 버킷의 정보를 등록해야 합니다.</p>
<pre><code># Amazon S3 config
cloud.aws.credentials.accessKey=
cloud.aws.credentials.secretKey=
cloud.aws.s3.bucket=
cloud.aws.region.static=
cloud.aws.stack.auto=false

# 파일이 업로드 되는 위치
file.upload.url=</code></pre><ul>
<li><code>cloud.aws.credentials.accessKey</code> : IAM 사용자 Access key를 입력합니다.</li>
<li><code>cloud.aws.credentials.secretKey</code> : IAM 사용자 Secret key를 입력합니다.</li>
<li><code>cloud.aws.s3.bucket</code> : 생성한 S3 버킷의 이름을 입력합니다. </li>
<li><code>cloud.aws.region.static</code> : 버킷을 생성한 리전을 입력합니다. 만약 한국리전인 경우 ap-northeast-2를 입력하면 됩니다.</li>
<li><code>cloud.aws.stack.auto=false</code> (선택) : 해당 설정은 EC2에 배포하는 용도인 경우에 설정하면 됩니다. EC2는 기본적으로 CloudFormation을 구성합니다. 그래서 사용할지 안할지를 선택하는데 해당 게시글에서는 사용하지 않기 때문에 false로 설정하겠습니다.<blockquote>
<p>CloudFormation : AWS 리소스를 생성하기 위한 각종 설정을 템플릿 파일로 만들어서 사용하는 도구입니다.</p>
</blockquote>
</li>
</ul>
<p>(IAM의 Access Key, Secret Key를 분실한 경우 사용자에서 액세스 키를 추가로 생성해야 합니다.)</p>
<br>

<h3 id="amazon-s3-의존성-추가">Amazon S3 의존성 추가</h3>
<pre><code class="language-gradle">dependencies {
    implementation &#39;org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE&#39;
}</code></pre>
<p>해당 AWS를 연결하는 의존성을 추가합니다.</p>
<br>

<h3 id="amazon-s3-config-파일-생성">Amazon S3 config 파일 생성</h3>
<pre><code class="language-java">import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

@Configuration
public class s3SetConfig {

    @Value(&quot;${cloud.aws.credentials.accessKey}&quot;)
    private String accessKey;   // IAM Access Key

    @Value(&quot;${cloud.aws.credentials.secretKey}&quot;)
    private String secretKey;   // IAM Secret Key

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

    @Bean
    public AmazonS3Client amazonS3Client() {

        // 임시로 자격증명
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); 

        return (AmazonS3Client) AmazonS3ClientBuilder // S3용 빌더
                .standard() // 기본 
                .withCredentials(new AWSStaticCredentialsProvider(credentials)) // IAM 사용자
                .withRegion(region)  // S3 버킷이 위치한 리전
                .build();
    }
}</code></pre>
<p>해당 config파일을 보면 먼저 properies파일에 입력한 IAM의 accessKey, secretKey, region을 가져옵니다. 그리고 amazonS3Client Bean 객체를 등록하고 AWSCredentials 객체에 사용자를 등록한 후 리턴값으로 S3 빌더를 생성하는 코드 입니다.</p>
<br>

<h3 id="s3에-파일을-등록하는-기능-클래스-생성">S3에 파일을 등록하는 기능 클래스 생성</h3>
<p>이제 S3에 파일을 저장하는 클래스를 만들어 보겠습니다.</p>
<pre><code class="language-java">import java.io.File;
import java.io.IOException;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;

import lombok.extern.slf4j.Slf4j;

/**
 * S3 Bucket에 포스터 이미지 파일 업로드 class
 */
@Component
@Slf4j // 로그 기록 목적으로 추가한 lombok의 어노테이션 입니다.
public class awsfileUtil {

    // Amazon S3 버킷 이름
    @Value(&quot;${cloud.aws.s3.bucket}&quot;)
    private String filebucket;

    // 파일 업로드 경로
    @Value(&quot;${file.upload.url}&quot;)
    private String fileUrl;

    private final AmazonS3Client amazonS3Client;

    public awsposterfileUtil(AmazonS3Client amazonS3Client){
        this.amazonS3Client = amazonS3Client;
    }

    public String Upload(MultipartFile multipartFile){

        UUID uuid = UUID.randomUUID();

        // UUID 난수 + _ + 파일 원본 이름 
        String fileName = uuid + &quot;_&quot; + multipartFile.getOriginalFilename();
        String uploadImageUrl; // s3 저장경로
        File saveFile = new File(fileUrl, fileName); // 파일 위치정보

        if(!saveFile.getParentFile().exists()){ // 폴더 존재 여부 체크
            saveFile.getParentFile().mkdir(); // 폴더가 없으면 폴더 생성
        }

        try{
            multipartFile.transferTo(saveFile);// 생성한 파일 경로에 저장
            // 파일 업로드 후 S3경로 리턴
            uploadImageUrl = this.S3upload(saveFile, fileName);
        } catch(IOException e) { // 저장 실패
            saveFile.delete(); 
            uploadImageUrl = &quot;&quot;; 
            e.printStackTrace();
        }

        return uploadImageUrl; // s3 bucket에 올린 파일 URL 혹은 &quot;&quot; 리턴
    }

    /**
     * Amazon S3에 파일 업로드 후 폴더의 원본파일 삭제
     * 
     * @param uploadFile
     * @param fileName
     * @return String
     */
    private String S3upload(File uploadFile, String fileName){

        /**
         * 로컬 업로드 -&gt; S3upload(저장한 로컬파일 위치, 새로 지정한 파일이름)
         * -&gt; 로컬 업로드 파일 삭제
         */

        String uploadImageUrl = putS3(uploadFile, fileName); // s3 업로드

        if(uploadFile.delete()) { // 기존 로컬 환경에 업로드한 파일 삭제
            log.info(&quot;파일이 삭제되었습니다.&quot;);
        }else {
            log.info(&quot;파일이 삭제되지 못했습니다.&quot;);
        }

        return uploadImageUrl; // s3 bucket에 올린 파일 URL 리턴
    }

    /**
     * Amazon S3에 파일 업로드
     * 
     * @param uploadFile
     * @param fileName
     * @return String
     */
    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject( // s3 파일 업로드
                new PutObjectRequest(filebucket, fileName, uploadFile) 
                        .withCannedAcl(CannedAccessControlList.PublicRead)    
                        // PublicRead 권한으로 업로드 (사용자 모두가 접근 가능)
        );

        // s3 bucket에 올린 파일 URL
        return amazonS3Client.getUrl(filebucket, fileName).toString();
    }
}</code></pre>
<p>저의경우에는 해당 기능을 다른 서비스에서도 활용이 가능하도록 작성했습니다, 해당 코드에서 중요한 부분중에 하나가 S3 CannedAccessControlList 권한 종류인데 권한 종류는 하단에 작성했으니 참고바랍니다.</p>
<h4 id="s3-cannedaccesscontrollist-권한-종류">S3 CannedAccessControlList 권한 종류</h4>
<p>Amazon S3의 <strong>CannedAccessControlList(Canned ACL)</strong>는 버킷이나 객체에 대해 미리 정의된 권한을 설정할 수 있는 간단한 방식입니다. 각 Canned ACL의 권한은 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th><strong>Canned ACL</strong></th>
<th><strong>설명</strong></th>
<th><strong>소유자 권한</strong></th>
<th><strong>부여된 권한</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Private</strong></td>
<td>버킷 또는 객체 소유자만 전체 제어 권한을 가집니다.</td>
<td>전체 제어(Full Control)</td>
<td>없음(None)</td>
</tr>
<tr>
<td><strong>PublicRead</strong></td>
<td>소유자는 전체 제어 권한을 가지며, 모든 사용자에게 읽기 권한을 부여합니다.</td>
<td>전체 제어(Full Control)</td>
<td>모두 읽기(Read for Everyone)</td>
</tr>
<tr>
<td><strong>PublicReadWrite</strong></td>
<td>소유자는 전체 제어 권한을 가지며, 모든 사용자에게 읽기와 쓰기 권한을 부여합니다.</td>
<td>전체 제어(Full Control)</td>
<td>모두 읽기 및 쓰기(Read and Write for Everyone)</td>
</tr>
<tr>
<td><strong>AuthenticatedRead</strong></td>
<td>소유자는 전체 제어 권한을 가지며, 인증된 AWS 사용자에게 읽기 권한을 부여합니다.</td>
<td>전체 제어(Full Control)</td>
<td>인증된 사용자 읽기(Read for Authenticated Users)</td>
</tr>
<tr>
<td><strong>BucketOwnerRead</strong></td>
<td>소유자는 전체 제어 권한을 가지며, 버킷 소유자에게 읽기 권한을 부여합니다. (계정 간 접근 시 사용)</td>
<td>전체 제어(Full Control)</td>
<td>버킷 소유자 읽기(Read for Bucket Owner)</td>
</tr>
<tr>
<td><strong>BucketOwnerFullControl</strong></td>
<td>소유자와 버킷 소유자 모두 전체 제어 권한을 가집니다. (계정 간 접근 시 사용)</td>
<td>전체 제어(Full Control)</td>
<td>버킷 소유자 전체 제어(Full Control for Bucket Owner)</td>
</tr>
<tr>
<td><strong>LogDeliveryWrite</strong></td>
<td>로그 전달 그룹(Log Delivery Group)에게 버킷에 로그를 작성할 수 있는 권한을 부여합니다. (S3 서버 액세스 로깅에 사용)</td>
<td>전체 제어(Full Control)</td>
<td>로그 전달 그룹의 쓰기 및 액세스 제어 정책 읽기(Write and Read ACP for Log Delivery Group)</td>
</tr>
<tr>
<td><strong>AWSExecRead</strong></td>
<td>AWS 서비스(예: Amazon CloudFront)가 버킷 객체를 읽을 수 있는 권한을 부여합니다. (드물게 사용)</td>
<td>전체 제어(Full Control)</td>
<td>AWS 관리 서비스 읽기(Read for AWS Managed Services)</td>
</tr>
</tbody></table>
<p><strong>추가 사항</strong></p>
<ol>
<li><strong>소유자</strong>: 버킷 또는 객체를 생성한 AWS 계정입니다.</li>
<li><strong>모두</strong>: 인증된 사용자와 비인증 사용자를 포함합니다.</li>
<li><strong>인증된 사용자</strong>: 유효한 AWS 자격 증명을 사용하여 로그인한 사용자입니다.</li>
<li><strong>계정 간 접근</strong>: 다른 AWS 계정과 객체 또는 버킷을 공유할 때 사용합니다.</li>
</ol>
<p>Canned ACL은 S3 리소스에 대한 간단한 액세스 제어를 설정하는 데 유용하며, 상황에 맞게 적절한 ACL을 선택하는 것이 중요합니다.</p>
<br>

<h3 id="spring-boot-service-layer에서-활용">Spring Boot service layer에서 활용</h3>
<pre><code class="language-java">@Service
@Slf4j
public class fileService {

    private final awsfileUtil awsfileUtil;

    public fileService(awsfileUtil awsfileUtil){
        this.awsfileUtil = awsfileUtil;
    }

    public void fileSave(MultipartFile file) {
         String storedFileName = awsfileUtil.posterUpload(file);
    }

}</code></pre>
<p>(단순사용은 이렇게 표현할 수 있지만 <code>awsfileUtil</code>에서 Exception을 발생할 수 있기 때문에 추가 설정사항이 필요할 수 있습니다.)</p>
<br>

<h2 id="주의사항">주의사항</h2>
<p>만약 <code>putS3</code> 부분에서 Exception혹은 에러가 발생하면 properties파일을 제대로 작성했는지 그리고 IAM의 권한 부분을 확인하기 바랍니다.</p>
<br>

<h2 id="참고-사이트">참고 사이트</h2>
<p><a href="https://jojoldu.tistory.com/300">https://jojoldu.tistory.com/300</a></p>
<p>(항상 감사합니다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] Spring Boot S3 이미지 업로드 (1)]]></title>
            <link>https://velog.io/@half-phycho/AWS-Spring-Boot-S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-1</link>
            <guid>https://velog.io/@half-phycho/AWS-Spring-Boot-S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-1</guid>
            <pubDate>Thu, 19 Jun 2025 08:49:50 GMT</pubDate>
            <description><![CDATA[<h2 id="작성계기">작성계기</h2>
<p>사이드 프로젝트를 진행하던중 이미지 업로드기능이 필요했고 그래서 초기에는 로컬PC에 이미지를 저장하는 방법을 썼고 브라우저에서 로컬PC로 파일을 저장하는 것 까지는 성공 했지만 </p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/7ca5e982-5bed-4145-aeb0-e1241015da26/image.png" alt=""></p>
<p>로컬PC에 저장된 이미지를 브라우저에 출력하는 것은 성공하지 못했습니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/023cbc48-07a3-4ec9-880b-257585e3c31f/image.png" alt=""></p>
<p>그러던 와중에 Amazon S3에 파일을 저장하는 방식을 발견했고 예전에도 S3를 이용해 CI/CD를 구축하기 위해 S3를 사용한 경험이 있어서 S3에 업로드하는 방식으로 바꾸기로 했습니다. 또한 나중에 프로젝트를 현재는 로컬로 개발하고 있지만 운영은 AWS에서 할 생각이기에 나중에 파일 업로드 관련해서 별도 AWS환경에 맞게 수정을 많이 거칠 필요가 없기 때문에 이렇게 하기로 결정했습니다.</p>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/30a3489f-7ceb-45f9-a381-f6a708551241/image.png" alt=""></p>
<p>(해당 주제는 내용이 많기에 Amazon에서의 작업과 Spring Boot 코드작성 부분 이렇게 두 개로 나누어 작성할 계획입니다.)</p>
<br>

<h2 id="amazon-aws-준비목록">Amazon AWS 준비목록</h2>
<ul>
<li>S3 bucket 등록</li>
<li>IAM 사용자 생성</li>
</ul>
<h2 id="s3-bucket-생성">S3 bucket 생성</h2>
<p>S3 bucket은 이미지를 저장하는 저장소 입니다. </p>
<h3 id="1-버킷-생성">1. 버킷 생성</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/1d567938-49f3-4df2-8e58-146be0978e42/image.png" alt=""></p>
<p>다음과 같이 Amazon S3으로 이동하고 <code>버킷 만들기</code> 버튼을 클릭하여 버킷 생성 준비를 합니다.</p>
<br>

<h3 id="2-버킷-이름-지정">2. 버킷 이름 지정</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/41f81f44-07ef-4b76-9e66-74cf926094c7/image.png" alt=""></p>
<p>버킷이름을 지정합니다.</p>
<br>

<h3 id="3-객체-소유권-지정">3. 객체 소유권 지정</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/30f8a0a2-b792-40c2-8e7f-1a68c3368db0/image.png" alt=""></p>
<p>객체 소유권은 ACL 비활성화 시키는데 다른 계정은 접근하지 못하도록 하기위해 설정 한 것 입니다.</p>
<br>

<h3 id="4-퍼블릭-액세스-차단설정">4. 퍼블릭 액세스 차단설정</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/11d50879-3d56-4f08-b2b5-bcfc8b0cc821/image.png" alt=""></p>
<p>만약 데이터가 중요도가 낮다면 퍼블릭 액세스를 차단하지 않아도 상관 없습니다. 하지만 뒤에서 설정할 IAM에서 허용한 사용자만 이미지를 입력하도록 하기위해 다음과 같이 설정합니다.</p>
<br>

<h3 id="5-버킷-버전-관리--기본-암호화-활성화">5. 버킷 버전 관리 / 기본 암호화 활성화</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/fe864aea-f1be-46cd-9dc6-e01c8310a635/image.png" alt=""></p>
<ul>
<li>버킷 버전 관리 : 활성화하면 나중에 모든 객체의 보존, 검색 및 복원할 수 있습니다.</li>
<li>기본 암호화 : 서버 암호화는 기본으로 체크되어있는 것을 세팅합니다.</li>
</ul>
<p>마지막으로 <code>버킷 만들기</code>버튼을 클릭하면 버킷이 생성됩니다.</p>
<br>

<h2 id="iam-사용자-생성">IAM 사용자 생성</h2>
<p>IAM(Identity and Access Management)은 AWS에서 사용자, 역할, 정책을 기반으로 리소스 접근을 제어하고, 안전하게 클라우드 환경을 구축하기위한 필수 서비스 입니다.</p>
<h3 id="1-iam-사용자-생성">1. IAM 사용자 생성</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/668fec7b-a9f7-4611-8c2d-2fdd47973932/image.png" alt=""></p>
<p>먼저 사용자에서 <code>사용자 생성</code>버튼을 클릭하여 사용자를 생성합니다.</p>
<br>

<h3 id="2-사용자-이름-설정">2. 사용자 이름 설정</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/c4ce7d5d-a893-4237-9a4b-580099e8f1d2/image.png" alt=""></p>
<p>생성할 사용자 이름을 입력하고 <code>다음</code> 버튼을 클릭합니다.</p>
<br>

<h3 id="3-권한-설정">3. 권한 설정</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/ef00ea20-61e6-4d9b-aae0-652f97fd3b32/image.png" alt=""></p>
<p>권한을 설정하는데 2가지 방법이 있습니다. AWS에서 제공하는 권한 정책을 사용하거나 아니면 직접 정책을 생성하는 방법 이렇게 2가지가 있습니다. </p>
<p><strong>첫 번째</strong>는 AmazonS3FullAccess 정책을 선택하는 방법입니다, 저는 개인적으로 이 방법을 선호하지 않고 있는데 AmazonS3FullAccess는 사용자가 S3의 모든 버킷에 접근을 가능하다는 의미이기에 보안 이슈가 생길 우려가 있습니다.</p>
<p><strong>두 번째</strong>는 정책을 생성하는 방법입니다, 이 방법은 상세하게 create,delete,update,read 권한을 직접 지정할 수 있고 또한 특정 버킷에만 접근이 가능하도록 설정이 가능합니다.</p>
<br>

<h3 id="3-1-권한-제작">3-1 권한 제작</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/277936ab-0eb5-4347-9c7c-46d6289ec70d/image.png" alt=""></p>
<p>스크립트 예시</p>
<pre><code>{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: [
                &quot;s3:GetObject&quot;,
                &quot;s3:PutObject&quot;,
                &quot;s3:PutObjectAcl&quot;,
                &quot;s3:DeleteObject&quot;
            ],
            &quot;Resource&quot;: [
                &quot;arn:aws:s3:::{bucketname1}/*&quot;,
                &quot;arn:aws:s3:::{bucketname2}/*&quot;
            ]
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &quot;s3:ListBucket&quot;,
            &quot;Resource&quot;: [
                &quot;arn:aws:s3:::{bucketname1}/*&quot;,
                &quot;arn:aws:s3:::{bucketname2}/*&quot;
            ]
        }
    ]
}</code></pre><p>스크립트는 다음과 같이 작성 할 수 있겠습니다. </p>
<ul>
<li><strong>Effect</strong> : 는 블록에 있는 권한을 허용할지 여부를 결정하는 내용 입니다. 그래서 <code>Allow/Deny</code> 이렇게 둘 중 허용하면 Allow 거부하면 Deny를 작성합니다.</li>
<li><strong>Action</strong> : 이 부분은 &quot;문 편집&quot; 이하는 부분의 작업추가 항목에 서비스를 검색하면 쉽게 추가할 수 있습니다.</li>
<li><strong>Resource</strong> : 여기는 접근할 영역을 선택하는 부분인데 여기서는 접근할 S3버킷을 입력하면 됩니다. 작성 규칙은 <code>arn:aws:s3:::{버킷이름}/{접근할 리소스}</code> 이런식으로 작성하는데 만약 접근할 리소스 부분에 <code>*</code>를 입력하면 해당 버킷의 모든 내용에 접근하겠다는 의미 입니다, 그리고 버킷을 여러개 설정하고 싶으면 위의 예시대로 <code>,</code>을 붙이고 뒤에 버킷을 추가합니다.</li>
</ul>
<p>만약 권한을 생성한다면 해당 스크립트를 참고 하시기 바랍니다.</p>
<br>

<h3 id="사용자-생성완료">사용자 생성완료</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/440ab3a7-b193-400c-89ad-c54b1a660609/image.png" alt=""></p>
<p>사용자 생성을 완료하고 사용자 목록 페이지에서 생성한 사용자로 이동하면 다음과 같이 확인할 수 있습니다.</p>
<br>

<h3 id="액세스-키-생성">액세스 키 생성</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/2ed9a344-b754-464c-ad90-6f7c12b19865/image.png" alt=""></p>
<p>실제로 S3에 업로드를 하기위해서는 액세스 키가 필요합니다. 첫 번째로 액세스 키 대안을 선택해야하는데 위의 이미지처럼 맨 처음 사례를 선택하고 확인 부분을 체크 합니다.</p>
<br>

<h3 id="액세스-키-생성-완료">액세스 키 생성 완료</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/ed38f38d-d6aa-4640-8094-30c941f1a371/image.png" alt=""></p>
<p>선택적으로 태그를 입력하고 다음단계로 넘어가면 이렇게 액세스 키를 생성되는 것을 확인할 수 있습니다. </p>
<p>(액세스 키의 secret을 분실하면 다시 확인이 불가능하나 액세스 키는 추가적으로 생성이 가능합니다.)</p>
<br>

<h2 id="참고-사이트">참고 사이트</h2>
<p><a href="https://smilesharkhelp.zendesk.com/hc/ko/articles/6193081783823--IAM-IAM-S3-Access-Key-%EB%B0%9C%EA%B8%89-%EA%B0%80%EC%9D%B4%EB%93%9C">https://smilesharkhelp.zendesk.com/hc/ko/articles/6193081783823--IAM-IAM-S3-Access-Key-%EB%B0%9C%EA%B8%89-%EA%B0%80%EC%9D%B4%EB%93%9C</a></p>
<p><a href="https://j-d-i.tistory.com/271">https://j-d-i.tistory.com/271</a></p>
<p><a href="https://somaz.tistory.com/181">https://somaz.tistory.com/181</a></p>
<p>(항상 감사합니다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker 설치 및 사용법]]></title>
            <link>https://velog.io/@half-phycho/Docker-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@half-phycho/Docker-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Mon, 05 May 2025 09:22:10 GMT</pubDate>
            <description><![CDATA[<h2 id="작성계기">작성계기</h2>
<p>일단 크게 2가지 이유가 있다. 최근에 과제테스트를 하면서 Docker 파일을 만들고 Docker 파일만으로 프로젝트를 실행하라는 과제를 받았다. 과제를 무사히 수행했지만 부족한 부분이 있었고 어떻게 보면 이유가 가장 컸다. 최근에 만든 서비스인 <a href="https://jpboard.co.kr/main">JPBoard</a> 서비스를 운영하면서 AWS로부터 영수증이 왔는데 금액이 <strong>60달러</strong>나 되었다.</p>
<img src="https://velog.velcdn.com/images/half-phycho/post/2e0cef18-175b-4274-b430-4be4a91f9f82/image.png">

<p>여기서부터는 예상이 된다고 생각하는데 간단히 이야기하자면 <strong>돈으로 두들겨 맞고 정신 차린 것이다.</strong> 애초에 운영 Linux 서버를 Red Hat을 사용한 것이 문제점이라고 생각한다. 정확히는 Red Hat을 사용한 이유가 문제라고 할 수 있다. 내가 원하는 mariaDB버전이 없어서 Red Hat을 썼다. 만약 지금처럼 Docker를 사용하여 프로젝트를 운영했다면 비용이 최소한 지금보다는 적게 나왔을 것이다.</p>
<p>현재는 문제를 해결했으니 정리해보자는 차원에서 작성하게 되었다.</p>
<br>

<h2 id="docker-기본설명">Docker 기본설명</h2>
<p>Docker는 간단히 이야기 하면 컨테이너를 실행하기 위한 소프트웨어이다. 자세히 이야기 하면 Docker는 GO언어 기반으로 만든 리눅스 컨테이너 기반으로 하는 오픈소스 가상화 플랫폼이다. </p>
<br>

<h2 id="hypervisor-컨테이너-특징-및-비교">Hypervisor, 컨테이너 특징 및 비교</h2>
<p>Docker를 설명하기 이전에 Hypervisor와 컨테이너를 설명하고 넘어갈 필요가 있다. </p>
<blockquote>
<p><strong>가상화</strong>
물리적인 하드웨어를 추상화하여 여러 개의 가상 컴퓨터처럼 활용하는 기술</p>
</blockquote>
<blockquote>
<p><strong>가상머신</strong> 
가상머신은 물리적 하드웨어 시스템에 구축되어 자체 CPU, 메모리, 네트워크 인터페이스 및 저장공간을 갖추고 가상 컴퓨터 시스템으로 작동하는 가상환경이다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/709db25f-fee3-4eab-93d8-0eaf45b31a35/image.png" alt=""></p>
<h3 id="hypervisor">Hypervisor</h3>
<p>하이퍼바이저는 가상화 계층을 구현해 주는 소프트웨어로 위의 그림을 보면 하이퍼바이저 위에 Virtualzation으로 그려진 곳이 가상화 계층인데 원래는 가상화 구조에서 호스트 OS의 하드웨어 자원과 가상머신은 직접 연결을 해줄 수 없는데 여기서 하이퍼바이저가 가상머신에 자원 및 네트워크를 할당해 주어 일종의 가상머신 매니저 역할을 수행한다. 하이퍼바이저는 실제로 가상머신의 생성, 실행, 삭제 등 모든 환경을 관리하기에 <strong>(VMM : Virtual Machine Manager)</strong>으로 불리기도 한다.</p>
<h3 id="컨테이너container">컨테이너(Container)</h3>
<p>컨테이너는 애플리케이션을 실행하는 데 필요한 모든 구성요소와 기능을 갖춘 소프트웨어 단위이다. 기존 OS를 가상화 시키는 것에서 <strong>OS레벨의 가상화로 프로세스를 격리시켜 동작한다.</strong> 가상머신과 비교했을 때 소프트웨어 단위이기에 용량이 적고 이식성이 뛰어나다. 대표적으로 MSA(Micro Service Architecture)기반 서비스에 많이 쓰인다.</p>
<h3 id="가상머신-vs-컨테이너">가상머신 VS 컨테이너</h3>
<table>
<thead>
<tr>
<th></th>
<th>가상머신</th>
<th>컨테이너</th>
</tr>
</thead>
<tbody><tr>
<td>가상화</td>
<td>물리적 인프라를 가상화한다.</td>
<td>현재 운영체제를 가상화 한다.</td>
</tr>
<tr>
<td>캡슐화</td>
<td>운영체제, 그 위의 소프트웨어 계층, 그리고 어플리케이션이 포함된다.</td>
<td>운영체제를 가상화 한다.</td>
</tr>
<tr>
<td>조율</td>
<td>하이퍼바이저 기반 운영체제 또는 하드웨어를 조율한다.</td>
<td>현재 운영체제와 리소스를 조율한다.</td>
</tr>
<tr>
<td>크기</td>
<td>GB 단위</td>
<td>MB 단위</td>
</tr>
<tr>
<td>제어</td>
<td>가상머신 전체환경을 제어할 수 있다.</td>
<td>컨테이너 입장에서 보면 외부환경을 제어할 수 있는 권한이 적다.</td>
</tr>
<tr>
<td>유연성</td>
<td>마이그레이션이 어려울 수 있다.</td>
<td>크기 자체가 적기에 유연성이 좋다.</td>
</tr>
<tr>
<td>확장성</td>
<td>확장하는데 비용이 많이 들 수 있다.</td>
<td>마이크로서비스를 통해 세분화된 확장이 가능하기에 확장성이 뛰어나다.</td>
</tr>
</tbody></table>
<br>

<h2 id="docker-설치">Docker 설치</h2>
<p>Docker는 앞에서 컨테이너를 운영하기 위한 소프트웨어의 일종이라고 했다. 실제로 컨테이너를 만드는 것만 보면 Docker가 필요없긴 하다. 하지만 컨테이너를 효과적으로 다루고 싶으면 Docker가 필요하다.</p>
<blockquote>
<p><a href="https://docs.docker.com/engine/install/">https://docs.docker.com/engine/install/</a></p>
</blockquote>
<p>위의 사이트는 Docker를 설치하는 방법을 알려주는 사이트이다. 하지만 이 게시글에서는 Docker를 설치하는 OS환경은 <strong>Amazon 2023 AMI</strong> 이다.</p>
<br>

<h3 id="yum-업데이트">yum 업데이트</h3>
<p>시스템의 모든 패키지를 최신 버전으로 업데이트</p>
<pre><code>&gt; sudo yum update -y</code></pre><h3 id="docker-설치-1">Docker 설치</h3>
<pre><code>&gt; sudo yum install docker -y</code></pre><h3 id="docker-실행">Docker 실행</h3>
<p>Docker를 설치하는 순간 자동으로 실행이 된다.</p>
<pre><code>&gt; sudo service docker start</code></pre><br>


<h2 id="docker-관련-명령어">Docker 관련 명령어</h2>
<h3 id="컨테이너-생성-및-시작-run">컨테이너 생성 및 시작 (run)</h3>
<pre><code>&gt; docker run [옵션] 이미지명[:태그명] [인수]</code></pre><ul>
<li>옵션들<ul>
<li>--attach, -a : 표준 입/출력/오류 출력을 첨부한다.</li>
<li>--cidfile : 컨테이너 ID를 파일로 출력한다.</li>
<li>--detach, -d : 컨테이너를 생성하고 백그라운드에서 실행한다.</li>
<li>--interactive, -i : 컨테이너의 표준 입력을 연다.</li>
<li>--tty, -t : 단말기 디바이스를 사용한다.</li>
<li>--name : 컨테이너 이름을 지정한다.</li>
<li>--rm : 실행이 끝나고 즉시 제거한다.</li>
</ul>
</li>
</ul>
<h3 id="컨테이너-생성-create">컨테이너 생성 (create)</h3>
<pre><code>&gt; docker create [이름]</code></pre><h3 id="컨테이너-시작-start">컨테이너 시작 (start)</h3>
<p>컨테이너 시작은 기본적으로 백그라운드로 실행된다. </p>
<pre><code>&gt; docker start [옵션] [컨테이너 식별자]</code></pre><ul>
<li><p>옵션들</p>
<ul>
<li>--attach, -a : 표준 입/출력/오류 출력을 첨부한다.</li>
<li>--interactive, -i : 컨테이너의 표준 입력을 연다.</li>
</ul>
</li>
<li><p>컨테이너 식별자</p>
<ul>
<li>컨테이너 고유 ID 혹은 Name</li>
</ul>
</li>
</ul>
<h3 id="컨테이너-정지-stop">컨테이너 정지 (stop)</h3>
<pre><code>&gt; docker stop [옵션] [컨테이너 식별자]</code></pre><ul>
<li><p>옵션들</p>
<ul>
<li>--time, -t : 몇초 후 정지할 것인지 지정 (기본값은 10초)</li>
<li>강제정지 명령어 : <code>docker contianer kill</code></li>
</ul>
</li>
<li><p>컨테이너 식별자</p>
<ul>
<li>컨테이너 고유 ID 혹은 Name</li>
</ul>
</li>
</ul>
<h3 id="컨테이너-삭제-rm">컨테이너 삭제 (rm)</h3>
<pre><code>&gt; docker rm [옵션] [컨테이너 식별자]</code></pre><ul>
<li><p>옵션들</p>
<ul>
<li>--force, -f : 실행 중인 컨테이너를 강제로 삭제</li>
<li>--volume, -v : 할당한 볼륨을 삭제</li>
<li>도커 컨테이너 전부 삭제 : <code>docker rm `docker ps -a --quiet`</code></li>
<li>불필요한 이미지/컨테이너 전부 삭제 : <code>docker system prune</code></li>
</ul>
</li>
<li><p>컨테이너 식별자</p>
<ul>
<li>컨테이너 고유 ID 혹은 Name</li>
</ul>
</li>
</ul>
<h3 id="컨테이너-로그-logs">컨테이너 로그 (logs)</h3>
<pre><code>&gt; docker logs [옵션] [컨테이너 식별자]</code></pre><h3 id="모든-컨테이너-목록-보기">모든 컨테이너 목록 보기</h3>
<pre><code>&gt; docker ps -a</code></pre><h3 id="docker-이미지-목록-보기">docker 이미지 목록 보기</h3>
<pre><code>&gt; docker images</code></pre><h3 id="docker-이미지-삭제">docker 이미지 삭제</h3>
<pre><code>&gt; docker rmi [이미지 식별자]</code></pre><ul>
<li>이미지 식별자<ul>
<li>이미지 고유 ID 혹은 Name</li>
</ul>
</li>
</ul>
<br>

<h2 id="참고-사이트">참고 사이트</h2>
<p><a href="https://khj93.tistory.com/entry/Docker-Docker-%EA%B0%9C%EB%85%90">https://khj93.tistory.com/entry/Docker-Docker-%EA%B0%9C%EB%85%90</a></p>
<p><a href="https://aws.amazon.com/ko/compare/the-difference-between-containers-and-virtual-machines/">https://aws.amazon.com/ko/compare/the-difference-between-containers-and-virtual-machines/</a></p>
<p><a href="https://www.redhat.com/ko/topics/containers/containers-vs-vms">https://www.redhat.com/ko/topics/containers/containers-vs-vms</a></p>
<p><a href="https://selog.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94-%EA%B0%80%EC%83%81%EB%A8%B8%EC%8B%A0VM%EA%B3%BC-%ED%95%98%EC%9D%B4%ED%8D%BC%EB%B0%94%EC%9D%B4%EC%A0%80-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0">https://selog.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94-%EA%B0%80%EC%83%81%EB%A8%B8%EC%8B%A0VM%EA%B3%BC-%ED%95%98%EC%9D%B4%ED%8D%BC%EB%B0%94%EC%9D%B4%EC%A0%80-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</a></p>
<p><a href="https://velog.io/@milkskfk5677/AWS-EC2%EC%97%90-Docker-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0">https://velog.io/@milkskfk5677/AWS-EC2에-Docker-설치하기</a></p>
<p><a href="https://imjeongwoo.tistory.com/111">https://imjeongwoo.tistory.com/111</a></p>
<p>(항상 감사합니다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내한일정 한번에 보고싶다 (JPBoard)]]></title>
            <link>https://velog.io/@half-phycho/%EB%82%B4%ED%95%9C%EC%9D%BC%EC%A0%95-%ED%95%9C%EB%B2%88%EC%97%90-%EB%B3%B4%EA%B3%A0%EC%8B%B6%EB%8B%A4-JPBoard</link>
            <guid>https://velog.io/@half-phycho/%EB%82%B4%ED%95%9C%EC%9D%BC%EC%A0%95-%ED%95%9C%EB%B2%88%EC%97%90-%EB%B3%B4%EA%B3%A0%EC%8B%B6%EB%8B%A4-JPBoard</guid>
            <pubDate>Wed, 26 Mar 2025 06:04:37 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>지난번 VSCode 게시글을 작성하고 오랜만에 작성하는 글입니다.</p>
<p>올 해 시작하고 지금까지 정신없이 바빴던거 같습니다, 현재는 회사를 그만두고 지금 열심히 구직활동을 하고있지만 연이은 실패에 스트레스로 응급실에 갈 정도로 심하기도 했습니다, 하지만 그럴때 마다. 여기서 절망하면 영원히 돌아오지 못할거라는걸 본능으로 알았는지 계속 무언가를 시도 한 거 같습니다. 면접도 꾸준히 보고 처음으로 과제테스트라는 것도 해보았는데 좋은 경험이었습니다.</p>
<blockquote>
<p>결국 탈락했지만 그 회사에서 답변으로 기본기있게 세심하게 잘 만들었고 유닛테스트도 빠지지 않고 진행한 흔적을 보고 꼼꼼함을 느낄 수 있었지만 동시성 문제에 대한 고민이 없던 것에 아쉬움이 있어서 불합격을 시켰다고 한다. (뭐 어쩔수 없지~~)</p>
</blockquote>
<p>그리고 면접을 계속봤지만 3번중에 2군데는 탈락하고 1군데 결과를 기다리고 있는 상태입니다.(합격했으면 좋겠다.) 현황이야기는 여기까지만 하고 서비스에 대해 설명드리겠습니다.
<br></p>
<h2 id="서비스를-만들게-된-계기">서비스를 만들게 된 계기</h2>
<p>게시글 제목대로 서비스의 이름은 JPBoard인데 JPBoard는 간단하게 말하자면 일본의 아티스트들이 한국에 내한을 하게되면 그 일정을 하나의 달력에서 볼 수 있게하는 서비스입니다.</p>
<p>이 서비스를 만든 이유를 설명하자면 작년으로 거슬러 올라가야 합니다.
<img src="https://velog.velcdn.com/images/half-phycho/post/c9383699-0324-41e8-a95c-61be8bf04d2a/image.jpg" alt=""></p>
<p>위의 사진은 작년에 <strong>YOASOBI</strong>라는 아티스트 그룹이 한국에서 공연했을때 거기서 찍은 사진입니다.</p>
<p>공연이 끝나고 인천에서 서울로 돌아가는 버스 안에서 새로운 서비스의 주제를 생각하던 중 일본에 내한하는 아티스트들의 일정을 한번에 확인하면 좋을텐데 라는 생각을 하게 되었고 그 날 버스에서 생각한 아이디어가 지금의 서비스를 만들게된 계기가 되었습니다.</p>
<br>

<h2 id="서비스-설명">서비스 설명</h2>
<p>현재 AWS에서 동작하고는 있지만 현재 도메인을 등록하지 않아서 IP로 접속해야합니다. 향후 안정화가 되면 도메인을 신청고 정식적으로 운영및 홍보를 할 계획입니다.</p>
<blockquote>
<p><a href="https://jpboard.co.kr/main">https://jpboard.co.kr/main</a></p>
</blockquote>
<h3 id="메인화면">메인화면</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/13993c48-4735-46c3-b0c3-8c0804d1a44c/image.gif" alt=""></p>
<p>위의 화면이 메인화면으로 간단하게 달력에 일정을 표시하고 일정을 클릭하면 해당 콘서트의 상세정보가 modal창으로 출력하는 방식입니다.
<br></p>
<h3 id="회원가입">회원가입</h3>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/e5cee02a-57fb-43a1-b183-803f6a1316ef/image.png" alt=""></p>
<p>회원가입은 일단 아이디, 비밀번호, 닉네임은 필수입력이고 이메일 수신여부와 선호하는 아티스트를 선택입력으로 넣었습니다. 이메일 수신여부는 나중에 추가할 예정이지만 매달 1일마다 등록한 이메일로 월간 콘서트 및 예매 일정을 송신하기 위해 허가를 받는 것 입니다.
<br></p>
<h3 id="앞으로-추가할-기능들">앞으로 추가할 기능들</h3>
<p>현재는 단순하게 회원가입 및 콘서트 일정 확인만 있지만 앞으로 다음과 같은 기능 및 개편을 할 예정입니다.</p>
<ul>
<li>메인페이지에 달력과 더불어 공연 포스터를 출력하고 마찬가지로 클릭하면 modal창으로 콘서트 정보 조회</li>
<li>웹사이트 소개 페이지</li>
<li>이메일 문의 페이지</li>
<li>도메인 등록</li>
<li>일본 아티스트 소개 페이지</li>
<li>자유게시판 기능</li>
<li>공연후기 페이지</li>
<li>이메일 전송 기능</li>
<li>SNS로그인 도입</li>
<li>공지사항 페이지</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Gradle] Deamon 메모리 문제 해결]]></title>
            <link>https://velog.io/@half-phycho/Gradle-Deamon-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@half-phycho/Gradle-Deamon-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Mon, 10 Mar 2025 09:05:10 GMT</pubDate>
            <description><![CDATA[<h2 id="작성계기">작성계기</h2>
<p>프로젝트를 진행하는 과정에서 재부팅을 많이 해야하는 상황이 왔었다, 그래서인지는 몰라도 메모리를 많이 사용하게 되었다. 결과적으로 Gradle을 이용한 clean 및 Build가 되지 않았다, 결론부터 말하자면 메모리 부족으로 일어난 현상이었는데 나중을 대비해 이렇게 작성해둔다. 
<br></p>
<h2 id="원인파악">원인파악</h2>
<p>확인해보니 Deamon을 실행할 수 없다는 메세지가 출력되었고 나아가 어째서 Deamon을 실행 할 수 없는지 원인을 검색해보니 다음과 같은 원인을 파악했다.</p>
<blockquote>
<p><strong>Deamon</strong> : Gradle에서 제공하고 있는 백그라운드 프로세서로 원래는 JVM이 실행되고 난 후에 Gradle이 실해되는데 그 과정에서 걸리는 시간을 줄여주고자 만들어진 프로세스이다.</p>
</blockquote>
<ul>
<li>메모리 부족</li>
<li>Gradle과 java의 호환성 문제</li>
<li>build.gradle 스크립트 오류</li>
</ul>
<p>이렇게 일어날 수 있는 원인을 파악했다, Gradle과 java의 호환성 문제는 일단 가능성이 적다, 왜냐하면 <a href="https://start.spring.io/">Spring Initializr</a>로 프로젝트를 구성했기에 버전 호환성은 알아서 맞춰준다, 그리고 build.gradle 스크립트 오류또한 문제가 없다. 애초에 스크립트 에러가 났으면 실행조차 되지 않았을 것이다. 그렇다면 이제 메모리 문제가 남는데 제일 까다로운 문제이기도 하다.
<br></p>
<h2 id="해결과정">해결과정</h2>
<h3 id="1-ide-재부팅">1. IDE 재부팅</h3>
<p>나는 작업을 VSCode로 하는데 재부팅을 해도 전혀 효과가 없었다.</p>
<h3 id="2-pc-재부팅">2. PC 재부팅</h3>
<p>PC 재부팅을 통해 메모리를 정리하고 여유공간을 만들어 주는 것도 시도했다. 하지만 마찬가지로 전혀 효과가 없었다.</p>
<h3 id="3-최대-heap메모리-확장">3. 최대 Heap메모리 확장</h3>
<p>Deamon은 기본적으로 아무런 세팅이 없으면 512mb로 세팅되어있다. 만약 변경하고 싶으면 프로젝트 root디렉터리에 <code>gradle.properties</code>파일을 생성하고 확장하는 설정을 넣어주어야한다.
<img src="https://velog.velcdn.com/images/half-phycho/post/1d889b18-57ed-4d9b-b7d5-ca8a3b5b51bb/image.png" alt=""></p>
<p>파일을 추가했다면 하단의 설정을 properties파일에 작성하면 되는데 하단의 설정은 앞에서 말한대로 1GB로 확장하는 설정이다.</p>
<pre><code>org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g</code></pre><p>다행히 문제를 해결할 수 있었다.</p>
<h2 id="결론">결론</h2>
<p>다행히 문제를 해결할 수 있었지만 동시에 그런생각도 들었다, 대체 어디서 메모리를 많이 사용하길래 최대 Heap영역을 확장까지 해야 하는가 시간이 나면 필요없는 설정들을 정리해야겠다.</p>
<br>

<h2 id="참고-게시글">참고 게시글</h2>
<p><a href="https://docs.gradle.org/8.11.1/userguide/gradle_daemon.html">https://docs.gradle.org/8.11.1/userguide/gradle_daemon.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Redis] Docker + Redis 사용하기]]></title>
            <link>https://velog.io/@half-phycho/Redis-Docker-Redis-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@half-phycho/Redis-Docker-Redis-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 21 Feb 2025 08:26:52 GMT</pubDate>
            <description><![CDATA[<h2 id="시작한-계기">시작한 계기</h2>
<p>사이드 프로젝트를 진행하면서 동시성 문제 때문에 Redis를 사용하기로 결정했는데 Redis는 메모리기반 데이터베이스라는 것만 알았지 어떻게 사용하는건지 전혀 모르는 상태였다. 하지만 뭐든지 시작이 반이라고 Redis를 사용하는 방법을 찾아보니 직접 설치하는 방법부터 Docker를 사용하여 컨테이너로 실행시키는 방법이 있었다. 나는 기존에 Docker를 사용하고 있으니 Docker를 사용하여 Redis를 설치해보겠다.
<br></p>
<h2 id="docker에-redis-가져오기">docker에 Redis 가져오기</h2>
<blockquote>
<p>만약 내가 현재 작성하고있는 게시글대로 Redis를 설치한다면 나는 <strong>Window11</strong> 에서 세팅하고 있기에 주의바란다, 또한 WSL와 함께 Docker 그리고 Ubuntu도 설치되어있다는 가정하에 설명한다.</p>
</blockquote>
<h3 id="1-docker-설치확인">1. docker 설치확인</h3>
<p>먼저 Docker가 설치되었는지 확인해야한다, 다음 명령어를 window Powershell에 입력하여 설치가 되었는지 확인한다.</p>
<pre><code>docker --version</code></pre><p><img src="https://velog.velcdn.com/images/half-phycho/post/d4d394e5-e4db-46eb-bc9a-659d3b4d9c92/image.png" alt=""></p>
<br>

<h3 id="2-redis-image-설치">2. Redis Image 설치</h3>
<p>Docker에 Redis를 정확히는 Image를 설치하는 방법인데 기호에 맞게 설치하도록 하자</p>
<pre><code>// Redis image 최신버전 설치 명령어
docker pull redis

// Redis image 특정버전 설치 명령어
docker pull redis:[버전]</code></pre><p>해당 명령어를 실행하면 다음과 같이 설치된다.
<img src="https://velog.velcdn.com/images/half-phycho/post/3529eedb-fbf2-460b-8b32-9f328c362c89/image.png" alt=""></p>
<p>위의 명령어로 Redis 이미지 설치를 완료한 후 다음 명령어를 입력하여 제대로 설치되었는지 확인한다.</p>
<pre><code>docker images</code></pre><p><img src="https://velog.velcdn.com/images/half-phycho/post/fc2941f6-7f64-46f3-adbf-6b2216456263/image.png" alt=""></p>
<p>docker Desktop에서도 확인해보면 설치가 잘된 것을 볼 수 있다.
<img src="https://velog.velcdn.com/images/half-phycho/post/2e4c9fa8-8e57-4a8f-84d0-317eac7999a4/image.png" alt=""></p>
<br>

<h3 id="2-redis-container-설치">2. Redis Container 설치</h3>
<p>이미지를 설치했으니 실제 동작하도록 이미지를 컨테이너로 실행해보도록 하자</p>
<pre><code>docker run --name [컨테이너 이름] -d -p 6379:6379 [이미지 이름/이미지 ID]</code></pre><blockquote>
<p>-d : 컨테이너가 백그라운드로 실행하는 옵션
-p : &lt;호스트 포트&gt;:&lt;컨테이너 포트&gt;설정하는 옵션</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/half-phycho/post/14ff46ff-3100-4be8-9fdf-6157f09c5acf/image.png" alt="">
해당 명령어를 실행하고 마찬가지로 Docker Desktop을 확인해보면 
<img src="https://velog.velcdn.com/images/half-phycho/post/be13fbec-a16a-4c4c-972f-3c65bfc1298b/image.png" alt="">
앞에서 입력한 명령어대로 실행되고 있는 것을 확인 할 수 있겠다.</p>
<br>

<h3 id="3-redis-접속해보기">3. Redis 접속해보기</h3>
<p>현재 실행중인 Redis에 접속을 해보도록하자</p>
<pre><code>// Docker에서 Redis가 설치되어있는 디렉터리 이동
docker exec -it [컨테이너 이름] /bin/bash</code></pre><pre><code>// Redis Server에 Redis-cli로 접속하기
redis-cli</code></pre><p><img src="https://velog.velcdn.com/images/half-phycho/post/37e3c517-a491-45cd-818b-f95685399b57/image.png" alt="">
이렇게 Redis Cli로 접속에 시도했고 접속되었다면 완료된 것이다.</p>
<br>

<h3 id="4-제대로-설치했는지-확인">4. 제대로 설치했는지 확인</h3>
<p>설치라면 여기까지 설명하는것이 맞지만 위에서 봤듯이 나는 테스트 목적으로 Redis를 최신버전과 구 버전 이미지를 동시에 설치하고 컨테이너를 생성했다. 그래서 원하는 버전의 Redis를 설치했는지도 확인해야한다. </p>
<p>말은 이렇게 했지만 확인방법은 정말 간단하다. Redis-cli 입력에 <strong>info</strong>를 입력한 후 실행하면 설치된 Redis의 버전정보를 포함한 대부분의 정보를 확인할 수 있다.
<img src="https://velog.velcdn.com/images/half-phycho/post/0400eafe-e6f5-45c4-8cb8-21ee7b43ee8f/image.png" alt="">
보면 내가 원하는대로 6.2.6버전이 설치되있는 것을 확인 할 수 있겠다.</p>
<br>

<h2 id="참고-사이트">참고 사이트</h2>
<p><a href="https://msyu1207.tistory.com/entry/Redis-PubSub">https://msyu1207.tistory.com/entry/Redis-PubSub</a></p>
<p><a href="https://velog.io/@juno0713/Windows-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-Docker%EC%97%90-Redis-%EC%84%A4%EC%B9%98">https://velog.io/@juno0713/Windows-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-Docker%EC%97%90-Redis-%EC%84%A4%EC%B9%98</a></p>
<p><a href="https://wooono.tistory.com/348">https://wooono.tistory.com/348</a></p>
<p>(항상 감사합니다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] JPA 사용팁 / 실수들]]></title>
            <link>https://velog.io/@half-phycho/JPA-JPA-%EC%82%AC%EC%9A%A9%ED%8C%81-%EC%8B%A4%EC%88%98%EB%93%A4</link>
            <guid>https://velog.io/@half-phycho/JPA-JPA-%EC%82%AC%EC%9A%A9%ED%8C%81-%EC%8B%A4%EC%88%98%EB%93%A4</guid>
            <pubDate>Thu, 13 Feb 2025 08:26:07 GMT</pubDate>
            <description><![CDATA[<h2 id="작성계기">작성계기</h2>
<p>Spring Data JPA, QueryDSL, EntityManager를 쓰면서 실수 한 것들도 있었고 이거는 좀 괜찮은데? 라고 생각한 것들도 있었다, 실수를 줄이고 팁들을 공유하기위해 작성하기로 했다.
<br></p>
<h2 id="좋은-팁">좋은 팁</h2>
<h3 id="1-querydsl와-jparepository를-같이-쓰고-싶을-때">1. QueryDSL와 JPARepository를 같이 쓰고 싶을 때</h3>
<p>프로젝트를 진행하다가 기본적인 CRUD를 작성하자니 생산성이 떨어져 그냥 QueryDSL와 JPARepository를 같이 쓰면 어떨까? 라는 생각에서 찾아보니 방법이 간단했다.</p>
<blockquote>
<p><a href="https://jaehoney.tistory.com/230">https://jaehoney.tistory.com/230</a></p>
</blockquote>
<p>간단히 기존의 인터페이스에 JPARepository, QueryDSL가 적용되어있는 인터페이스를 상속시켜주면 된다.</p>
<pre><code class="language-java">// JpaRepository : Spring Data JPA
// boardRepositoryCustom : QueryDSL을 사용하고 있는 인터페이스

@Repository
public interface boardRepository extends JpaRepository&lt;Board, Integer&gt;, boardRepositoryCustom{

}  
</code></pre>
<p>이렇게 작성하면 JPARepository와 QueryDSL을 동시에 사용하면서 SOLID규칙 중 DIP를 준수할 수 있다.</p>
<br>

<h2 id="겪은-실수들">겪은 실수들</h2>
<h3 id="1-이름작성-실수">1. 이름작성 실수</h3>
<p>사이드 프로젝트를 하면서 Spring Data JPA에 커스텀 메서드를 만들어야하는 경우가 있었는데 딱히 기능에 문제는 없어 보였지만 계속 컴파일 에러가 났다.
<img src="https://velog.velcdn.com/images/half-phycho/post/5e68996e-08f1-4a69-b8c9-7baa98a09d37/image.png" alt=""></p>
<p>뭐라고 막 써있는데 likeRepository에서 문제가 났다고 한다, 하지만 거기에 있는 거는 JPARepository를 확장한 것과 커스텀으로 만든 메서드만 존재해서 처음에는 이상함을 느끼지 못했다, 그러다가 혹시 이름에 문제가 있는게 아닐까 싶어서 확인해보니 JPARepository를 추가하고 Keyword규칙기반으로 이름을 작성하니 정상적으로 동작하는 것을 확인했다.</p>
<blockquote>
<p><a href="https://velog.io/@633jinn/JPARepository-%EB%A9%94%EC%86%8C%EB%93%9C-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%98%EA%B8%B0">https://velog.io/@633jinn/JPARepository-메소드-커스텀하기</a></p>
</blockquote>
<p>그래서 기존에는 유저와 게시판하나를 선택하는 커스텀 메서드를 만들었는데 이렇게 이름을 지었었다.</p>
<pre><code class="language-java">@Repository
public interface likeRepository extends JpaRepository&lt;Likes, Long&gt;{
    Optional&lt;Likes&gt; findByUserBoard(User user, Board board);
} </code></pre>
<p>Keyword 규칙을 지키면서 이렇게 바꾸었다.</p>
<pre><code class="language-java">@Repository
public interface likeRepository extends JpaRepository&lt;Likes, Long&gt;{
    Optional&lt;Likes&gt; findByUserAndBoard(User user, Board board);
} </code></pre>
<br>

<h3 id="2-component로-생성되는-bean-2개-동시에-사용하기">2. Component로 생성되는 Bean 2개 동시에 사용하기</h3>
<p>사용 팁 1번을 사용하면서 생긴 문제였다, JPARepository와 QueryDSL을 동시에 사용하다가 Bean주입을 2개를 하고말아 우선순위가 꼬여서 생긴 컴파일 에러였다.</p>
<p>그래서 방법을 찾아보니 방법이 2가지가 있었다. </p>
<p><code>@Primary</code>방법을 사용하여 우선 순위를 지정하는 방법이고 다른 하나는 Bean의 이름을 다르게 지정하는 <code>@Qualifier</code>를 사용하는 방법이 있었다, </p>
<p>타입당 주입을 받는 클래스가 2개 였기에 <code>@Primary</code>방법을 사용했다. 하지만 <code>@Primary</code>에 주의사항이 있다, 동일한 타입당 <strong>한 번만</strong> 사용해야한다.</p>
<pre><code class="language-java">@Repository
@Primary
public interface mainRepository extends JpaRepository&lt;Test, Integer&gt;, subRepository {

}

...

@Repository
public class subRepositoryImpl implements subRepository {
    @Override
    public String getManagerName() {
        return &quot;Department manager&quot;;
    }
}</code></pre>
]]></description>
        </item>
    </channel>
</rss>