<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev-yuniljun.log</title>
        <link>https://velog.io/</link>
        <description>한정된 자원으로 더 많은 가치를 제공하려고 노력하는 백엔드 엔지니어입니다.</description>
        <lastBuildDate>Sun, 01 Feb 2026 09:38:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dev-yuniljun.log</title>
            <url>https://velog.velcdn.com/images/dev-yuniljun/profile/48211e9f-f3ba-4ae5-adf4-622cbe8b9607/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dev-yuniljun.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev-yuniljun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[리트리버(Retriever) 개념 정리 요약]]></title>
            <link>https://velog.io/@dev-yuniljun/%EB%A6%AC%ED%8A%B8%EB%A6%AC%EB%B2%84Retriever-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-%EC%9A%94%EC%95%BD</link>
            <guid>https://velog.io/@dev-yuniljun/%EB%A6%AC%ED%8A%B8%EB%A6%AC%EB%B2%84Retriever-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-%EC%9A%94%EC%95%BD</guid>
            <pubDate>Sun, 01 Feb 2026 09:38:49 GMT</pubDate>
            <description><![CDATA[<p>RAG(Retrieval Augmented Generation)에서 리트리버(Retriever) 는
질문에 맞는 문서를 찾아주는 검색기(Search Engine) 역할을 합니다.</p>
<hr>
<h3 id="1-리트리버-기본-개념">1. 리트리버 기본 개념</h3>
<p>동작 흐름</p>
<pre><code>사용자 질문 → Retriever → 관련 문서 → LLM → 답변 생성</code></pre><p>핵심 역할</p>
<ul>
<li>질문을 임베딩(벡터)으로 변환</li>
<li>벡터 스토어(FAISS, Chroma 등)에서 유사 문서 검색</li>
<li>검색 결과를 LLM에 전달</li>
</ul>
<h3 id="2-리트리버의-유형">2. 리트리버의 유형</h3>
<p>희소 리트리버 (Sparse)
: 단어 기반,    TF-IDF, BM25,    빠르고 직관적, 동의어 인식 약함</p>
<p>밀집 리트리버 (Dense)<br>: 의미 기반,    DPR, SBERT, OpenAI Embedding,    의미 유사 문장 검색 가능</p>
<p>혼합형 (Hybrid)<br>: 두 방식을 결합,    EnsembleRetriever,    키워드 + 의미 결합 검색</p>
<h3 id="3langchain-리트리버-설정">3.LangChain 리트리버 설정</h3>
<pre><code>retriever = vectorstore.as_retriever(
    search_type=&quot;mmr&quot;,
    search_kwargs={&quot;k&quot;: 5, &quot;lambda_mult&quot;: 0.5}
)</code></pre><p>search_type: 검색 전략 (similarity, mmr 등)
search_kwargs: 검색 세부 설정 (k, threshold, 다양성 등)
ConfigurableField: invoke 실행 시점에 동적으로 검색 전략 변경 가능</p>
<h3 id="4-문서-압축기-document-compressor">4. 문서 압축기 (Document Compressor)</h3>
<p>검색된 문서를 요약하거나 필터링하여
LLM이 처리하기 쉽게 만드는 중간 필터입니다.</p>
<p>LLM 요약형 (LLMChainExtractor)
: 질문 중심으로 요약</p>
<p>점수 기반 필터형    (ScoreThresholdDocumentCompressor)
: 유사도 기준 문서 필터링</p>
<p>길이 제한형    (TokenClippingCompressor)
: 토큰 수 초과 문서 자르기</p>
<p>혼합형    (DocumentCompressorPipeline)
: 여러 압축기 조합</p>
<p>장점: 토큰 절감, 품질 향상
주의: 과도한 요약은 정보 손실 위험</p>
<h3 id="5-앙상블-리트리버-ensemble-retriever">5. 앙상블 리트리버 (Ensemble Retriever)</h3>
<p>여러 리트리버를 조합해 각자의 장점을 살리는 방식입니다.
예: BM25(희소) + FAISS(밀집)</p>
<pre><code>from langchain.retrievers import EnsembleRetriever

ensemble = EnsembleRetriever(
    retrievers=[bm25_retriever, faiss_retriever],
    weights=[0.4, 0.6]
)</code></pre><p>결과는 정규화 후 가중 평균으로 결합
장점: 정확도 향상, 다양성 확보
단점: 속도와 비용 증가</p>
<h3 id="6-긴-문맥-재정렬-long-context-reordering">6. 긴 문맥 재정렬 (Long Context Reordering)</h3>
<p>검색된 문서들을 LLM이 이해하기 좋은 순서로 재배치합니다.
문맥, 시간, 중요도 기반으로 순서를 조정합니다.</p>
<p>유사도 재랭킹
: Cross-Encoder 등으로 점수 재계산    (ColBERT, MiniLM)</p>
<p>질문 중심 재배열
: 쿼리와 문서 연관성 재평가    (OpenAI, ReRank API)</p>
<p>LLM 기반 재정렬
: LLM이 직접 읽기 순서 판단    (LongContextReorderer)</p>
<p>LangChain에서는 transform_documents() 대신
compress_documents() 방식으로 통합되었습니다.</p>
<h3 id="7-부모-문서-리트리버-parent-document-retriever">7. 부모 문서 리트리버 (Parent Document Retriever)</h3>
<p>검색은 청크 단위로, 반환은 부모 문서 단위로 수행합니다.</p>
<p>구조</p>
<pre><code>부모 문서 → Chunk 분할 → VectorStore 저장
검색 → 관련 Chunk 찾기 → 부모 문서 전체 반환
</code></pre><p>장점: 문맥 유지, 세부정보 보존
단점: 메모리 사용량 증가, 토큰 비용 증가</p>
<h3 id="8-다중-쿼리-리트리버-multi-query-retriever">8. 다중 쿼리 리트리버 (Multi-Query Retriever)</h3>
<p>사용자 질문을 여러 의미적 쿼리로 확장해
검색 범위를 넓히는 방식입니다.</p>
<p>예시:
“Kafka offset 관리 방법?”
→ “Kafka consumer commit”, “offset 저장 위치”, “auto.commit 설정” 등으로 변환</p>
<p>장점: 표현 다양성 확보, Recall 향상
단점: LLM 호출 비용 증가</p>
<h3 id="9-다중-벡터-스토어-리트리버-multi-vector-retriever">9. 다중 벡터 스토어 리트리버 (Multi-Vector Retriever)</h3>
<p>하나의 문서를 여러 임베딩(원문, 요약, 제목 등)으로 저장하고
다양한 관점에서 검색 결과를 병합합니다.</p>
<p>작은 청크 방식
: 문서를 작은 단위로 임베딩</p>
<p>요약 임베딩 방식
: 문서의 요약을 임베딩</p>
<p>가설 쿼리(HyDE)
: 질문의 가상 답변을 임베딩 후 검색</p>
<p>장점: 검색 정확도 향상
단점: 리소스 사용량 증가</p>
<hr>
<h3 id="결론">결론</h3>
<p>Sparse / Dense<br>기본 검색<br>속도 vs 의미</p>
<p>Hybrid / Ensemble<br>품질 향상<br>키워드 + 의미 결합</p>
<p>Parent / Multi-Query<br>문맥 강화<br>더 풍부한 정보</p>
<p>Multi-Vector / Compressor<br>효율화    중복 제거 · 요약</p>
<p>Reorderer<br>논리적 흐름 개선<br>LLM 최적화</p>
<h3 id="요약">요약</h3>
<p>리트리버는 RAG 시스템에서 “검색의 품질”을 결정하는 핵심입니다.
하나의 리트리버로는 충분하지 않으며,
데이터 특성에 따라 여러 리트리버를 조합·압축·재정렬하는 것이
고품질 검색형 AI를 만드는 핵심 전략입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS에서 미니 PC 홈서버로 이전한 실전 기록 (1)]]></title>
            <link>https://velog.io/@dev-yuniljun/AWS%EC%97%90%EC%84%9C-%EB%AF%B8%EB%8B%88-PC-%ED%99%88%EC%84%9C%EB%B2%84%EB%A1%9C-%EC%9D%B4%EC%A0%84%ED%95%9C-%EC%8B%A4%EC%A0%84-%EA%B8%B0%EB%A1%9D-1</link>
            <guid>https://velog.io/@dev-yuniljun/AWS%EC%97%90%EC%84%9C-%EB%AF%B8%EB%8B%88-PC-%ED%99%88%EC%84%9C%EB%B2%84%EB%A1%9C-%EC%9D%B4%EC%A0%84%ED%95%9C-%EC%8B%A4%EC%A0%84-%EA%B8%B0%EB%A1%9D-1</guid>
            <pubDate>Sun, 25 Jan 2026 11:24:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>React + Spring Boot + RDS → 미니 PC 홈서버</strong></p>
<p>CI/CD, Nginx, HTTPS, DNS까지 직접 옮기며 겪은 시행착오와 설계 정리</p>
</blockquote>
<hr>
<h2 id="1-왜-홈서버로-이전했나">1. 왜 홈서버로 이전했나</h2>
<p>기존 서비스는 AWS 기반으로 구성되어 있었다.</p>
<ul>
<li>Frontend: React</li>
<li>Backend: Spring Boot</li>
<li>DB: RDS</li>
<li>CI/CD: GitHub Actions</li>
</ul>
<p>구성 자체는 안정적이었지만, 개인 프로젝트를 운영하기엔 <strong>비용과 복잡도</strong>가 점점 부담이 됐다.</p>
<p>그래서 목표를 이렇게 잡았다.</p>
<ul>
<li>클라우드 의존도 최소화</li>
<li>운영은 최대한 단순하게</li>
<li>비용은 거의 0에 가깝게</li>
<li>그래도 <strong>실서비스 수준의 구조</strong>는 유지</li>
</ul>
<p>결론은 <strong>미니 PC 홈서버로 이전</strong>이었다.</p>
<hr>
<h2 id="2-전체-목표-아키텍처">2. 전체 목표 아키텍처</h2>
<p>이전 후의 목표 구조는 아래와 같았다.</p>
<ul>
<li>미니 PC 1대</li>
<li>Nginx (Reverse Proxy)</li>
<li>React 정적 빌드 서빙</li>
<li>Spring Boot 백엔드 (systemd)</li>
<li>GitHub Actions self-hosted runner</li>
<li>HTTPS (Let’s Encrypt)</li>
</ul>
<blockquote>
<p>핵심은 “한 대지만, 운영 서버처럼 쓰자”였다.</p>
</blockquote>
<hr>
<h2 id="3-디렉터리-구조-설계">3. 디렉터리 구조 설계</h2>
<p>여러 프로젝트를 올릴 것을 고려해 <strong>루트 디렉터리부터 명확히 설계</strong>했다.</p>
<pre><code class="language-text">/dadamda
 ├─ _shared
 │   └─ nginx
 │       └─ conf.d
 │
 └─ dadamda-web
     ├─ frontend
     │   └─ build
     └─ backend
         └─ app.jar</code></pre>
<ul>
<li><code>/dadamda</code> : 조직/서비스 루트</li>
<li><code>_shared</code> : 여러 프로젝트 공통 자원</li>
<li>프로젝트 단위로 완전히 분리</li>
</ul>
<p>이 구조 덕분에:</p>
<ul>
<li>배포 경로 고정</li>
<li>CI/CD 단순화</li>
<li>나중에 프로젝트 추가도 쉬워졌다.</li>
</ul>
<hr>
<h2 id="4-nginx-설정과-프록시-구조">4. Nginx 설정과 프록시 구조</h2>
<p>Nginx는 두 가지 역할만 하도록 제한했다.</p>
<ol>
<li>React 정적 파일 서빙</li>
<li><code>/api</code> 요청을 Spring Boot로 프록시</li>
</ol>
<pre><code class="language-nginx">location / {
    try_files $uri /index.html;
}

location /api/ {
    proxy_pass http://127.0.0.1:8080/;
}</code></pre>
<p>프론트와 백엔드를 <strong>하나의 도메인</strong>으로 묶으면서도,
CORS나 인증 문제 없이 깔끔하게 동작했다.</p>
<hr>
<h2 id="5-spring-boot-실행-방식">5. Spring Boot 실행 방식</h2>
<p>백엔드는 Docker 대신 <strong>systemd 서비스</strong>로 올렸다.</p>
<pre><code class="language-ini">[Service]
User=godhkekf24
WorkingDirectory=/dadamda/dadamda-web/backend
ExecStart=/usr/bin/java -jar app.jar
Restart=always</code></pre>
<p>이 방식을 선택한 이유는 단순하다.</p>
<ul>
<li>빠르게 이전 및 검증</li>
<li>재부팅 시 자동 실행</li>
<li>로그는 journald로 관리</li>
<li>장애 시 자동 재시작</li>
</ul>
<p>홈서버에서는 <strong>단순함이 곧 안정성</strong>이었다.</p>
<hr>
<h2 id="6-cicd-재구성">6. CI/CD 재구성</h2>
<p>아직 작업하진 못했지만 기존 GitHub Actions를 그대로 유지하되,
<strong>self-hosted runner</strong>를 미니 PC에 설치하려고 한다.</p>
<p>이 선택의 장점은 명확했다.</p>
<ul>
<li>GitHub Actions 비용 0원</li>
<li>빌드/배포 모두 로컬 자원 사용</li>
<li>외부 포트 오픈 불필요 (outbound only)</li>
</ul>
<p>결과적으로 Jenkins 없이도 충분히 강력한 파이프라인을 만들 수 있을 것이다.</p>
<hr>
<h2 id="7-dns-가장-많이-막혔던-부분">7. DNS: 가장 많이 막혔던 부분</h2>
<p>이 구간이 이번 이전 작업에서 <strong>가장 시간을 많이 잡아먹은 부분</strong>이었다.</p>
<p>처음에는 단순히 DNS 전파 시간 문제라고 생각했다.</p>
<ul>
<li>A 레코드는 정상적으로 추가했고</li>
<li>TTL도 낮게 설정했고</li>
<li>포트포워딩과 Nginx도 모두 준비된 상태였다.</li>
</ul>
<p>그런데 <strong>몇 시간이 지나도, 하루가 지나도 DNS가 잡히지 않았다.</strong></p>
<pre><code class="language-bash">nslookup dadamda.site</code></pre>
<p>결과는 계속 <code>NXDOMAIN</code>.</p>
<hr>
<h3 id="문제의-진짜-원인-dns-권한authoritative-불일치">문제의 진짜 원인: DNS 권한(Authoritative) 불일치</h3>
<p>원인은 <strong>&quot;시간&quot;이 아니라 &quot;권한&quot; 문제</strong>였다.</p>
<p>도메인의 네임서버(NS)는 이미 <strong>AWS Route53</strong>로 위임된 상태였는데,
실제 A 레코드 설정은 <strong>가비아 DNS 관리 화면</strong>에서 하고 있었던 것이다.</p>
<p>즉 구조는 이랬다.</p>
<pre><code class="language-text">도메인 등록: 가비아
네임서버(NS): Route53
DNS 레코드 설정: 가비아 ❌</code></pre>
<p>DNS 동작 원리를 기준으로 보면 이건 명확한 오류다.</p>
<blockquote>
<p><strong>네임서버가 Route53를 가리키고 있으면,
DNS 레코드는 반드시 Route53 Hosted Zone에 존재해야 한다.</strong></p>
</blockquote>
<p>가비아에서 아무리 A 레코드를 추가해도,
전 세계 DNS 서버는 <strong>Route53에 질의</strong>하기 때문에
결과는 계속 &quot;존재하지 않는 도메인&quot;이 된다.</p>
<hr>
<h3 id="그래서-왜-하루가-지나도-안-됐나">그래서 왜 하루가 지나도 안 됐나</h3>
<p>이 상황에서는:</p>
<ul>
<li>DNS 전파를 아무리 기다려도 ❌</li>
<li>TTL을 줄여도 ❌</li>
<li>포트포워딩을 다시 해도 ❌</li>
</ul>
<p><strong>절대 해결되지 않는다.</strong></p>
<p>왜냐하면 DNS는 &quot;기다리면 퍼지는 값&quot;이 아니라,
&quot;권한 있는 서버에 존재해야만 조회되는 값&quot; 이기 때문이다.</p>
<hr>
<h3 id="해결-방법">해결 방법</h3>
<p>해결은 두 가지 중 하나였다.</p>
<ol>
<li>네임서버를 가비아 DNS로 되돌리거나</li>
<li>Route53에 Hosted Zone을 생성하고
A / CNAME 레코드를 Route53에서 직접 관리</li>
</ol>
<p>나는 <strong>두 번째 방법</strong>을 선택했다.</p>
<ul>
<li>Route53 Public Hosted Zone 생성</li>
<li><code>dadamda.site</code>에 대한 A 레코드 추가</li>
<li>네임서버는 그대로 유지</li>
</ul>
<p>그 순간부터:</p>
<pre><code class="language-bash">nslookup dadamda.site 8.8.8.8</code></pre>
<p>에서 즉시 IP가 반환되었고,
Let’s Encrypt 인증서도 문제없이 발급됐다.</p>
<hr>
<h3 id="이-경험에서-얻은-교훈">이 경험에서 얻은 교훈</h3>
<blockquote>
<p><strong>DNS 문제의 절반은 &quot;설정&quot;이고,
나머지 절반은 &quot;어디가 권한을 가지고 있는지&quot;다.</strong></p>
</blockquote>
<p>&quot;기다리면 된다&quot;는 말은
<strong>권한이 올바를 때만</strong> 성립한다.</p>
<p>DNS가 안 잡힐 때는 항상 먼저 이 질문을 해야 한다.</p>
<ul>
<li>지금 이 도메인의 <strong>authoritative DNS는 누구인가?</strong></li>
<li>내가 레코드를 추가한 곳이, 정말 그 서버인가?</li>
</ul>
<hr>
<hr>
<h2 id="8-공유기-포트포워딩과-관리자-페이지-이슈">8. 공유기 포트포워딩과 관리자 페이지 이슈</h2>
<p>80/443 포트포워딩을 설정한 뒤,
공인 IP로 접속하니 공유기 페이지 대신 서비스 화면이 나왔다.</p>
<p>처음엔 당황했지만, 이는 정상적인 동작이었다.</p>
<ul>
<li>외부 요청은 미니 PC로 전달</li>
<li>공유기 관리자 페이지는 내부 IP + 별도 포트</li>
</ul>
<p>오히려 보안적으로 더 안전한 상태였다.</p>
<hr>
<h2 id="9-결과-지금의-상태">9. 결과: 지금의 상태</h2>
<p>현재 상태는 다음과 같다.</p>
<ul>
<li>미니 PC 홈서버 운영 중</li>
<li>React + Spring Boot 정상 서비스</li>
<li>HTTPS 적용 완료</li>
</ul>
<p>해야하는 작업</p>
<ul>
<li>CI/CD 자동 배포</li>
<li>DB 이전</li>
</ul>
<p>무엇보다 <strong>구조를 전부 이해하고 운영한다는 안정감</strong>이 가장 큰 수확이었다.</p>
<hr>
<h2 id="10-마치며">10. 마치며</h2>
<p>이번 이전 작업은 단순한 서버 이동이 아니라,</p>
<ul>
<li>네트워크</li>
<li>DNS</li>
<li>Linux</li>
<li>배포</li>
<li>운영</li>
</ul>
<p>을 한 번에 복습하는 경험이었다.</p>
<p>클라우드가 편한 건 사실이지만,
<strong>직접 굴려본 홈서버 경험은 다른 차원의 이해를 준다.</strong></p>
<p>비슷한 고민을 하고 있다면,
작은 프로젝트 하나라도 홈서버로 옮겨보는 걸 추천한다.</p>
<hr>
<blockquote>
<p>다음 글에서는</p>
<ul>
<li><p>CI/CD 자동 배포</p>
</li>
<li><p>DB 이전</p>
<p>도 정리해볼 예정이다.</p>
</li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[1월 18일 (2주차 주간 회고)]]></title>
            <link>https://velog.io/@dev-yuniljun/1%EC%9B%94-18%EC%9D%BC-2%EC%A3%BC%EC%B0%A8-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev-yuniljun/1%EC%9B%94-18%EC%9D%BC-2%EC%A3%BC%EC%B0%A8-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 18 Jan 2026 15:16:33 GMT</pubDate>
            <description><![CDATA[<p>이번 주도 정신없이 흘러갔다.
1월부터 본격적으로 넘어온 프로젝트가 있는데 마감일이 얼마 남지않아 업무 시간이 굉장히 길어지고 있다. 정말로 개인시간이 없었다. 다음주까지 최대한 완성도를 높여야하는데 3주차 주간 회고까지 개인 공부를 못할수도 있을 것 같다.</p>
<p>넘어온 프로젝트의 웹 백엔드 부분을 담당하고 있는데 작업 관련 문서가 너무 없다. 코드 기반으로 AI를 통해 문서를 생성해서 전체 로직을 파악하고 문제가 되는 부분들의 코드를 집중적으로 확인하는 식으로 진행하고 있는데 코드의 의도가 명확하지 않은 부분들이 많다. 그리고 사실 단순한 기능인데 메서드의 깊이가 너무 깊다. 마지막으로 AI로 생성된 로직들에 대한 검증이 부족하다. 잘 만들어진 문서를 기반으로 생성된 코드는 꽤나 깔끔하다고 생각한다. 하지만 전체적인 설계 없이 특정 기능을 구현하기 위해서만 생성된 코드는 상당히 난해하고 의미를 파악하기 힘들어진다.</p>
<p>개발 처음 배울 때, &#39;좋은 코드는 무엇인가&#39;에 대한 고민을 했었는데 항상 결론짓지 못하였다. 하지만 좋지 않은 코드를 경험적으로 쌓아가면서 이것들을 회피하며 전체적으로 좋은 코드로 향해가고 있는 것 같다.</p>
<p>여러 잡생각들이 드는 한 주였지만 일단 눈 앞에 주어진 프로젝트가 있으니 모든 에너지를 해당 업무를 완수하는데에 집중하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1월 11일 (1주차 주간 회고)]]></title>
            <link>https://velog.io/@dev-yuniljun/1%EC%9B%94-11%EC%9D%BC-1%EC%A3%BC%EC%B0%A8-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev-yuniljun/1%EC%9B%94-11%EC%9D%BC-1%EC%A3%BC%EC%B0%A8-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 11 Jan 2026 13:38:32 GMT</pubDate>
            <description><![CDATA[<p>잦은 야근에 다른 활동을 거의 하지 못했다.
그럼에도 주간 회고는 꾸준히 작성하면서 날아가는 시간들을 조금이나마 잡아보려고 한다.</p>
<p>연말에 잠시 중단되었던 RAG 스터디를 재개했다.
스터디원의 개인 프로젝트 발표 일정이 진행되었고, n8n을 이용해 급등주를 찾는 서비스를 구현해 설명해주셨다. 새로 알게 된 부분이 있었는데 주가 데이터를 그대로 llm에 전달하는 것이 아닌 주가 데이터를 이미지로 생성해서 llm에 전달하는 것이 더 높은 수준의 답변을 얻을 수 있다는 것이다. 이미지보다 데이터 자체가 더 정확하기 때문에 좋은 답변을 얻을 수 있다고 생각했는고 LLM에 해당 질문을 요청 했을 때에도 주가 예측, 추론에는 데이터가 더 유리하다고 하는데 이 부분에 대해서는 추가적인 학습과 경험이 더 필요하다.</p>
<p>26년 목표를 위해 어떤 행동들을 진행하고 있는지 주간 점검을 진행해보자.</p>
<p>2026년 목표</p>
<ol>
<li>알고리즘 학습</li>
<li>개인 블로그 구축</li>
<li>월급 외 수익 만들기</li>
<li>SAA, CKA</li>
<li>회사 서비스 모니터링 시스템 구축 및 고도화</li>
</ol>
<p>아무것도 진행하고 있는 것이 없다.
다음 주에는 회사 업무를 최대한 업무 시간에 정리해두고 알고리즘 학습과, SAA 학습을 진행해보려고 한다. 알고리즘은 문제를 보고 어떤 식으로 풀어야 할지 설계를 진행하고 LLM에 문제풀이를 요청해 여러 알고리즘을 빠르고 많이 접할 수 있도록 하려고 한다.
SAA 학습은 이전에 봤던 블로그 문제풀이를 적극 활용하려고 한다.</p>
<p>짧은 시간이라도 업무 외에 개인 학습을 진행해야겠다고 생각이 들었다. 업무 시간 내에 최대한 많은 업무들을 진행하고 그 외에는 운동과 목표를 위한 학습을 꾸준히 진행해보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2025년 회고]]></title>
            <link>https://velog.io/@dev-yuniljun/2025%EB%85%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev-yuniljun/2025%EB%85%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 04 Jan 2026 04:29:28 GMT</pubDate>
            <description><![CDATA[<p>&quot;운동을 시작하다.&quot;</p>
<p>개발블로그에 무슨 소리인가 싶기도 하겠지만 그만큼 2025년은 운동에 몰입했던 해였다.
나는 고등학교 이후에 군대를 제외하면 운동을 전혀 하지않고 있었다. 방심한 순간에 말랐던 몸에 살이 붙기 시작했고 통통하다는 느낌까지 받게 되었다.</p>
<p>2024년 말에 친구의 조언에 러닝을 시작했고 재미를 붙여 하프 대회까지 나갔고 2026년에는 풀 마라톤까지 신청해놨다. 상반기에는 수영을 했고 하반기에는 웨이트를 했기 때문에 거의 쉬는 시간 전부를 운동에 투자했다고 봐도 무방하다. 운동을 하긴 해야지라고 생각하고 계속해서 미뤄왔기 때문에 이렇게 운동하는 것을 습관화  한 것 자체만으로도 올해 의미있게 보냈다고 생각을 한다.</p>
<p>개발적인 성장으로는 올해 여러 개발 모임들을 진행하면서 다양한 사람들을 만나고 동기부여를 얻은 해이다. 사실 기존에도 여러 모임에 속해있었는데 올해 특히 활발하게 세미나에 참여하고 스터디를 진행했다. 오산 개발자 방에서 카프카 스터디와 RAG 스터디를 진행하면서 각각 기술에 대한 개념 뿐만 아니라 관련된 컴퓨터 지식들을 서로 공유했다. 생소한 개념이 나올때마다 LLM을 활용해 개념을 확장하는 작업을 꾸준히 진행하였고 일부는 실습을 진행하면서 익숙해지려고 노력했다. 최근에 진행한 RAID개념 같은 경우가 그렇다. </p>
<p>추상화 되어있는 기능들을 잘 활용하는 것도 중요하겠지만 실제로 어떻게 동작하는지 인지하고 사용하는 것과 아닌 것과는 차이가 있다. 특히나 최근 AI를 활용한 개발이 늘어나면서 단순히 사용하거나 결과를 만들어 내는 것은 허들이 상당히 낮아졌다고 생각한다. 하지만 트래픽이 증가하거나 특정 이슈가 생겼을 때 원인을 분석해내는 능력은 추상화된 기술의 장단점을 아는 것으로는 부족하고 실제로 동작 과정을 인지하고 있는데에서 온다고 생각한다. 따라서 앞으로도 주요 기술들에 대한 학습을 진행하면서도 내부 동작에 집중해보려고 한다.</p>
<hr>
<p>2026년 목표</p>
<ol>
<li>알고리즘 학습</li>
<li>개인 블로그 구축</li>
<li>월급 외 수익 만들기</li>
<li>SAA, CKA</li>
<li>회사 서비스 모니터링 시스템 구축 및 고도화</li>
</ol>
<p>먼저 알고리즘 학습이다. 구현보다는 알고리즘의 종류와 사용처 위주로 학습할 예정이고 실제 구현은 AI에 맡길 것이다. 단순 구현은 이제 AI가 더 꼼꼼하게 잘한다. 알고리즘 문제 상황별로 어떤 알고리즘을 사용하고 제한 조건을 어떤 방식으로 해결해나갈 것인지만 학습하여 짧은 시간 여러 케이스들을 다룰 것이다.</p>
<p>개인 블로그 구축은 하반기에 진행할 예정이다. 티스토리에서 현재 Velog를 이용하고 있지만 원하는 기능이 많지 않아 직접 수정할 수 있는 개인 블로그를 만들어 볼 생각이다.</p>
<p>3, 4, 5에 대한 내용은 구체적으로 언급하기 어렵다. SAA는 곧 응시 예정이고 CKA는 하반기를 목표로 공부하려고 한다. 회사 시스템을 언급하기 어려운데 부족하다고 생각하고 있고 효율적인 방식으로 변경한 뒤에 개선점을 블로그에 작성하려고 한다.</p>
<hr>
<p>올 한해 업무를 많이 진행한 것 같은데 되돌아보면 기억이 잘 나지 않는다. 2026년 부터는 주간 회고를 통해 현재 위치를 꾸준히 확인하고 목표를 향해 정진하는 해를 만들어보려고 한다. 주간 회고는 매주 토요일 오전에 작성하려고 한다. 2026년 회고 글에는 더 풍성한 내용들이 들어있기를 기원한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RAID란? (2)]]></title>
            <link>https://velog.io/@dev-yuniljun/RAID%EB%9E%80-2</link>
            <guid>https://velog.io/@dev-yuniljun/RAID%EB%9E%80-2</guid>
            <pubDate>Sun, 14 Dec 2025 06:54:45 GMT</pubDate>
            <description><![CDATA[<p>이론으로만 외우면 금방 잊기 때문에 실제로 세팅해보고 테스트한 결과를 작성하려고 한다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/b0819229-e836-44ea-a4e3-0e6587a91017/image.jpeg" alt=""></p>
<h1 id="raid0--raid1--raid5--raid10-전체-성능-비교-분석">RAID0 / RAID1 / RAID5 / RAID10 전체 성능 비교 분석</h1>
<p>실험 환경</p>
<ul>
<li>HDD 4개 (SATA)</li>
<li>Ubuntu(persistent live USB) + mdadm</li>
<li>fio 벤치마크</li>
</ul>
<p>테스트 종류:</p>
<ul>
<li>순차 읽기 (1MB)</li>
<li>순차 쓰기 (1MB)</li>
<li>랜덤 읽기 (4K)</li>
<li>랜덤 쓰기 (4K)</li>
</ul>
<h2 id="순차-읽기-sequential-read--1mb-block"><strong>순차 읽기 (Sequential Read) — 1MB block</strong></h2>
<p>데이터를 앞에서부터 뒤까지 <strong>순서대로 연속해서 읽는 작업</strong>이다. 디스크 헤드(특히 HDD)가 이동할 필요 없이 연속된 영역을 읽기 때문에 가장 빠른 성능이 나온다.</p>
<p>0MB → 1MB → 2MB → 3MB → …</p>
<p>케이스 정리</p>
<ul>
<li>대용량 파일 읽기 </li>
<li>영상 스트리밍 </li>
<li>백업 데이터 읽기 </li>
<li>로그 파일 연속 스캔 </li>
<li>OS 부팅 중 연속 파일 로딩</li>
</ul>
<h2 id="순차-쓰기-sequential-write--1mb-block"><strong>순차 쓰기 (Sequential Write) — 1MB block</strong></h2>
<p>데이터를 앞에서부터 뒤까지 <strong>연속된 공간에 순서대로 기록하는 작업</strong>이다.<br>순차 읽기와 마찬가지로 디스크 헤드가 크게 이동하지 않아 <strong>HDD에서도 높은 쓰기 성능</strong>을 낼 수 있다.</p>
<p>0MB ← 1MB ← 2MB ← 3MB ← …</p>
<p>다만 RAID 구조에 따라 쓰기 성능 차이가 크게 난다.<br>특히 RAID5는 패리티 계산 때문에 순차 쓰기가 매우 느릴 수 있다.</p>
<p>케이스 정리</p>
<ul>
<li>대용량 로그 파일 기록  </li>
<li>백업 파일 저장  </li>
<li>영상 녹화(스트리밍 업로드)  </li>
<li>대규모 ETL 파이프라인 쓰기  </li>
<li>장기 보관용 아카이브 파일 생성  </li>
</ul>
<hr>
<h2 id="랜덤-읽기-random-read--4kb-block"><strong>랜덤 읽기 (Random Read) — 4KB block</strong></h2>
<p>데이터를 <strong>임의(random) 위치에서 작은 단위(4KB)</strong> 로 읽는 작업이다.<br>HDD에서는 디스크 헤드가 다양한 위치로 이동해야 하기에 <strong>가장 느린 접근 패턴</strong>이다.</p>
<p>200MB → 12GB → 5MB → 340KB → …</p>
<p>랜덤 읽기는 OS와 데이터베이스에서 가장 자주 발생하며, RAID 구조에 따라 성능 차이가 크게 난다.<br>HDD 기반에서는 여러 디스크에 분산된 데이터를 병렬로 읽을 수 있는 RAID5가 상대적으로 이점이 있을 수 있다.</p>
<p>케이스 정리</p>
<ul>
<li>데이터베이스 SELECT 쿼리  </li>
<li>파일 시스템 메타데이터 읽기  </li>
<li>작은 파일 다수 읽기  </li>
<li>캐시 미스 발생 시 OS 페이지 읽기  </li>
<li>VM(가상머신) 스토리지 읽기  </li>
</ul>
<hr>
<h2 id="랜덤-쓰기-random-write--4kb-block"><strong>랜덤 쓰기 (Random Write) — 4KB block</strong></h2>
<p>임의의 위치에 <strong>작은 블록(4KB)</strong> 을 기록하는 작업이다.<br>디스크 헤드 이동 + RAID 패리티/미러 구조 때문에 RAID 종류에 따라 성능 차이가 매우 큰 영역이다.</p>
<p>3GB 위치 ← 450MB 위치 ← 12KB 위치 ← 77GB 위치 …</p>
<p>특히 RAID5는 패리티 연산으로 인해 랜덤 쓰기가 매우 느리고,<br>RAID10은 미러 구조 + 스트라이핑 덕분에 <strong>랜덤 쓰기 성능이 가장 뛰어나다</strong>.</p>
<p>케이스 정리</p>
<ul>
<li>데이터베이스 INSERT / UPDATE  </li>
<li>애플리케이션 로그 기록  </li>
<li>VM 디스크 이미지 랜덤 기록  </li>
<li>파일 시스템 메타데이터 업데이트  </li>
<li>캐시 파일 저장  </li>
</ul>
<h2 id="📌-성능-요약표-4가지-raid-모두-포함">📌 성능 요약표 (4가지 RAID 모두 포함)</h2>
<table>
<thead>
<tr>
<th>RAID</th>
<th>Seq Read (MiB/s)</th>
<th>Seq Write (MiB/s)</th>
<th>Rand Read (IOPS)</th>
<th>Rand Write (IOPS)</th>
</tr>
</thead>
<tbody><tr>
<td>RAID0</td>
<td>191.7</td>
<td>185</td>
<td>131</td>
<td>453</td>
</tr>
<tr>
<td>RAID1</td>
<td>26.6</td>
<td>67.6</td>
<td>13</td>
<td>45</td>
</tr>
<tr>
<td>RAID5</td>
<td>144</td>
<td>13.1</td>
<td>1864</td>
<td>94</td>
</tr>
<tr>
<td>RAID10</td>
<td>189</td>
<td>182</td>
<td>132</td>
<td>455</td>
</tr>
</tbody></table>
<h4 id="1-순차-읽기-성능-비교-1mb">1) 순차 읽기 성능 비교 (1MB)</h4>
<p>1위: RAID0 (191 MiB/s)
최고 속도    스트라이핑 효과로 4개 디스크를 모두 활용</p>
<p>2위: RAID10 (189 MiB/s)
RAID0와 거의 동일    2×2 미러이지만 스트라이핑으로 높은 대역폭</p>
<p>3위: RAID5 (144 MiB/s)<br>패리티 불필요한 읽기는 빠름    </p>
<p>4위: RAID1 (26.6 MiB/s)<br>단일 디스크 수준    </p>
<p>📌 해석
순차 읽기는 디스크 헤드가 한 방향으로 쭉 움직이기 때문에,
스트라이핑 구조(RAID0, RAID10)가 절대적으로 유리하다.</p>
<p>📌 의문점
Q. 
RAID0(4-striping)은 RAID10(2-striping + 2-mirroring)보다 대역폭이 2배 더 높으니까 그만큼의 성능을 보여줘야 하는 것 아닌가?
A. 
순차 I/O 병목은 디스크의 최대 처리량(SATA 인터페이스 병목)에 걸려 있기 때문에 실제 순차 I/O는 디스크 개수만큼 선형 증가하지 않는다. 디스크 2~3개만 써도 이미 최대 대역폭 근접한다. 만약 SSD를 사용하게 된다면 처리량 차이를 볼 수 있다.</p>
<h4 id="2-순차-쓰기-성능-비교-1mb">2) 순차 쓰기 성능 비교 (1MB)</h4>
<p>1위: RAID0 (185 MiB/s)<br>패리티 없음, 미러 없음 → 순수 스트라이핑</p>
<p>2위: RAID10 (182 MiB/s)<br>미러 쓰기 + 스트라이프 → RAID5보다 훨씬 유리    </p>
<p>3위: RAID1 (67.6 MiB/s)<br>2개 디스크에 동일 데이터 기록    </p>
<p>4위: RAID5 (13.1 MiB/s)<br>패리티 계산 병목 때문에 최악</p>
<p>📌 해석
RAID5는 쓰기 시 “패리티 연산 + 읽기 후 쓰기(read-modify-write)” 작업이 필요해
속도가 HDD 환경에서는 가장 낮게 나온다.</p>
<h4 id="3-랜덤-읽기-성능-비교-4k">3) 랜덤 읽기 성능 비교 (4K)</h4>
<p>1위: RAID5 (1864 IOPS)
HDD 기준에서는 읽기가 매우 빠름 (패리티 필요 없음)    </p>
<p>RAID10 (132 IOPS)<br>pair 당 1디스크 읽기 → HDD에서는 RAID5에 밀릴 수 있음    </p>
<p>RAID0 (131 IOPS)<br>스트라이프이지만 헤드 seek 병목 동일    </p>
<p>RAID1 (13 IOPS)<br>단일 디스크 수준    </p>
<p>📌 해석
랜덤 읽기에서는 헤드가 여러 디스크에서 동시에 seek 할 수 있는 RAID5가 압도적이다.
패리티 연산이 필요 없는 read path이기 때문.</p>
<p>📌 의문점
Q.
RAID5 값이 너무 높은 수치가 아닌가? 사실 RAID 0과 비슷할 것이라고 생각했다.
W.
실제로는 RAID0 수치와 비슷해질수는 있지만 이렇게 압도적으로 높은 수치가 나오는 것은 이상수치이다. 테스트시에 direct 옵션을 주지 않아 캐시가 개입하여 더 높은 수치가 나오는 것으로 추측하고 있다.</p>
<h4 id="4-랜덤-쓰기-성능-비교-4k">4) 랜덤 쓰기 성능 비교 (4K)</h4>
<p>1위: RAID10 (455 IOPS)<br>병렬 쓰기 효과 + 패리티 없음    </p>
<p>2위: RAID0 (453 IOPS)<br>패리티 없음 → 순수 스트라이핑    </p>
<p>RAID5 (94 IOPS)<br>패리티 연산 때문에 낮은 편</p>
<p>RAID1 (45 IOPS)<br>미러 쓰기 때문에 디스크 head 움직임 증가    </p>
<p>📌 해석
RAID10과 RAID0은 랜덤 쓰기에서도 압도적인 성능을 보인다.
특히 RAID10은 내구성 + 빠른 랜덤 쓰기를 동시에 갖는다는 점에서
DB, 트랜잭션 처리에 가장 적합하다.</p>
<h4 id="결론">결론</h4>
<ul>
<li>RAID0는 가장 빠르지만 가장 위험하다.</li>
<li>RAID5는 쓰기 병목이 크다.</li>
<li>RAID1은 안정성 중심이다.</li>
<li>RAID10이 전체적인 밸런스(성능+안정성) 측면에서 가장 우수하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[RAID란? (1)]]></title>
            <link>https://velog.io/@dev-yuniljun/RAID%EB%9E%80-1</link>
            <guid>https://velog.io/@dev-yuniljun/RAID%EB%9E%80-1</guid>
            <pubDate>Sun, 07 Dec 2025 06:09:51 GMT</pubDate>
            <description><![CDATA[<p>어제 SSAFYnity 동문회에서 여러 분들과 이야기를 나누다가,
한 분께서 최근 면접에서 RAID 관련 질문을 정말 많이 받는다는 이야기를 들려주셨다.</p>
<p>그 얘기를 들으면서 문득,
“레이드… 아, 저장소 디스크를 여러 개 묶어서 관리하는 그거!”
라고만 알고 있었지,
정작 각 RAID 방식이 어떤 원리로 동작하는지는 명확하게 이해하지 못하고 있었다는 걸 깨달았다.</p>
<p>그래서 오늘은 RAID 개념을 처음부터 다시 공부했고,
앞으로는 직접 RAID를 구성해보면서
각 방식이 제공하는 성능·안정성의 차이를 실제로 체감하며 정리해보려고 한다.</p>
<h4 id="1-raidredundant-array-of-independent-disks란">1. RAID(Redundant Array of Independent Disks)란?</h4>
<p>RAID는 여러 개의 디스크 드라이브를 배열(Array) 형태로 묶어
스트라이핑(striping) 또는 미러링(mirroring) 방식으로 데이터를 운영하는 저장장치 구성 기술이다.</p>
<p>이를 통해 다음과 같은 이점이 있다:</p>
<ul>
<li>성능 향상</li>
<li>장애 허용 능력(오류 내성) 향상</li>
<li>더 많은 저장 용량 확보</li>
<li>비용 효율적인 스토리지 구성</li>
</ul>
<p>RAID는 서버나 스토리지 시스템에서
안정성과 성능을 높이기 위한 대표적 기술로 널리 사용된다.</p>
<h4 id="2-raid에서-사용되는-운영-방식">2. RAID에서 사용되는 운영 방식</h4>
<p>RAID는 크게 두 가지 기본 동작 방식을 조합하여 구성된다.</p>
<ol>
<li>스트라이핑(Striping)</li>
</ol>
<p>데이터를 여러 블록으로 나누고 그 블록들을 여러 드라이브에 분산 저장하는 방식
성능 향상 목적 (읽기/쓰기 속도 증가)</p>
<ol start="2">
<li>미러링(Mirroring)</li>
</ol>
<p>동일한 데이터를 두 개 이상의 디스크에 복제하여 저장
장애 대응 목적</p>
<p>RAID는 위 두 방법을 어떻게 조합하느냐에 따라 서로 다른 RAID 레벨로 구분된다.</p>
<h4 id="1-raid-0">1) RAID 0</h4>
<p>2개 이상의 디스크에 데이터를 번갈아 저장하여 최고의 성능을 낼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev-yuniljun/post/73388dcd-96d1-4ff6-b518-fcf4e6fa675a/image.png" alt=""></p>
<p>패리티가 없어 디스크 1개라도 고장나면 데이터 손실이 발생한다. 데이터를 여러 디스크에 저장하기 때문에 하나의 디스크라도 고장이 나면 프로그램을 실행할 수 없다.</p>
<p>SSD 캐시 등 속도만 필요한 경우에 제한적으로 사용한다.</p>
<h4 id="2-raid-1">2) RAID 1</h4>
<p>동일한 데이터의 완전한 복제본을 저장하여 디스크 1개 고장 시에도 데이터 손실이 없다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/624e775e-220f-46ed-bd5c-da75c8d3e50c/image.png" alt=""></p>
<p>용량은 가장 작은 디스크 용량 기준으로 제한된다.
데이터를 두 번 써야 하므로 쓰기 성능은 RAID0보다 떨어진다.
비용이 높지만 안정성이 높다.</p>
<h4 id="3-raid-5">3) RAID 5</h4>
<p>스트라이핑 + 패리티 1개
3개 이상의 디스크가 필요하다.
패리티는 항상 데이터가 있는 디스크와는 다른 디스크에 저장된다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/f8fdfc85-c72b-49f0-bfc0-0f12e262ec04/image.png" alt=""></p>
<p>디스크 1개 고장 시 패리티 기반으로 데이터 복구 가능하다.
읽기 성능은 높고 용량 효율도 좋다.
하지만 쓰기 속도가 느리고 디스크 재구성(Rebuild) 시간이 길다.</p>
<blockquote>
<p>패리티란?
패리티는 여러 데이터 조각을 XOR 연산으로 더해 놓은 값이다.
이 패리티만 있으면 디스크 1개가 고장 나도 그 데이터를 다시 만들어 낼 수 있다.</p>
</blockquote>
<p><strong>패리티 예시</strong></p>
<p>RAID5 (3개의 디스크)
Disk1: 데이터 A1 A2 A3
Disk2: 데이터 B1 B2 B3
Disk3: 패리티 P1 P2 P3 (A1+B1, A2+B2 … XOR)</p>
<p>Disk2가 고장 나면:
B1 = A1 XOR P1
B2 = A2 XOR P2
B3 = A3 XOR P3</p>
<p>→ 계산으로 B를 다시 만들어서 RAID가 살아남음.</p>
<p>만약 패리티를 2개 만들어 놓으면 디스크가 2개가 고장나도 복구할 수 있다. (RAID 6)
P 패리티: XOR 연산 사용
Q 패리티: 갈루아 필드(GF(2^8)) 연산 사용</p>
<p>쉽게 얘기하면 2개의 변수가 있는 문제는 2개의 방정식으로 풀 수 있는 원리와 같다.</p>
<blockquote>
<p>리빌드(Rebuild)란?
RAID 구성에서 고장난 디스크를 새 디스크로 교체한 후,
실종된 데이터를 다시 ‘재생성’해서 RAID를 정상 상태로 회복시키는 과정.</p>
</blockquote>
<p>디스크가 하나 고장나면 RAID가 degraded(경고 상태)가 된다.
새 디스크를 꽂으면 RAID가 남은 디스크와 패리티 정보를 이용해 사라진 데이터를 다시 계산해서 새 디스크에 채워 넣는 작업을 진행하는데 이 과정을 “리빌드”라고 한다.</p>
<h4 id="4-raid-10-10">4) RAID 10 (1+0)</h4>
<p>RAID 1(미러링) + RAID 0(스트라이핑) 결합하여 최소 4개의 디스크 필요하다.
짝수 개의 디스크만 추가 가능하다.</p>
<p>1단계: 디스크를 미러 쌍으로 구성
2단계: 각 미러링 쌍을 스트라이핑하여 논리 볼륨 생성</p>
<p><img src="https://velog.velcdn.com/images/dev-yuniljun/post/bff5d7dd-54cf-4a99-8c2f-9851d0d66519/image.png" alt=""></p>
<p>쓰기 성능이 매우 뛰어나며, 높은 안정성과 성능을 동시에 확보할 수 있다.
용량의 50%만 사용할 수 있어 비용이 높다.</p>
<h4 id="3-raid-사용-방법">3. RAID 사용 방법</h4>
<p>RAID는 운영 방식에 따라 소프트웨어 RAID와 하드웨어 RAID 두 가지로 나뉜다.</p>
<p><strong>소프트웨어 RAID</strong>
운영체제(OS)가 CPU를 사용하여 RAID를 관리한다.</p>
<p>리눅스는 RAID0, 1, 5, 6 등 대부분을 지원
윈도우도 RAID0, 1, 5 일부 지원</p>
<p>예: mdadm 기반 RAID 구성
→ 일반 PC에서도 바로 구성 가능</p>
<p><strong>하드웨어 RAID</strong>
RAID를 지원하는 HBA(Host Bus Adapter) 카드나 RAID 컨트롤러가 관리한다.
RoC(RAID on Chip) 기반으로 CPU 부하가 적다.
RAID 레벨 지원 범위는 각 카드 사양에 따라 다르다.</p>
<p>서버/스토리지 전문 환경에서 주로 사용</p>
<p>RAID별 특성 정리
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/4da7957e-4745-48c5-9b0d-8b4ab42ade9e/image.png" alt=""></p>
<p>※ RAID는 백업의 대체제가 아니다는 점을 항상 명심해야 한다.</p>
<p>(출처: 삼성전자 데이터센터 <a href="https://download.semiconductor.samsung.com/resources/others/Samsung_SSD_845DC_07_Redundant_Array_of_Independent_Disks_RAID.pdf">Samsung_SSD_845DC_07_Redundant_Array_of_Independent_Disks_RAID.pdf</a>)</p>
<hr>
<p>보통 학습을 진행해도 실습을 진행해보지 않으면 금방 잊혀진다. 따라서 HDD 4대를 직접 데스크탑에 레이드 연결해보고 각각의 레이드 환경에 따라 성능과 리빌드 과정을 확인하는 작업을 진행하려고 한다. SATA HDD 4대를 주문해놨고 도착하면 차례로 실습한 후에 실습 결과를 포스팅하겠다.</p>
<h4 id="앞으로-진행할-실습-계획">앞으로 진행할 실습 계획</h4>
<p>RAID0 구성 → 성능 측정
RAID1 구성 → 안정성 테스트
RAID5 구성 → 디스크 고장 시나리오 실험
RAID6 구성 → 2개 고장 대응 확인
RAID10 구성 → 미러링 + 스트라이핑 성능 비교
Hot Spare 구성 → 자동 복구 동작 확인</p>
<ul>
<li>fio로 성능 측정</li>
<li>SMART로 디스크 상태 확인</li>
</ul>
<h4 id="마무리">마무리</h4>
<p>단순히 일 때문에 참석했던 행사였지만, 오히려 개발과 관련된 새로운 키워드들을 얻게 되었고, 그 과정에서 다시 호기심이 살아나는 계기가 되었다. 다양한 개발자들을 만나 이야기 나누면서 기술뿐 아니라 사람과 경험을 통해 성장할 수 있음을 다시 느꼈다.</p>
<p>특히 RAID라는 주제를 접하게 되면서, 평소 깊게 다루지 않았던 스토리지 구조나 데이터 보호 방식에 대해 스스로 공부해보는 기회까지 이어졌다. 단순히 “디스크 여러 개 묶는 기술” 정도로 알던 RAID가, 성능, 안정성, 데이터 구조까지 연결된 깊이 있는 개념이라는 걸 배우면서 개발자로서 시야가 더 넓어진 느낌이다.</p>
<p>앞으로도 이렇게 예상치 못한 곳에서 새로운 지식을 얻고, 커뮤니티 활동을 통해 성장하는 경험을 계속 이어가고 싶다. 작은 호기심 하나가 또 어떤 배움을 만들어낼지 기대된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전혀 모르는 사람이 공부하는 n8n (2)]]></title>
            <link>https://velog.io/@dev-yuniljun/%EC%A0%84%ED%98%80-%EB%AA%A8%EB%A5%B4%EB%8A%94-%EC%82%AC%EB%9E%8C%EC%9D%B4-%EA%B3%B5%EB%B6%80%ED%95%98%EB%8A%94-n8n-2-2jdekmni</link>
            <guid>https://velog.io/@dev-yuniljun/%EC%A0%84%ED%98%80-%EB%AA%A8%EB%A5%B4%EB%8A%94-%EC%82%AC%EB%9E%8C%EC%9D%B4-%EA%B3%B5%EB%B6%80%ED%95%98%EB%8A%94-n8n-2-2jdekmni</guid>
            <pubDate>Tue, 25 Nov 2025 12:57:00 GMT</pubDate>
            <description><![CDATA[<p>저번 글에서 구성해놓은 n8n 서버로 더 많은 것들을 할 수 있을 것 같아 추가로 실습을 진행했다.
평소에 마라톤 접수 일정을 알림으로 주는 서비스가 있으면 좋겠다고 생각했는데, n8n으로 간단히 만들어보려고 한다.</p>
<h2 id="마라톤-신청-알람-서비스">마라톤 신청 알람 서비스</h2>
<p>먼저 주기적으로 마라톤 정보를 가져오는 크롤링 워크플로우 1개, 매일 아침 마라톤 참여 시작일을 판단하여 문자 또는 이메일을 날려주는 워크플로우 1개가 필요하다.</p>
<p>처음에는 일일이 마라톤 일정을 입력해 넣으려고 했지만 세상에는 생각보다 더 많은 마라톤 일정들이 존재했고 1인이 모두 관리하기에는 어려웠기 때문에 기존에 마라톤 일정을 제공하는 사이트를 크롤링해서 DB를 구성하기로 했다.</p>
<p><a href="https://marathongo.co.kr/raceSchedule/domestic">마라톤GO</a> 라는 웹 사이트는 마라톤 일정을 등록 신청을 하고 검증을 통해 데이터로 밀어넣는 것 같아 꽤나 신뢰성 높은 사이트라고 판단했고, 해당 사이트에서 일정을 가져오기로 했다.</p>
<h4 id="크롤링-워크-플로우">크롤링 워크 플로우<img src="https://velog.velcdn.com/images/dev-yuniljun/post/73b961a6-74d6-4ea2-9328-95fc1053e03a/image.png" alt=""></h4>
<p>마라톤 일정이 자주 바뀌는 것도 아니고 해당 사이트에 부하를 자주 줄 필요도 없다고 생각하여 일주일에 한번 트리거할 수 있도록 설정했다. (요청 URL이 계속 변경되는 것 같은데 한 달에 한번정도 확인해서 URL 변경 후 직접 트리거 하는 방식으로 진행해도 좋을 것 같다.)
그리고 HTTP request 노드에서 응답을 받아오고 응답을 파싱하여 마라톤 일정 객체 리스트로 만들어주는 함수 노드를 두었다. 
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/36deb681-eefc-4113-bc64-45af211772f6/image.png" alt=""></p>
<p>그 다음으로 만들어진 객체 리스트를 구글 시트에 넣는 노드로 마라톤 일정 DB 저장을 마무리했다. 다음 번에 자동 트리거 할 때 기존의 데이터와 중복된 데이터가 들어가는 것을 방지하기 위해 raceName을 unique값으로 지정했다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/ef9a9691-9ee9-4340-966a-4c8687c63423/image.png" alt=""></p>
<p>최종적으로 크롤링으로 만들어진 마라톤 일정 DB 리스트 구글 시트는 다음과 같다.
554개의 마라톤 일정을 가져왔다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/ebd9d4e6-c097-43e9-a492-dea02625d17a/image.png" alt=""></p>
<p>이제 매일 아침 8시에 확인하여 당일 접수가 시작되는 마라톤을 추려 구독자에게 보내주는 워크 플로우를 만들어야 한다.</p>
<p>먼저 구독 신청은
구글폼으로 만들었다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/a11119da-845d-4742-aa29-cea4c9a964cc/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/fbe57669-9ff5-4476-a156-f2c4da331ea6/image.png" alt=""></p>
<p>단순히 이메일과 지역 옵션만을 사용하여 신청을 간소화했다.
이제 해당 지역 필터를 적용하여 마라톤 리스트를 가져와 문자로 보내는 2번 워크플로우를 작성해보자.</p>
<p><img src="https://velog.velcdn.com/images/dev-yuniljun/post/0e2f9541-9a05-48e4-903b-8974835d4d69/image.png" alt=""></p>
<p>매일 오전 8시에 자동 트리거하면 구독자와 마라톤 정보를 가져온다. 오늘 접수가 시작될 마라톤 정보만 추려 해당 지역을 구독하고 있는 구독자에게 보낼 이메일을 HTML 코드로 작성했다.</p>
<p>최종적으로 이메일을 보낸 모습은 다음과 같다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/3642c2cc-1e56-434e-b70c-75f386aa638c/image.png" alt=""></p>
<p>사실 작업을 진행하면서 여러 어려움이 생길 것이라 생각해, 이를 정리하기 위해 블로그 작성을 시작했다.
그런데 막상 n8n을 사용해보니 UI도 직관적이고 지원하는 노드(툴)도 다양해서, 생각보다 훨씬 간단하게 서비스를 만들 수 있었다.</p>
<p>n8n을 몰랐을 때는 마라톤 알림 서비스를 구축하기 위해</p>
<ul>
<li>크롤링을 위한 서비스 하나,</li>
<li>SMTP 발송용 스프링 서비스 하나,</li>
<li>Jenkins는 과하니 cron으로 트리거를 처리하는 방식
을 구성해야겠다고 생각했다.</li>
</ul>
<p>하지만 n8n을 사용해보니 기존처럼 복잡하게 아키텍처를 나눌 필요 없이, 워크플로우 단위로 작은 서비스들을 손쉽게 만들어 연결할 수 있었다.
다양하고 사용자 친화적인 노드들을 활용해 필요한 기능을 조합만 하면 되기 때문에, 전체 개발 비용이 크게 줄어든다는 점이 인상적이었다.</p>
<p>또한 n8n은 여러 워크플로우 템플릿도 제공한다.
웹에서 필요한 템플릿을 찾아 그대로 복사해와 사용할 수 있어, 직접 구성하는 것이 어렵거나 귀찮은 사람들에게 특히 유용하다.
반복적인 업무를 자동화하고 싶은 사람들이라면, 이미 만들어진 템플릿을 활용해 일상의 루틴을 벗어나 더 중요한 작업에 시간을 써보는 것도 좋아보인다.</p>
<p>혹시나 마라톤 알림 서비스를 제공받아 볼 사람들은 다음 링크로 구글폼을 작성해두면 된다.
불가피한 사정으로 서비스가 말 없이 내려갈 수 있으니 감안하길 바란다.</p>
<p>마라톤 접수 알림 서비스 신청폼 (무료)
<a href="https://forms.gle/gcp9nrsusfPNmc1w8">https://forms.gle/gcp9nrsusfPNmc1w8</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전혀 모르는 사람이 공부하는 n8n (1)]]></title>
            <link>https://velog.io/@dev-yuniljun/%EC%A0%84%ED%98%80-%EB%AA%A8%EB%A5%B4%EB%8A%94-%EC%82%AC%EB%9E%8C%EC%9D%B4-%EA%B3%B5%EB%B6%80%ED%95%98%EB%8A%94-n8n</link>
            <guid>https://velog.io/@dev-yuniljun/%EC%A0%84%ED%98%80-%EB%AA%A8%EB%A5%B4%EB%8A%94-%EC%82%AC%EB%9E%8C%EC%9D%B4-%EA%B3%B5%EB%B6%80%ED%95%98%EB%8A%94-n8n</guid>
            <pubDate>Sat, 15 Nov 2025 11:22:18 GMT</pubDate>
            <description><![CDATA[<p>최근 RAG 스터디를 진행하고 있는데 시니어 개발자분들이 n8n에 대해 이야기하셨다.
수많은 얘기들이 오고 갔지만 이해한 것은 n8n 키워드 하나뿐이었다.
그래서 n8n이 뭔데? 궁금증을 참지 못해 토요일 저녁 메가커피에 앉아있다.</p>
<p>새로운 기술에 대해 전반적인 지식을 얻기 위해서는 LLM만한 것이 없다.</p>
<p>Q. n8n이 뭐야? 개념과 유스케이스를 포함해서 알려줘.</p>
<p><strong>n8n은 오픈소스 자동화 플랫폼이다. Zapier, Make(구 Integromat) 같은 서비스와 비슷하지만 완전한 오픈소스이고 자체 호스팅이 가능하다는 점이 가장 큰 특징이다.</strong></p>
<blockquote>
<p>“코드 없이(또는 최소한의 코드로) 여러 서비스와 API를 연결해서 자동화 시나리오를 만드는 도구”</p>
</blockquote>
<p>Zapier, Make같은 친구들을 한 번도 사용해본 적 없지만 n8n이 워낙 편안하고 범용성이 좋다고 하니까 n8n에 먼저 집중해보자.</p>
<p>n8n을 통해 다음과 같은 워크플로우를 최소한의 코드로 동작이 가능하다고 한다.</p>
<p>&quot;메일 오면 → Slack 알림 보내기 → DB에 저장하기&quot;</p>
<p>메일, 슬랙, DB까지? 호환성이 얼마나 좋은거야.</p>
<p>트리거와 노드로 이루어져 있다.</p>
<p><strong>트리거</strong></p>
<ul>
<li>Cron 시간마다</li>
<li>Webhook 호출되면</li>
<li>Slack 메시지 수신 시</li>
<li>파일 업로드되면</li>
<li>이메일 수신 시</li>
</ul>
<p><strong>노드</strong></p>
<ul>
<li>HTTP Request</li>
<li>MySQL</li>
<li>MongoDB</li>
<li>Slack</li>
<li>Telegram</li>
<li>GitHub</li>
<li>Cron</li>
<li>Function (JavaScript 코드 작성 가능)</li>
</ul>
<p>이메일 수신 시? 최근에 싸피니티 대외협력팀에서 업무를 진행하고 있는데 메일 수신 여부를 확인해서 단톡방에 공유하는 것도 업무이다. 메일이 오면 메일 내용을 AI로 요약해서 카카오톡에 공유해주는 자동화가 가능할까 기대해본다.</p>
<p>추가로 n8n은 k8s처럼 nodemation의 축약어라고 한다.</p>
<p>개념은 간단하다. 실습을 진행해본다.
t3.micro ec2 하나를 빌려서 도커를 설치하고 docker-compose.yml를 구성해서 올려봤다.</p>
<p><img src="https://velog.velcdn.com/images/dev-yuniljun/post/54523629-fa8f-487f-911c-2a66ed7baf87/image.png" alt=""></p>
<p>TLS는 추후에 적용하도록 하고 secure cookie를 사용하지 않도록 하여 진행했다.</p>
<p><img src="https://velog.velcdn.com/images/dev-yuniljun/post/689f82e7-e774-43f5-a9c0-53d3dbb42e83/image.png" alt=""></p>
<p>아름답다. 오픈소스를 사용하다가 이렇게 UI가 제공되면 마음이 편안해지고 감사한 마음이 든다.</p>
<p>실습 내용은 다음과 같다.</p>
<ol>
<li>트리거가 될 Email Trigger(IMAP) 노드를 세팅하고,</li>
<li>HTML에서 내용을 뽑아내고 (함수),</li>
<li>LLM으로 요약하고,</li>
<li>이스케이프 문자 정리하고 (함수),</li>
<li>요약한 결과를 텔레그램 bot으로 메세지를 보낸다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev-yuniljun/post/f1ea996d-1d72-4585-b11c-3e34757c5dbb/image.png" alt="">
각각의 노드들을 연결할 때 앱비밀번호(google)나 토큰(telegram), key(llm) 등으로 credential로 추가해두고 연결하는 것을 제외하면 번거로운 작업이 크게 없었다.</p>
<p>node 이름을 통해 원하는 데이터들을 가져와서 다음 노드로 가공된 데이터를 넘긴다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/1bb2c251-af24-42f4-950c-c168f77d3333/image.png" alt=""></p>
<p>완성된 워크플로우를 active로 변경하고, 실제로 메일을 보내봤다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/0dc87ca5-ce80-4f1e-9770-86fd92e55359/image.png" alt=""></p>
<p>메일을 나에게 보내고 빠른 시간 내에 텔레그램 메세지가 전달됐다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/fde31d05-40ce-4d50-9226-7b8a29b9c18b/image.png" alt=""></p>
<p>나름 중요한 부분들을 잘 요약해서 보낸 것 같다. &#39;\-&#39; 처럼 문자열 처리가 잘못된 부분이 있었지만 혼자 사용하기에는 충분했다.</p>
<p>마지막으로 n8n으로 할 수 있는 여러 워크플로우 예제들을 리스팅하고 마무리하겠다.</p>
<p>📌** 1. 이메일 자동화(Email Automation)**
✔ 새 이메일 → AI 요약 → Slack/Telegram 알림
✔ 이메일 첨부파일 자동 다운로드 → S3 업로드
✔ 이메일 제목/본문 키워드로 자동 분류 및 라벨링
✔ 특정 발신자 메일을 구글 시트에 자동 기록
✔ 매일 아침 “지난 하루간 중요한 이메일 요약” 생성
✔ Gmail → Notion 프로젝트 페이지로 자동 전송
✔ 고객 문의 이메일 → 자동 티켓 생성(Jira/Asana 등)</p>
<p>📌 <strong>2. Slack / Teams 업무 자동화</strong>
✔ 장애 알람 → Slack 메시지 + 담당자 자동 Mentions
✔ 영업 CRM → Slack 실시간 리드 알림
✔ 특정 키워드 포함 메시지 감지 → GPT 응답 자동 생성
✔ 회사 일정 Google Calendar → Slack 주간 알림
✔ Jenkins/배포 완료 → Slack에 릴리즈 노트 자동 발송</p>
<p><strong>📌 3. 개발자/DevOps 자동화</strong>
✔ GitHub Issue 생성 → 자동 Slack 알림
✔ GitHub Pull Request → 변경 사항 GPT 요약
✔ AWS CloudWatch 경보 → Telegram/Slack 알림
✔ 특정 서버 CPU/RAM 폭주 감지 → 자동 재부팅
✔ S3에 새로운 로그 생성 → ETL 자동 처리
✔ DockerHub 새 이미지 태깅 → 자동 배포 트리거</p>
<p><strong>📌 4. 데이터 처리 &amp; ETL 자동화</strong>
✔ 매시간 REST API 데이터 요청 → MongoDB/MySQL 저장
✔ 웹페이지 크롤링 → 데이터 정제 → 구글 시트 업데이트
✔ 데이터 CSV 파일 → 자동 파싱 → DB 삽입
✔ ChatGPT 기반 “데이터 요약/분석” 자동 처리
✔ PDF → 텍스트 변환 → 핵심 요약 자동 생성
✔ Notion → ElasticSearch 인덱싱 자동화</p>
<p><strong>📌 5. AI 기반 자동화 (LLM Workflow)</strong>
✔ 긴 문서 업로드 → GPT 요약 → Slack 전송
✔ 고객 리뷰 자동 분류(AI Sentiment Analysis)
✔ 문의 이메일 → GPT 자동 답변 초안 생성
✔ 정기 리포트 자동 생성(보고서, 회의록, 요약문)
✔ OCR로 이미지에서 텍스트 추출 → GPT 정리
✔ 블로그 자동 포스팅(AI 글 생성 → Wordpress API 업로드)</p>
<p><strong>📌 6. SNS/마케팅 자동화(Social &amp; Marketing)</strong>
✔ RSS/뉴스 자동 수집 → 요약 → 텔레그램 뉴스봇
✔ 인스타그램 게시물 자동 업로드(사진+문구)
✔ 트위터 Mentions 모니터링 → Slack 알림
✔ 유튜브 새 영상 업로드 감지 → SNS 자동 공유
✔ 쇼핑몰 주문/문의 데이터 자동 처리
✔ 네이버 카페/블로그 업데이트 알림봇</p>
<p><strong>📌 7. 조직/내부 운영 자동화</strong>
✔ 직원 휴가(Annual Leave) 신청 → Slack 승인 플로우
✔ 구글 캘린더 일정 → 팀별 요약 리포트
✔ Expense 지출증빙 첨부 → 자동 구글 드라이브 정리
✔ 주간 회의록 자동 생성(음성 → 텍스트 → 요약)
✔ 매출/현황 데이터 정리 → 매일 아침 요약 리포트 발송</p>
<p><strong>📌 8. 자동 리포트/모니터링</strong>
✔ 매일 매출 요약 → 엑셀 생성 → 이메일 발송
✔ 재고 수량 임계점 도달 시 → 자동 알림
✔ 크롤링으로 가격 변동 체크 → 급등락 알림
✔ 서버 로그 읽고 오류 감지 → 알람 발송
✔ 회사 포털에서 데이터 스크랩 → 주간 리포트 생성</p>
<p><strong>📌 9. 외부 API 연동(Integration)</strong>
✔ 카카오 비즈메시지 자동 전송
✔ 네이버 톡톡 챗봇 응답 자동화
✔ CRM/ERP/HR 시스템 간 동기화
✔ Telegram → ChatGPT → 응답 자동 생성 봇
✔ GPT로 PDF 분석 → Notion 페이지 자동 생성</p>
<p><strong>📌 10. 개인/일상 자동화</strong>
✔ 메일로 오는 영수증 → 자동 정리/카테고리화
✔ 운동 기록 자동 정리(Apple Health → Google Sheet)
✔ 집안 IoT 자동화 (모닝루틴: 조명/알림/뉴스)
✔ 환율 변동 알림
✔ 블로그/포트폴리오 자동 업데이트
✔ 주식/코인 시세 모니터링 → 급등락 감지</p>
<p>전혀 n8n에 대해 모르는 사람도 금방 워크플로우 하나를 제작할 정도로 매우 간편하고 사용자 친화적인 툴이었다. 추후에 n8n을 이용해 생활이나 개발 환경을 개선해보고 후기를 작성하려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[velog 조회수 트래킹]]></title>
            <link>https://velog.io/@dev-yuniljun/velog-%EC%A1%B0%ED%9A%8C%EC%88%98-%ED%8A%B8%EB%9E%98%ED%82%B9</link>
            <guid>https://velog.io/@dev-yuniljun/velog-%EC%A1%B0%ED%9A%8C%EC%88%98-%ED%8A%B8%EB%9E%98%ED%82%B9</guid>
            <pubDate>Mon, 18 Aug 2025 04:14:47 GMT</pubDate>
            <description><![CDATA[<p><img src="https://smilejune-blog-traffic.s3.ap-northeast-2.amazonaws.com/1x1.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[.gitattributes를 통한 submodule 머지 충돌 해결]]></title>
            <link>https://velog.io/@dev-yuniljun/.gitattributes%EB%A5%BC-%ED%86%B5%ED%95%9C-submodule-%EB%A8%B8%EC%A7%80-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dev-yuniljun/.gitattributes%EB%A5%BC-%ED%86%B5%ED%95%9C-submodule-%EB%A8%B8%EC%A7%80-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sat, 16 Aug 2025 15:31:16 GMT</pubDate>
            <description><![CDATA[<h3 id="1-들어가며">1. 들어가며</h3>
<p>최근 상위 저장소의 <code>prod</code> 브랜치와 <code>dev</code> 브랜치가 각각 다른 submodule 커밋을 가리키고 있었는데, 머지하는 순간 submodule 충돌이 발생했습니다. 단순히 git status만 보면 “서브모듈 충돌”이라는 메시지로 끝이라 처음엔 왜 그런지 이해하기 어려웠습니다.</p>
<h3 id="2-submodule이란">2. submodule이란?</h3>
<p>submodule은 Git 저장소 안에 또 다른 Git 저장소를 넣는 기능입니다. 일반 파일 처럼 복사해오는 것이 아니라 특정 커밋 해시를 가리키는 포인터를 저장합니다. 그렇기 때문에 공통 모듈을 여러 프로젝트에서 동일하게 재사용할 수 있습니다.</p>
<p>하지만 브랜치별로 submodule이 다른 커밋을 가리킬 때, 머지를 하면 충돌이 발생할 수 있습니다. 예를 들어 상위 저장소의 <code>prod</code>브랜치는 submodule의 <code>prod</code>브랜치를 가리키고 상위 저장소의 <code>dev</code>브랜치는 submodule의 <code>dev</code>브랜치를 가리키고 있는데 각각 다른 커밋을 갖고 있는 두 브랜치 사이에 머지를 하려고 하면 충돌이 발생합니다.</p>
<h3 id="3-gitattributes로-해결">3. <code>.gitattributes</code>로 해결</h3>
<p><code>.gitattributes</code>는 Git이 파일을 다룰 방식을 정의하는 역할을 합니다.
특히 submodule 충돌이 날 때 유용합니다. 예를 들어,</p>
<pre><code>common_module merge=ours</code></pre><p>위와 같이 설정하면, common_module submodule에서 머지 충돌이 발생했을 때 현재 브랜치의 버전(ours)을 자동으로 선택하게 됩니다.</p>
<p>또 다른 예시는 diff 설정입니다.</p>
<pre><code>*.md diff=markdown
*.png binary</code></pre><p>마크다운 파일은 markdown diff를 사용하라는 뜻이고, png 파일은 binary이니까 diff 하지 말라는 뜻입니다. markdown diff를 사용하게 되면 마크다운 문서를 단순히 줄 단위로만 보는 게 아니라, 마크다운 전용 diff드라이버를 적용할 수 있습니다. 제목, 리스트, 링크 등 마크다운 문법 요소를 인식해서 더 의미 있는 비교를 제공합니다.</p>
<p>다시 문제 상황으로 돌아와서 브랜치 A/B가 서로 다른 submodule 커밋을 가리켜서 충돌이 날 때, <code>.gitattributes</code>로 정책 반영해주면 됩니다. 브랜치 A/B에 각각 <code>.gitattributes</code>파일을 수정 및 커밋시켜주고 머지를 다시 시도하면 정책이 반영되어 머지를 하더라도 각각의 브랜치가 가리키는 submodule로 정리됩니다.</p>
<h3 id="4-gitignore으로는-해결할-수-없을까">4. <code>.gitignore</code>으로는 해결할 수 없을까?</h3>
<p><code>.gitignore</code>은 말 그대로 Git이 추적하지 않도록 설정하는 파일입니다. 예를 들어,</p>
<pre><code>common_module</code></pre><p>위와 같이 <code>.gitignore</code>파일을 작성했다면 “아예 추적하지 않는다”는 의미라 Git이 submodule 버전을 관리하지 않습니다. 충돌은 피할 수 있지만, 팀원마다 submodule 버전이 달라져 빌드나 테스트가 엉킬 수 있습니다. 반면 <code>.gitattributes</code>는 추적은 하되, 충돌 시 어떻게 해석할지 정의하기 때문에 일관성을 유지하면서 충돌을 자동으로 해결할 수 있습니다.</p>
<h3 id="5-마무리">5. 마무리</h3>
<p>정리하면,</p>
<ul>
<li>submodule은 공통 모듈을 일괄 관리할 수 있어 유용하지만,</li>
<li>브랜치마다 다른 커밋을 가리킬 때는 머지 충돌을 일으키기 쉽습니다.</li>
<li><code>.gitattributes</code>를 활용해 충돌 처리 정책을 정의하면 이러한 문제를 근본적으로 해결할 수 있습니다.</li>
</ul>
<p>사실 이 문제는 꽤 오래 겪고 있었지만, 그동안은 당장 눈앞의 충돌만 해결하느라 근본적인 방법을 찾지 못했습니다. 이번 경험을 통해 새로운 문제 상황이 생겼을 때는 먼저 원인과 근본 해결 방법을 확인하고 빠르게 반영하는 습관이 중요하다는 걸 다시 한번 깨닫게 되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka가 MQ를 대체할 수 있을까?]]></title>
            <link>https://velog.io/@dev-yuniljun/Kafka%EA%B0%80-MQ%EB%A5%BC-%EB%8C%80%EC%B2%B4%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@dev-yuniljun/Kafka%EA%B0%80-MQ%EB%A5%BC-%EB%8C%80%EC%B2%B4%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Wed, 06 Aug 2025 14:18:12 GMT</pubDate>
            <description><![CDATA[<p>Kafka와 MQ(RabbitMQ, ActiveMQ 등)는 모두 메시징 시스템으로 자주 비교되는 것들 입니다. 
Kafka를 공부하던 중 이런 질문이 생겼습니다.</p>
<blockquote>
<p><strong>&quot;MQ 대신 Kafka만 써도 되지 않을까?&quot;</strong><br><strong>&quot;Kafka가 MQ를 완전히 대체할 수 있을까?&quot;</strong></p>
</blockquote>
<hr>
<h2 id="mq로도-로그-수집-되지-않나요">MQ로도 로그 수집 되지 않나요?</h2>
<p>Kafka는 로그 수집 시스템에 자주 사용됩니다.<br>그런데 이런 의문이 들 수 있습니다.</p>
<blockquote>
<p>&quot;어차피 중앙에서 로그를 수집할 거면 MQ로도 충분하지 않나?&quot;</p>
</blockquote>
<p>맞습니다. MQ도 로그 수집에 사용 가능합니다.<br>각 서비스가 메시지를 큐로 보내고, 중앙 로그 서버가 소비하면 됩니다.</p>
<p>하지만 Kafka는 다음과 같은 이유로 더 적합합니다.</p>
<hr>
<h2 id="kafka가-로그-수집에-더-적합한-이유">Kafka가 로그 수집에 더 적합한 이유</h2>
<h3 id="1-고성능--수평-확장">1. 고성능 / 수평 확장</h3>
<ul>
<li>Kafka는 파티션 기반 구조로 소비자 수를 늘려 수평 확장이 쉬움</li>
<li>MQ는 수직 확장 위주, 소비자 수에 제한 있음</li>
</ul>
<h3 id="2-여러-소비자가-같은-메시지를-읽을-수-있음">2. 여러 소비자가 같은 메시지를 읽을 수 있음</h3>
<ul>
<li>MQ는 메시지를 1번 소비하면 삭제됨</li>
<li>Kafka는 Consumer Group마다 오프셋을 따로 관리함 → 여러 소비자가 동일 메시지 처리 가능</li>
</ul>
<h3 id="3-메시지를-오래-저장하고-다시-읽을-수-있음">3. 메시지를 오래 저장하고 다시 읽을 수 있음</h3>
<ul>
<li>Kafka는 디스크에 메시지를 일정 기간 저장 가능 (기본 7일, 설정 가능)</li>
<li>장애 발생 시 과거 메시지를 다시 읽어 재처리 가능</li>
</ul>
<hr>
<h2 id="그렇다면-kafka로-mq를-전부-대체해도-될까">그렇다면 Kafka로 MQ를 전부 대체해도 될까?</h2>
<blockquote>
<p><strong>기술적으로는 대부분 대체 가능하지만, 무조건 Kafka가 더 나은 건 아닙니다.</strong></p>
</blockquote>
<p>Kafka가 <strong>오버스펙</strong>이 되는 상황도 있습니다.</p>
<hr>
<h2 id="kafka로-대체-가능한-mq-용도">Kafka로 대체 가능한 MQ 용도</h2>
<table>
<thead>
<tr>
<th>용도</th>
<th>Kafka 적합성</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>서비스 간 이벤트 전달</td>
<td>✅ 적합</td>
<td>Kafka의 대표 사용 사례</td>
</tr>
<tr>
<td>로그 수집 및 분석</td>
<td>✅ 매우 적합</td>
<td>대용량 스트리밍 처리</td>
</tr>
<tr>
<td>실시간 파이프라인 처리</td>
<td>✅ 적합</td>
<td>데이터 흐름 처리에 최적화</td>
</tr>
<tr>
<td>DB 변경 이벤트 전파 (CDC)</td>
<td>✅ 적합</td>
<td>Debezium 등과 연동 쉬움</td>
</tr>
<tr>
<td>장애 발생 시 재처리</td>
<td>✅ 유리</td>
<td>오프셋 기반으로 재시도 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="kafka로-대체하기-어려운-mq-용도">Kafka로 대체하기 어려운 MQ 용도</h2>
<table>
<thead>
<tr>
<th>용도</th>
<th>Kafka 적합성</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>단순 알림 메시지</td>
<td>❌ 과함</td>
<td>MQ가 더 가볍고 직관적</td>
</tr>
<tr>
<td>지연 메시지 / 예약 메시지</td>
<td>❌ 불편함</td>
<td>MQ는 기본 제공, Kafka는 직접 구현 필요</td>
</tr>
<tr>
<td>우선순위 메시지 처리</td>
<td>❌ 없음</td>
<td>Kafka는 메시지 우선순위 기능 없음</td>
</tr>
<tr>
<td>전체 순서 보장</td>
<td>❌ 복잡함</td>
<td>Kafka는 <strong>파티션 내 순서만 보장</strong></td>
</tr>
<tr>
<td>트랜잭션 메시징 중심</td>
<td>⚠️ 어려움</td>
<td>Kafka도 가능하지만 설정과 운영이 복잡함</td>
</tr>
</tbody></table>
<hr>
<h2 id="결론">결론</h2>
<p>Kafka는 단순한 MQ가 아닙니다.<br><strong>대용량 스트리밍 처리와 이벤트 중심 아키텍처에 특화된 플랫폼</strong>입니다.</p>
<blockquote>
<p><strong>Kafka는 MQ를 대체할 수 있지만,<br>항상 Kafka가 더 좋은 선택은 아닙니다.</strong></p>
</blockquote>
<hr>
<p>이 글은 GPT와 공부한 내용을 바탕으로 정리했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[javax.net.ssl.SSLException 이슈 정리 – TLS 설정]]></title>
            <link>https://velog.io/@dev-yuniljun/javax.net.ssl.SSLException-%EC%9D%B4%EC%8A%88-%EC%A0%95%EB%A6%AC-TLS-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@dev-yuniljun/javax.net.ssl.SSLException-%EC%9D%B4%EC%8A%88-%EC%A0%95%EB%A6%AC-TLS-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Wed, 06 Aug 2025 00:35:38 GMT</pubDate>
            <description><![CDATA[<p>최근 A 서버에서 B 서버를 호출할 때 일부 요청은 정상적으로 처리되는 반면, 일부 요청에서 javax.net.ssl.SSLException 에러가 발생하는 현상을 겪었습니다. 더 확인해봐야할 것은 이 문제가 B 서버뿐 아니라 C, D 서버를 호출할 때도 동일하게 발생했다는 점이었습니다.</p>
<p>이러한 에러 발생 패턴을 통해, 문제의 원인은 A 서버 자체 또는 공통으로 사용하는 네트워크 통신 로직에 있을 것이라고 판단했습니다. 그리고 실제로 원인을 찾아보니 TLS 설정 문제였습니다.</p>
<h3 id="🔐-tls란-무엇인가">🔐 TLS란 무엇인가?</h3>
<p><strong>TLS(Transport Layer Security) 는 인터넷 환경에서 데이터를 안전하게 주고받기 위해 사용하는 암호화 통신 프로토콜입니다.</strong>
우리가 흔히 사용하는 HTTPS는 바로 이 TLS 위에서 작동하는 HTTP 프로토콜이라고 보시면 됩니다.</p>
<p>TLS의 핵심 역할:
암호화: 데이터를 제3자가 볼 수 없도록 보호
무결성: 데이터가 전송 중에 변경되지 않았는지 확인
인증: 통신하는 상대방이 신뢰할 수 있는지 검증 (주로 인증서를 통해)</p>
<p>❓ 모든 요청에 TLS 설정이 필요한가?
모든 요청이 TLS 설정을 요구하지는 않습니다.
하지만 중요한 기준은 바로 요청이 HTTPS를 사용하는지 여부입니다.</p>
<p>요청 방식 TLS 필요 여부 설명
HTTP ❌ 필요 없음 암호화되지 않은 평문 통신
HTTPS ✅ 반드시 필요 TLS를 사용한 보안 통신</p>
<p>즉, HTTPS로 통신하는 모든 요청에는 TLS 설정이 필요하며, 서버와 클라이언트는 서로 호환되는 TLS 버전 및 Cipher suite를 지원해야 합니다.</p>
<h3 id="🧪-문제-상황-정리">🧪 문제 상황 정리</h3>
<p>이번 케이스에서 A 서버는 B 서버뿐 아니라 C, D 서버에도 요청을 보내고 있었고, 이들 모두 AWS Load Balancer(LB)를 통해 구성되어 있었습니다.</p>
<p>A 서버의 요청 흐름:</p>
<blockquote>
<p>A 서버 → AWS Load Balancer → B, C, D 서버</p>
</blockquote>
<p>이때 요청 중 일부만 javax.net.ssl.SSLException 에러가 발생했고, 그 이유는 바로 AWS Load Balancer의 TLS 설정(Security policy)과 A 서버의 TLS 설정이 맞지 않았기 때문이었습니다.</p>
<h3 id="🔍-원인-분석">🔍 원인 분석</h3>
<p>AWS에서는 Load Balancer에 대해 <strong>Security policy</strong>라는 설정 항목을 제공합니다. 이 설정을 통해 허용할 <strong>TLS 버전과 Cipher suite를 제한</strong>할 수 있습니다.</p>
<p>최근 서버를 재구성하면서, 이전과 다른 Security policy인 ELBSecurityPolicy-TLS13-1-2-Res-2021-06을 적용했는데, 이 정책은 TLS 1.2 이상과 특정 암호화 방식만을 허용합니다.</p>
<p>하지만 A 서버는 기본 설정으로 TLS 요청을 보냈기 때문에 TLS설정이 무작위로 설정되었고 그 중 일부 요청들이 협상 과정에서 실패하여 SSLException이 발생한 것이었습니다.</p>
<h3 id="🛠-해결-방법">🛠 해결 방법</h3>
<p>문제를 해결하기 위해 A 서버에서 명시적으로 TLS 버전과 Cipher suite를 지정했습니다. 이렇게 하면 Load Balancer와 TLS 협상 과정에서 호환 문제 없이 통신이 가능해집니다.</p>
<p>✅ 참고: Java에서는 SSLContext, SSLSocketFactory, HttpClient.Builder, RestTemplate 등을 통해 TLS 설정을 직접 지정할 수 있습니다.</p>
<p>✅ 정리</p>
<ul>
<li>HTTPS 통신을 사용하는 경우, 항상 TLS 설정이 필요합니다.</li>
<li>서버와 클라이언트 간 TLS 협상이 실패하면 javax.net.ssl.SSLException이 발생할 수 있습니다.</li>
<li>중간에 AWS Load Balancer 같은 인프라가 있다면, Security policy 설정이 통신에 영향을 줄 수 있습니다.</li>
<li>명시적인 TLS 설정을 통해 안정적인 통신을 구성하는 것이 중요합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[블로그 이전]]></title>
            <link>https://velog.io/@dev-yuniljun/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EC%A0%84</link>
            <guid>https://velog.io/@dev-yuniljun/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EC%A0%84</guid>
            <pubDate>Fri, 09 Feb 2024 13:09:32 GMT</pubDate>
            <description><![CDATA[<p>티스토리에서는 마크다운을 붙여넣으면 가독성이 떨어지는 문제가 발생해서 velog로 블로그를 이전했다.</p>
<p>티스토리 블로그 링크
<a href="https://godhkekf24.tistory.com/">https://godhkekf24.tistory.com/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GitHub Action 스케줄이 제 시간에 돌지 않는 문제]]></title>
            <link>https://velog.io/@dev-yuniljun/GitHub-Action-%EC%8A%A4%EC%BC%80%EC%A4%84%EC%9D%B4-%EC%A0%9C-%EC%8B%9C%EA%B0%84%EC%97%90-%EB%8F%8C%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@dev-yuniljun/GitHub-Action-%EC%8A%A4%EC%BC%80%EC%A4%84%EC%9D%B4-%EC%A0%9C-%EC%8B%9C%EA%B0%84%EC%97%90-%EB%8F%8C%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Tue, 30 Jan 2024 11:27:50 GMT</pubDate>
            <description><![CDATA[<p>최근에 GitHub Actions에 cron 기능을 이용해 액션을 설정해놨다.</p>
<pre><code class="language-bash">on:
  schedule:
    - cron: &#39;0 15 * * 1-5&#39;</code></pre>
<p>하지만 실제로 돌아간 시간은 17분이 지난 후에 진행되었다.
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/711cc2a3-f5b3-460c-acb6-e501c6001c4a/image.png" alt=""></p>
<p>나뿐만 아니라 여러 사람들이 같은 문제를 겪고 있었다.
<a href="https://github.com/orgs/community/discussions/52477">https://github.com/orgs/community/discussions/52477</a></p>
<p><a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule">공식문서</a>를 참고해보니 이 기능은 00분으로 설정해놓으면 다른 액션과 겹쳐 딜레이 될 수 있다는 주의사항이 나와있었다. 대부분의 스케줄이 00분에 시작하는 것이 많기 때문에 모든 요청들이 발생하면 큐에 담아 두고 나름의 우선 순위에 따라 처리되는 것 같았다. 요청이 몰리면 심지어는 jobs들이 날아갈 수 있다고 한다. 
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/7b9d4ffc-04cf-43fc-98a2-97e7c718dfbd/image.png" alt=""></p>
<p>만약 정확한 시점에 액션을 돌려야 한다면 개인 서버를 구축하는 것이 안전해보이고 firebase의 cloud function을 이용하는 개발자도 있었다.</p>
<p>GitHub Actions을 써야하나 Jenkins를 써야하나 기준이 모호할 때가 있었는데 이로서 하나의 기준을 더 만들 수 있었던 좋은 계기가 되었다.</p>
<p>앞으로도 많이 개발하고 많이 막히고 올바르게 해결해가며 성장해나가고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 레포 커밋 자동 확인]]></title>
            <link>https://velog.io/@dev-yuniljun/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%A0%88%ED%8F%AC-%EC%BB%A4%EB%B0%8B-%EC%9E%90%EB%8F%99-%ED%99%95%EC%9D%B8-od9b4yxc</link>
            <guid>https://velog.io/@dev-yuniljun/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%A0%88%ED%8F%AC-%EC%BB%A4%EB%B0%8B-%EC%9E%90%EB%8F%99-%ED%99%95%EC%9D%B8-od9b4yxc</guid>
            <pubDate>Sat, 27 Jan 2024 15:23:13 GMT</pubDate>
            <description><![CDATA[<p>나 포함 3명에서 알고리즘 스터디를 진행하는데 한 명의 팀원이 워낙 알고리즘을 빼먹어서 주기적으로 알람을 줄 수 있는 방법이 없을까 고민하다가 해당 토이 프로젝트를 시작하게 됐다.</p>
<p><strong>구현 기능</strong></p>
<ol>
<li>평일 자정이 되면 팀원들의 레포지토리를 확인하고 새로운 커밋 유무를 확인하여 오늘 알고리즘 문제를 풀었는지 확인한다.</li>
<li>커밋 유무를 날짜별로 정리해, README 파일에 작성하고 새로운 커밋을 진행한다.</li>
<li>커밋하지 않은 팀원들에게는 이메일로 알람을 보낸다.</li>
</ol>
<p>1번 기능 구현</p>
<p>먼저 특정 레포지토리에서 커밋 이력을 가져올 수 있는 지 확인해본다.</p>
<p><a href="https://docs.github.com/ko/rest/commits/commits?apiVersion=2022-11-28">커밋 - GitHub Docs</a></p>
<p>GitHub Actions 공식 문서에서 List commits 를 가져올 수 있는 API를 제공해주는 것을 확인했다.</p>
<p><img src="https://velog.velcdn.com/images/dev-yuniljun/post/3a3aed3e-4f57-4059-955d-82c5b8269327/image.png" alt=""></p>
<pre><code class="language-bash">curl -L \
  -H &quot;Accept: application/vnd.github+json&quot; \
  -H &quot;Authorization: Bearer &lt;YOUR-TOKEN&gt;&quot; \
  -H &quot;X-GitHub-Api-Version: 2022-11-28&quot; \
  https://api.github.com/repos/OWNER/REPO/commits</code></pre>
<p>예제 응답</p>
<pre><code class="language-json">[
  {
    &quot;url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e&quot;,
    &quot;sha&quot;: &quot;6dcb09b5b57875f334f61aebed695e2e4193db5e&quot;,
    &quot;node_id&quot;: &quot;MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ==&quot;,
    &quot;html_url&quot;: &quot;https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e&quot;,
    &quot;comments_url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments&quot;,
    &quot;commit&quot;: {
      &quot;url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e&quot;,
      &quot;author&quot;: {
        &quot;name&quot;: &quot;Monalisa Octocat&quot;,
        &quot;email&quot;: &quot;support@github.com&quot;,
        &quot;date&quot;: &quot;2011-04-14T16:00:49Z&quot;
      },
      &quot;committer&quot;: {
        &quot;name&quot;: &quot;Monalisa Octocat&quot;,
        &quot;email&quot;: &quot;support@github.com&quot;,
        &quot;date&quot;: &quot;2011-04-14T16:00:49Z&quot;
      },
      &quot;message&quot;: &quot;Fix all the bugs&quot;,
      &quot;tree&quot;: {
        &quot;url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e&quot;,
        &quot;sha&quot;: &quot;6dcb09b5b57875f334f61aebed695e2e4193db5e&quot;
      },
      &quot;comment_count&quot;: 0,
      &quot;verification&quot;: {
        &quot;verified&quot;: false,
        &quot;reason&quot;: &quot;unsigned&quot;,
        &quot;signature&quot;: null,
        &quot;payload&quot;: null
      }
    },
    &quot;author&quot;: {
      &quot;login&quot;: &quot;octocat&quot;,
      &quot;id&quot;: 1,
      &quot;node_id&quot;: &quot;MDQ6VXNlcjE=&quot;,
      &quot;avatar_url&quot;: &quot;https://github.com/images/error/octocat_happy.gif&quot;,
      &quot;gravatar_id&quot;: &quot;&quot;,
      &quot;url&quot;: &quot;https://api.github.com/users/octocat&quot;,
      &quot;html_url&quot;: &quot;https://github.com/octocat&quot;,
      &quot;followers_url&quot;: &quot;https://api.github.com/users/octocat/followers&quot;,
      &quot;following_url&quot;: &quot;https://api.github.com/users/octocat/following{/other_user}&quot;,
      &quot;gists_url&quot;: &quot;https://api.github.com/users/octocat/gists{/gist_id}&quot;,
      &quot;starred_url&quot;: &quot;https://api.github.com/users/octocat/starred{/owner}{/repo}&quot;,
      &quot;subscriptions_url&quot;: &quot;https://api.github.com/users/octocat/subscriptions&quot;,
      &quot;organizations_url&quot;: &quot;https://api.github.com/users/octocat/orgs&quot;,
      &quot;repos_url&quot;: &quot;https://api.github.com/users/octocat/repos&quot;,
      &quot;events_url&quot;: &quot;https://api.github.com/users/octocat/events{/privacy}&quot;,
      &quot;received_events_url&quot;: &quot;https://api.github.com/users/octocat/received_events&quot;,
      &quot;type&quot;: &quot;User&quot;,
      &quot;site_admin&quot;: false
    },
    &quot;committer&quot;: {
      &quot;login&quot;: &quot;octocat&quot;,
      &quot;id&quot;: 1,
      &quot;node_id&quot;: &quot;MDQ6VXNlcjE=&quot;,
      &quot;avatar_url&quot;: &quot;https://github.com/images/error/octocat_happy.gif&quot;,
      &quot;gravatar_id&quot;: &quot;&quot;,
      &quot;url&quot;: &quot;https://api.github.com/users/octocat&quot;,
      &quot;html_url&quot;: &quot;https://github.com/octocat&quot;,
      &quot;followers_url&quot;: &quot;https://api.github.com/users/octocat/followers&quot;,
      &quot;following_url&quot;: &quot;https://api.github.com/users/octocat/following{/other_user}&quot;,
      &quot;gists_url&quot;: &quot;https://api.github.com/users/octocat/gists{/gist_id}&quot;,
      &quot;starred_url&quot;: &quot;https://api.github.com/users/octocat/starred{/owner}{/repo}&quot;,
      &quot;subscriptions_url&quot;: &quot;https://api.github.com/users/octocat/subscriptions&quot;,
      &quot;organizations_url&quot;: &quot;https://api.github.com/users/octocat/orgs&quot;,
      &quot;repos_url&quot;: &quot;https://api.github.com/users/octocat/repos&quot;,
      &quot;events_url&quot;: &quot;https://api.github.com/users/octocat/events{/privacy}&quot;,
      &quot;received_events_url&quot;: &quot;https://api.github.com/users/octocat/received_events&quot;,
      &quot;type&quot;: &quot;User&quot;,
      &quot;site_admin&quot;: false
    },
    &quot;parents&quot;: [
      {
        &quot;url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e&quot;,
        &quot;sha&quot;: &quot;6dcb09b5b57875f334f61aebed695e2e4193db5e&quot;
      }
    ]
  }
]</code></pre>
<p>해당 API는 본인 GITHUB 토큰만 있으면 어떤 레포지토리든지 확인 가능했다.</p>
<p>해당 API를 요청하고 가장 최근에 올라온 커밋의 날짜를 확인해 전 날 올라온 커밋인지 체크한다. 예를 들면 1월 24일 0시에 액션이 돌아간다면 1월 23일 0시부터 1월 24일 0시까지 확인한다.</p>
<p>확인 결과는 README.md 파일에 날짜 별로 업데이트 해두려고 한다.</p>
<p>그리고 결과들을 추후에 활용할 수 있도록 history.json 파일에 인원 별로 날짜별 커밋 유무를 업데이트 해놓으려고 한다.</p>
<p>README.md 파일은 추후에 다시 꾸미는 것으로 하고 매일매일 커밋 여부 확인하는 MVP 기능만 먼저 만들겠다.</p>
<pre><code class="language-json">[
    {
        &quot;name&quot;: &quot;smilejune&quot;,
        &quot;repo&quot;: &quot;daily-problem-solving/daily-ps&quot;
    },
    {
        &quot;name&quot;: &quot;2522001&quot;,
        &quot;repo&quot;: &quot;daily-problem-solving/252&quot;
    },
    {
        &quot;name&quot;: &quot;harin1212&quot;,
        &quot;repo&quot;: &quot;daily-problem-solving/algo&quot;
    }
]</code></pre>
<p>각각의 레포지토리 리스트를 json 파일로 저장해두고 리스트에 추가, 변경이 발생 시 간편하게 바꿀 수 있도록 유도했다.</p>
<p>객체를 다루게 되면서 파이썬으로 작성할까 고민했지만 리눅스 상에서 반복문, 조건문을 연습하는 셈 치고 그대로 밀고 나가기로 했다.</p>
<pre><code class="language-bash">#!/usr/bin/bash

cat $0

count=$(cat users.json | jq &#39;. | length&#39;)

for((i = 0; i &lt; $count; i++)); do
    curl -L \
    -H &quot;Accept: application/vnd.github+json&quot; \
    -H &quot;Authorization: Bearer ${{secrets.TOKEN_GITHUB}}&quot; \
    -H &quot;X-GitHub-Api-Version: 2022-11-28&quot; \
    https://api.github.com/repos/daily-problem-solving/252/commits
done</code></pre>
<p>해당 스크립트를 실행하려고 하는데 for문을 인식하지 못하는 문제가 있었다.</p>
<p>최상단에 선언된 #!/bin/sh 을 #!/bin/bash 로 변경해주었고, 현재는 #!/usr/bin/bash로 변경해봤지만 해결하지 못했다.</p>
<p>GitHub Actions runner 에서 기본으로 bash를 사용하고 있기 때문에 쉘 스크립트를 따로 만들어주지 않고 yml파일에 바로 작성하기로 했다.</p>
<pre><code class="language-yaml">name: algo commit autocheck

on:
  schedule:
    - cron: &#39;0 15 * * 2-6&#39;

  workflow_dispatch:

jobs:
  check-commit:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v4

      - name: check commit 
        run: |
          count=$(cat users.json | jq &#39;. | length&#39;)
          for ((i = 0; i &lt; count; i++)); do
            cat users.json | jq -r &quot;.[0].name&quot; &gt;&gt; README.md
            repo=$(cat users.json | jq -r &quot;.[$i].repo&quot;)

            latest_date=$(curl -L \
            -H &quot;Accept: application/vnd.github+json&quot; \
            -H &quot;Authorization: Bearer ${{ secrets.TOKEN_GITHUB }}&quot; \
            -H &quot;X-GitHub-Api-Version: 2022-11-28&quot; \
            https://api.github.com/repos/$repo/commits | jq -r &quot;.[0].commit.committer.date&quot;)
          done

          cat README.md</code></pre>
<pre><code class="language-yaml">on:
  schedule:
    - cron: &#39;0 15 * * 1-5&#39;</code></pre>
<p>먼저 월요일 자정부터 금요일 자정까지 확인 해야하기 때문에 해당 액션은 UTC기준 월요일 15시 - 금요일 15시에 각각 돌아가면 된다.</p>
<p> 시간은 00:00시 부터 24:00시 사이인지 확인해야 하기 때문에 UTC 기준으로는 전날 15:00 부터 당일 15:00시 사이에 있는지 확인하며 된다.</p>
<p>타임 스탬프는 복잡하기 때문에 epoch time으로 사이 값에 있는지 확인해준다.</p>
<p>중간 코드</p>
<pre><code class="language-yaml">name: algo commit autocheck

on:
  schedule:
    - cron: &#39;0 15 * * 1-5&#39;

  workflow_dispatch:

jobs:
  check-commit:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v4

      - name: check commit 
        run: |
          count=$(cat users.json | jq &#39;. | length&#39;)
          date -u -I &gt;&gt; temp.txt
          for ((i = 0; i &lt; count; i++)); do
            cat users.json | jq -r &quot;.[$i].name&quot; &gt;&gt; temp.txt
            repo=$(cat users.json | jq -r &quot;.[$i].repo&quot;)

            latest_date=$(curl -L \
            -H &quot;Accept: application/vnd.github+json&quot; \
            -H &quot;Authorization: Bearer ${{ secrets.TOKEN_GITHUB }}&quot; \
            -H &quot;X-GitHub-Api-Version: 2022-11-28&quot; \
            https://api.github.com/repos/$repo/commits | jq -r &quot;.[0].commit.committer.date&quot;)

            latest_date_epoch=$(date -d &quot;$latest_date&quot; +%s)
            start_date_epoch=$(date -d &quot;yesterday 15:00&quot; +%s)
            end_date_epoch=$(date -d &quot;today 15:00&quot; +%s)

            if [[ $latest_date_epoch -ge $start_date_epoch &amp;&amp; $latest_date_epoch -le $end_date_epoch ]]; then
              echo &quot;Y&quot; &gt;&gt; temp.txt
            else
              echo &quot;N&quot; &gt;&gt; temp.txt
            fi
          done

          cat temp.txt &gt;&gt; README.md
</code></pre>
<p>정상적으로 나오는 것을 확인했고 이후에는 변경된 <a href="http://README.md">README.md</a> 파일을 반영하기 위해 커밋, 푸시가 가능한 actions를 찾아서 적용했다.</p>
<p>아래는 <strong>최종 코드</strong>이다. </p>
<pre><code class="language-yaml">name: algo commit autocheck

on:
  schedule:
    - cron: &#39;0 15 * * 1-5&#39;

  workflow_dispatch:

permissions: write-all

jobs:
  check-commit:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v4

      - name: check commit 
        run: |
          count=$(cat users.json | jq &#39;. | length&#39;)
          date -u -I &gt;&gt; temp.txt
          for ((i = 0; i &lt; count; i++)); do
            cat users.json | jq -r &quot;.[$i].name&quot; &gt;&gt; temp.txt
            repo=$(cat users.json | jq -r &quot;.[$i].repo&quot;)

            latest_date=$(curl -L \
            -H &quot;Accept: application/vnd.github+json&quot; \
            -H &quot;Authorization: Bearer ${{ secrets.TOKEN_GITHUB }}&quot; \
            -H &quot;X-GitHub-Api-Version: 2022-11-28&quot; \
            https://api.github.com/repos/$repo/commits | jq -r &quot;.[0].commit.committer.date&quot;)

            latest_date_epoch=$(date -d &quot;$latest_date&quot; +%s)
            start_date_epoch=$(date -d &quot;yesterday 15:00&quot; +%s)
            end_date_epoch=$(date -d &quot;today 15:00&quot; +%s)

            if [[ $latest_date_epoch -ge $start_date_epoch &amp;&amp; $latest_date_epoch -le $end_date_epoch ]]; then
              echo &quot;Y&quot; &gt;&gt; temp.txt
            else
              echo &quot;N&quot; &gt;&gt; temp.txt
            fi
            echo &quot; / &quot; &gt;&gt; temp.txt
          done

          cat temp.txt &gt;&gt; README.md
          rm -rf temp.txt
          echo &quot;&lt;br&gt;&quot; &gt;&gt; README.md
      - name: Commit &amp; Push changes
        uses: actions-js/push@master
        with:
          github_token: ${{ secrets.TOKEN_GITHUB }}</code></pre>
<p>commit, push가 안되는 문제도 있었지만 permissions를 추가적으로 적용하면서 해결했다.</p>
<p><strong>액션 테스트</strong>
<img src="https://velog.velcdn.com/images/dev-yuniljun/post/6649b8b4-5466-4cb6-9a0b-fd1dea954b3f/image.png" alt=""></p>
<p>추후에 이메일 기능과 json으로 데이터를 저장하는 기능을 구현할 예정이다.</p>
]]></description>
        </item>
    </channel>
</rss>