<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sh__.log</title>
        <link>https://velog.io/</link>
        <description>안녕하세요</description>
        <lastBuildDate>Thu, 17 Oct 2024 05:25:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sh__.log</title>
            <url>https://velog.velcdn.com/images/sh__/profile/30c47320-4819-41d7-aa0b-3da973e191ff/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sh__.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sh__" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.10.10 WIL - 트러블 슈팅: DockerFile의 수정사항이 이미지에 반영되지 않는 경우가 발생]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.10.10-WIL-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-DockerFile%EC%9D%98-%EC%88%98%EC%A0%95%EC%82%AC%ED%95%AD%EC%9D%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%97%90-%EB%B0%98%EC%98%81%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EA%B2%BD%EC%9A%B0%EA%B0%80-%EB%B0%9C%EC%83%9D</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.10.10-WIL-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-DockerFile%EC%9D%98-%EC%88%98%EC%A0%95%EC%82%AC%ED%95%AD%EC%9D%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%97%90-%EB%B0%98%EC%98%81%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EA%B2%BD%EC%9A%B0%EA%B0%80-%EB%B0%9C%EC%83%9D</guid>
            <pubDate>Thu, 17 Oct 2024 05:25:10 GMT</pubDate>
            <description><![CDATA[<h2 id="상황">상황</h2>
<ul>
<li><p>기존의 프로그램에서 버그를 수정을 위해 코드를 수정하여 docker compose를 이용해 로컬에 spring 프로젝트들을 띄워 두었다.</p>
</li>
<li><p>redis의 설정 파일</p>
<pre><code class="language-yml">spring:
data:
  redis:
    host: host.docker.internal
    port: 6379</code></pre>
</li>
</ul>
<hr>
<p>spring.config.activate.on-profile: local
spring:
  data:
    redis:
      host: host.docker.internal
      port: 6379</p>
<hr>
<p>spring.config.activate.on-profile: dev
spring:
  data:
    redis:
      host: ${redis.host}
      port: 6379</p>
<pre><code>- 위와 같이 local 환경과 dev 환경의 redis.host가 분리되어 있다. dev 환경의 redis.host는 aws의 Elasticache 주소로 설정되어 있고, 로컬에서 레디스를 띄워 테스트를 진행해 볼 예정이었기에 프로파일 설정을 local로 변경하여 진행하려 하였다.

- 현재 Dockerfile의 상태

```DockerFile
# build 에서 사용할 이미지
FROM gradle:8.10.2-jdk21 AS build

WORKDIR /app

ARG FILE_DIRECTORY

COPY $FILE_DIRECTORY /app

# 실제 컨테이너로 만들 이미지 베이스
FROM openjdk:21-jdk-slim

# build 단계로부터 파일을 가져올 수 있음!
# AS build로 선언해놨기에 --from=build!
COPY --from=build /app/build/libs/*SNAPSHOT.jar /app.jar

CMD [&quot;java&quot;, &quot;-Dspring.profiles.active=dev&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code></pre><ul>
<li><p>위와 같이 <code>-Dspring.profiles.active</code>가 dev로 설정되어 있기에 local로 변경하고 docker compose를 이용해 스프링 프로젝트들을 실행시켰다.</p>
</li>
<li><p>실행 후 모든 프로젝트의 콘솔 창을 살펴 보니 </p>
<h3 id="the-following-1-profile-is-active-dev"><code>The following 1 profile is active: &quot;dev&quot;</code></h3>
<p>라는 문구와 함께 레디스에 연결할 수 없다는 오류가 나타났다.</p>
</li>
</ul>
<hr>
<h2 id="해결-과정">해결 과정</h2>
<p><img src="https://velog.velcdn.com/images/sh__/post/209ff5df-5a05-42c9-b320-cfbba5fd5d24/image.png" alt=""></p>
<ul>
<li>DockerFile의 <code>-Dspring.profiles.active</code>를 local로 설정하였음에도 불구하고 왜 이미지에 반영이 되지 않는 것일까?</li>
</ul>
<hr>
<h3 id="solution-1">Solution #1.</h3>
<blockquote>
<p>docker-compose up 명령어는 기존 이미지가 존재할 시에, <strong>이미지를 재빌드하지 않는다.</strong>
따라서 기존에 이미지가 있다면 Dockerfile 이 수정되었더라도 이미지에 변경사항이 적용되지 않는다.</p>
</blockquote>
<ul>
<li>따라서 이미지를 모두 지운 뒤 gradle을 이용해 clean 및 build를 수행하고 다시 이미지를 띄워 보았다.</li>
</ul>
<pre><code class="language-bash">$ ./gradlew clean

$ ./gradlew build

$ docker compose up --build</code></pre>
<ul>
<li>그러나 오류는 해결되지 않았고, 같은 오류가 반복되어 나타났다.</li>
</ul>
<hr>
<h3 id="solution-2">Solution #2.</h3>
<blockquote>
<p>build 를 해도 Dockerfile 의 변경사항을 <strong>캐시로 처리</strong>해서 변경이 되지 않는 경우가 있다고 한다.
그럴 때는 이미지 빌드 시 캐시를 사용하지 않도록 옵션을 붙혀주면 된다.</p>
</blockquote>
<pre><code class="language-bash">$ docker compose up --force-recreate</code></pre>
<ul>
<li>역시나 같은 문제가 반복되었다.</li>
</ul>
<hr>
<h3 id="solution-3">solution #3.</h3>
<blockquote>
<p>docker 에서는 layer를 쌓을 때 cache를 이용해서 이미지 빌드와 컨테이너 생성 시간을 줄인다.
이 cache 데이터는 이미지와 컨테이너를 제거하더라도 로컬에 남아있다.
cache 가 계속 쌓이는 것이 장점이 크긴 하지만 단점도 있다.</p>
</blockquote>
<ul>
<li>혹시 몰라 로컬의 캐시를 모두 제거 후 다시 이미지를 빌드해보았다.</li>
</ul>
<pre><code class="language-bash">$ docker system prune -a</code></pre>
<ul>
<li>나도 몰래 13GB나 쌓여있었던 캐시가 삭제되었고, 이미지를 다시 빌드해 보았다.</li>
</ul>
<h3 id="the-following-1-profile-is-active-local">The following 1 profile is active: &quot;local&quot;</h3>
<ul>
<li>위의 문구와 함께 버그가 잘 수정되었다❗❗</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.10.10 WIL - 트러블 슈팅]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.10.10-WIL-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.10.10-WIL-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Thu, 10 Oct 2024 02:59:55 GMT</pubDate>
            <description><![CDATA[<hr>
<h1 id="금주의-트러블-슈팅">금주의 트러블 슈팅</h1>
<p><img src="https://velog.velcdn.com/images/sh__/post/0040c934-e3be-44ef-b8d8-c1805d90cf81/image.png" alt=""></p>
<ul>
<li><p>위는 현재 진행중인 멀티 모듈 프로젝트의 모듈 구조이다. 평소와 같이 개발을 진행하던 도중, 예외 처리 및 에러 핸들링을 하는 과정에서 다음의 문제가 발생했다.</p>
</li>
<li><p>우선 api 모듈의 Controller단에서 domain 모듈의 Service단을 호출하고, Service단의 메소드에서 try-catch문으로 예외 발생을 감지하고, 해당 오류를 support-api 모듈에 정의된 Api Response 클래스로 감싸서 반환한다.</p>
</li>
<li><p>Service 단에서의 여러 분기(없는 Entity 조회, 권한 부족 등)에 따른 예외를 발생시키기 위해 직접 구현한 ErrorType 코드, Exception 클래스가 필요했다. </p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/6af6aabe-dd33-475b-a2ab-dd48cabfb997/image.png" alt=""></p>
<ul>
<li>그러나 ErrorType, Exception 클래스는 support-api 모듈에 정의되어 있어 Service 로직에서 참조가 불가하였다.</li>
</ul>
<ul>
<li>그래서 고안한 방법들 아래와 같다.</li>
</ul>
<hr>
<h1 id="solution-1">Solution #1</h1>
<p><img src="https://velog.velcdn.com/images/sh__/post/f26b2f75-31e1-4318-85af-e8523ef575f6/image.png" alt=""></p>
<ul>
<li>기본적으로 domain 모듈은 support-api 모듈을 참조하는 것이 불가하지만, api 모듈은 최상위 모듈이기에 다른 support 모듈들을 참조할 수 있도록 설계되어 있다.</li>
<li>따라서 Custom Exception 관련 코드들을 support-domain 모듈로 옮기게 되면 api 모듈에서 참조할 수 있었다.</li>
</ul>
<h2 id="문제-발생">문제 발생</h2>
<p><img src="https://velog.velcdn.com/images/sh__/post/b0a28f35-f172-4a22-90e4-b4a54cc4a7f1/image.png" alt=""></p>
<ul>
<li>그러나 support-api 모듈의 Api Response를 구현한 패키지에서 Custom Exception을 구현한 패키지 내의 코드를 재사용하고있었다.</li>
<li>support 모듈끼리는 서로 참조가 불가했기 때문에 이 부분에서 오류가 발생하는 것을 확인할 수 있었다.</li>
<li>발생한 연쇄적인 오류를 해결하기 위해 support-api 모듈과 support-domain 모듈이 상호작용하도록 설계를 하게 되면 DDD를 구현한 의미가 퇴색된다고 판단되어 다른 방법을 모색하기로 하였다.</li>
</ul>
<hr>
<h1 id="solution-2">Solution #2</h1>
<p><img src="https://velog.velcdn.com/images/sh__/post/7cf023a5-3ebc-433a-b99e-a8bebe9b4115/image.png" alt=""></p>
<ul>
<li>Custom Exception을 구현한 코드를 위와 같이 domain 모듈에도 두면 Service 단에서도 예외 처리를 구현할 수 있다.</li>
</ul>
<h2 id="문제-발생-1">문제 발생</h2>
<p><img src="https://velog.velcdn.com/images/sh__/post/d7b721ea-f4d5-4cfb-92d6-a6daa40e11aa/image.png" alt=""></p>
<ul>
<li><p>코드에 중복성이 생기게 되어 좋은 코드는 아니다.</p>
</li>
<li><p>Custom Exception 관련 코드 중 하나의 클래스인 ErrorType에 정의된 코드 중 <code>DEFAULT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.E500, &quot;An unexpected error has occurred.&quot;, LogLevel.ERROR)</code> 와 같이 HttpStatus enum을 사용하려면 <code>&#39;org.springframework.boot:spring-boot-starter-web&#39;</code> 의존성을 주입해야 한다.</p>
</li>
<li><p>support-domain 모듈은 기본적으로 spring boot 의존성을 모두 배제하고 순수 자바코드만 사용하기로 컨벤션이 정의가 되어있던 상태라, 의존성을 주입하기 애매한 상황이다.</p>
</li>
</ul>
<hr>
<h1 id="solution-3">Solution #3</h1>
<p><img src="https://velog.velcdn.com/images/sh__/post/413db367-567a-41ca-814f-fcebd62c0bb0/image.png" alt=""></p>
<ul>
<li>최종적으로 수정된 구조이다. 공통적으로 사용되는 error 모듈을 새로운 공통모듈로 생성하여 api 모듈, domain 모듈에 주입하여 사용할 수 있었다.</li>
<li>추가로 Api Response를 구현한 클래스에도 Custom Exception을 구현한 클래스의 <code>ErrorType</code>이 필요하기에 순환 참조만 주의하여 support-api 모듈에 support-error 모듈을 주입하면 <strong>Solution #1</strong>의 문제도 해결할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.09.27 WIL]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.09.27-WIL</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.09.27-WIL</guid>
            <pubDate>Sat, 05 Oct 2024 09:00:25 GMT</pubDate>
            <description><![CDATA[<hr>
<ul>
<li><strong>Week I Learned</strong>, 이번 주 프로젝트를 수행하며 겪은 일들, 배운 내용들을 정리하는 시간을 가져 보자.</li>
</ul>
<p>먼저, 노션의 5분 기록 보드를 활용해 프로젝트를 진행하며 기록했던것들을 리마인드해보자.</p>
<p><img src="https://velog.velcdn.com/images/sh__/post/ca14ea8d-f961-4fab-b834-27c2b77d5f34/image.png" alt=""></p>
<ul>
<li>챕터 초반부인 만큼, 기획, 설계와 관련된 부분들이 많았다. 요구사항을 정의하고, 관련 자료들을 찾아 용어들을 정리하며 팀원들과 함께 프로젝트에 대한 이해도를 높였다.</li>
<li>이후 API 명세서, 테이블 명세서, ERD를 작성하며 설계하는 시간을 가졌고 프로젝트의 목표 중 기본 기능 구현을 제외하고 도전과제까지 정의할 수 있었다.</li>
</ul>
<hr>
<h1 id="프로젝트-요구-사항">프로젝트 요구 사항</h1>
<ul>
<li>사용자 관리<ul>
<li>사용자 정보 관리:<ul>
<li>사용자 엔티티는 모든 사용자 정보를 관리하며, 사용자 비활성화 시 is_deleted 필드 사용</li>
</ul>
</li>
<li>권한 관리:<ul>
<li>마스터 관리자만 생성, 수정, 삭제가 가능하며 사용자 본인만 자신의 정보를 조회할 수 있습니다. 마스터 관리자는 모든 사용자 정보를 조회할 수 있음</li>
<li>MASTER, USER, LAWYER(변호사) 권한 존재</li>
</ul>
</li>
</ul>
</li>
<li>변호사 관리<ul>
<li>변호사 등록:<ul>
<li>변호사 등록을 위해 사이트에서 변호사 등록 신청을 받고 MASTER 관리자의 승인이 필요함</li>
<li>변호사는 등록 신청 시 변호사 소개, 자격 정보, 경력 사항들을 함께 업로드</li>
<li>승인이 완료되면 변호사 목록 페이지에 해당 변호사의 정보가 업로드 됨</li>
</ul>
</li>
<li>상담 신청:<ul>
<li>변호사 정보 조회 시 변호사의 이름, 이메일, 소개, 자격 정보, 경력 사항들이 제공되며 변호사 초대 버튼 클릭시 변호사에게 상담 신청 요청<ul>
<li>변호사 수락시 기존 대화에서 변호사와 셋이서(예비 채권자, 예비 채무자, 변호사) 대화 가능</li>
</ul>
</li>
</ul>
</li>
<li>변호사 역할:<ul>
<li>P2P 대출이 이루어질 때, 유저는 변호사에게 사업 정보를 제공 (관련 자료 → <a href="https://s3-ap-northeast-2.amazonaws.com/hf-froala/file%2F1598400704247-P2P%EB%8C%80%EC%B6%9C+%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8+%EA%B0%9C%EC%A0%95%EC%95%88+%EC%A0%84%EB%AC%B8.pdf"><code>클릭</code></a>)<ul>
<li>P2P 대출의 구조 및 영업 방식</li>
<li>상품 유형별 누적 대출 금액</li>
<li>차용증</li>
<li>전자 서명</li>
</ul>
</li>
</ul>
</li>
<li>변호사 리뷰:<ul>
<li>계약이 체결되면 사용자는 상담을 신청했던 변호사에게 리뷰를 달 수 있음</li>
<li>별점과 코멘트를 남김</li>
</ul>
</li>
</ul>
</li>
<li>채팅방 관리<ul>
<li>채팅방 정보 관리:<ul>
<li>사용자는 누구든 원하는 상대와 채팅을 할 수 있고, 본인의 채팅방만 조회 가능</li>
<li>마스터 관리자만 타인의 채팅방을 조회, 수정, 삭제가 가능</li>
<li>기본적인 정보(채팅방의 UUID, 소속된 유저들의 정보 등)는 RDB에 저장</li>
<li>여러 조인들로 인한 성능 저하 방지를 위해 세부 정보들(채팅 내역)은 NoSQL로 저장</li>
</ul>
</li>
</ul>
</li>
<li>게시글 관리<ul>
<li>게시글 정보 관리:<ul>
<li>모든 사용자는 게시글을 조회할 수 있고 자신의 게시글만 등록, 수정, 삭제가 가능</li>
<li>MASTER 관리자는 모든 게시글의 등록, 수정, 삭제가 가능</li>
<li>게시글에는 ‘대출해드립니다’, ‘대출받습니다’ 두 개의 카테고리가 존재</li>
<li>게시글에는 해당 대출에 적용되는 계약 상태를 게시글 상단에 노출시켜야 함<ul>
<li>모집 중(OPEN) → 대출자를 모집 중인 단계</li>
<li>진행 중(IN_PROGRESS) → 대출이 진행중인 단계</li>
<li>계약 완료(COMPLETED) → 계약이 체결된 단계</li>
</ul>
</li>
<li>계약이 생성되거나 해당 계약이 성사되면 게시글의 상태가 업데이트됨</li>
</ul>
</li>
</ul>
</li>
<li>계약 관리<ul>
<li>계약 관리 :<ul>
<li>계약은 게시글에 종속된 상태</li>
<li>계약은 게시글에서 채권자, 채무자 간 합의에 의해 생성<ul>
<li>한쪽의 계약 생성 요청 후 상대의 요청 허가 후 생성</li>
<li>계약이 생성된 후 게시글의 상태는 계약 완료로 변경</li>
</ul>
</li>
<li>일반 사용자는 자신의 계약을 조회 가능</li>
<li>계약 이해 관계자 간 합의가 있다면 계약 수정 가능<ul>
<li>계약은 수정 요청후 상대방의 요청 허가 후 수정 반영</li>
</ul>
</li>
<li>MASTER 관리자는 모든 계약의 조회 가능</li>
</ul>
</li>
<li>계약 상태:<ul>
<li>모든 계약은 다음의 상태를 가짐<ul>
<li>채무 이행중</li>
<li>채무 완료</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<ul>
<li>위의 요구사항 중 내가 맡은 부분은 게시글 관리와 계약 관리였고, 현재까지는 게시글 도메인에서 유저 권한에 따른 분기, 예외 처리, 에러 핸들링을 제외하고 기본 CRUD + Search 기능까지는 구현이 된 상태이다. 계약 도메인 또한 비슷하게 진행되고 있는 상태이다.</li>
<li>내가 맡은 부분의 도전 기능으로는 배포 시 S3를 이용한 계약서 파일 업로드 기능을 구현하기로 정해졌다.</li>
</ul>
<hr>
<h1 id="트러블-슈팅">트러블 슈팅</h1>
<ul>
<li><p>멀티 모듈 프로젝트도, DDD 구조로 된 프로젝트도 처음이었기에 두 구조가 결합된 프로젝트를 구현하는것은 쉽지 않았고, 그 과정에서 <strong>Lombok이 제대로 동작하지 않는 경우가 발생</strong>했다.</p>
</li>
<li><p>프로젝트 모듈의 구조는 아래와 같이 설계되어 있다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/0fc94a0b-58f7-47e2-9f8c-ddc5191853a2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/b51a6fc6-8657-440e-8667-8c974edf47ff/image.png" alt=""></p>
<ul>
<li>각 App에서 api, domain, storage 모듈을 구분하고 support 모듈에서 대응되는 각 모듈에게 의존성을 주입하여 support 모듈에 정의된 클래스들을 사용하게 된다.</li>
<li>각 서포트 모듈에서 아래의 코드를 공통적으로 주입하고 있었는데, 대응되는 상위 모듈들에서 Lombok이 적용되지 않는 경우가 발생했다.</li>
</ul>
<pre><code class="language-java">api &#39;org.projectlombok:lombok&#39;
annotationProcessor &#39;org.projectlombok:lombok&#39;</code></pre>
<p>Support 모듈이 아닌 각 모듈에 </p>
<p><code>compileOnly &#39;org.projectlombok:lombok&#39;</code></p>
<p><code>annotationProcessor &#39;org.projectlombok:lombok&#39;</code></p>
<p>위의 두 코드를 넣어 해결하였다.</p>
<p>Support 모듈에서는 위 두 의존성이 전파되지 않는 것을 확인할 수 있었다.</p>
<hr>
<h1 id="그-외-배운점">그 외 배운점</h1>
<h3 id="1-querydsl의-like-vs-contains">1. QueryDSL의 like vs contains</h3>
<ul>
<li>like(str)은 쿼리가 나갈 때 str자체가 나간다<ul>
<li>즉, 정확히 일치해야한다</li>
<li>like는 내가 % 연산을 선택할 수 있다</li>
</ul>
</li>
<li>contains(str)은 쿼리가 나갈 때 %str%가 나간다</li>
</ul>
<h3 id="2-무분별한-transactional-사용의-개선">2. 무분별한 @Transactional 사용의 개선</h3>
<ul>
<li>다른 App으로부터 Feign Client를 통해 Entity를 불러오거나 다른 테이블과의 조인을 통해 여러 CRUD 기능이 한 서비스 함수에 몰려있는 경우 문제 발생 시 한번에 롤백하기 위해 트랜잭션 관리가 필요하지만 하나의 쿼리만 수정하는 함수에서는 @Transactional 어노테이션을 붙여 트랜잭션 관리를 할 필요가 없다. </li>
</ul>
<h3 id="3-jpaquery-t-fetchone">3. JPAQuery&lt; T &gt;.fetchOne()</h3>
<pre><code class="language-java">JPAQuery&lt;ContractEntity&gt; query = queryFactory.selectFrom(contractEntity)
                                .where(contractEntity.contractUuid.eq(contractUuid));

                List&lt;ContractEntity&gt; result = query.fetch();</code></pre>
<ul>
<li>위의 코드에서 contractUuid 는 기본키이므로 쿼리는 하나임이 보장될 때, list가 아닌 단일 entity로 반환하는 방법은? → fetchOne(); 을 사용</li>
</ul>
<pre><code class="language-java">JPAQuery&lt;ContractEntity&gt; query = queryFactory.selectFrom(contractEntity)
                .where(contractEntity.contractUuid.eq(contractUuid));

        return query.fetchOne();</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[에러 - Formatting violations found in the following files]]></title>
            <link>https://velog.io/@sh__/%EC%97%90%EB%9F%AC-Formatting-violations-found-in-the-following-files</link>
            <guid>https://velog.io/@sh__/%EC%97%90%EB%9F%AC-Formatting-violations-found-in-the-following-files</guid>
            <pubDate>Tue, 01 Oct 2024 01:40:26 GMT</pubDate>
            <description><![CDATA[<h1 id="상황">상황</h1>
<p><img src="https://velog.velcdn.com/images/sh__/post/ffdce3c3-1263-461e-b921-81f146d10603/image.png" alt=""></p>
<p>멀티 모듈 프로젝트를 설계 후 빌드 중 위와 특정 모듈에서 같은 오류가 났다.</p>
<hr>
<h1 id="해결-방법">해결 방법</h1>
<p>터미널을 열고 루트 폴더로 이동 후 
<code>./gradlw format</code> 을 실행한다.</p>
<p><img src="https://velog.velcdn.com/images/sh__/post/c02d0630-8eec-4243-993f-22f4450f9c16/image.png" alt=""></p>
<p>빌드가 잘 되는 것을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.09.27 TIL]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.09.27-TIL</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.09.27-TIL</guid>
            <pubDate>Fri, 27 Sep 2024 14:37:35 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sh__/post/5b063696-604f-4f70-aa7d-b397e94a1a23/image.png" alt=""></p>
<p>CI/CD 전략과 관련하여 페이즈와 Branch 전략에 대한 특강을 들었다.</p>
<hr>
<h2 id="1-개발-서버-phases">1. 개발 서버 Phases</h2>
<h3 id="1-개발-dev"><strong>1. 개발 (Dev)</strong></h3>
<ul>
<li><strong>목적</strong>: <strong>개발(Dev)</strong> 환경은 개발자가 코드를 작성하고 초기 테스트를 수행하는 공간! 새로운 기능을 구현하거나 버그를 수정하며, 시스템을 개선해 나감.</li>
<li><strong>특징</strong>:<ul>
<li>자주 업데이트되며 불안정한 상태.</li>
<li>각 개발자가 자신의 로컬 환경이나 공유된 개발 환경에서 작업할 수 있음.</li>
<li>빠른 반복 작업을 지원하는 지속적 통합 도구와 연계됨.</li>
</ul>
</li>
</ul>
<h3 id="2-스테이징-stage"><strong>2. 스테이징 (Stage)</strong></h3>
<ul>
<li><strong>목적</strong>: <strong>스테이징(Stage)</strong> 환경은 <strong>프로덕션(Prod)</strong> 환경을 미리 시뮬레이션하는 공간으로, 실제 서비스가 배포되기 전에 애플리케이션의 동작을 테스트하는 단계. 모든 개발 작업이 완료된 후에 새로운 기능이나 수정 사항을 확인하는 용도로 사용됨.</li>
<li><strong>특징</strong>:<ul>
<li>생략된 회사가 생각보다 많음</li>
<li><strong>프로덕션</strong> 환경과 최대한 유사하게 설정된 안정적인 환경</li>
<li>QA(품질 보증) 테스트, 사용자 수용 테스트(UAT), 성능 테스트 등에 사용됨</li>
<li>새로운 기능이나 버그 수정이 <strong>프로덕션</strong> 환경에서 예상대로 작동하는지 확인함.</li>
<li>데이터는 <strong>프로덕션</strong> 데이터의 익명화된 복사본이거나, 가상의 데이터일 수 있음</li>
<li>스테이징 단계에서는 코드가 고정되고, 주요 버그 수정만 허용됨</li>
</ul>
</li>
</ul>
<h3 id="3-프로덕션-prod"><strong>3. 프로덕션 (Prod)</strong></h3>
<ul>
<li><strong>목적</strong>: <strong>프로덕션(Prod)</strong> 환경은 애플리케이션이 실제 사용자에게 서비스되는 라이브 환경!</li>
<li><strong>특징</strong>:<ul>
<li>매우 안정적이며, 성능, 보안, 확장성을 최적화</li>
<li>변경 사항은 신중하게 관리되며, 보통 예정된 릴리즈나 업데이트를 통해 배포됨</li>
<li>가동 시간이 매우 중요하며, 다운타임을 최소화하기 위해 모니터링 시스템과 백업이 필요함</li>
<li>실제 데이터를 사용하므로 데이터 백업은 필수</li>
<li><strong>프로덕션</strong>에서 발생하는 문제는 사용자에게 직접 영향을 미치므로, 앞선 환경에서 충분히 테스트한 후 릴리즈가 이루어짐</li>
</ul>
</li>
</ul>
<h3 id="환경-간의-흐름"><strong>환경 간의 흐름</strong></h3>
<ol>
<li><strong>개발 환경에서 코드가 작성되고 테스트됨</strong></li>
<li><strong>안정화되면 스테이징 환경으로 이동하여 정밀한 테스트를 거침</strong></li>
<li><strong>스테이징 환경에서 테스트가 성공적으로 완료되면 프로덕션 환경으로 배포되어 사용자에게 제공됨</strong></li>
</ol>
<h1 id="trunk-based-development">Trunk-Based-Development</h1>
<ul>
<li><p>개요: 개발자들이 코드 변경을 자주 <code>main</code>(또는 trunk) 브랜치에 병합하는 방식! 짧은 주기로 코드를 통합하고 배포하는 것을 목표로 한다. 모든 개발이 짧은 피드백 루프 안에서 이루어지며, 장기적으로 코드 병합을 지연시키지 않기 때문에 더 빠른 배포 주기가 가능하다!</p>
</li>
<li><p><strong>CI/CD와의 적합성</strong>:</p>
<ul>
<li><strong>지속적인 통합</strong>: Trunk-Based Development는 자주 병합이 이루어지므로, 코드가 항상 통합된 상태에서 테스트와 배포가 이루어 짐. 이는 빠른 피드백을 제공하여 코드 품질을 유지하고 배포 속도를 높임</li>
<li><strong>단기 브랜치 사용</strong>: 기능 개발이 단기 브랜치에서 이루어지며, 완료되면 바로 trunk로 병합됨. 병합 시점에 CI/CD 파이프라인이 자동으로 테스트와 배포를 실행하여, 최신 코드를 항상 배포 가능한 상태로 유지</li>
<li><strong>CI/CD 장점</strong>:<ul>
<li>빠른 피드백 루프와 높은 배포 빈도.</li>
<li>병합 충돌 감소와 간소화된 브랜치 관리.</li>
<li>자동화된 테스트 및 배포를 통해 언제든지 최신 코드를 배포 가능.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="git-flow">Git Flow</h2>
<ul>
<li><p>개요: develop와 master 같은 장기 브랜치를 사용하여 개발과 배포를 관리한다. 기능 브랜치, 릴리즈 브랜치, 핫픽스 브랜치 등 여러 브랜치를 사용하여 코드의 상태를 관리하며, 특히 대규모 프로젝트에서 구조화된 배포 전략을 제공한다.</p>
</li>
<li><p><strong>CI/CD와의 적합성</strong>:</p>
<ul>
<li><strong>긴 통합 주기</strong>: Git Flow는 <code>develop</code> 브랜치에서 코드 개발이 이루어지며, <code>master</code>에 병합하기 전에 충분한 테스트와 검증 과정을 거침. 이는 릴리즈 빈도를 줄이고 안정성을 높이는 반면, CI/CD 파이프라인이 더 복잡해질 수 있다.</li>
<li><strong>여러 브랜치 관리</strong>: 각 브랜치에 대한 별도의 CI/CD 파이프라인이 필요할 수 있으며, 특히 릴리즈 브랜치와 핫픽스 브랜치가 별도로 존재하는 경우 파이프라인 관리가 복잡해진다.</li>
<li><strong>CI/CD 장점</strong>:<ul>
<li>릴리즈를 위한 명확한 주기와 더 많은 통제 가능.</li>
<li>대규모 조직이나 긴 개발 주기를 요구하는 프로젝트에 적합.</li>
<li><code>develop</code> 브랜치를 통한 지속적인 개발 진행이 가능하나, 배포는 주로 <code>master</code>에서 이루어진다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="비교"><strong>비교</strong></h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>Trunk-Based Development</th>
<th>Git Flow</th>
</tr>
</thead>
<tbody><tr>
<td><strong>브랜치 모델</strong></td>
<td>단일 메인 브랜치(trunk) + 짧은 생명주기 브랜치</td>
<td>여러 장기 브랜치(develop, master, feature 등)</td>
</tr>
<tr>
<td><strong>통합 빈도</strong></td>
<td>자주 통합, 매일 또는 여러 번 통합</td>
<td>릴리즈 준비 시 통합, 통합 주기 길어짐</td>
</tr>
<tr>
<td><strong>병합 충돌</strong></td>
<td>적음, 빠른 병합과 짧은 브랜치 생명 주기</td>
<td>더 많은 병합 충돌 가능, 긴 브랜치 유지</td>
</tr>
<tr>
<td><strong>CI/CD 속도</strong></td>
<td>빠름, 자동화된 테스트 및 배포로 빠른 릴리즈</td>
<td>느림, 여러 브랜치로 인한 추가적인 수동 절차 필요</td>
</tr>
<tr>
<td><strong>복잡성</strong></td>
<td>간단, 브랜치 관리가 단순</td>
<td>복잡, 브랜치 간 충돌 관리와 여러 릴리즈 브랜치 관리 필요</td>
</tr>
<tr>
<td><strong>적합한 프로젝트</strong></td>
<td>빠른 배포, 애자일, 마이크로서비스 구조</td>
<td>대규모 조직, 긴 릴리즈 주기를 필요로 하는 프로젝트</td>
</tr>
</tbody></table>
<h3 id="결론"><strong>결론</strong></h3>
<ul>
<li><p><strong>Trunk-Based Development</strong>는 <strong>빠른 배포와 CI/CD를 중점으로 두는 애자일 팀에 적합</strong>. 자주 병합하고 작은 변경을 지속적으로 배포할 수 있어 <strong>마이크로서비스나 빠른 피드백이 중요한 프로젝트에 매우 유리</strong>.</p>
</li>
<li><p><strong>Git Flow</strong>는 <strong>더 구조화된 배포 주기와 릴리즈 관리를 선호하는 팀에 적합</strong>. 큰 프로젝트나 조직에서는 Git Flow가 더 많은 통제력을 제공하지만, CI/CD 속도가 느려질 수 있고 관리 복잡성이 증가할 수 있음.
각 방법은 팀의 요구와 프로젝트의 성격에 따라 달라질 수 있으며, CI/CD 자동화 수준에 따라 두 가지 전략을 혼합하여 사용하는 것도 가능함.</p>
</li>
</ul>
<hr>
<ul>
<li>아래는 실제 특정 회사에서 사용중인 브랜치 전략</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/fb0c33e0-9dc9-4a85-b82f-7880a4e275ee/image.png" alt=""></p>
<ul>
<li>dev-deploy(s) 브랜치는 개발 서버에서 빠르게 내 작업에 대한 테스트를 해보기 위한 브랜치</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.09.23 TIL]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.09.23-TIL</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.09.23-TIL</guid>
            <pubDate>Mon, 23 Sep 2024 05:56:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sh__/post/2c4b4c1b-e5fa-4f54-80f4-2a21e30daa14/image.png" alt=""></p>
<ul>
<li><h4 id="httpsgithubcomasd0236spartahubflow"><a href="https://github.com/asd0236/SpartaHubFlow">https://github.com/asd0236/SpartaHubFlow</a></h4>
</li>
</ul>
<p>MSA를 이용한 Ch.4의 <code>물류관리 및 배송 시스템</code> 프로젝트가 끝이 나고 Swagger를 통해 API 명세서를 작성하던 중 Swagger Editor에서 다음과 같은 오류가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/sh__/post/a5c97fd6-7d60-474b-a7cc-58b055b8725a/image.png" alt=""></p>
<pre><code>Errors
Hide

Semantic error at paths./api/v1/orders/{orderId}.delete.requestBody
DELETE operations cannot have a requestBody.
Jump to line 223
</code></pre><ul>
<li><p>지금까지는 크게 인지하지 못하고 있었는데, DELETE Mapping을 진행할 때에는 RequestBody를 사용하지 않는 것이 일반적이라고 한다.</p>
</li>
<li><p>더 정확히 이야기 하자면, HTTP DELETE 요청은 바디를 가지지 않는 것이 일반적</p>
</li>
</ul>
<h2 id="대체-방법으로는">대체 방법으로는?</h2>
<ul>
<li>@DeleteMapping 을 통해 데이터 전달이 필요한 경우 @PathVariable, @RequestParam을 사용하거나</li>
<li>@PutMapping 을 대신 사용하는 것도 방법이 될 수 있다!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.09.12 TIL]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.09.12-TIL</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.09.12-TIL</guid>
            <pubDate>Thu, 12 Sep 2024 12:47:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sh__/post/08dc0773-abe9-4e76-87b0-ae1bbdf23bf1/image.png" alt=""></p>
<p>물류 관리 및 배송 시스템을 MSA 구조로 구현하며 Docker를 사용하여 프로젝트 실행하였다. 이번 포스팅에서는 해당 과정에서 겪은 일들을 회고하여 정리하여 보자.</p>
<hr>
<h1 id="개요">개요</h1>
<p><img src="https://velog.velcdn.com/images/sh__/post/1fc736c6-9e09-4f0b-8fab-dbab9e8d7a05/image.png" alt=""></p>
<ul>
<li><p>물류 관리 및 배송 시스템을 MSA 구조로 구현하였다.</p>
</li>
<li><p>위의 ERD에서 내가 기능 구현을 맡은 테이블은 p_company, p_product, p_order였다. 세 개의 기본 CRUD+Search 기능을 구현하였고, 권한에 따른 요청 관리는 Gateway에서 처리하도록 하고, 권한에 따른 기능 분리는 각 App의 서비스단에서 처리한다.</p>
</li>
<li><p>Swagger를 적용하여 API 명세서 생성을 자동화하였다.</p>
</li>
<li><p>Gemini API와 연동하여 각 App마다 AI와 질문을 주고받고 대화 내역을 DB에 저장하도록 하였다.</p>
</li>
<li><p>Docker를 사용하여 각 Application을 실행하도록 하였다.</p>
</li>
</ul>
<hr>
<h2 id="docker를-사용하며-알게-된-것">Docker를 사용하며 알게 된 것</h2>
<ul>
<li><p>local 환경에서와는 다르게 DB를 사용할 경우 DB 또한 Docker를 사용해 컨테이너를 생성해야 한다.</p>
</li>
<li><p>MSA 구조와 같이 APP이 여러개라면 docker compose를 사용해 여러 APP의 컨테이너화 과정을 단순화하는 편이 훨씬 편하다.</p>
</li>
</ul>
<h3 id="dockerfile-작성">Dockerfile 작성</h3>
<ul>
<li>각 App마다 실행에 별 다른 차이가 없어 모두 같은 Dockerfile을 사용했다.<pre><code class="language-dockerfile"># build 에서 사용할 이미지
FROM gradle:8.10.1-jdk17 AS build
</code></pre>
</li>
</ul>
<p>WORKDIR /app</p>
<p>ARG FILE_DIRECTORY</p>
<p>COPY $FILE_DIRECTORY /app</p>
<p>RUN gradle clean bootJar</p>
<h1 id="실제-컨테이너로-만들-이미지-베이스">실제 컨테이너로 만들 이미지 베이스</h1>
<p>FROM openjdk:17-jdk-slim</p>
<h1 id="build-단계로부터-파일을-가져올-수-있음">build 단계로부터 파일을 가져올 수 있음!</h1>
<h1 id="as-build로-선언해놨기에---frombuild">AS build로 선언해놨기에 --from=build!</h1>
<p>COPY --from=build /app/build/libs/*SNAPSHOT.jar /app.jar</p>
<p>CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</p>
<pre><code>
### docker-compose.yml 작성

- 우선 eureka-server, gateway, product, order, company App만 도커 이미지로 띄워 실행했다.

```yml
services:
  database:
    image: postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: 1234
      POSTGRES_DB: spartahubflow
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U admin -d user&quot;]
      interval: 10s
      retries: 5
      start_period: 30s
      timeout: 10s

  eureka:
    build:
      dockerfile: Dockerfile
      args:
        - FILE_DIRECTORY=./eureka-server
    ports:
      - &quot;8761:8761&quot;

  gateway:
    build:
      dockerfile: Dockerfile
      args:
        - FILE_DIRECTORY=./gateway
    ports:
      - &quot;8080:8080&quot;

  product:
    environment:
      AI_SECRET_KEY: ${AI_SECRET_KEY}
    build:
      dockerfile: Dockerfile
      args:
        - FILE_DIRECTORY=./product
    depends_on:
      database:
        condition: service_healthy
    ports:
      - &quot;5:5&quot;

  order:
    environment:
      AI_SECRET_KEY: ${AI_SECRET_KEY}
    build:
      dockerfile: Dockerfile
      args:
        - FILE_DIRECTORY=./order
    depends_on:
      database:
        condition: service_healthy
    ports:
      - &quot;4:4&quot;

  company:
    environment:
      AI_SECRET_KEY: ${AI_SECRET_KEY}
    build:
      dockerfile: Dockerfile
      args:
        - FILE_DIRECTORY=./company
    depends_on:
      database:
        condition: service_healthy
    ports:
      - &quot;88:88&quot;</code></pre><ul>
<li>위와 같이 environment: 아래에 각 application.yml에서 사용되는 환경 변수 할당해줄 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/475be93e-5e58-443a-8846-39f68c241cbe/image.png" alt=""></p>
<ul>
<li><h3 id="그렇다면-docker-composeyml-파일에서의-환경변수는-어떻게-처리하게-될까">그렇다면 docker-compose.yml 파일에서의 환경변수는 어떻게 처리하게 될까?</h3>
<ul>
<li>application.yml에 넣을 환경 변수를 주입하기 위해 docker-compose.yml 파일에 그대로 값을 넣은 후 github에 업로드 할 수는 없는 노릇이다.</li>
<li>때문에 위와 같이 또 다시 환경변수로 등록을 하고 같은 <strong>디렉토리 내에 .env 파일을 만들어 그 곳에 주입하고 싶은 값을 넣어주면 된다!</strong></li>
<li><code>AI_SECRET_KEY=my_secret_key</code> 와 같은 형식으로 넣어주면 된다.</li>
</ul>
</li>
</ul>
<hr>
<ul>
<li>이후 실행 시 프로젝트들이 잘 실행되는 것을 확인할 수 있다.  </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[에러 - org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ....]]></title>
            <link>https://velog.io/@sh__/%EC%97%90%EB%9F%AC-org.springframework.beans.factory.UnsatisfiedDependencyException-Error-creating-bean-with-name</link>
            <guid>https://velog.io/@sh__/%EC%97%90%EB%9F%AC-org.springframework.beans.factory.UnsatisfiedDependencyException-Error-creating-bean-with-name</guid>
            <pubDate>Sun, 08 Sep 2024 09:28:01 GMT</pubDate>
            <description><![CDATA[<h1 id="상황">상황</h1>
<p>Spring Boot를 통해 애플리케이션을 생성한 후 실행하며 위의 에러가 발생했다. 컨트롤러단, 서비스단, 레포지토리 세 부분에서 모두 같은 오류가 동시에 발생했다.</p>
<h1 id="에러-발생-원인">에러 발생 원인</h1>
<p>구글링 결과 설정파일이나 DB 쿼리문에 문제가 있을 수 있다고 하였다. 설정파일에는 큰 문제가 없어보였고, DB 쿼리를 살펴보았다.</p>
<pre><code class="language-java">@Query(&quot;UPDATE Product p SET p.deletedAt = : deletedAt, p.deletedBy = :deletedBy, p.isDeleted = true &quot; +
            &quot;WHERE p.productId = :productId&quot;)</code></pre>
<p>자세히 보면 <code>p.deletedAt = : deletedAt</code>의 deletedAt 앞에 공백이 하나 있는 것을 확인할 수 있다.</p>
<h1 id="해결-방법">해결 방법</h1>
<p>deletedAt 앞의 공백을 제거하여 해결할 수 있었다.</p>
<pre><code class="language-java">@Query(&quot;UPDATE User u SET u.deletedAt = :deletedAt, u.deletedBy = :deletedBy, u.isDeleted = true &quot; +
            &quot;WHERE u.userId = :userId&quot;)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.09.03 TIL]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.09.03-TIL</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.09.03-TIL</guid>
            <pubDate>Tue, 03 Sep 2024 04:31:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sh__/post/3a7cb5d8-8069-4077-a55e-966a4ab4f5f0/image.png" alt=""></p>
<p>프로젝트를 마치며 발표 준비 중 Spring Data Jpa의 Batch 처리에 대해 궁금한 점이 생겼다.</p>
<hr>
<h1 id="batch-insert란">Batch Insert란?</h1>
<ul>
<li>여러 개의 SQL Statement를 하나의 구문으로 처리할 수 있다.</li>
<li>hibernate 에서 위 기능(jdbc batch 기능)을 이용해 처리하는 것이다.</li>
<li><strong>여러 개의 구문을 여러 번 network를 통해 보내는 것이 아니라 합쳐서 하나로 보내는 것이기에 성능을 향상시킬 수 있다.</strong><ul>
<li>JPA의 경우 트랜잭션이 commit 되는 순간 한꺼번에 flush가 이루어짐</li>
<li>batch_size 옵션이 없다면 단건으로 데이터를 network를 통해 보낼 것이다.</li>
<li>그러나 batch_size 옵션을 설정 시 해당 사이즈만큼 네트워크를 통해 데이터를 보낼 것이다.</li>
</ul>
</li>
</ul>
<h4 id="아래-예시인-3개의-insert-statements를-하나의-preparedstatement로-실행해준다는-것이다">아래 예시인 3개의 insert statements를 하나의 PreparedStatement로 실행해준다는 것이다.</h4>
<pre><code class="language-sql"># 옵션 켜기 전
insert into test (column01, column02) values (&#39;test01&#39;, &#39;test02&#39;);
insert into test (column01, column02) values (&#39;test03&#39;, &#39;test04&#39;);
insert into test (column01, column02) values (&#39;test05&#39;, &#39;test06&#39;);

# 옵션 켠 후
insert into test (column01, column02) 
values (&#39;test01&#39;, &#39;test02&#39;),
       (&#39;test03&#39;, &#39;test04&#39;),
       (&#39;test05&#39;, &#39;test06&#39;) </code></pre>
<h2 id="spring-data-jpa-batch-insert를-사용하는-방법">Spring Data Jpa Batch Insert를 사용하는 방법</h2>
<ul>
<li><p>설정 파일에서 <code>hibernate.jdbc.batch_size</code>을 설정해준다. 최대 몇 개 까지 statements를 batch 처리를 할 것인지에 대한 옵션이다.</p>
</li>
<li><p>즉, 한 번에 데이터베이스로 보낼 최대 구문을 의미한다</p>
</li>
<li><p>application.yml</p>
</li>
</ul>
<pre><code class="language-yml">spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 1000</code></pre>
<h2 id="insert-구문-외에도-사용할-수-있을까">Insert 구문 외에도 사용할 수 있을까?</h2>
<ul>
<li><p>Spring Data JPA의 Batch 기능은 기본적으로 Insert 작업에 주로 사용되지만, Update 및 Delete 작업에도 적용할 수 있다. Batch 기능은 동일한 유형의 SQL 쿼리를 한 번에 여러 개 실행함으로써 성능을 최적화하는 방법이다.</p>
</li>
<li><p>Spring Data JPA의 Batch 기능은 Select 작업에는 적용되지 않는다.</p>
</li>
<li><p>Batch 기능은 주로 데이터베이스에 변경을 가하는 작업(Insert, Update, Delete)에 최적화되어 있으며, 동일한 유형의 SQL 명령을 묶어서 실행하는 방식이다. Select 작업은 데이터를 조회하는 것이므로, Batch 처리와는 개념적으로 맞지 않다.</p>
</li>
</ul>
<h3 id="hibernateorder_updates-hibernateorder_inserts">hibernate.order_updates, hibernate.order_inserts</h3>
<ul>
<li><p>위 batch_size 옵션 외에도 이 2개 옵션은 jdbc batch 기능을 더욱 효과적으로 사용할 수 있도록 도와준다.</p>
</li>
<li><p>위 기능을 먼저 살펴보고 넘어가면 update, insert 문의 실행 순서를 정렬해주는 옵션이다.
예를 들면 아래와 같다.</p>
</li>
<li><p>이렇게 되면 jdbc batch 기능을 통해 일괄 처리를 할 때, 더욱 효율적으로 처리가 가능하다.</p>
</li>
</ul>
<pre><code class="language-sql"># before apply option
update test set column01 = &#39;test3&#39; where regdate = &#39;20210510&#39;;
update test2 set column02 = &#39;test5&#39; where regdate = &#39;20210511&#39;;
update test set column01 = &#39;test4&#39; where regdate = &#39;20210512&#39;;
update test2 set column02 = &#39;test6&#39; where regdate = &#39;20210513&#39;;

# after apply option
update test set column01 = &#39;test3&#39; where regdate = &#39;20210510&#39;;
update test set column01 = &#39;test4&#39; where regdate = &#39;20210512&#39;;
update test2 set column02 = &#39;test5&#39; where regdate = &#39;20210511&#39;;
update test2 set column02 = &#39;test6&#39; where regdate = &#39;20210513&#39;;</code></pre>
<ul>
<li>application.yml</li>
</ul>
<pre><code class="language-yml">spring:
  jpa:
    properties:
      hibernate:
        jdbc:
        order_inserts: true
        order_updates: true</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.08.29 TIL]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.29-TIL</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.29-TIL</guid>
            <pubDate>Thu, 29 Aug 2024 07:56:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sh__/post/51253b91-f21d-401d-a905-f98e92102ab4/image.png" alt=""></p>
<ul>
<li>오늘 진행한 일<ul>
<li>User Role 관련 코드 리팩토링</li>
<li>ai api 에러 로그 작성</li>
<li>Payment, Ai API 도메인 구현 마무리 및 PR 요청</li>
<li>전반적인 API 테스팅</li>
</ul>
</li>
</ul>
<hr>
<h2 id="user-role-관련-코드-리팩토링">User Role 관련 코드 리팩토링</h2>
<p>아래와 같이 UserRole을 Enum으로 구현하였다.</p>
<p><img src="https://velog.velcdn.com/images/sh__/post/ba643cbc-83d5-40de-ba8e-a4389dc864e1/image.png" alt=""></p>
<ul>
<li><p>API 요청을 주고받을 때에는 Stirng 형식(USER, OWNER, ADMIN)으로 요청 및 응답이 되지만 DB 상에서는 Enum 값이 정수형(USER라면 1, OWNER라면 2, ADMIN이라면 3)으로 저장이 된다.</p>
</li>
<li><p>이런 경우에 enum 타입이 변경되거나 UserRole이 추가되면 예기치 못한 문제가 발생할 수 있기 때문에 DB에도 String으로 저장해주는 것이 안전하다.</p>
</li>
</ul>
<h3 id="해결-방법">해결 방법</h3>
<p>아래와 같이 User Entity에 있는 UserRoleEnum role 필드 위에 @Enumerated(EnumType.STRING) 어노테이션을 붙여주면 된다.</p>
<pre><code class="language-java">    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private UserRoleEnum role;</code></pre>
<h2 id="ai-api-에러-로그-작성">Ai API 에러 로그 작성</h2>
<p>Gemini api를 불러와 사용하는 도중 에러가 발생하였다. 에러의 내용과 원인 및 해결 방법은 아래 링크에서 확인할 수 있다.</p>
<p><a href="https://velog.io/@sh__/%EC%97%90%EB%9F%AC-org.springframework.dao.InvalidDataAccessApiUsageException-org.hibernate.query.sqm.UnknownPathException">링크</a></p>
<hr>
<h2 id="전반적인-api-테스팅">전반적인 API 테스팅</h2>
<p>구현된 API의 양이 꽤 많아 reqeust를 하나씩 만들어가며 폴더로 정리하고 테스팅을 진행했다.</p>
<p><img src="https://velog.velcdn.com/images/sh__/post/b5c713eb-5cf8-4448-8584-f9c4e92399ce/image.png" alt=""></p>
<ul>
<li>테스팅을 진행하며 오류 발생 시 로그를 확인하여 원인을 찾고 기록 후 팀원들에게 공유하였다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[에러 - org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.query.sqm.UnknownPathException]]></title>
            <link>https://velog.io/@sh__/%EC%97%90%EB%9F%AC-org.springframework.dao.InvalidDataAccessApiUsageException-org.hibernate.query.sqm.UnknownPathException</link>
            <guid>https://velog.io/@sh__/%EC%97%90%EB%9F%AC-org.springframework.dao.InvalidDataAccessApiUsageException-org.hibernate.query.sqm.UnknownPathException</guid>
            <pubDate>Thu, 29 Aug 2024 01:36:08 GMT</pubDate>
            <description><![CDATA[<h1 id="상황">상황</h1>
<p>API 테스트를 하던 도중 제목과 같은 오류가 발생하였다. 조사 결과 처음에는 쿼리문 작성에 있어 오류가 있는줄 알았다. &#39;userId&#39; 필드를 찾을 수 없다는 오류가 발생했다.
<img src="https://velog.velcdn.com/images/sh__/post/62bd2fd7-4d9e-40f5-aad7-6c7804ca6d06/image.png" alt=""></p>
<pre><code class="language-java">    @Query(&quot;SELECT a FROM AiChat a WHERE a.user.userId = :userId AND a.isDeleted = false&quot;)
    Page&lt;AiChat&gt; findChatForAUser(@Param(&quot;userId&quot;) Long userId, Pageable pageable);</code></pre>
<p>그러나 해당 필드는 외래키를 통해 제대로 참조되고 있었다.</p>
<p>그렇다면 왜 오류가 나게 된걸까?</p>
<hr>
<h1 id="원인-및-해결-방법">원인 및 해결 방법</h1>
<p><img src="https://velog.velcdn.com/images/sh__/post/9b4a80f9-1831-4aa4-abab-646a952fb81c/image.png" alt=""></p>
<p>위와 같이 GET 요청을 보냈을 때 오류가 발생하였다. 정렬 기준(sort)이 userId로 되어있는 것을 확인할 수 있다.</p>
<ul>
<li><p>페이지 객체로 반환될 entity에는 user라는 필드가 다대일로 매핑되어있고, userId는 존재하지 않는다. 이 때문에 오류가 발생한 것이다.</p>
</li>
<li><p><strong>entity에 존재하는 필드 이름</strong>을 넣어 정렬 기준으로 정해주면 응답이 잘 돌아오는 것을 확인할 수 있다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[에러 -org.springframework.dao.InvalidDataAccessApiUsageException: Query executed via 'getResultList()' or 'getSingleResult()' must be a 'select' query]]></title>
            <link>https://velog.io/@sh__/%EC%97%90%EB%9F%AC-org.springframework.dao.InvalidDataAccessApiUsageException-Query-executed-via-getResultList-or-getSingleResult-must-be-a-select-query</link>
            <guid>https://velog.io/@sh__/%EC%97%90%EB%9F%AC-org.springframework.dao.InvalidDataAccessApiUsageException-Query-executed-via-getResultList-or-getSingleResult-must-be-a-select-query</guid>
            <pubDate>Tue, 27 Aug 2024 05:36:45 GMT</pubDate>
            <description><![CDATA[<h1 id="상황">상황</h1>
<p>Spring Data JPA를 사용해 Soft Delete를 구현하던 중 제목과 같은 오류가 발생했다.</p>
<ul>
<li>아래와 같이 @Query를 사용하여 UPDATE문을 사용하였다.</li>
</ul>
<pre><code class="language-java">@Query(&quot;UPDATE User u SET u.deletedAt = :deletedAt, u.deletedBy = :deletedBy, u.isDeleted = true &quot; +
            &quot;WHERE u.userId = :userId&quot;)
    void deleteById(@Param(&quot;userId&quot;) Long userId, @Param(&quot;deletedAt&quot;) LocalDateTime deletedAt,
                    @Param(&quot;deletedBy&quot;) Long deletedBy); // userId : 삭제될 유저, DeleteBy : 삭제하는 유저</code></pre>
<hr>
<h1 id="원인">원인</h1>
<p>로그의 내용과 같이 @Query 어노테이션을 사용할 경우 SELECT문에만 사용할 수 있다. 그렇다면 나머지 C, U, D 연산은 어떻게 해야할까?</p>
<hr>
<h1 id="해결-방법">해결 방법</h1>
<p>아래와 같이 @Modifying 어노테이션을 붙여 수정 쿼리를 사용할 수 있다.</p>
<pre><code class="language-java">@Modifying
@Query(&quot;UPDATE User u SET u.deletedAt = :deletedAt, u.deletedBy = :deletedBy, u.isDeleted = true &quot; +
        &quot;WHERE u.userId = :userId&quot;)
void deleteById(@Param(&quot;userId&quot;) Long userId, @Param(&quot;deletedAt&quot;) LocalDateTime deletedAt,
                @Param(&quot;deletedBy&quot;) Long deletedBy); // userId : 삭제될 유저, DeleteBy : 삭제하는 유저</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Security] 에러 - 로직 수행 이후 403 Forbidden 발생 원인]]></title>
            <link>https://velog.io/@sh__/Spring-Security-%EC%97%90%EB%9F%AC-%EB%A1%9C%EC%A7%81-%EC%88%98%ED%96%89-%EC%9D%B4%ED%9B%84-403-Forbidden-%EB%B0%9C%EC%83%9D-%EC%9B%90%EC%9D%B8</link>
            <guid>https://velog.io/@sh__/Spring-Security-%EC%97%90%EB%9F%AC-%EB%A1%9C%EC%A7%81-%EC%88%98%ED%96%89-%EC%9D%B4%ED%9B%84-403-Forbidden-%EB%B0%9C%EC%83%9D-%EC%9B%90%EC%9D%B8</guid>
            <pubDate>Sat, 24 Aug 2024 10:54:05 GMT</pubDate>
            <description><![CDATA[<h1 id="상황">상황</h1>
<ul>
<li>스프링 시큐리티를 적용하여 필터를 구성하고 Security Config 파일 작성을 마쳤다. Postman을 사용하여 api 테스트를 진행해보았는데, 회원 가입 페이지, 로그인 페이지는 모두 .prtmitAll()을 통해 요청 허가를 했는데도 불구하고 403 Forbidden에러가 난 것이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/329bf87f-39d8-4a56-915c-4fc6b6c75054/image.png" alt=""></p>
<ul>
<li>로그를 살펴보니 회원 가입 로직은 잘 수행되어 DB에 저장된 모습을 볼 수 있었다. 그런데 왜 200 응답이 아닌 403 응답이 오게 된 것일까?</li>
</ul>
<hr>
<h1 id="에러-발생-원인">에러 발생 원인</h1>
<ul>
<li>에러 자체가 발생한 원인은 아래 제작된 컨트롤러 메소드를 보면 리턴값이 Dto인데 @ResponseBody 가 없어서 404 에러가 발생하게 되는 상황이었다.</li>
</ul>
<pre><code class="language-java">    @PostMapping
    public UserDto.Response createUser(@RequestBody UserDto.Create userDto) {

        UserDto.Response createdUser = userService.createUser(userDto);
        log.info(createdUser.toString());

        return createdUser;
    }</code></pre>
<p>?? 그렇다면 왜 404 에러가 나지 않고 403에러가 나서 삽질을 하게 만든 것일까?</p>
<ul>
<li>기본적으로 스프링에서는 따로 예외 처리를 하지 않았다면 예외 발생 시 500 에러가 발생한다. 그런데 <strong>스프링 시큐리티를 적용하면 메소드에서 예외가 발생했을 때 403 에러가 발생</strong>한다. 심지어 존재하지 않는 URL로 접속하여 <strong>404 Not Found가 발생해야 하는 상황에서도 403 Forbidden이 발생</strong>한다.</li>
</ul>
<h2 id="403-에러가-발생하는-원인">403 에러가 발생하는 원인</h2>
<p>우선 이 현상의 원인을 파악하기 위해선 스프링부트에서 에러가 발생했을 때 나타나는 Whitelabel Error Page가 어떻게 나타나는지 알아야한다.
<img src="https://velog.velcdn.com/images/sh__/post/76ebced9-5191-4210-b0ac-103a4e0df3c0/image.png" alt=""></p>
<p>스프링 공식 블로그에 따르면 스프링부트에서는 에러가 발생하면 /error라는 URI로 매핑을 시도한다. 실제로 해당 URI로 이동하면 아래와 같은 페이지가 나타난다.
<img src="https://velog.velcdn.com/images/sh__/post/7b48a5e5-59ab-44e5-8ee0-209e65bfa3f7/image.png" alt=""></p>
<p>따로 에러가 발생하여 자동으로 나타난 것이 아니라 강제로 이동했기 때문에 status가 999로 나타났지만 굉장히 익숙한 Whitelabel Error Page가 나타난다.</p>
<ul>
<li><p>Whitelabel Error Page 자체는 403에러와 관련이 없지만 에러가 발생하면 <code>/error</code>로 매핑을 시도한다는 것이 핵심이다.</p>
</li>
<li><p>일반적으로 permitAll()을 통해 모든 사용자의 접근을 허용할 URI에는 권한 검증이 필요하지 않은 URI만 추가한다. 그리고 위에서 언급했듯이 스프링부트 프로젝트에서는 에러 발생 시 <code>/error</code>로 매핑한다. 그런데 <code>/error</code>는 모두에게 허용된 URI에 포함되지 않는다.</p>
</li>
<li><p>그래서 결과적으로 에러 페이지에도 인증 절차가 요구되어 403에러가 발생하는 것이다.</p>
</li>
</ul>
<hr>
<h1 id="해결-방법">해결 방법</h1>
<p>일단 이 문제는 anyRequest().authenticated()로 인해 /error도 인증이 필요한 것으로 간주되어 발생한 것이기 때문에 모두에게 허용할 URI 목록에 /error를 추가하면  해결할 수 있다.</p>
<pre><code class="language-java">    .requestMatchers(&quot;/error&quot;).permitAll() // 에러 페이지 요청 허가</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.08.23 [17조] S.A]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.23-17%EC%A1%B0-S.A</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.23-17%EC%A1%B0-S.A</guid>
            <pubDate>Fri, 23 Aug 2024 02:37:47 GMT</pubDate>
            <description><![CDATA[<h1 id="목표">목표</h1>
<ul>
<li>요구사항에 맞게 <code>API 명세서</code>, <code>테이블 명세서</code>, <code>ERD 명세서</code>, <code>인프라 설계서</code> 작성</li>
</ul>
<hr>
<h1 id="api-명세서-링크">API 명세서 (<a href="https://www.notion.so/teamsparta/6d8b51af69364971a7f35788d3970279?v=7e57e10d95644d3e8f575aef033e7dc8&amp;p=0eb4efc200074a02b891e69c177c7db4&amp;pm=s">링크</a>)</h1>
<ul>
<li>각 셀을 클릭하면 <code>Request Header</code>, <code>Authorization Token</code>, <code>Query Parameters</code>, <code>Path Parameters</code>, <code>Response</code>, <code>Response Body</code>를 확인할 수 있습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/cd3d0caf-681e-4305-9377-af4f938742ab/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/0d698b19-c2be-4036-a6cb-d9db9bb11db2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/b9485d79-67e4-41b9-a365-9e79a654f927/image.png" alt=""></p>
<hr>
<h1 id="테이블-명세서-링크">테이블 명세서 (<a href="https://www.notion.so/teamsparta/76277df16a8948858cda9810e50045fd">링크</a>)</h1>
<p><img src="https://velog.velcdn.com/images/sh__/post/02b98aa4-06ef-4bc9-808c-b569ab53dfec/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/513bcce7-ac65-4945-84ba-21bc532af120/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/11e4011b-6be6-468c-a1ef-b821b3798758/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/d2d62da8-7420-44ab-aeed-866da078679b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/84136ff4-c7bc-4a41-a121-834ee1f664fb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/c96122e6-99b7-4f30-ba66-587d86ad95c6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/0f4e12bc-cd42-4d7b-99d2-9e7118dc81ff/image.png" alt=""></p>
<hr>
<h1 id="erd-명세서-링크">ERD 명세서 (<a href="https://drive.google.com/file/d/1J4Es3QT-9Xe_boIrb59_ScqI_awh5Aqy/view">링크</a>)</h1>
<p><img src="https://velog.velcdn.com/images/sh__/post/ebf2fca7-5460-4626-97ac-1a6a22e931b2/image.png" alt=""></p>
<hr>
<h1 id="인프라-설계서링크">인프라 설계서(<a href="https://drive.google.com/file/d/1dAIhgIS1hWYoEpxRT6Fx4h9njYv_mbDy/view">링크</a>)</h1>
<p><img src="https://velog.velcdn.com/images/sh__/post/080e2930-41fb-404d-85c8-a8efe6b89ff7/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 문자열 내림차순으로 배치하기(JAVA)]]></title>
            <link>https://velog.io/@sh__/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AC%B8%EC%9E%90%EC%97%B4-%EB%82%B4%EB%A6%BC%EC%B0%A8%EC%88%9C%EC%9C%BC%EB%A1%9C-%EB%B0%B0%EC%B9%98%ED%95%98%EA%B8%B0JAVA</link>
            <guid>https://velog.io/@sh__/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AC%B8%EC%9E%90%EC%97%B4-%EB%82%B4%EB%A6%BC%EC%B0%A8%EC%88%9C%EC%9C%BC%EB%A1%9C-%EB%B0%B0%EC%B9%98%ED%95%98%EA%B8%B0JAVA</guid>
            <pubDate>Thu, 22 Aug 2024 02:16:17 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sh__/post/642bcb7d-c0db-484b-8f1a-541aad595e5c/image.png" alt=""></p>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/12917">문제 링크</a></p>
<hr>
<h1 id="풀이">풀이</h1>
<p>정말 간단하게 문자열을 내림차순으로 정렬하면 끝나는 문제이다. C++ 에서는 algorithm 라이브러리의 sort() 함수를 쓰면 됐었지만 java는 익숙치 않아 조금 헤맸던 것 같다.</p>
<pre><code class="language-java">import java.util.*;

class Solution {
    public String solution(String s) {
        String answer = &quot;&quot;;

        char[] chars = s.toCharArray();
        Arrays.sort(chars);

        for(int i=chars.length - 1; i&gt;=0; i--)
            answer += String.valueOf(chars[i]);

        return answer;
    }
}</code></pre>
<p>위의 코드를 보면 sort를 진행하고, 반복문을 다시 돌려 내림차순으로 문자열을 정렬한다. 비효율적인 방법이라고 생각해 <code>Arrays.sort(chars, Collections.reverseOrder());</code> 를 사용하려 했으나 char[]에서 char는 클래스 타입이 아닌 원시 타입이라 Collentions.reversOrder()를 사용하지 못한다.</p>
<p>그래서 생각해 낸 코드가 위의 코드. 그러나 여러 블로그들을 돌아 다니며 코드를 찾아 본 결과 아래와 같은 코드를 발견했다.</p>
<pre><code class="language-java">  char[] c = s.toCharArray();
  Arrays.sort(c);
  String str = new String(c);
  String answer = new StringBuilder(str).reverse().toString();</code></pre>
<hr>
<h1 id="결론">결론</h1>
<p>원시 타입의 문자열을 처음부터 내림차순으로 정렬하긴 힘들지만, <strong>StringBuilder</strong>를 사용하여 문자열을 뒤집으면 반복문을 사용하지 않아도 된다! 위의 코드를 적용하여 조금 더 깔끔하게 문제를 풀 수 있는 것으로 보인다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.08.21 TIL]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.21-TIL</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.21-TIL</guid>
            <pubDate>Wed, 21 Aug 2024 05:37:03 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sh__/post/4eeed9e4-df39-49f2-a683-cb58450c5d70/image.png" alt=""></p>
<ul>
<li>지난 시간에 이어 낙관적 락, 데드 락 실습을 진행해보았다.</li>
<li>DB 복제 지연, 메모리 릭, 캐시 압력(Cache Pressure), 설정 버전 관리(Configuration Versioning)에 대한 이론적인 부분들을 수강하였다.</li>
</ul>
<hr>
<h1 id="낙관적-락-실습">낙관적 락 실습</h1>
<h2 id="낙관적-락의-동작방식">낙관적 락의 동작방식</h2>
<ul>
<li><p>낙관적 락(Optimistic Lock)은 트랜잭션 간의 충돌을 최소화하고 성능을 향상시키기 위해 사용되는 동시성 제어 메커니즘이다.</p>
</li>
<li><p>비관적 락이 데이터베이스 레벨에서 락을 걸어 다른 트랜잭션의 접근을 차단하는 방식이라면, 낙관적 락은 데이터베이스 락을 사용하지 않고, 대신 데이터가 변경되었는지 확인하여 충돌을 처리하는 방식이다.</p>
</li>
<li><p><strong>버전 관리</strong>:</p>
<ul>
<li>낙관적 락에서는 보통 version이라는 필드를 엔티티에 추가한다. 이 필드는 해당 엔티티의 수정 횟수를 추적하는 역할을 한다.</li>
<li>트랜잭션이 엔티티를 읽을 때, 현재의 버전 번호가 함께 읽혀온다.</li>
<li>트랜잭션이 엔티티를 수정하고 저장하려고 할 때, 현재 데이터베이스에 저장된 버전 번호와 트랜잭션이 처음 읽어온 버전 번호를 비교한다.</li>
</ul>
</li>
<li><p><strong>데이터 충돌 검출</strong>:</p>
<ul>
<li>트랜잭션이 데이터를 저장할 때, 데이터베이스에 저장된 버전 번호가 트랜잭션이 처음 읽어온 버전 번호와 동일하다면, 데이터가 수정되지 않았다고 간주하고 업데이트를 수행한다. 이때 버전 번호는 증가한다.</li>
<li>반면, 버전 번호가 다르면, 다른 트랜잭션이 데이터를 수정한 것으로 간주하고, 현재 트랜잭션을 롤백하거나 재시도하도록 한다.</li>
</ul>
</li>
</ul>
<h2 id="실습">실습</h2>
<ul>
<li>Product.java<pre><code class="language-java">import jakarta.persistence.*;
import lombok.Data;
</code></pre>
</li>
</ul>
<p>@Data
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;</p>
<pre><code>private String name;
private Double price;

@Version
private Integer version;  // 버전 필드를 통해 낙관적 락을 구현</code></pre><p>}</p>
<pre><code>- ProductRepository.java

```java
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository&lt;Product, Long&gt; {
}</code></pre><ul>
<li>ProductService.java</li>
</ul>
<pre><code class="language-java">import lombok.RequiredArgsConstructor;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;

    @Transactional
    public void updateProductPrice(Long productId, Double newPrice) {
        try {
            // 기존 데이터를 읽어옵니다.
            Product product = productRepository.findById(productId)
                    .orElseThrow(() -&gt; new RuntimeException(&quot;Product not found&quot;));

            // 가격을 수정합니다.
            product.setPrice(newPrice);

            // 저장 시 버전 충돌이 발생하면 예외가 발생합니다.
            productRepository.save(product);
        } catch (ObjectOptimisticLockingFailureException e) {
            // 낙관적 락 예외 처리
            System.err.println(&quot;낙관적 락 충돌이 발생했습니다. 다른 트랜잭션이 먼저 데이터를 수정했습니다.&quot;);
            throw e;
        }
    }
}

</code></pre>
<ul>
<li>ProductServiceTest.java</li>
</ul>
<pre><code class="language-java">import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.orm.ObjectOptimisticLockingFailureException;

import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
public class ProductServiceTest {
    @Autowired
    private ProductService productService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    public void testOptimisticLocking() throws InterruptedException {
        // 초기 데이터 설정
        Product product = new Product();
        product.setName(&quot;Product 1&quot;);
        product.setPrice(100.0);
        productRepository.save(product);

        // 첫 번째 트랜잭션: 상품 가격을 200.0으로 업데이트
        Thread thread1 = new Thread(() -&gt; {
            productService.updateProductPrice(product.getId(), 200.0);
        });

        // 두 번째 트랜잭션: 상품 가격을 300.0으로 업데이트
        Thread thread2 = new Thread(() -&gt; {
            assertThrows(ObjectOptimisticLockingFailureException.class, () -&gt; {
                productService.updateProductPrice(product.getId(), 300.0);
            });
        });

        // 두 스레드를 동시에 실행
        thread1.start();
        thread2.start();

        // 두 스레드가 종료될 때까지 대기
        thread1.join();
        thread2.join();
    }
}

</code></pre>
<h2 id="실행-결과">실행 결과</h2>
<ul>
<li>@Version 어노테이션을 통해 아래와 같이 version 컬럼이 생성된 것을 확인할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/310cf34f-981d-41c5-8845-8756696a13d7/image.png" alt=""></p>
<hr>
<h1 id="데드-락">데드 락</h1>
<ul>
<li>데이터베이스 환경에서 데드락은 두 개 이상의 트랜잭션이 서로가 점유하고 있는 자원을 기다리면서 영원히 대기 상태에 빠지는 상황을 의미한다.</li>
<li>이 상황이 발생하면 해당 트랜잭션들은 더 이상 진행될 수 없고, 시스템 성능에 큰 영향을 미칠 수 있다.</li>
</ul>
<h2 id="예시">예시</h2>
<ul>
<li><strong>트랜잭션 A</strong>는 테이블 X의 일부 행을 잠금(Lock)하고, 이후 테이블 Y의 행을 잠금하려고 한다.</li>
<li><strong>B</strong>는 테이블 Y의 일부 행을 잠금하고, 이후 테이블 X의 행을 잠금하려고 한다.</li>
<li>이 경우, 트랜잭션 A와 트랜잭션 B는 서로 상대방이 보유한 잠금을 기다리면서 영원히 대기하게 되며, 이로 인해 데드락이 발생한다.</li>
</ul>
<hr>
<h1 id="db-복제-지연">DB 복제 지연</h1>
<h3 id="복제-지연이란">복제 지연이란?</h3>
<ul>
<li><p>데이터가 쓰기 DB에서 읽기 DB로 복제되는 과정에서 발생하는 시간 지연을 의미</p>
</li>
<li><p>복제 지연으로 인해, 읽기 DB에서 최신 상태의 데이터를 읽지 못하고 이전 상태의 데이터를 읽게 되는 문제가 발생할 수 있음</p>
</li>
<li><p>시스템에서는 일반적으로 다음과 같은 방식으로 데이터베이스를 운영한다</p>
<ul>
<li><strong>쓰기 DB</strong>: 모든 쓰기 작업(데이터 삽입, 업데이트, 삭제 등)을 처리하는 데이터베이스입니다. 보통 마스터(master) 데이터베이스라고도 한다.</li>
<li><strong>읽기 DB</strong>: 읽기 작업만 처리하는 데이터베이스로, 보통 슬레이브(slave) 데이터베이스 또는 리플리카(replica)라고 한다. 이 데이터베이스는 쓰기 DB에서 복제(replication)된 데이터를 사용한다.</li>
</ul>
</li>
</ul>
<h3 id="문제-발생-예시">문제 발생 예시</h3>
<ul>
<li><p>쓰기와 읽기 DB가 분리된 시스템에서는 쓰기 작업이 일어난 후, 그 변경 내용이 읽기 DB로 복제되기까지 시간이 걸린다. 이 지연으로 인해 다음과 같은 상황이 발생할 수 있다.</p>
<ol>
<li><strong>쓰기 작업</strong>: 애플리케이션이 쓰기 DB에 새로운 데이터를 저장한다.</li>
<li><strong>지연</strong>: 이 데이터는 복제 지연 때문에 즉시 읽기 DB에 반영되지 않는다.</li>
<li><strong>읽기 작업</strong>: 쓰기 작업 직후에 애플리케이션이 읽기 DB에서 데이터를 조회하려고 한다.</li>
<li><strong>이전 데이터 조회</strong>: 이 시점에서 읽기 DB는 아직 새로운 데이터를 반영하지 않았기 때문에, 이전 데이터를 반환할 수 있다.</li>
</ol>
</li>
</ul>
<h3 id="해결-방법">해결 방법</h3>
<ul>
<li><strong>지연 적용 알고리즘 (Lag Compensating Logic)</strong><ul>
<li>쓰기 후 즉시 읽기를 시도하는 경우, 잠시 지연을 두고 읽기를 재시도하는 방법</li>
</ul>
</li>
</ul>
<ul>
<li><p><strong>쓰기 DB로의 직접 읽기 (Read-after-Write)</strong></p>
<ul>
<li>중요한 데이터에 대해 쓰기 직후 즉시 읽어야 하는 경우, 해당 읽기 작업을 쓰기 DB에서 직접 수행하도록 하는 방법</li>
</ul>
</li>
<li><p><strong>읽기 DB와 쓰기 DB 간의 복제 지연 최소화</strong></p>
<ul>
<li>복제 지연을 최소화하도록 데이터베이스 설정을 조정할 수 있다. 예를 들어, MySQL의 경우 복제 지연을 줄이기 위해 semi-synchronous replication을 사용하거나, 다른 복제 설정을 최적화할 수 있다.</li>
</ul>
</li>
<li><p><strong>CQRS 패턴</strong></p>
<ul>
<li>Command Query Responsibility Segregation (CQRS) 패턴을 적용하여, 쓰기 작업과 읽기 작업을 명확히 분리하고, 읽기 작업에 대해 일관성을 보장하는 별도의 메커니즘을 적용할 수 있다.</li>
<li>예를 들어, 쓰기 DB의 변경이 읽기 DB에 완전히 반영된 후에야 읽기 작업을 허용하는 방식을 도입할 수 있다.</li>
</ul>
</li>
<li><p><strong>캐시 사용</strong></p>
<ul>
<li>캐시 시스템을 도입하여, 쓰기 작업 후 바로 캐시를 갱신하고, 그 이후의 읽기 작업은 캐시에서 제공하는 방식을 사용한다. 캐시는 일관성을 보장하기 위해 일정 시간 동안(예: 1초) 쓰기 DB의 데이터를 캐시하는 방법을 사용할 수 있다.</li>
</ul>
</li>
</ul>
<hr>
<h1 id="메모리-릭">메모리 릭</h1>
<h3 id="메모리-릭이란">메모리 릭이란?</h3>
<ul>
<li><p>메모리 릭(Memory Leak)은 프로세스가 더 이상 필요하지 않은 메모리를 할당한 후 이를 해제하지 않음으로써, 해당 메모리가 지속적으로 점유된 상태로 남아있는 현상을 말한다.</p>
</li>
<li><p>Spring에서의 예시를 들어 보면, @Autowired 주입된 객체의 잘못된 관리, 스프링 빈의 라이프사이클 관리 문제, ThreadLocal의 잘못된 사용, 이벤트 리스너의 잘못된 관리 등이 있다.</p>
</li>
<li><p>이러한 메모리 릭은 메모리 사용량을 지속적으로 증가시켜, 결국에는 시스템의 메모리를 고갈시켜 성능 저하 또는 애플리케이션의 충돌을 유발할 수 있다.</p>
</li>
<li><p>메모리를 점유하는 객체의 경우 반드시 제거(해제)하여주도록 하자!</p>
</li>
</ul>
<hr>
<h1 id="캐시-압력cache-pressure">캐시 압력(Cache Pressure)</h1>
<h3 id="캐시-압력이란">캐시 압력이란?</h3>
<ul>
<li>캐시 압력은 캐시 메모리가 부족해지면서 발생하는 문제를 말한다. 캐시가 가득 차서 더 이상 새로운 데이터를 저장할 수 없고, 이로 인해 기존 데이터를 삭제해야 하는 상황이 발생할 수 있다.</li>
<li>캐시 압력이 증가하면, 캐시 항목을 삭제하거나 새 데이터를 캐시에 저장하는 동안 대기 상태가 발생할 수 있다.</li>
</ul>
<h3 id="해결-방법-1">해결 방법</h3>
<ol>
<li><strong>캐시 제한 설정</strong>:<ul>
<li><strong>TTL(Time-To-Live)</strong>: 캐시 항목에 TTL을 설정하여 일정 시간 후에 자동으로 만료되도록 설정할 수 있다. 이를 통해 오래된 캐시 데이터를 자동으로 제거하고, 새로운 항목을 저장할 공간을 확보할 수 있다.</li>
<li><strong>최대 캐시 크기 설정</strong>: 사용자별 페이징 데이터에 대해 캐시의 최대 크기를 설정하여, 용량이 초과되면 오래된 데이터를 자동으로 제거하도록 할 수 있다.</li>
</ul>
</li>
<li><strong>페이징 전략 최적화</strong>:<ul>
<li><strong>부분적 캐싱</strong>: 모든 페이지를 캐싱하기보다는 자주 요청되는 특정 페이지만을 캐싱하여, 캐시 공간을 효율적으로 사용할 수 있다.</li>
<li><strong>데이터베이스 인덱스 최적화</strong>: 캐시를 사용하는 대신, 데이터베이스 인덱스를 최적화하여 페이징 성능을 향상시킬 수 있다. 이렇게 하면 캐시 의존도를 줄이고, 데이터베이스에서 더 빠르게 결과를 가져올 수 있다.</li>
</ul>
</li>
<li><strong>분산 캐시 사용</strong>:<ul>
<li>Redis와 같은 분산 캐시 시스템을 사용하여, 여러 서버에 캐시 데이터를 분산시킬 수 있다. 이를 통해 한 서버의 메모리 용량 한계를 극복하고, 캐시 성능을 높일 수 있다.</li>
</ul>
</li>
<li><strong>캐시 우회 전략</strong>:<ul>
<li>특정 조건에서 캐시를 우회하고 직접 데이터베이스에서 데이터를 가져오는 전략을 사용할 수 있다. 예를 들어, 페이징 요청이 매우 세분화된 경우 캐시를 우회하도록 설정할 수 있다.</li>
</ul>
</li>
</ol>
<hr>
<h1 id="설정-버전-관리-configuration-versioning">설정 버전 관리 (Configuration Versioning)</h1>
<h3 id="설정-버전-관리란">설정 버전 관리란?</h3>
<ul>
<li>애플리케이션의 설정 데이터를 시간이나 버전별로 관리하는 방법을 의미한다. 최신 설정 데이터를 항상 사용할 수 있도록 각 설정에 버전(또는 날짜)을 부여하고, 이를 기반으로 최신 설정을 조회하고 적용하는 방식이다.</li>
</ul>
<h3 id="설정-버전-관리의-장점">설정 버전 관리의 장점</h3>
<ul>
<li><strong>빠른 롤백</strong><ul>
<li>설정 버전 관리를 통해 이전 버전의 설정 데이터를 보관하면, 장애가 발생했을 때 문제가 있는 최신 설정을 신속하게 이전 버전으로 롤백할 수 있다. 이는 서비스의 가동 중단 시간을 최소화하고, 문제를 빠르게 해결하는 데 큰 도움이 된다.</li>
</ul>
</li>
<li><strong>변경 이력 추적</strong><ul>
<li>각 설정이 버전별로 관리되기 때문에, 어떤 설정이 언제 변경되었는지 쉽게 추적할 수 있다. 이는 문제의 원인을 분석할 때 유용하며, 특정 설정 변경이 문제를 유발했는지 확인할 수 있다.</li>
</ul>
</li>
<li><strong>테스트와 배포 용이성</strong><ul>
<li>새로운 설정을 적용하기 전에 테스트 환경에서 해당 설정을 미리 적용해보고, 문제가 없다고 판단되면 실제 운영 환경에 적용할 수 있다. 이를 통해 설정 변경으로 인한 장애 발생 가능성을 줄일 수 있다.</li>
</ul>
</li>
<li><strong>신속한 복구</strong><ul>
<li>장애 발생 시 최신 설정을 빠르게 적용하거나, 문제가 된 설정을 즉시 변경할 수 있다. 또한, 설정의 버전 관리 덕분에 복구 작업이 체계적으로 이루어질 수 있다.</li>
</ul>
</li>
<li><strong>일관된 설정 관리</strong><ul>
<li>설정 데이터를 체계적으로 관리함으로써, 장애 발생 시에도 일관된 설정을 유지할 수 있다. 이로 인해 설정 오류로 인한 장애를 예방할 수 있다.</li>
</ul>
</li>
<li><strong>가시성 향상</strong><ul>
<li>설정 변경 사항이 명확하게 기록되고 관리되므로, 모든 팀원이 설정 상태를 명확히 파악할 수 있다. 이는 장애 대응을 위한 의사소통과 협업을 개선한다.</li>
</ul>
</li>
</ul>
<h3 id="설정-버전-관리의-단점">설정 버전 관리의 단점</h3>
<ul>
<li><strong>복잡성 증가</strong>: 여러 버전의 설정을 관리하는 것이 복잡성을 초래할 수 있다. 특히 버전 수가 많아질수록 관리가 어려워질 수 있다.</li>
<li><strong>저장 공간 부담</strong>: 여러 버전의 설정 데이터를 저장하는 데 필요한 저장 공간이 늘어나며, 시간이 지남에 따라 상당한 용량을 차지할 수 있다.</li>
<li><strong>인적 오류 가능성</strong>: 잘못된 버전을 관리하거나 롤백할 때 실수가 발생할 수 있어 복구 과정을 복잡하게 만들 수 있다.</li>
</ul>
<h3 id="예시-1">예시</h3>
<ul>
<li>설정 엔티티 생성</li>
</ul>
<pre><code class="language-java">    @Entity
    public class CoreSetting {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;

        @Column(columnDefinition = &quot;jsonb&quot;)
        private String configData;

        private LocalDateTime version;

        @CreationTimestamp
        private LocalDateTime createdAt;

        // Getters and Setters</code></pre>
<ul>
<li>데이터 저장 예시</li>
</ul>
<pre><code class="language-java">    CoreSetting coreSetting = new CoreSetting();
    coreSetting.setSettings(&quot;{\&quot;theme\&quot;: \&quot;dark\&quot;, \&quot;notifications\&quot;: \&quot;enabled\&quot;}&quot;);
    coreSetting.setVersion(LocalDateTime.now());
    coreSettingRepository.save(coreSetting);</code></pre>
<ul>
<li>리포지토리 예시</li>
</ul>
<pre><code class="language-java">    public interface CoreSettingRepository extends JpaRepository&lt;CoreSetting, Long&gt; {

        @Query(value = &quot;SELECT * FROM CoreSetting ORDER BY version DESC LIMIT 1&quot;, nativeQuery = true)
        Optional&lt;CoreSetting&gt; findLatestCoreSetting();
    }</code></pre>
<ul>
<li>사용</li>
</ul>
<pre><code class="language-java">    Optional&lt;CoreSetting&gt; latestCoreSetting = coreSettingRepository.findLatestCoreSetting();
    latestCoreSetting.ifPresent(setting -&gt; {
        // 최신 설정 데이터 사용
    });</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.08.20 TIL]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.20-TIL</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.20-TIL</guid>
            <pubDate>Tue, 20 Aug 2024 06:42:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sh__/post/e2c63f0b-f869-4e34-b435-c7073efaef7c/image.png" alt=""></p>
<ul>
<li>어제에 이어 장애 분석 및 진단, 장애 복구, 후속 조치 및 사후평가 개선, 예방 조치 등 장애 대응에 대한 이론들을 수강하였다.</li>
<li>DB Lock의 개념에 대해 수강하고 비관적 락 실습을 진행하였다. 오늘은 이 부분을 중심으로 TIL을 작성해보려 한다.</li>
</ul>
<hr>
<h1 id="db-lock">DB Lock</h1>
<h2 id="db-lock이란">DB Lock이란?</h2>
<ul>
<li>DB 락(Database Lock)은 데이터베이스에서 여러 트랜잭션이 동시에 같은 데이터에 접근할 때, 데이터의 무결성(일관성)을 보장하기 위해 사용되는 메커니즘이다.</li>
<li>쉽게 말해, 한 트랜잭션이 특정 데이터에 대해 작업을 하고 있을 때 다른 트랜잭션이 그 데이터에 접근하지 못하도록 잠그는 것이다.</li>
<li>이로써 데이터의 일관성을 유지하고, 동시에 발생할 수 있는 충돌을 방지할 수 있다.</li>
</ul>
<h2 id="db-lock의-필요성">DB Lock의 필요성</h2>
<ul>
<li>데이터베이스는 여러 사용자나 시스템이 동시에 데이터를 읽고 쓰는 환경에서 운영된다. 이러한 환경에서 문제가 발생할 수 있는 대표적인 사례는 다음과 같다.<ul>
<li>Dirty Read (더티 리드): 한 트랜잭션이 데이터를 수정 중일 때 다른 트랜잭션이 그 데이터를 읽는 상황. 만약 첫 번째 트랜잭션이 롤백된다면, 두 번째 트랜잭션은 잘못된 데이터를 읽은 것이 된다.</li>
<li>Non-repeatable Read (반복 불가능한 읽기): 한 트랜잭션이 데이터를 읽은 후, 다른 트랜잭션이 그 데이터를 수정하고 커밋하여 첫 번째 트랜잭션이 동일한 데이터를 다시 읽을 때 값이 달라지는 상황</li>
<li>Lost Update (업데이트 손실): 두 개의 트랜잭션이 동시에 같은 데이터를 수정하려고 할 때, 한 트랜잭션의 수정 내용이 다른 트랜잭션에 의해 덮어쓰여져 사라지는 상황</li>
</ul>
</li>
</ul>
<ul>
<li>DB 락을 통해 데이터에 대한 접근을 제어하면, 위와 같은 상황에서 발생할 수 있는 데이터 무결성 문제를 예방할 수 있다!</li>
</ul>
<h2 id="db-락의-종류">DB 락의 종류</h2>
<ul>
<li><p><strong>공유 락 (Shared Lock, S Lock)</strong></p>
<ul>
<li>공유 락은 데이터베이스에서 데이터를 읽을 때 사용된다. 여러 트랜잭션이 동시에 같은 데이터를 읽을 수 있지만, 공유 락이 걸린 동안에는 데이터를 수정할 수 없다.</li>
</ul>
</li>
<li><p><strong>배타 락 (Exclusive Lock, X Lock)</strong></p>
<ul>
<li>배타 락은 데이터를 수정할 때 사용된다. 배타 락이 걸린 데이터는 다른 트랜잭션이 읽거나 수정할 수 없다. 한 트랜잭션이 배타 락을 획득하면 다른 모든 트랜잭션은 해당 데이터에 접근할 수 없다.</li>
</ul>
</li>
<li><p><strong>비관적 락 (Pessimistic Locking)</strong></p>
<ul>
<li>비관적 락은 데이터를 읽을 때부터 락을 걸어 다른 트랜잭션이 접근하지 못하도록 하는 방식이다. 데이터의 충돌 가능성이 높을 때 유용하다.</li>
</ul>
</li>
<li><p><strong>낙관적 락 (Optimistic Locking)</strong></p>
<ul>
<li>낙관적 락은 데이터를 수정하기 전까지 락을 걸지 않고, 수정 시점에만 충돌을 확인하는 방식이다. 주로 데이터의 버전 번호를 사용하여 동시성 문제를 해결한다.</li>
</ul>
</li>
<li><p><strong>명명된 락 (Named Lock)</strong></p>
<ul>
<li><strong>명명된 락</strong>은 데이터베이스에서 특정 이름으로 락을 설정하여, 동시에 하나의 프로세스만 특정 리소스에 접근하도록 하는 방식이다. 주로 특정 리소스나 작업에 대한 접근을 직관적으로 제어하기 위해 사용된다.</li>
</ul>
</li>
<li><p><strong>분산 락 (Distributed Lock)</strong></p>
<ul>
<li>분산 락은 여러 시스템이나 인스턴스에서 동시에 동일한 자원에 접근할 때, 자원의 일관성을 유지하기 위해 사용되는 락이다. Redis와 같은 분산 시스템을 사용하여 구현된다.</li>
</ul>
</li>
</ul>
<hr>
<h1 id="비관적-락-실습">비관적 락 실습</h1>
<ul>
<li><a href="http://start.spring.io">start.spring.io</a> 에서 프로젝트를 생성합니다. 디펜던시는 아래와 같이 구성한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/513f20d8-ac1c-44f2-b287-43bc081d5bdd/image.png" alt=""></p>
<ul>
<li>sql 로깅을 위해 아래와 같이 application.propertise를 구성한다.</li>
</ul>
<pre><code class="language-yaml">spring.application.name=locking

spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comment=true

logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.orm.jdbc.bind=TRACE</code></pre>
<ul>
<li><p><strong>비관적 락의 동작 방식</strong></p>
<ul>
<li><strong>락의 개념</strong>: 비관적 락은 데이터에 대한 접근을 제어하기 위해 사용된다. 데이터베이스에서 특정 행(row)이나 테이블에 대해 락을 걸어, 다른 트랜잭션이 동시에 동일한 데이터에 접근하거나 수정하지 못하도록 한다.</li>
<li><strong>락의 종류</strong>:<ul>
<li><strong>PESSIMISTIC_READ</strong>: 읽기 락(Shared Lock)을 설정하여 다른 트랜잭션이 해당 데이터를 읽을 수는 있지만, 수정은 할 수 없도록 한다.</li>
<li><strong>PESSIMISTIC_WRITE</strong>: 쓰기 락(Exclusive Lock)을 설정하여 다른 트랜잭션이 해당 데이터를 읽거나 수정하지 못하도록 한다.</li>
</ul>
</li>
<li><strong>DB 레벨에서의 락 동작</strong>:<ul>
<li><strong>락 설정</strong>: 비관적 락을 사용하면 SQL 쿼리나 트랜잭션이 데이터베이스에 접근할 때 락이 설정된다. 예를 들어, PESSIMISTIC_WRITE 락을 설정하면, 해당 데이터에 대한 모든 읽기 및 쓰기 작업이 락이 해제될 때까지 대기하게 된다.</li>
<li><strong>락 해제</strong>: 락은 일반적으로 트랜잭션이 종료되거나 커밋될 때 해제된다. 트랜잭션이 커밋되면 락이 해제되어 다른 트랜잭션이 해당 데이터에 접근할 수 있게 된다. 트랜잭션이 롤백되는 경우에도 락이 해제된다.</li>
</ul>
</li>
</ul>
</li>
<li><p>비관적 락은 주로 데이터베이스 레벨에서 동작하며, 데이터의 무결성을 보장하는 데 매우 유용하다.</p>
</li>
<li><p>그러나 성능에 영향을 미칠 수 있으므로, 데이터 충돌 가능성이 높은 환경에서 신중하게 사용해야 한다.</p>
</li>
<li><p>스프링 부트와 같은 애플리케이션에서 비관적 락을 설정하면, 데이터베이스가 이 락을 처리하고 관리하게 된다.</p>
</li>
<li><p>실습</p>
<ul>
<li><p>Item.java</p>
<pre><code class="language-java">  import jakarta.persistence.Entity;
  import jakarta.persistence.GeneratedValue;
  import jakarta.persistence.GenerationType;
  import jakarta.persistence.Id;
  import lombok.Data;

  @Data
  @Entity
  public class Item {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;

      private String name;
      private Integer quantity;
  }</code></pre>
</li>
<li><p>ItemRepository.java</p>
<pre><code class="language-java">  import jakarta.persistence.LockModeType;
  import org.springframework.data.jpa.repository.JpaRepository;
  import org.springframework.data.jpa.repository.Lock;
  import org.springframework.data.jpa.repository.Query;

  public interface ItemRepository extends JpaRepository&lt;Item, Long&gt; {
      @Lock(LockModeType.PESSIMISTIC_WRITE)  // 비관적 락 적용
      @Query(&quot;select i from Item i where i.id = :id&quot;)
      Item findByIdWithLock(Long id);
  }
</code></pre>
</li>
<li><p>ItemService.java</p>
<pre><code class="language-java">  import lombok.RequiredArgsConstructor;
  import org.springframework.stereotype.Service;
  import org.springframework.transaction.annotation.Transactional;

  @Service
  @RequiredArgsConstructor
  public class ItemService {

      private final ItemRepository itemRepository;

      @Transactional
      public void updateItemQuantity(Long itemId, Integer newQuantity) {
          // 비관적 락을 사용하여 데이터를 조회합니다.
          Item item = itemRepository.findByIdWithLock(itemId);

          // 재고 수량을 수정합니다.
          item.setQuantity(newQuantity);

          // 수정된 데이터를 저장합니다.
          itemRepository.save(item);
      }

      @Transactional
      public Item findItemById(Long itemId) {
          // 비관적 락 없이 데이터를 조회합니다.
          return itemRepository.findById(itemId).orElse(null);
      }
  }
</code></pre>
</li>
<li><p>ItemServiceTest.java</p>
<pre><code class="language-java">  import org.junit.jupiter.api.Test;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.boot.test.context.SpringBootTest;

  @SpringBootTest
  public class ItemServiceTest {

      private static final Logger logger = LoggerFactory.getLogger(ItemServiceTest.class);

      @Autowired
      private ItemService itemService;

      @Autowired
      private ItemRepository itemRepository;

      @Test
      public void testPessimisticLocking() throws InterruptedException {
          // 초기 데이터 설정
          logger.info(&quot;초기 아이템 데이터를 설정합니다.&quot;);
          Item item = new Item();
          item.setName(&quot;Item 1&quot;);
          item.setQuantity(10);
          itemRepository.save(item);

          // 첫 번째 트랜잭션: 아이템 수량을 20으로 업데이트
          Thread thread1 = new Thread(() -&gt; {
              logger.info(&quot;스레드 1: 아이템 수량 업데이트를 시도합니다.&quot;);
              itemService.updateItemQuantity(item.getId(), 20);
              logger.info(&quot;스레드 1: 아이템 수량 업데이트 완료.&quot;);
          });

          // 두 번째 트랜잭션: 아이템 수량을 30으로 업데이트
          Thread thread2 = new Thread(() -&gt; {
              logger.info(&quot;스레드 2: 아이템 수량 업데이트를 시도합니다.&quot;);
              itemService.updateItemQuantity(item.getId(), 30);
              logger.info(&quot;스레드 2: 아이템 수량 업데이트 완료.&quot;);
          });

          // 두 스레드를 동시에 실행
          thread2.start();
          thread1.start();

          // 두 스레드가 종료될 때까지 대기
          thread1.join();
          thread2.join();

          // 최종 결과를 확인합니다.
          Item updatedItem = itemService.findItemById(item.getId());
          logger.info(&quot;최종 아이템 수량: {}&quot;, updatedItem.getQuantity());
      }
  }</code></pre>
<ul>
<li>결과
<img src="https://velog.velcdn.com/images/sh__/post/6a74756a-ded0-491f-8a60-5710f416a8e1/image.png" alt=""></li>
</ul>
</li>
</ul>
<ul>
<li>SQL문을 잘 살펴 보면 마지막에 <code>for update</code>가 있는 것을 확인할 수 있다. Locking이 잘 되었다는 것을 의미한다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.08.19 TIL]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.19-TIL</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.19-TIL</guid>
            <pubDate>Mon, 19 Aug 2024 11:04:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sh__/post/6b316823-4354-4967-be67-28441d0bdb11/image.png" alt=""></p>
<ul>
<li><p>시큐어 코딩 내용을 수강하며 SQL Injection과 그 외의 각종 보안 문제에 대해 학습했다.</p>
</li>
<li><p>장애 대응 강의를 수강하며 모니터링, 장애 분석 및 진단에 관하여 학습하였다.</p>
</li>
</ul>
<hr>
<h1 id="sql-injection">SQL Injection</h1>
<ul>
<li>예전 CS공부를 할 때 부터 많이 학습했던 부분이다. SQL Injection이란 공격자가 웹 애플리케이션의 데이터베이스를 조작하거나 민감한 정보를 탈취하는 공격이다. 이 취약점은 애플리케이션이 사용자 입력을 제대로 검증하지 않을 때 발생한다.</li>
</ul>
<h3 id="sql-injection의-위험성"><strong>SQL Injection의 위험성</strong></h3>
<ul>
<li><strong>데이터 탈취</strong>: 공격자가 데이터베이스에서 민감한 정보를 탈취할 수 있다.</li>
<li><strong>데이터 변조</strong>: 데이터베이스의 데이터를 변경하거나 삭제할 수 있다.</li>
<li><strong>권한 상승</strong>: 공격자가 데이터베이스 관리자 권한을 얻을 수 있다.</li>
<li><strong>전체 시스템 장악</strong>: 심각한 경우 서버 전체를 장악할 수 있다.</li>
</ul>
<h3 id="sql-injection-공격-예시"><strong>SQL</strong> injection 공격 예시</h3>
<pre><code class="language-jsx">import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class SqlInjectionExample {
    public static void main(String[] args) {
        String username = &quot;admin&#39;; --&quot;;
        String password = &quot;password&quot;;
        String query = &quot;SELECT * FROM users WHERE username = &#39;&quot; + username + &quot;&#39; AND password = &#39;&quot; + password + &quot;&#39;&quot;;

        try (Connection conn = DriverManager.getConnection(&quot;jdbc:mysql://localhost:3306/testdb&quot;, &quot;root&quot;, &quot;password&quot;);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(query)) {

            if (rs.next()) {
                System.out.println(&quot;User authenticated&quot;);
            } else {
                System.out.println(&quot;Authentication failed&quot;);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}</code></pre>
<ol>
<li>공격자는 username 필드에 admin&#39;; --를 입력한다.</li>
<li>SQL 쿼리는 SELECT * FROM users WHERE username = &#39;admin&#39;; --&#39; AND password = &#39;password&#39;로 변경된다.</li>
<li>--는 SQL 주석 처리 기호로, 이후의 코드는 무시된다.</li>
<li>결과적으로 쿼리는 SELECT * FROM users WHERE username = &#39;admin&#39;;가 되어, 공격자가 비밀번호를 입력하지 않아도 인증이 된다.</li>
</ol>
<h3 id="sql-injection-방어-기법"><strong>SQL Injection 방어 기법</strong></h3>
<ul>
<li><p><strong>Prepared Statements (준비된 문)</strong>: SQL 쿼리를 미리 컴파일하여 파라미터화된 쿼리를 사용한다.</p>
</li>
<li><p><strong>Stored Procedures (저장 프로시저)</strong>: 데이터베이스에서 미리 정의된 저장 프로시저를 호출하여 실행한다.</p>
</li>
<li><p><strong>ORM (객체 관계 매핑)</strong>: Hibernate 같은 ORM 프레임워크를 사용하여 데이터베이스 접근을 추상화한다.</p>
<ul>
<li><p>예시</p>
<ul>
<li><p>Hibernate와 같은 ORM 프레임워크를 사용할 경우, 내부적으로 Prepared Statement를 사용하여 SQL Injection 공격을 방지한다. Spring Data JPA를 사용하는 경우에도 이에 해당함!</p>
<pre><code class="language-jsx">import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

import java.util.List;

public class HibernateExample {
  public static void main(String[] args) {
      Configuration cfg = new Configuration().configure();
      SessionFactory sessionFactory = cfg.buildSessionFactory();
      Session session = sessionFactory.openSession();

      String username = &quot;admin&quot;; // 사용자 입력으로부터 받은 값
      String password = &quot;password&quot;; // 사용자 입력으로부터 받은 값

      // HQL (Hibernate Query Language) 쿼리
      String hql = &quot;FROM User WHERE username = :username AND password = :password&quot;;

      List&lt;User&gt; users = session.createQuery(hql)
                                .setParameter(&quot;username&quot;, username)
                                .setParameter(&quot;password&quot;, password)
                                .list();

      if (!users.isEmpty()) {
          System.out.println(&quot;User authenticated&quot;);
      } else {
          System.out.println(&quot;Authentication failed&quot;);
      }

      session.close();
      sessionFactory.close();
  }
}</code></pre>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>입력 검증 및 인코딩</strong>: 사용자 입력을 철저히 검증하고 인코딩하여 SQL 쿼리에 직접 포함시키지 않는다.</p>
</li>
<li><p><strong>최소 권한 원칙</strong>: 데이터베이스 사용자에게 최소한의 권한만 부여한다.</p>
</li>
</ul>
<h2 id="그-외-보안-문제들">그 외 보안 문제들</h2>
<h3 id="open-redirect"><strong>Open Redirect</strong></h3>
<ul>
<li><strong>Open Redirect</strong>는 공격자가 웹 애플리케이션의 리디렉션 기능을 악용하여 사용자를 악성 사이트로 유도하는 공격</li>
</ul>
<h3 id="directory-traversal"><strong>Directory Traversal</strong></h3>
<ul>
<li><strong>Directory Traversal</strong>은 공격자가 웹 애플리케이션의 파일 시스템에서 허용되지 않은 파일이나 디렉토리에 접근할 수 있도록 하는 공격</li>
</ul>
<h3 id="clickjacking"><strong>Clickjacking</strong></h3>
<ul>
<li><strong>Clickjacking</strong>은 공격자가 웹 페이지의 투명한 프레임을 사용하여 사용자가 클릭하도록 유도하는 공격이다. 이를 통해 공격자는 사용자가 알지 못하는 사이에 악성 동작을 수행할 수 있다.</li>
</ul>
<h3 id="sensitive-data-exposure"><strong>Sensitive Data Exposure</strong></h3>
<ul>
<li><strong>Sensitive Data Exposure</strong>는 애플리케이션이 민감한 데이터를 충분히 보호하지 않아 공격자가 이를 탈취하는 공격이다.</li>
<li>민감한 데이터가 안전하게 저장되거나 전송되지 않을 때 발생한다. 예를 들어, 비밀번호가 평문으로 저장되거나, 신용카드 정보가 암호화되지 않은 채로 전송되는 경우가 있다.</li>
<li>이와 같이 비밀번호나 신용카드 정보와 같은 정보를 암호화하지 않고 평문으로 저장할 경우 정보통신망법 위반!</li>
<li>따라서 HTTPS(HyperText Transfer Protocol Secure) 사용 또한 필수</li>
</ul>
<h3 id="insecure-deserialization"><strong>Insecure Deserialization</strong></h3>
<ul>
<li><strong>Insecure Deserialization</strong>은 공격자가 악의적으로 조작된 객체를 애플리케이션에 전달하여 실행시키는 공격이다.</li>
<li>이를 통해 공격자는 시스템 내에서 임의의 코드를 실행하거나 데이터를 조작할 수 있다.</li>
<li>애플리케이션이 직렬화된 데이터를 역직렬화할 때, 해당 데이터가 신뢰할 수 있는지 검증하지 않으면 공격자가 악의적으로 조작된 직렬화 데이터를 주입할 수 있다.</li>
<li>이를 통해 공격자는 애플리케이션에서 임의의 코드를 실행하거나 데이터를 조작할 수 있다.</li>
</ul>
<h3 id="insufficient-logging--monitoring"><strong>Insufficient Logging &amp; Monitoring</strong></h3>
<ul>
<li><strong>Insufficient Logging &amp; Monitoring</strong>은 애플리케이션이 적절한 로그를 기록하지 않거나, 이상 행동을 감시하지 않아 공격을 조기에 발견하지 못하는 경우이다.</li>
</ul>
<h2 id="cve-cvss"><strong>CVE, CVSS</strong></h2>
<ul>
<li>CVE (Common Vulnerabilities and Exposures)는 특정 소프트웨어 및 하드웨어의 취약점을 고유하게 식별하기 위해 사용되는 표준화된 명명 시스템이다.
CVSS 점수는 0에서 10까지의 범위로, 낮은 점수는 덜 심각한 취약점을, 높은 점수는 매우 심각한 취약점을 나타낸다.</li>
</ul>
<hr>
<h1 id="장애-식별">장애 식별</h1>
<aside>
👉 가장 좋지 않은 장애 식별은 고객으로부터 장애가 접수되는 경우이다. 
이는 고객 경험에 직접적인 영향을 미치며, 기업의 평판에도 부정적인 영향을 줄 수 있다. 따라서 장애를 사전에 식별하고 해결하는 것이 중요하다.

</aside>

<p>❓ 그렇다면 어떻게 고객보다 먼저 장애를 파악하고 조치를 취할 수 있을까?</p>
<h2 id="모니터링">모니터링</h2>
<ul>
<li><p>이전에 모니터링 실습을 진행했던 것 처럼 Prometheus, Grafana 등의 툴을 통해 시스템의 성능 상태, 장애를 모니터링하고 초기에 이를 진단할 수 있다. (이전 포스팅 👉 (<a href="https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.16-TIL#cors-cross-origin-resource-sharing">클릭</a>))</p>
</li>
<li><p>모니터링 기술을 <strong>크게 세 가지로 정리</strong>해보자.</p>
</li>
</ul>
<h3 id="시스템-모니터링"><strong>시스템 모니터링</strong></h3>
<ul>
<li><p><strong>리소스 사용량 추적</strong>: CPU, 메모리, 디스크 I/O, 네트워크 대역폭 등의 리소스 사용량을 실시간으로 모니터링한다. 이는 시스템의 성능 병목을 조기에 식별하는 데 중요하다.</p>
</li>
<li><p><strong>서버 및 애플리케이션 상태</strong>: 서버의 가용성, 응답 시간, 오류 로그 등을 모니터링하여 서버 상태와 애플리케이션의 작동 여부를 점검한다.</p>
</li>
<li><p><strong>장애 예측</strong>: 머신러닝 기반의 예측 분석을 통해 리소스 사용 패턴을 학습하고, 이상 패턴을 사전에 감지하여 장애를 예방할 수 있다.</p>
</li>
</ul>
<h3 id="애플리케이션-모니터링"><strong>애플리케이션 모니터링</strong></h3>
<ul>
<li><p><strong>로그 모니터링</strong>: 애플리케이션 로그를 실시간으로 수집 및 분석하여 오류나 예외를 조기에 탐지한다. 중앙 집중형 로그 관리 시스템을 통해 모든 로그를 통합하고 분석하는 것이 효과적이다.</p>
</li>
<li><p><strong>성능 모니터링</strong>: 애플리케이션의 응답 시간, 처리량(throughput), 오류율 등을 측정하여 성능 문제를 조기에 식별한다. APM(Application Performance Monitoring) 도구를 활용하여 성능 병목을 탐지할 수 있다.</p>
</li>
<li><p><strong>서비스 헬스 체크</strong>: 서비스의 상태를 주기적으로 점검하는 헬스 체크를 구현하여, 서비스의 정상 동작 여부를 확인하고 장애 발생 시 자동으로 알림을 받을 수 있다.</p>
</li>
</ul>
<h3 id="사용자-경험-모니터링"><strong>사용자 경험 모니터링</strong></h3>
<ul>
<li><p><strong>엔드-투-엔드 모니터링</strong>: 사용자가 서비스를 사용하는 전체 경로를 모니터링하여, 실제 사용자 경험을 기반으로 장애를 식별한다. 사용자 지연, 실패한 요청 등의 데이터를 수집한다.  (ex. 셀레니움)</p>
</li>
<li><p><strong>사용자 피드백 수집</strong>: 고객 지원 채널을 통해 사용자 피드백을 수집하고, 이를 분석하여 잠재적인 문제를 조기에 식별한다.</p>
</li>
<li><p><strong>리얼 유저 모니터링 (RUM)</strong>: 실제 사용자의 웹 브라우저에서 데이터를 수집하여 페이지 로드 시간, 응답시간 등을 모니터링하고 사용자 경험을 개선한다. (ex. 구글 애널리틱스)</p>
</li>
</ul>
<h3 id="모니터링-고려사항"><strong>모니터링 고려사항</strong></h3>
<ul>
<li><p><strong>지속적인 모니터링 및 개선</strong>: 모니터링 시스템은 정기적으로 점검하고 조정하여 최신 상태를 유지해야 한다.</p>
</li>
<li><p><strong>중앙 집중식 로그 관리</strong>: 로그는 한 곳에 모아 분석하여 시스템의 전반적인 상태를 명확하게 파악할 수 있어야 한다.</p>
</li>
<li><p><strong>협업과 공유</strong>: 모니터링 데이터를 팀 간에 공유하여 보다 효과적인 장애 대응이 가능하도록 한다.</p>
</li>
<li><p><strong>테스트 및 시뮬레이션</strong>: 장애 시나리오를 테스트하고 모니터링 시스템이 정확하게 작동하는지 검증한다.</p>
</li>
</ul>
<h2 id="장애-분석-및-진단">장애 분석 및 진단</h2>
<aside>
👉 장애가 발생하면 신속하고 정확한 분석과 진단이 필요하다. 장애의 근본 원인을 파악하고 적절한 해결책을 찾는 과정은 다음과 같은 단계로 구성된다.

</aside>

<h3 id="1-초기-대응"><strong>1. 초기 대응</strong></h3>
<ul>
<li><strong>장애 보고 및 인지</strong><ul>
<li>장애 보고 체계 확립: 누군가가 계속해서 모니터링을 하기에는 실 서비스에서는 많은 인프라가 동작하고 있다. 자동 알림 시스템을 통해 장애 발생을 즉시 인지할 수 있도록 설정한다. 이전 포스팅에서 실습을 진행했던 <strong>슬랙을 통한 그라파나 Alert 설정 등</strong>이 있다.</li>
<li>장애 상황 인지: 장애 발생 시, 즉시 관련 팀에게 통보하여 초동 조치를 취할 수 있도록 한다.</li>
</ul>
</li>
<li><strong>초기 평가</strong><ul>
<li>장애의 심각도 평가: 장애의 영향을 평가하여 우선순위를 정한다. 서비스 중단, 성능 저하, 데이터 손상 등의 영향을 고려한다.</li>
<li>초기 대응 전략 수립: 장애 상황에 맞는 초기 대응 계획을 수립하고, 관련 리소스를 할당한다.</li>
</ul>
</li>
<li><strong>장애 원인 분석</strong><ul>
<li><strong>문제 재현</strong>: 장애 상황을 재현하여 근본 원인을 명확히 파악한다. 이를 통해 동일한 상황에서 재발 방지를 위한 구체적인 조치를 설계할 수 있다.</li>
<li><strong>장애 패턴 인식</strong>: 장애 로그와 메트릭을 기반으로 과거 유사한 장애 패턴을 분석하여 패턴에 따른 원인을 찾아낸다.</li>
</ul>
</li>
</ul>
<h3 id="2-로그-및-메트릭-분석"><strong>2. 로그 및 메트릭 분석</strong></h3>
<h3 id="👉-가장-먼저-분석해야-할-사항은-로그-및-메트릭">👉 가장 먼저 분석해야 할 사항은 로그 및 메트릭!</h3>
<ul>
<li><strong>로그 분석</strong><ul>
<li>로그 수집: 중앙 집중식 로그 관리 시스템에서 로그 데이터를 수집하고 분석한다.</li>
<li>오류 및 예외 분석: 로그를 통해 애플리케이션에서 발생한 오류와 예외를 추적하여 장애의 원인을 파악한다.</li>
<li>로그 패턴 분석: 패턴 인식을 통해 과거의 유사한 장애 사례를 파악하고 대응책을 도출한다.</li>
</ul>
</li>
<li><strong>메트릭 분석</strong><ul>
<li>실시간 메트릭 모니터링: CPU 사용률, 메모리 사용량, 네트워크 트래픽 등 실시간 메트릭을 모니터링하여 비정상적인 활동을 탐지한다.</li>
<li>메트릭 데이터 비교: 정상 상태와 장애 상태의 메트릭을 비교하여 이상 현상을 식별한다.</li>
<li>임계값 분석: 설정된 임계값을 초과하는 메트릭을 확인하여 장애의 징후를 발견한다.</li>
</ul>
</li>
</ul>
<h3 id="3-시스템-및-네트워크-분석"><strong>3. 시스템 및 네트워크 분석</strong></h3>
<ul>
<li><strong>시스템 분석</strong><ul>
<li>프로세스 상태 점검: 시스템에서 실행 중인 프로세스를 점검하여 비정상적으로 작동 중인 프로세스를 식별한다.</li>
<li>리소스 사용 추적: 시스템 리소스(CPU, 메모리, 디스크 I/O)의 과도한 사용을 추적하여 장애의 원인을 분석한다.</li>
<li>스레드 덤프 분석: JVM 스레드 덤프를 분석하여 교착 상태(데드락) 및 스레드 고갈 문제를 진단한다.</li>
</ul>
</li>
<li><strong>네트워크 분석</strong><ul>
<li>네트워크 트래픽 모니터링: 네트워크 트래픽을 분석하여 패킷 손실, 지연, 대역폭 초과 등의 문제를 식별한다.</li>
<li>네트워크 토폴로지 점검: 네트워크 구조를 점검하여 구성상의 문제를 진단한다.</li>
<li>연결 상태 분석: 외부 시스템 및 API와의 연결 상태를 점검하여 네트워크 장애를 진단한다.</li>
</ul>
</li>
</ul>
<h3 id="4-데이터베이스-분석"><strong>4. 데이터베이스 분석</strong></h3>
<ul>
<li><strong>쿼리 성능 분석</strong><ul>
<li>슬로우 쿼리 로그 분석: 성능이 저하된 쿼리를 식별하고, 인덱스 최적화 및 쿼리 재작성으로 해결한다.</li>
<li>데이터베이스 락 분석: 락 대기 및 데드락 상황을 식별하고 해결책을 찾는다.</li>
<li>커넥션 풀 상태 점검: 데이터베이스 커넥션 풀의 상태를 점검하여 연결 문제를 진단한다.</li>
</ul>
</li>
<li><strong>데이터 일관성 점검</strong><ul>
<li>데이터 무결성 확인: 데이터베이스의 무결성을 점검하여 데이터 손상 여부를 확인한다.</li>
<li>데이터 복구 절차 실행: 손상된 데이터의 복구 절차를 실행하여 데이터베이스의 정상 상태를 복구한다.</li>
</ul>
</li>
</ul>
<hr>
<h1 id="결론">결론</h1>
<h3 id="가장-좋은-방법은-장애가-일어나지-않도록-하는-것❗">가장 좋은 방법은 장애가 일어나지 않도록 하는 것❗</h3>
<ul>
<li><p>설계 단계에서 협업자와 충분한 대화를 나눌 것</p>
</li>
<li><p>프로젝트에서 무언가 이상한 로직을 발견했다면( ex) 무한 루프 가능성이 있는 함수 코드 등 ) 해당 사항을 인지하고 그냥 놔둔것 같은 느낌(이것도 못봤을까??)이 들더라도 해당 영역 작업자와 반드시 논의해볼 것</p>
</li>
<li><p>프로젝트가 배포되더라도 지속적인 모니터링 및 보안에 힘쓸 것</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 Spring 심화] 2024.08.16 TIL]]></title>
            <link>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.16-TIL</link>
            <guid>https://velog.io/@sh__/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Spring-%EC%8B%AC%ED%99%94-2024.08.16-TIL</guid>
            <pubDate>Fri, 16 Aug 2024 07:18:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sh__/post/0a39cead-aa00-4045-887c-bd996486e895/image.png" alt=""></p>
<p>모니터링 시스템 및 시큐어 코딩 관련 강의들을 수강하며 여러 가지를 배웠다.</p>
<p><strong>모니터링 시스템</strong>이 무엇인지 이론적으로 간단하게 학습했고, <strong>Spring Boot Actutor, Prometheus, Grafana, loki 실습</strong>을 진행하였으며 <strong>CORS, CSRF</strong>에 대해 학습을 진행하였다.</p>
<p>이번 TIL 에서는 그 중 <strong>Prometheus, Grafana 실습, CORS, CSRF</strong>를 학습하며 있었던 내용들을 중심적으로 작성해보려 한다.</p>
<hr>
<h1 id="prometheus">Prometheus</h1>
<h2 id="prometheus란">Prometheus란?</h2>
<ul>
<li><p>오픈소스 시스템 모니터링 및 경고 도구</p>
</li>
<li><p>SoundCloud에서 시작되어 현재는 Cloud Native Computing Foundation(CNCF)에서 호스팅하고 있다.</p>
</li>
<li><p>Prometheus는 시계열 데이터베이스를 사용하여 메트릭 데이터를 수집하고, 쿼리 및 시각화를 통해 시스템 상태를 모니터링하고 경고를 설정할 수 있다.</p>
</li>
</ul>
<h2 id="주요-구성-요소">주요 구성 요소</h2>
<ul>
<li><p><strong>Prometheus 서버</strong>:</p>
<ul>
<li>메트릭 데이터를 수집하고 저장하는 핵심 컴포넌트이다. 각 타겟으로부터 데이터를 주기적으로 스크랩(scrape)하여 시계열 데이터베이스에 저장한다.</li>
<li>시계열 데이터베이스(Time Series Database, TSDB)는 시간에 따라 변화하는 데이터를 효율적으로 저장하고 조회할 수 있도록 최적화된 데이터베이스이다.</li>
</ul>
</li>
<li><p><strong>Exporters</strong>:</p>
<ul>
<li>Prometheus는 기본적으로 애플리케이션에서 메트릭 데이터를 수집한다.</li>
<li>Exporter는 특정 애플리케이션이나 시스템의 메트릭 데이터를 Prometheus가 이해할 수 있는 형식으로 변환해주는 도구이다.</li>
</ul>
</li>
<li><p><strong>Pushgateway</strong>:</p>
<ul>
<li>짧은 수명의 작업(job)에서 메트릭을 수집하여 Prometheus 서버에 푸시(push)할 수 있다.</li>
<li>일반적으로 지속적으로 실행되지 않는 작업에서 사용된다. 예를 들어 배치 작업, 스크립트 실행, 크론 작업 등이 있다.</li>
</ul>
</li>
<li><p><strong>Alertmanager</strong>:</p>
<ul>
<li>Prometheus 서버에서 발생하는 경고(alert)를 처리하고, 이메일, PagerDuty, Slack 등 다양한 방법으로 알림을 보낼 수 있다.</li>
</ul>
</li>
<li><p><strong>Grafana</strong>:</p>
<ul>
<li>Prometheus 데이터를 시각화하기 위해 자주 사용되는 대시보드 도구이다.</li>
<li>Grafana를 사용하면 Prometheus에서 수집한 메트릭 데이터를 대시보드 형태로 시각화할 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="실습">실습</h2>
<ul>
<li><p>Prometheus 를 보다 쉽게 설치하기 위해서 Docker 를 사용한다. 이후 Grafana에서도 동일하게 Docker를 사용할 예정이다.</p>
</li>
<li><p>먼저 스프링 프로젝트 부터 생성한다. 아래와 같이 디펜던시를 구성한다.</p>
<p><img src="https://velog.velcdn.com/images/sh__/post/e8512066-a91b-44a8-90f4-30ed860b022f/image.png" alt=""></p>
</li>
</ul>
<ul>
<li><p><strong>application.properties</strong></p>
<pre><code class="language-bash">  spring.application.name=sample

  server.port=8080

  #모든 엔드포인트 노출 설정
  management.endpoints.web.exposure.include=* 

  #헬스 체크 엔드포인트 상세 정보 표시 설정
  management.endpoint.health.show-details=always # 이 설정은 /actuator/health 엔드포인트에서 헬스 체크 정보를 항상 상세히 보여주도록 설정합니다. 기본적으로, 헬스 체크 엔드포인트는 요약된 상태 정보만 제공하며, 상세 정보는 노출되지 않습니다.

  management.endpoint.prometheus.enabled=true</code></pre>
</li>
</ul>
<ul>
<li><p><a href="http://localhost:8080/actuator/prometheus">http://localhost:8080/actuator/prometheus</a> 에 접속하여 프로메테우스 매트릭스를 확인할 수 있다.</p>
<p>  <img src="https://velog.velcdn.com/images/sh__/post/26c7cdc3-a276-40b9-801d-da17c1eff741/image.png" alt=""></p>
</li>
</ul>
<ul>
<li><p><strong>Prometheus 설정 파일 생성</strong></p>
<ul>
<li><p>Prometheus가 모니터링할 타겟과 기타 설정을 정의하는 설정 파일(prometheus.yml)을 생성한다.</p>
</li>
<li><p><code>host.docker.internal</code>은 Docker에서 제공하는 특수한 DNS 이름으로, Docker 컨테이너가 호스트 머신(즉, Docker를 실행하는 컴퓨터)의 네트워크 서비스에 접근할 수 있도록 한다. 이를 통해 컨테이너 내부에서 호스트 머신의 네트워크 주소를 참조할 수 있다.</p>
<pre><code class="language-yaml">  global:
    scrape_interval: 15s

  scrape_configs:
    - job_name: &#39;spring-boot&#39;
      metrics_path: &#39;/actuator/prometheus&#39;
      static_configs:
        - targets: [&#39;host.docker.internal:8080&#39;]</code></pre>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>Prometheus 실행</p>
<ul>
<li><p>Docker 명령어를 사용해 Prometheus컨테이너를 실행한다.</p>
</li>
<li><p>-v 옵션의 앞부분은 방금전 생성한 prometheus.yml의 경로를 포함하여 작성한다.</p>
<pre><code class="language-yaml">docker run -d --name=prometheus -p 9090:9090 -v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus</code></pre>
</li>
</ul>
</li>
<li><p><a href="http://localhost:9090">localhost:9090</a> 에 접속해보자.프로메테우스 서버에 접속할 수 있습니다.</p>
<p>  <img src="https://velog.velcdn.com/images/sh__/post/1982e967-b441-42e6-9652-b7f5d4e406a5/image.png" alt=""></p>
</li>
</ul>
<ul>
<li>상단 메뉴에서 Status &gt; Targets에 접속하여 스프링 애플리케이션의 매트릭스를 수집하고 있는것을 확인 할 수 있다 (나오지 않는다면 스프링 애플리케이션을 Run하고 있는지 확인한다.)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/e4cdf7c7-72ca-442c-8f4e-1efd578ec1cd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/45f57178-103d-4481-aeea-430f27030075/image.png" alt=""></p>
<hr>
<h1 id="grafana">Grafana</h1>
<h2 id="grafana란">Grafana란?</h2>
<ul>
<li><p>오픈소스 데이터 시각화 및 모니터링 도구</p>
</li>
<li><p>다양한 데이터 소스를 지원하여 데이터를 시각화하고 분석할 수 있도록 돕는다.</p>
</li>
<li><p>Grafana는 대시보드를 생성하고, 데이터를 그래프나 차트 형태로 표현하며 알림 기능을 제공하여 모니터링을 강화할 수 있다.</p>
</li>
</ul>
<h2 id="그라파나-주요기능">그라파나 주요기능</h2>
<ul>
<li><strong>대시보드 생성</strong>:<ul>
<li>Grafana는 사용자가 데이터를 시각화할 수 있는 대시보드를 생성할 수 있도록 한다. 여러 가지 그래프, 차트, 게이지 등을 사용하여 데이터를 시각적으로 표현할 수 있다.</li>
</ul>
</li>
</ul>
<ul>
<li><strong>다양한 데이터 소스 지원</strong>:<ul>
<li>Prometheus, InfluxDB, Graphite, Elasticsearch, MySQL, PostgreSQL 등 다양한 데이터 소스를 지원한다. 이를 통해 여러 시스템과 애플리케이션의 데이터를 통합하여 시각화할 수 있다.</li>
</ul>
</li>
</ul>
<ul>
<li><p><strong>알림 기능</strong>:</p>
<ul>
<li>조건을 설정하여 조건이 충족되면 이메일, Slack, PagerDuty 등 다양한 채널을 통해 알림을 보낼 수 있다. 이를 통해 시스템 상태를 실시간으로 모니터링하고 문제가 발생했을 때 즉시 대응할 수 있다.</li>
</ul>
</li>
<li><p><strong>플러그인 지원</strong>:</p>
<ul>
<li>Grafana는 플러그인 아키텍처를 지원하여, 다양한 플러그인을 통해 기능을 확장할 수 있다. 예를 들어, 새로운 데이터 소스나 시각화 유형을 추가할 수 있다.</li>
</ul>
</li>
<li><p><strong>사용자 관리</strong>:</p>
<ul>
<li>사용자를 관리하고, 대시보드와 데이터 소스에 대한 접근 권한을 설정할 수 있다. 이를 통해 팀 내에서 협업을 강화하고 데이터 보안을 유지할 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="실습-1">실습</h2>
<ul>
<li>이번 실습에서는 Docker를 사용하여 그라파나 컨테이너를 실행시키고, <strong>그라파나에서 Slack으로 Alert를 보내도록 구현</strong>해볼 것이다.</li>
</ul>
<h3 id="슬랙-앱-생성">슬랙 앱 생성</h3>
<ul>
<li>먼저 슬랙에 가입해서 워크스페이스를 생성한다.</li>
<li><a href="https://api.slack.com/apps">https://api.slack.com/apps</a> 에 접속해서 Create an App을 클릭한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/120fa1de-bcc9-41a7-94e2-d96c9d40d071/image.png" alt=""></p>
<ul>
<li>팝업이 뜨면 From scratch 를 클릭한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/a6e3b2f1-d726-4f3a-8032-42f806bce571/image.png" alt=""></p>
<ul>
<li>앱이름을 입력하고 생성한 워크스페이스를 선택한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/e7e96c29-1f06-4878-8881-111a0b75e01e/image.png" alt=""></p>
<ul>
<li>OAuth &amp; Permissions 메뉴에 접속한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/e60fc54d-e47a-41fe-a464-0f9d05dff217/image.png" alt=""></p>
<ul>
<li>스크롤 하여 Scopes 항목으로 간 후 Bot Token Scopes에 “chat:write”를 추가한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/5b343c3b-5984-4560-aca5-55724681de67/image.png" alt=""></p>
<ul>
<li>그후 스크롤을 올라와서  Install to Workspace 를 클릭하고 허용을 클릭한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/40a21b31-6a78-45d5-9a23-e0530fc5e76e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/5b075340-6792-44f1-87bc-fff395323613/image.png" alt=""></p>
<ul>
<li>Incoming Webhooks 메뉴에 접속한다. Activate Incoming Webhooks 를 활성화 한 후, 하단에 Add New Webhook to Workspace를 클릭한다. 다음 화면에서 전달 받고 싶은 슬랙 채널을 선택한다.
그리고 생성된 Webhook URL을 복사해둔다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/c4b8005c-54e8-4798-8bd2-3e4d8bd7f8cc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/aafe7990-673b-43c8-9b63-66583b07599e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/56b7ee60-9a32-4715-9115-fcc60d2a06b8/image.png" alt=""></p>
<ul>
<li>슬랙 앱에서 해당 채널에 가서 생성한 App을  “@”를 사용하여 추가해준다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/80719ed1-5c6c-4f5a-a0b1-fceab8a5ad64/image.png" alt=""></p>
<h3 id="그라파나-alert-설정">그라파나 Alert 설정</h3>
<ul>
<li>그라파나에서 사이드메뉴에 Alerting &gt; Contact points 에 접속하여 Add contact point 버튼을 클릭한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/7abacfa0-91be-49e5-97bb-710de573a126/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/d69f82b0-ac04-4690-aa1d-e437f2c47e82/image.png" alt=""></p>
<ul>
<li>Name 을 입력한 후, Integration 을 Slack을 선택한다. 그후 Webhook URL에 아까 앱에서 복사한 URL을 입력한다. 테스트 버튼을 클릭하면 테스트 메시지가 슬랙 채널로 오는 것을 확인할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/6f60f580-b594-42a6-9a5f-21e2ee976aac/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/01fdcf46-8517-4a53-a795-f11415a1824c/image.png" alt=""></p>
<ul>
<li>그라파나 사이드 메뉴에서 Alerting &gt; Notification policies 로 들어간 후 Default policy의 edit 버튼을 클릭한다. 이후 나온 Edit창에서 Default contact point 를 이전에 생성한 contact point로 선택한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/4952a769-881d-427e-9cb8-36457a413e61/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/f0615445-e2b1-49da-acdb-4f25b9e41688/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/101586b8-47f0-4ab4-ab83-31303a9392fd/image.png" alt=""></p>
<ul>
<li>Alerting &gt; Alert rules 를 클릭하여 “New alert rule”을 클릭한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/8fc3f3fd-83a5-41b7-be2a-7f9d8e6c8a05/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/b657827e-52f5-48a4-91ea-f90c87ef9868/image.png" alt=""></p>
<ul>
<li><p>알림 이름을 입력한다. Define query and alert condition 에서 matric을 UP을 선택하고 Label filter 에서 Job , spring-boot 를 선택한다.</p>
<p>  Expression 의 Threshold에서 IS BLOW를 선택 숫자는 1을 입력한다. 이를 통해 만약 애플리케이션이 정지되면 알람이 발송되게 된다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/6ede810e-8705-4e00-86f5-993ccb44f713/image.png" alt=""></p>
<ul>
<li><p>스크롤 하여 내려가면 Set evaluation behavior 섹션을 볼 수 있다.</p>
<p>  Folder 및 Evaluation group을 선택또는 새로 생성한다. </p>
<p>  pending period, Evaluation internal 은 빠른 확인을 위해 1m으로 설정한다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/8f89c421-a6a5-4cae-889e-ad4d4d815eaa/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/d90ce729-c9b0-446d-b4f5-21ba36278c21/image.png" alt=""></p>
<ul>
<li>좀더 스크롤해 내려가면 Confifure labels and notifications 메뉴가 있다. 해당 메뉴의 contact point 을 이전에 설정한 slack으로 설정한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/349c8294-b4ab-4135-9ea0-556cf60da33a/image.png" alt=""></p>
<h3 id="그라파나-alert-설정-확인">그라파나 Alert 설정 확인</h3>
<ul>
<li>Alert rules에 생성한 Alert이 노출된다. 상태는 Normal인 것을 확인할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/75f9a9f3-d4d9-4f72-a10b-e702686e986a/image.png" alt=""></p>
<ul>
<li><p>스프링 애플리케이션을 정지 시키면 Normal 이었던 상태가 Pending &gt; Firing으로 변경된다.</p>
<p>  그후 잠시 기다려면 슬랙 채널로 Firing 알람이 오는것을 확인할 수 있다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/75841da7-9ca9-4a25-9748-b6004d489f28/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/05712929-2964-44c1-9e3f-7e619893ca7c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/51d84e6e-e7d3-40a9-bfe3-f072b26d71e7/image.png" alt=""></p>
<ul>
<li><p>다시 스프링 애플리케이션을 실행한후 기다리면 슬랙 채널로 Resolved 알람이 오는것을 확인할수 있다.</p>
<p>  (해당 알람은 도착하는데 시간이 걸린다!)</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/df592197-314f-4672-a5b0-bf2381902e82/image.png" alt=""></p>
<hr>
<h1 id="cors-cross-origin-resource-sharing">CORS (Cross-Origin Resource Sharing)</h1>
<aside>
📌 애플리케이션을 개발한 후 방화벽에서 GET, POST, PATCH, PUT, DELETE 메서드를 허용했다. 
SPA 로 개발된 프론트 페이지에 접속하여 요청을 했는데 403, 405 에러가 났다.
이유가 무엇이었을까?

</aside>

<h3 id="cors란">CORS란?</h3>
<ul>
<li><p>한 출처(도메인, 프로토콜, 포트)에서 실행 중인 웹 애플리케이션이 다른 출처의 리소스에 접근할 수 있도록 브라우저에서 제공하는 보안 기능</p>
</li>
<li><p>웹 애플리케이션은 기본적으로 동일 출처 정책(Same-Origin Policy)에 따라 동작하며, 이는 보안상의 이유로 다른 출처의 리소스 접근을 제한</p>
</li>
<li><p>CORS는 이러한 제한을 완화하여 특정 조건 하에 다른 출처의 리소스 접근을 허용</p>
</li>
</ul>
<h3 id="동일-출처-정책same-origin-policy">동일 출처 정책(Same-Origin Policy)</h3>
<ul>
<li><p>동일 출처 정책은 보안 메커니즘으로, 웹 브라우저가 스크립트가 로드된 출처(origin)와 동일한 출처의 리소스만 접근할 수 있도록 제한</p>
</li>
<li><p><strong>출처의 구성 요소</strong>: 출처는 스키마(프로토콜), 호스트(도메인), 포트의 조합으로 정의</p>
<ul>
<li>예: <a href="http://example.com:80%EC%99%80">http://example.com:80와</a> <a href="http://example.com:8080%EC%9D%80">http://example.com:8080은</a> 포트가 다르므로 동일 출처가 아님.</li>
</ul>
</li>
</ul>
<h3 id="cors의-필요성">CORS의 필요성</h3>
<ul>
<li><strong>API 호출</strong>: SPA(Single Page Application)와 같이 클라이언트 중심의 웹 애플리케이션은 종종 다른 도메인에서 호스팅되는 API를 호출해야 한다.</li>
<li><strong>리소스 공유</strong>: 여러 도메인 간의 이미지, 스타일시트, 스크립트, 폰트 등의 리소스를 공유할 필요가 있다.</li>
</ul>
<h3 id="cors의-동작-원리">CORS의 동작 원리</h3>
<ul>
<li><p>Preflight 란?</p>
<ul>
<li><p>Preflight 요청은 CORS (Cross-Origin Resource Sharing) 요청의 일종으로, 브라우저가 실제 요청을 보내기 전에 서버에 요청할 권한이 있는지 확인하는 과정</p>
</li>
<li><p>이는 보안상의 이유로, 특정 조건을 만족하는 HTTP 요청이 서버에 전송되기 전에 실행</p>
<ul>
<li><strong>HTTP 메서드가 단순 요청이 아닐 때</strong> (GET, HEAD, POST 외의 메서드, 예: PUT, DELETE).</li>
<li><strong>특정 헤더를 사용할 때</strong>: 커스텀 헤더 또는 특정 표준 헤더를 사용할 때.</li>
<li><strong>특정 Content-Type을 사용할 때</strong>: application/x-www-form-urlencoded, multipart/form-data, text/plain이 아닌 Content-Type을 사용할 때.</li>
</ul>
</li>
<li><p>동작 방식은 다음과 같다.</p>
<ol>
<li><p><strong>브라우저가 Preflight 요청을 보냄</strong>:</p>
<ul>
<li>OPTIONS 메서드를 사용하여 서버에 사전 요청을 보냄</li>
<li>이 요청에는 실제 요청의 메서드와 헤더 정보가 포함됨</li>
</ul>
</li>
<li><p><strong>서버가 Preflight 요청에 응답</strong>:</p>
<ul>
<li>서버는 요청된 메서드와 헤더를 허용할지 여부를 결정하여 응답</li>
<li>응답 헤더에는 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers 등이 포함됨</li>
</ul>
</li>
<li><p><strong>브라우저가 응답을 확인</strong>:</p>
<ul>
<li>브라우저는 서버의 응답을 확인하고, 요청이 허용되면 실제 요청을 보냄</li>
<li>요청이 허용되지 않으면, 브라우저는 실제 요청을 차단함</li>
</ul>
</li>
</ol>
</li>
</ul>
</li>
<li><p><strong>Simple Request (단순 요청)</strong></p>
<ul>
<li>간단한 HTTP 요청으로, Preflight 요청 없이 바로 서버에 전달<ul>
<li>HTTP 메서드가 GET, POST, HEAD 중 하나.</li>
<li>커스텀 헤더가 없고, Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Preflight Request (사전 요청)</strong></p>
<ul>
<li>단순 요청 조건을 충족하지 않는 요청은 서버의 CORS 정책을 확인하기 위해 브라우저가 먼저 OPTIONS 메서드를 사용하여 Preflight 요청을 보낸다.</li>
<li>서버가 Preflight 요청에 적절히 응답하면 실제 요청이 진행된다.</li>
</ul>
</li>
</ul>
<h3 id="cors-설정-시-주의사항">CORS 설정 시 주의사항</h3>
<ul>
<li><p><strong>보안 고려사항</strong>:</p>
<ul>
<li><p>신뢰할 수 없는 출처를 허용하지 않도록 주의.</p>
</li>
<li><p>allowedOrigins에 와일드카드(*)를 사용하면 모든 출처에서의 요청을 허용하므로 주의가 필요.</p>
</li>
<li><p>민감한 정보를 보호하기 위해 Access-Control-Allow-Credentials를 신중하게 설정.</p>
</li>
</ul>
</li>
<li><p><strong>성능 고려사항</strong>:</p>
<ul>
<li>Preflight 요청이 빈번하면 성능 저하가 발생할 수 있으므로, Access-Control-Max-Age를 설정하여 Preflight 요청을 캐싱.</li>
<li>불필요한 Preflight 요청을 최소화하기 위해 단순 요청 조건을 충족하도록 API 설계를 검토.</li>
</ul>
</li>
</ul>
<h3 id="cors-해결하기">CORS 해결하기</h3>
<ul>
<li><p>백앤드 애플리케이션에서 CORS 설정을 해준다.</p>
</li>
<li><p>전역설정</p>
<pre><code class="language-java">  import org.springframework.context.annotation.Bean;
  import org.springframework.context.annotation.Configuration;
  import org.springframework.web.servlet.config.annotation.CorsRegistry;
  import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

  @Configuration
  public class WebConfig {

      @Bean
      public WebMvcConfigurer corsConfigurer() {
          return new WebMvcConfigurer() {
              @Override
              public void addCorsMappings(CorsRegistry registry) {
                  registry.addMapping(&quot;/**&quot;)
                          .allowedOrigins(&quot;http://localhost:3000&quot;)  // 허용할 출처
                          .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;, &quot;OPTIONS&quot;)
                          .allowedHeaders(&quot;*&quot;)
                          .allowCredentials(true)
                          .maxAge(3600);  // Preflight 요청 캐시 시간
              }
          };
      }
  }</code></pre>
</li>
<li><p>컨트롤러 레벨 설정</p>
<pre><code class="language-java">  import org.springframework.web.bind.annotation.CrossOrigin;
  import org.springframework.web.bind.annotation.GetMapping;
  import org.springframework.web.bind.annotation.RestController;

  @RestController
  public class MyController {

      @CrossOrigin(origins = &quot;http://localhost:3000&quot;)
      @GetMapping(&quot;/api/data&quot;)
      public String getData() {
          return &quot;Data from server&quot;;
      }
  }</code></pre>
</li>
</ul>
<h1 id="csrf-cross-site-request-forgery">CSRF (Cross-Site Request Forgery)</h1>
<h3 id="csrf란">CSRF란?</h3>
<ul>
<li><p>웹 애플리케이션의 취약점을 이용해 사용자가 의도하지 않은 요청을 보내도록 하는 공격 기법</p>
</li>
<li><p>공격자는 사용자가 인증된 상태를 악용하여 사용자가 원하지 않는 행동을 수행하게 만든다.</p>
</li>
<li><p>예를 들어, 사용자가 로그인된 상태에서 악의적인 웹사이트를 방문하면, 그 웹사이트가 사용자의 권한을 이용해 은행 계좌에서 돈을 송금하도록 할 수 있다.</p>
</li>
</ul>
<h3 id="csrf-상황">CSRF 상황</h3>
<ol>
<li><p><strong>사용자가 로그인</strong>: 사용자가 웹 애플리케이션에 로그인</p>
</li>
<li><p><strong>세션 유지</strong>: 로그인 후 세션 쿠키가 브라우저에 저장</p>
</li>
<li><p><strong>악성 웹사이트 방문</strong>: 사용자가 다른 웹사이트를 방문. 이 웹사이트는 CSRF 공격 코드를 포함</p>
</li>
<li><p><strong>악의적인 요청 전송</strong>: 악성 웹사이트는 사용자의 세션 쿠키를 이용해 원본 웹 애플리케이션으로 요청</p>
</li>
<li><p><strong>서버 처리</strong>: 서버는 요청을 정상적인 사용자의 요청으로 인식하고 처리</p>
</li>
</ol>
<h3 id="공격예시">공격예시</h3>
<ul>
<li><p>공격자 웹페이지 코드</p>
<pre><code class="language-java">  &lt;!DOCTYPE html&gt;
  &lt;html&gt;
  &lt;body&gt;
    &lt;h1&gt;Free Gift&lt;/h1&gt;
    &lt;img src=&quot;http://bank.com/transfer?amount=1000&amp;to=attacker&quot; style=&quot;display:none;&quot; /&gt;
  &lt;/body&gt;
  &lt;/html&gt;</code></pre>
<ul>
<li>이 예시에서 사용자가 이 페이지를 방문하면, 이미지 태그를 통해 <a href="http://bank.com/transfer?amount=1000&amp;to=attacker">http://bank.com/transfer?amount=1000&amp;to=attacker</a> 요청이 자동으로 실행된다. 사용자가 이미 bank.com에 로그인되어 있다면, 이 요청은 인증된 상태로 처리된다.</li>
</ul>
</li>
</ul>
<h3 id="방지방법">방지방법</h3>
<ul>
<li><p><strong>Referer 헤더 검증</strong></p>
<ul>
<li>서버는 요청의 Referer 헤더를 확인하여 요청이 신뢰할 수 있는 출처에서 온 것인지 확인할 수 있다. 그러나, Referer 헤더는 사용자가 조작할 수 있고, 일부 브라우저에서는 이 헤더를 포함하지 않을 수 있다.</li>
</ul>
</li>
<li><p><strong>CSRF 토큰 사용</strong></p>
<ul>
<li>가장 일반적인 방지 방법은 CSRF 토큰을 사용하는 것. 서버는 각 요청에 대해 고유한 토큰을 생성하고, 이를 폼에 포함시킨다. 서버는 요청이 들어올 때 이 토큰을 검증한다.</li>
</ul>
</li>
<li><p>form 대신 API사용</p>
<ul>
<li>API를 통해 JSON데이터로 통신한다면 해당 이슈를 피할 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="csrf-토큰-실습">CSRF 토큰 실습</h2>
<ul>
<li><a href="http://start.spring.io">start.spring.io</a> 에 접속하여 프로젝트를 생성한다. 디펜던시는 아래와 같이 구성한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/756372ba-b7d5-4324-b1e6-644f86a639c0/image.png" alt=""></p>
<ul>
<li><p>SampleContorller.java</p>
<pre><code class="language-java">  import org.springframework.stereotype.Controller;
  import org.springframework.web.bind.annotation.GetMapping;
  import org.springframework.web.bind.annotation.PostMapping;
  import org.springframework.web.bind.annotation.RequestParam;

  @Controller
  public class SampleContorller {

      @GetMapping(&quot;/&quot;)
      public String showForm() {
          return &quot;form&quot;;
      }

      @PostMapping(&quot;/submit&quot;)
      public String  handleFormSubmit(@RequestParam(&quot;name&quot;) String name, @RequestParam(&quot;_csrf&quot;) String csrfToken) {
          // CSRF 토큰 로그 출력
          System.out.println(&quot;Received CSRF token: &quot; + csrfToken);
          System.out.println(&quot;Received name: &quot; + name);
          return &quot;result&quot;;
      }
  }
</code></pre>
</li>
<li><p>SampleSecurityConfig.java</p>
<pre><code class="language-java">  import org.springframework.context.annotation.Bean;
  import org.springframework.context.annotation.Configuration;
  import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  import org.springframework.security.web.SecurityFilterChain;
  import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

  @Configuration
  public class SampleSecurityConfig {

      @Bean
      public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
          http
                  .authorizeHttpRequests(authorize -&gt; authorize
                          .anyRequest().permitAll()
                  )
                  .csrf(csrf -&gt; csrf
                          .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                  );

          return http.build();
      }
  }
</code></pre>
</li>
<li><p>resources/templates/form.html</p>
<pre><code class="language-html">  &lt;!DOCTYPE html&gt;
  &lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
  &lt;head&gt;
      &lt;title&gt;CSRF Example&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
  &lt;form th:action=&quot;@{/submit}&quot; method=&quot;post&quot;&gt;
      &lt;label for=&quot;name&quot;&gt;Name:&lt;/label&gt;
      &lt;input type=&quot;text&quot; id=&quot;name&quot; name=&quot;name&quot;/&gt;
      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;
  &lt;/form&gt;
  &lt;/body&gt;
  &lt;/html&gt;</code></pre>
</li>
<li><p>resources/templates/result.html</p>
<pre><code class="language-html">  &lt;!DOCTYPE html&gt;
  &lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
  &lt;head&gt;
      &lt;title&gt;Result&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
  &lt;h1&gt;Form submitted successfully!&lt;/h1&gt;
  &lt;a th:href=&quot;@{/}&quot;&gt;Go back to form&lt;/a&gt;
  &lt;/body&gt;
  &lt;/html&gt;</code></pre>
</li>
<li><p>애플리케이션을 살행시킨 후,  <a href="http://localhost:8080/">http://localhost:8080</a>에 접속한다. 크롬 검사 탭을 클릭하여 Elements 탭을 확인한다. 폼 안에 _csrf가 있는것을 확인할 수 있다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/248a5be5-c6bd-49bb-bdd3-553e124a5ccb/image.png" alt=""></p>
<ul>
<li>폼의 값을 입력하고 제출하면 애플리케이션 로그에 CSRF 값이 노출 되는것을 볼 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/5b3237c1-31f3-45fc-b25c-bb9c12e32c00/image.png" alt=""></p>
<ul>
<li>만약 폼dml _csrf 값을 임의로 수정 한 후 제출하면, 403 페이지로 이동하는 것을 볼 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh__/post/a6fb6a9c-cdf7-4101-ae6f-d33822b70882/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sh__/post/f15e6e0f-3420-466f-b792-8eaa2d5ccc42/image.png" alt=""></p>
<hr>
<h1 id="마무리">마무리</h1>
<p>CORS, CSRF에 대해 공부하다 궁금한점이 생겼다.</p>
<p><strong>❓ CORS의 Origin 설정을 프론트 서버로 한정할 경우 CSRF 공격에 대한 방어(CSRF 토큰 사용 등)를 할 필요가 없는가?</strong></p>
<p>튜터님과 ChatGPT로부터 얻은 답변을 통해 아래와 같이 결론내릴 수 있었다.</p>
<ul>
<li><p><strong>CORS는 보안 기능, CSRF 토큰은 일회용 키!</strong></p>
</li>
<li><p>현업에서는 <strong>CORS 보안과 CSRF 토큰 사용 둘 다 적용함</strong></p>
</li>
<li><p>만약 공격자가 <strong>프론트 화면(폼)에서만 변조</strong>하여 사용할 경우, 프론트 서버 도메인에서도 요청을 변조하여 보낼 수 있다.</p>
</li>
<li><p>또는 사용자가 의도치 않게 악성 사이트에서 HTTP 요청을 전송하게 되면, 그 요청은 <strong>동일한 Origin에서 발생한 것처럼 서버에 전달되게 할 수 있다.</strong></p>
</li>
<li><p>CSRF 토큰 사용 시 공격자는 사용자가 해당 사이트에 인증된 상태라는 점을 이용하더라도, <strong>유효한 CSRF 토큰을 생성할 수 없기 때문에 공격이 방어</strong>됨</p>
</li>
</ul>
<p>따라서 <strong>CORS 보안, CSRF 토큰 사용 두 가지 방법의 통합</strong>을 통해 더 뛰어난 보안성을 갖출 수 있을 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[에러 - Logging system failed to initialize using configuration from 'null']]></title>
            <link>https://velog.io/@sh__/%EC%97%90%EB%9F%AC-Logging-system-failed-to-initialize-using-configuration-from-null</link>
            <guid>https://velog.io/@sh__/%EC%97%90%EB%9F%AC-Logging-system-failed-to-initialize-using-configuration-from-null</guid>
            <pubDate>Fri, 16 Aug 2024 04:48:32 GMT</pubDate>
            <description><![CDATA[<h1 id="상황">상황</h1>
<ul>
<li>로그를 찍기 위해 @Slf4j 어노테이션을 사용하지 않고 import문을 이용해 로깅을 구현하다 제목과 같은 오류가 발생했다.</li>
</ul>
<pre><code class="language-java">import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.logging.Logger;

@RestController
public class SampleController {

    private static final Logger logger = (Logger) LoggerFactory.getLogger(SampleController.class);

    @GetMapping(&quot;/&quot;)
    public String hello(HttpServletResponse response) throws IOException {
        logger.info(&quot;403 Forbidden&quot;);
        response.sendError(HttpServletResponse.SC_FORBIDDEN, &quot;Access Denied&quot;);
        return null;

    }
}</code></pre>
<h1 id="에러-발생-원인-및-해결">에러 발생 원인 및 해결</h1>
<ul>
<li><p>로깅 시 Slf4j에서 import를 해야하는데 LoggerFactory는 제대로 가져왔지만 Logger 객체가 java.util.logging에서 가져온 객체인것을 확인할 수 있다.</p>
</li>
<li><p><code>import java.util.logging.Logger;</code>를 <code>import org.slf4j.Logger;</code>로 변경한다.</p>
</li>
<li><p>이후 Logger 선언문도 아래와 같이 수정한다.</p>
<ul>
<li>수정 전<pre><code>private static final Logger logger = (Logger) LoggerFactory.getLogger(SampleController.class);</code></pre></li>
<li>수정 후<pre><code>private static final Logger logger = LoggerFactory.getLogger(SampleController.class);</code></pre></li>
</ul>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>