<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>silver_cherry.log</title>
        <link>https://velog.io/</link>
        <description>전 체리 알러지가 있어요!</description>
        <lastBuildDate>Wed, 07 Aug 2024 08:18:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>silver_cherry.log</title>
            <url>https://velog.velcdn.com/images/silver_cherry/profile/4e50baae-2343-4b13-b21b-e6edc3307714/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. silver_cherry.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/silver_cherry" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2024 인프콘 참여 후기]]></title>
            <link>https://velog.io/@silver_cherry/2024-%EC%9D%B8%ED%94%84%EC%BD%98-%EC%B0%B8%EC%97%AC-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@silver_cherry/2024-%EC%9D%B8%ED%94%84%EC%BD%98-%EC%B0%B8%EC%97%AC-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 07 Aug 2024 08:18:15 GMT</pubDate>
            <description><![CDATA[<p align="center">
  <img src="https://velog.velcdn.com/images/silver_cherry/post/3fbde6d1-513c-4bc5-8835-26770c95608b/image.jpeg" alt="인프콘 참가확인증" width="45%" />
  <img src="https://velog.velcdn.com/images/silver_cherry/post/5545089d-007a-40fb-8738-273e2232e3f6/image.png" alt="인프콘 첫 입장시 받는 네임태그" width="45%" />
</p>


<p>2024 인프콘을 다녀왔습니다.
같이 가기로 했던 분들은 아쉽게도 떨어져서 혼자 다녀왔어요.
저는 지방(대구)에서 서울로 출발해서 새벽부터 분주하게 준비했습니다.
<del>(TMI이지만, 대구-서울 기준 일반 티켓보다 내일로로 해서 가는게 더 저렴합니다.)</del></p>
<p align="center">
  <img src="https://velog.velcdn.com/images/silver_cherry/post/f459f496-7e97-4362-a6c1-f17c61aa71d1/image.png" alt="서울역 도착" width="45%" />
  <img src="https://velog.velcdn.com/images/silver_cherry/post/9545bed1-3c81-4aa2-82e5-963a26c6b4d1/image.png" alt="등록부스 위치 안내" width="45%" />
</p>

<p>도착하자마자 인프콘 가방과 물 받았습니다.
너무 덥고 정신 없어서 사진은 따로 못찍었네요.
<img src="https://velog.velcdn.com/images/silver_cherry/post/4ba646fb-313d-4bfb-ac5c-0ff161b5eeb9/image.png" alt="등록부스 위치 안내" width="30%" /></p>
<p>정말 사람이 많았고, 1층은 조금 더웠습니다. 2층은 정말 시원했어요.</p>
<p align="center">
  <img src="https://velog.velcdn.com/images/silver_cherry/post/acd7b8fe-b758-47da-80d2-be4d676f5eff/image.png" alt="SIPE" width="30%" />
  <img src="https://velog.velcdn.com/images/silver_cherry/post/5d5ae245-7b82-4c51-be4f-80fd19e2f03b/image.png" alt="인프콘 참여자 목록" width="30%" />
  <img src="https://velog.velcdn.com/images/silver_cherry/post/c497e130-fb86-4fbf-b0f8-4d11d8ad5068/image.png" alt="네트워킹 맵" width="30%" />
</p>

<p>다양한 기업, 동아리가 있었습니다. 
여러 부스들을 돌면서 기업에 대한 설명과 동아리에 대한 설명을 들을 수 있었어요.
더군다나 설문 조사 참여를 통한 경품 추첨도 가능했어요.
제 기억속엔 채용하는 곳은 없었고 인재풀 등록은 가능했습니다.</p>
<p>여러개의 세션을 들었지만, 기억에 남는 세션은 2가지가 있었습니다.</p>
<h3 id="1-인프런-아키텍처-2024--2025">1. 인프런 아키텍처 2024 ~ 2025</h3>
<p><img src="https://velog.velcdn.com/images/silver_cherry/post/ec25edf7-5f9a-43e6-924c-7ba13d0aa21c/image.png" alt=""></p>
<h4 id="요약">요약</h4>
<ol>
<li>2023 리뷰
각 조직의 점진적 개선을 목표로 하였지만, 현실은 어떤 건 레거시가 개선되고 어떤 건 안되었다.
DevOps를 제외한 개발팀(FE/BE)만 20명인데 글로벌 진출 하려고하니 힘들었다.
예를 들어, 랠릿 개발자인데 인프런 개편을 하려고 하니 속도가 나지 않았다.</li>
<li>트래픽 비용 개선
환율 증가도 한 몫 했다. 국제화 오픈 전에 비효율을 개편하고자 했다.
그 중 쉬운 것은 이미지 트래픽 개선이다.
인프런에 들어가면 강의 썸네일 이미지가 있다. png와 jpeg는 파일 크기가 크다.
50kb * 60개 = 1페이지(약 3mb)
avif라는 포맷이 각광받고 있다.
요즘 대부분의 브라우저를 지원해준다.</li>
<li>API 환경 개선
내부 백엔드끼리는 세션체크X(특정한 인증키로만 체크) → 외부에서 노출 되어버림 → 외부, 내부 코드를 분리하자 → 이러면 모든 백엔드 인프라가 2배로 늘어남 → API Path 1단계 추가(프론트에 /client 추가)
traefik이라는 Reverse Proxy 사용 → 서버 재시작 없는 변경 가능, 안정적이고 편함</li>
<li>앞으로
다음 달부터 국제화가 시작될거다. 현재는 사전작업(인프라, 타팀 노하우 축적)
내년부터 인프런 일부 다국어 지원이 가능할거다. 아마 이건 2025 인프콘에서 다룰 듯 하다.
아무도 경험이 없다. 잘 할수 있을까? → <strong>펠리컨적 사고</strong></li>
</ol>
<h4 id="느낀-점">느낀 점</h4>
<p>특강 다시보기가 곧 올라오기 때문에 특강 때 찍어두었던 사진은 올리지 않겠습니다.
이 세션을 들으며 느꼈던 점은, &quot;와... 정말 물 흐르듯이 이해가 잘 된다..&quot; 였습니다.
마치 내가 회사 직원이 된 것 처럼 말씀 해주셨던 이야기의 흐름이 너무 좋았습니다.
개발을 잘 모르는 분들이 들어도 이해가 잘 될 만큼 정말 설명을 잘 해주셨습니다.
이 세션을 들으면서 포트폴리오를 수정해야겠다는 생각만 오만번 정도 들었습니다..ㅋㅋ</p>
<hr>
<h3 id="2-클린-스프링">2. 클린 스프링</h3>
<p><img src="https://velog.velcdn.com/images/silver_cherry/post/35e0b0dc-3c06-4a17-8400-741081ef6829/image.png" alt=""></p>
<h4 id="요약-1">요약</h4>
<p>클린 코드는 작동하는 코드여야한다.
클린 코드는 한마디로 유지보수성이다. (변경 가능성과 동의어)
내가 작성한 코드를 다른 개발자도 봐야하기 때문에 클린 코드를 사용한다.
<em>클린 아키텍처</em> 이 책엔 실용적인 예제가 많다.
생산성과 유지보수는 배타적이지 않다. 근데 왜 다들 반비례라고 하지?
<strong>기술 부채(Technical Dept): 빠르게 출시하고 리팩터링(부채를 상환한다.) 진행하자! 그렇지 않으면 잊가 쌓여서 큰 부담이 된다.</strong>
코드를 대충 하라는게 아니고, 처음부터 리팩터링하기 좋은 코드를 개발하란 뜻
부채가 효과를 발휘하려면 리팩터링(부채상환)이 필요하다.</p>
<p>그럼 시작은 어떻게 해야할까?
개발 시작은 빠르고 간단하게, 핵심 기능에 집중하자.
리팩터링 잘 하려면 테스트가 반드시 필요하다.
테스트를 하는 것에 대해 지원하는 팀이면 좋지만, 현실적으로 힘들다. 그럼 <strong>테스트를 빨리 만들면 된다</strong>
요즘엔 깃허브 코파일럿이 테스트 코드 작성을 잘 도와준다.
팀원과 함께 탐험하는 걸 즐거워하라
교양 있는 개발자가 되어라.
기분 나쁠 것 같다고 말 하지 말아야지는 교양이 아니다.
혼자 많은 짐을 지려고 하지 말고, 죄책감을 가지지 말아라.</p>
<h4 id="느낀-점-1">느낀 점</h4>
<p><em>테스트를 안 만들거면 스프링은 뭐하러?</em> 라는 말이 정말 인상 깊었습니다.
테스트의 중요성을 알고 있긴 했지만, 다시 한 번 일깨워주는 세션이였고 해이해질때마다 이 세션을 다시 보겠다고 다짐하였습니다.
테스트에 대하여 지지하지 않는 팀이여도, 테스트를 빨리 만들면 된다..ㅋㅋㅋ
테스트도 만들다보면 언젠가는 속도가 늘지 않을까요? 그 순간이 오길 기대하며 테스트를 많이 만드는 경험을 해봐야할 것 같습니다.</p>
<hr>
<p>이번에 인프런을 처음 다녀왔습니다.
다녀오고나서 열이 39도 이상 넘고, 응급실에 일반 병원에.. 사실 아직까지도 컨디션이 별로 좋지는 않습니다.
하지만 후회는 없고, 내년에도 기회만 된다면 인프콘은 꼭 가고 싶습니다.
리프레쉬 되는 느낌이였고 개발자라는 꿈을 가지게 되었을 때 그 느낌, 개발을 처음 성공 했을 때 그 느낌을 다시 한 번 느낄 수 있었습니다.
더군다나 좋은 정보를 알 수 있어서 너무 좋았습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀 미배정자 조회 API 개선 및 보안 강화]]></title>
            <link>https://velog.io/@silver_cherry/%ED%8C%80-%EB%AF%B8%EB%B0%B0%EC%A0%95-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A1%B0%ED%9A%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%8B%9C-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EB%B0%8F-API-%EC%A0%91%EA%B7%BC-%EA%B6%8C%ED%95%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@silver_cherry/%ED%8C%80-%EB%AF%B8%EB%B0%B0%EC%A0%95-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A1%B0%ED%9A%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%8B%9C-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EB%B0%8F-API-%EC%A0%91%EA%B7%BC-%EA%B6%8C%ED%95%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sun, 30 Jun 2024 14:11:32 GMT</pubDate>
            <description><![CDATA[<h4 id="1-문제-정의-및-초기-상황"><strong>1. 문제 정의 및 초기 상황</strong></h4>
<p>팀 미배정 사용자를 조회하는 API를 구현하는 과정에서 여러 가지 문제가 발생했습니다. 주요 문제는 다음과 같습니다:</p>
<ol>
<li><p><strong>팀 미배정 사용자 조회 시, 모든 트랙의 사용자가 조회됨</strong>:</p>
<ul>
<li><p>GET 요청을 통해 특정 트랙에 속한 사용자를 조회해야 했지만, 모든 트랙의 사용자가 조회되는 문제가 발생했습니다.
<img src="https://velog.velcdn.com/images/silver_cherry/post/e261ed3f-8a71-45b7-adc0-a2c5aa72f3d0/image.png" alt="">
<img src="https://velog.velcdn.com/images/silver_cherry/post/a1c84dce-bf6a-410c-8741-26f8599f10c6/image.png" alt=""></p>
</li>
<li><p>예상 원인: 쿼리나 서비스 로직에서 트랙 조건이 제대로 반영되지 않음</p>
</li>
</ul>
</li>
<li><p><strong>로그인 시 LazyInitializationException 발생</strong>:</p>
<ul>
<li>사용자 로그인 후 JWT를 생성하는 과정에서 Hibernate의 LazyInitializationException이 발생하여 세션을 초기화하지 못하는 문제가 있었습니다.</li>
<li>예상 원인: 지연 로딩된 엔티티에 접근하려고 할 때 세션이 닫혀 있었기 때문</li>
</ul>
</li>
<li><p><strong>API 접근 권한 문제</strong>:</p>
<ul>
<li>ROLE_ADMIN 권한이 필요한 API 엔드포인트에 일반 사용자(ROLE_USER)도 접근할 수 있는 문제가 발생했습니다.</li>
<li>예상 원인: Spring Security 설정이나 메서드의 권한 검증 로직에 문제가 있었을 가능성</li>
</ul>
</li>
</ol>
<hr>
<h4 id="2-문제-해결-과정"><strong>2. 문제 해결 과정</strong></h4>
<h5 id="21-팀-미배정-사용자-조회-문제-해결"><strong>2.1. 팀 미배정 사용자 조회 문제 해결</strong></h5>
<p><strong>문제 분석</strong>:</p>
<ul>
<li>팀 미배정 사용자를 특정 트랙에 대해서만 조회해야 하는데, 모든 트랙의 사용자가 조회되는 문제가 발생했습니다.</li>
</ul>
<p><strong>해결 방안</strong>:</p>
<ul>
<li>UserRepository에 JPQL 쿼리를 작성하여 trackId와 weekId를 조건으로 사용자 조회</li>
<li>UserService의 getUsersWithoutTeam 메서드를 수정하여 트랙과 주차 정보를 제대로 반영하도록 함</li>
</ul>
<pre><code class="language-java">// UserRepository.java
@Query(&quot;SELECT u FROM User u WHERE u.userId IN &quot; +
       &quot;(SELECT tp.user.userId FROM TrackParticipants tp WHERE tp.track.trackId = :trackId) &quot; +
       &quot;AND u.userId NOT IN &quot; +
       &quot;(SELECT tm.user.userId FROM TeamMembers tm JOIN tm.team t WHERE t.trackWeek.trackWeekId = :weekId)&quot;)
List&lt;User&gt; findUsersWithoutTeam(@Param(&quot;trackId&quot;) Long trackId, @Param(&quot;weekId&quot;) Long weekId);</code></pre>
<pre><code class="language-java">// UserService.java
@Transactional(readOnly = true)
public List&lt;UnassignedUserResponseDTO&gt; getUsersWithoutTeam(Long trackId, Long weekId) {
    Track track = trackRepository.findById(trackId)
            .orElseThrow(() -&gt; new CustomException(ErrorCode.TRACK_NOT_FOUND));
    TrackWeek trackWeek = trackWeekRepository.findById(weekId)
            .orElseThrow(() -&gt; new CustomException(ErrorCode.TRACK_WEEK_NOT_FOUND));

    List&lt;User&gt; users = userRepository.findUsersWithoutTeam(trackId, weekId);

    return users.stream()
            .map(user -&gt; new UnassignedUserResponseDTO(user.getUserId(), user.getUsername(), user.getEmail()))
            .collect(Collectors.toList());
}</code></pre>
<p><strong>결과</strong>:</p>
<ul>
<li>수정 후, 특정 트랙에 속한 팀 미배정 사용자만 정확히 조회되는 것을 확인했습니다.</li>
</ul>
<hr>
<h5 id="22-로그인-시-lazyinitializationexception-문제-해결"><strong>2.2. 로그인 시 LazyInitializationException 문제 해결</strong></h5>
<p><strong>문제 분석</strong>:</p>
<ul>
<li>Hibernate의 LazyInitializationException은 세션이 닫힌 상태에서 지연 로딩된 엔티티에 접근하려고 할 때 발생합니다.</li>
</ul>
<p><strong>해결 방안</strong>:</p>
<ul>
<li>TrackParticipantsRepository에 fetch join을 사용하여 필요한 엔티티를 한 번에 로딩</li>
<li>UserService의 getUsersWithoutTeam 메서드에서 사용자를 조회할 때 지연 로딩을 피하기 위해 fetch join 사용</li>
</ul>
<pre><code class="language-java">// TrackParticipantsRepository.java
@Query(&quot;SELECT tp FROM TrackParticipants tp JOIN FETCH tp.track t WHERE tp.user.userId = :userId&quot;)
List&lt;TrackParticipants&gt; findByUserUserId(Long userId);</code></pre>
<p><strong>결과</strong>:</p>
<ul>
<li>fetch join을 사용하여 로그인 시 LazyInitializationException이 더 이상 발생하지 않음을 확인했습니다.</li>
</ul>
<hr>
<h5 id="23-api-접근-권한-문제-해결"><strong>2.3. API 접근 권한 문제 해결</strong></h5>
<p><strong>문제 분석</strong>:</p>
<ul>
<li>ROLE_ADMIN 권한이 필요한 엔드포인트에 ROLE_USER도 접근할 수 있었습니다.</li>
</ul>
<p><strong>해결 방안</strong>:</p>
<ul>
<li>Spring Security 설정에서 @EnableGlobalMethodSecurity 대신 @EnableMethodSecurity 사용</li>
<li>UserService의 메서드에서 SecurityContextHolder를 통해 현재 사용자 권한을 명시적으로 확인</li>
</ul>
<pre><code class="language-java">// WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
    // 기존 설정 유지
}

// UserService.java
@Transactional(readOnly = true)
public List&lt;UnassignedUserResponseDTO&gt; getUsersWithoutTeam(Long trackId, Long weekId) {
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if (principal instanceof UserDetailsImpl) {
        UserDetailsImpl userDetails = (UserDetailsImpl) principal;
        boolean isAdmin = userDetails.getAuthorities().stream()
                .anyMatch(grantedAuthority -&gt; grantedAuthority.getAuthority().equals(&quot;ROLE_ADMIN&quot;));
        if (!isAdmin) {
            throw new CustomException(ErrorCode.FORBIDDEN);
        }
    } else {
        throw new CustomException(ErrorCode.FORBIDDEN);
    }
    // 나머지 로직
}</code></pre>
<p><strong>결과</strong>:</p>
<ul>
<li>ROLE_USER가 ROLE_ADMIN 권한이 필요한 엔드포인트에 접근할 수 없도록 제한했습니다.</li>
</ul>
<hr>
<h3 id="3-기술적-의사결정"><strong>3. 기술적 의사결정</strong></h3>
<ul>
<li><strong>JPQL 사용</strong>: 특정 트랙과 주차 조건을 적용하여 사용자 조회 로직을 구현하기 위해 JPQL 쿼리를 사용했습니다.</li>
<li><strong>fetch join 사용</strong>: Hibernate의 지연 로딩 문제를 해결하기 위해 fetch join을 사용하여 필요한 엔티티를 한 번에 로딩하도록 했습니다.</li>
<li><strong>Spring Security 설정 변경</strong>: 최신 Spring Security 버전에 맞게 <code>@EnableMethodSecurity</code>를 사용하고, 메서드 레벨에서 권한 검증을 명시적으로 수행하도록 수정했습니다.</li>
</ul>
<hr>
<h3 id="4-향후-계획"><strong>4. 향후 계획</strong></h3>
<ul>
<li>추가적인 테스트를 통해 다른 부분에서도 동일한 문제가 발생하지 않는지 확인할 예정입니다.</li>
<li>코드 리뷰를 통해 개선된 부분을 팀과 공유하고, 향후 유사한 문제 발생 시 신속히 대응할 수 있도록 문서화할 계획입니다.</li>
<li>권한 관리 로직을 더욱 강화하여 프로젝트의 보안성을 지속적으로 향상시킬 예정입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT 토큰의 저장 방식]]></title>
            <link>https://velog.io/@silver_cherry/JWT-%ED%86%A0%ED%81%B0%EC%9D%98-%EC%A0%80%EC%9E%A5-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@silver_cherry/JWT-%ED%86%A0%ED%81%B0%EC%9D%98-%EC%A0%80%EC%9E%A5-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Sun, 23 Jun 2024 09:52:25 GMT</pubDate>
            <description><![CDATA[<p><strong>프로젝트 배경</strong>: 서버사이드 렌더링을 활용한 농촌 일자리 플랫폼 구축 프로젝트에서 초기에는 사용자 인증 정보를 HTTP 헤더를 통해 JWT 토큰 형태로 전송하였습니다. 이 방식은 사용자가 매 API 요청마다 토큰을 수동으로 첨부해야 하는 불편함과 함께, 웹 애플리케이션의 보안 취약점이 노출되는 문제가 있었습니다.</p>
<p><strong>변경 요청</strong>: 프런트엔드 개발자로부터, 보안을 강화하고 클라이언트의 토큰 관리를 자동화할 수 있는 방안으로 쿠키 기반의 토큰 관리를 제안받았습니다. 특히, 쿠키를 통해 HTTPOnly 및 Secure 플래그를 설정하여 XSS 공격으로부터 토큰을 보호하고 HTTPS 통신만을 통해 토큰이 전송되게 할 수 있음을 지적받았습니다.</p>
<p><strong>기술적 고민</strong>: 토큰을 쿠키에 저장하기로 결정하기 전, 주요 보안 위험 중 하나인 CSRF 공격 가능성을 고려했습니다. 이를 해결하기 위해, CSRF 토큰을 도입하거나 쿠키의 SameSite 속성을 조정하는 방안을 심도 있게 검토했습니다.</p>
<p><strong>구현 경험</strong>:</p>
<ul>
<li><strong>JWT 토큰 생성 및 쿠키 설정</strong>: 인증 후, 생성된 JWT 토큰을 <code>Set-Cookie</code> 헤더를 통해 클라이언트에 전달했습니다. 쿠키는 <code>HTTPOnly</code>, <code>Secure</code>, <code>SameSite=Strict</code> 속성을 설정하여 보안을 강화했습니다. 이는 개발 초기에 스크럼을 통한 시뮬레이션을 통해 여러 보안 설정의 효과를 확인한 결과를 바탕으로 결정되었습니다.</li>
<li><strong>토큰 인증 및 쿠키 관리</strong>: 모든 요청에서 쿠키를 통해 자동으로 토큰이 서버에 전송되었으며, 서버에서는 이 쿠키를 파싱하여 사용자의 인증 정보를 검증했습니다. 쿠키 기반 인증은 특히 고객의 접근성과 편의성을 크게 향상시켰습니다.</li>
</ul>
<p><strong>기술적 성과 및 반성</strong>:</p>
<ul>
<li><strong>성과</strong>: 이 변경으로 인해 사용자 인증의 안정성과 보안이 크게 향상되었습니다. 또한, 서버와 클라이언트 사이의 통신이 간소화되면서 전반적인 시스템 성능이 개선되었습니다.</li>
<li><strong>반성 및 향후 계획</strong>: CSRF 공격 방어 전략은 지속적으로 검토하고 강화할 필요가 있습니다. 추가로, 쿠키 기반 인증의 여러 측면에서 발생할 수 있는 보안 문제에 대해 주기적인 스크럼과 리뷰를 통해 개선 방안을 모색할 계획입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[기업회원 전체 조회 + 페이지네이션 + 필터링 기능 구현중 발생한 트러블과 기술적 의사결정]]></title>
            <link>https://velog.io/@silver_cherry/%EA%B8%B0%EC%97%85%ED%9A%8C%EC%9B%90-%EC%A0%84%EC%B2%B4-%EC%A1%B0%ED%9A%8C-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-%ED%95%84%ED%84%B0%EB%A7%81-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%EC%A4%91-%EB%B0%9C%EC%83%9D%ED%95%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EA%B3%BC-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95</link>
            <guid>https://velog.io/@silver_cherry/%EA%B8%B0%EC%97%85%ED%9A%8C%EC%9B%90-%EC%A0%84%EC%B2%B4-%EC%A1%B0%ED%9A%8C-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-%ED%95%84%ED%84%B0%EB%A7%81-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%EC%A4%91-%EB%B0%9C%EC%83%9D%ED%95%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EA%B3%BC-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95</guid>
            <pubDate>Sat, 22 Jun 2024 21:30:13 GMT</pubDate>
            <description><![CDATA[<h4 id="1-문제-정의-및-초기-요구사항">1. 문제 정의 및 초기 요구사항</h4>
<p>기업회원의 전체 조회 기능은 다음과 같은 요구사항을 포함하고 있었습니다:</p>
<ul>
<li>페이지네이션을 통해 대량의 데이터를 효율적으로 처리</li>
<li>필터링을 통해 사용자가 원하는 조건에 맞는 데이터만 조회</li>
<li>조회 시 각 기업회원의 조회수(viewCount)를 증가시킴</li>
</ul>
<h4 id="2-트러블슈팅-과정">2. 트러블슈팅 과정</h4>
<h5 id="문제-1-viewcount-초기화-문제">문제 1: <code>viewCount</code> 초기화 문제</h5>
<p>초기에는 <code>viewCount</code> 필드가 <code>null</code>인 상태로 조회되는 경우가 발생했습니다. 이는 신규 기업회원 등록 시 <code>viewCount</code>가 명시적으로 설정되지 않았기 때문입니다.</p>
<p><strong>해결책</strong>: 엔티티 코드에서 <code>viewCount</code> 필드의 디폴트 값을 0으로 설정하였습니다.</p>
<pre><code class="language-java">@Column(nullable = false, columnDefinition = &quot;INTEGER DEFAULT 0&quot;)
private Integer viewCount;</code></pre>
<h5 id="문제-2-데이터베이스-쿼리-최적화">문제 2: 데이터베이스 쿼리 최적화</h5>
<p>대량의 기업회원 데이터를 필터링하고 페이지네이션 하면서 성능 이슈가 발생했습니다. 특히, 특정 필드로 정렬 및 필터링하는 과정에서 쿼리 응답 시간이 길어졌습니다.</p>
<p><strong>해결책</strong>: 다음과 같은 최적화 작업을 수행했습니다:</p>
<ul>
<li><strong>인덱스 설정</strong>: 자주 필터링 및 정렬에 사용되는 필드에 대해 인덱스를 추가했습니다.<pre><code class="language-sql">CREATE INDEX idx_cp_name ON CompanyUsers(cpName);
CREATE INDEX idx_cp_rating ON CompanyUsers(averageRating);</code></pre>
</li>
<li><strong>JPQL 및 Criteria API 활용</strong>: JPA에서 제공하는 JPQL 및 Criteria API를 통해 동적 쿼리를 생성하고 최적화된 쿼리를 실행했습니다.<pre><code class="language-java">public Page&lt;CompanyUser&gt; findAllWithFilters(String sort, Float minRating, Float maxRating, Pageable pageable) {
    // 동적 쿼리 생성 로직
}</code></pre>
</li>
</ul>
<h5 id="문제-3-필터링-로직의-복잡성">문제 3: 필터링 로직의 복잡성</h5>
<p>필터링 조건이 복잡해지면서 코드의 가독성이 떨어지고 유지보수가 어려워졌습니다.</p>
<p><strong>해결책</strong>: 필터링 로직을 별도의 서비스 클래스로 분리하여 책임을 분산시켰습니다.</p>
<ul>
<li><strong>Specification 패턴</strong>: JPA Specification을 사용하여 필터링 조건을 캡슐화하고, 이를 조합하여 동적 쿼리를 생성했습니다.<pre><code class="language-java">public class CompanyUserSpecification implements Specification&lt;CompanyUser&gt; {
    @Override
    public Predicate toPredicate(Root&lt;CompanyUser&gt; root, CriteriaQuery&lt;?&gt; query, CriteriaBuilder cb) {
        // 필터링 조건 로직
    }
}</code></pre>
</li>
</ul>
<h4 id="3-기술적-의사결정">3. 기술적 의사결정</h4>
<h5 id="의사결정-1-페이지네이션-전략-선택">의사결정 1: 페이지네이션 전략 선택</h5>
<p>페이지네이션은 클라이언트와 서버 간의 데이터 전송량을 줄이고 응답 속도를 개선하기 위해 필수적입니다. 이를 위해 Spring Data JPA의 <code>Pageable</code> 인터페이스를 활용했습니다.</p>
<ul>
<li><strong>장점</strong>: 표준화된 방식으로 쉽게 페이지네이션을 구현할 수 있으며, Spring Data JPA에서 제공하는 다양한 기능을 활용할 수 있습니다.</li>
<li><strong>결정 이유</strong>: 복잡한 커스터마이징 없이도 강력한 페이지네이션 기능을 제공하며, 프로젝트의 유지보수성을 높일 수 있습니다.</li>
</ul>
<h5 id="의사결정-2-동적-필터링-구현">의사결정 2: 동적 필터링 구현</h5>
<p>기업회원 조회 시 다양한 조건으로 필터링할 수 있도록 요구되었습니다. 이를 위해 Criteria API와 Specification 패턴을 도입했습니다.</p>
<ul>
<li><strong>장점</strong>: 동적 쿼리를 효율적으로 생성할 수 있으며, 코드의 재사용성과 가독성을 높일 수 있습니다.</li>
<li><strong>결정 이유</strong>: 다양한 필터링 조건을 유연하게 처리할 수 있으며, 추후 필터링 조건이 추가되거나 변경되더라도 쉽게 확장할 수 있습니다.</li>
</ul>
<h5 id="의사결정-3-조회수-증가-로직">의사결정 3: 조회수 증가 로직</h5>
<p>기업회원 프로필 조회 시마다 <code>viewCount</code>를 증가시키는 요구사항이 있었습니다. 이를 트랜잭션 내부에서 처리하여 데이터의 일관성을 보장했습니다.</p>
<ul>
<li><strong>장점</strong>: 트랜잭션 내에서 안전하게 조회수 증가 로직을 처리할 수 있으며, 동시성 문제를 예방할 수 있습니다.</li>
<li><strong>결정 이유</strong>: 데이터의 일관성과 무결성을 보장하기 위해 트랜잭션 내에서 처리하는 것이 적절하다고 판단했습니다.</li>
</ul>
<h4 id="4-결론">4. 결론</h4>
<p>이번 기업회원 전체 조회, 페이지네이션 및 필터링 기능 구현은 다음과 같은 트러블슈팅과 기술적 의사결정을 통해 성공적으로 완성되었습니다:</p>
<ul>
<li><code>viewCount</code> 필드 초기화 문제 해결</li>
<li>데이터베이스 쿼리 최적화</li>
<li>필터링 로직의 복잡성 해소</li>
<li>페이지네이션 및 동적 필터링 전략 결정</li>
</ul>
<p>이 과정에서 성능과 유지보수성을 고려한 다양한 최적화 작업과 디자인 패턴을 적용하였으며, 이를 통해 보다 효율적이고 확장 가능한 시스템을 구축할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[IMAP 이메일 답신에 따른 리뷰 삭제 처리 기능 구현 중 발생한 문제와 기술적 의사결정 과정]]></title>
            <link>https://velog.io/@silver_cherry/IMAP-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%8B%B5%EC%8B%A0%EC%97%90-%EB%94%B0%EB%A5%B8-%EB%A6%AC%EB%B7%B0-%EC%82%AD%EC%A0%9C-%EC%B2%98%EB%A6%AC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%EC%A4%91-%EB%B0%9C%EC%83%9D%ED%95%9C-%EB%AC%B8%EC%A0%9C%EC%99%80-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@silver_cherry/IMAP-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%8B%B5%EC%8B%A0%EC%97%90-%EB%94%B0%EB%A5%B8-%EB%A6%AC%EB%B7%B0-%EC%82%AD%EC%A0%9C-%EC%B2%98%EB%A6%AC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%EC%A4%91-%EB%B0%9C%EC%83%9D%ED%95%9C-%EB%AC%B8%EC%A0%9C%EC%99%80-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Fri, 21 Jun 2024 08:20:38 GMT</pubDate>
            <description><![CDATA[<h4 id="1-문제-정의-및-요구사항-분석">1. 문제 정의 및 요구사항 분석</h4>
<p>프로젝트의 요구사항은 사용자가 이메일을 통해 리뷰 삭제 요청을 받으면, 해당 이메일의 답신을 기반으로 리뷰를 삭제하거나 상태를 업데이트하는 기능을 구현하는 것이었습니다. 이를 위해 IMAP 프로토콜을 사용하여 이메일 서버와 상호작용하며, 이메일 답신을 분석하여 적절한 데이터베이스 조작을 수행해야 했습니다.</p>
<h4 id="2-트러블-슈팅-과정">2. 트러블 슈팅 과정</h4>
<h5 id="21-외래-키-제약-조건-위반-문제">2.1. 외래 키 제약 조건 위반 문제</h5>
<p><strong>문제</strong>: 리뷰 삭제 시, <code>review_visibility_requests</code> 테이블의 외래 키 제약 조건이 위반되는 오류가 발생했습니다.</p>
<ul>
<li><strong>오류 메시지</strong>: <code>Cannot delete or update a parent row: a foreign key constraint fails</code></li>
</ul>
<p><strong>분석 및 해결</strong>:</p>
<ul>
<li><strong>원인</strong>: <code>reviews</code> 테이블의 리뷰가 삭제될 때, 해당 리뷰를 참조하는 <code>review_visibility_requests</code> 테이블의 데이터가 남아 있어 외래 키 제약 조건이 위반되었습니다.</li>
<li><strong>해결</strong>: 리뷰를 물리적으로 삭제하는 대신, 리뷰의 상태를 <code>DELETED</code>로 업데이트하여 논리적으로 삭제 처리하는 방법을 선택했습니다. 이를 통해 외래 키 제약 조건을 유지하면서 데이터의 무결성을 보장했습니다.</li>
</ul>
<h5 id="22-이메일-답신-처리-로직-문제">2.2. 이메일 답신 처리 로직 문제</h5>
<p><strong>문제</strong>: 이메일 답신을 제대로 분석하지 못해, 올바른 리뷰 ID를 추출하거나 답신에 따라 적절한 작업을 수행하지 못하는 문제 발생.</p>
<ul>
<li><strong>오류 메시지</strong>: 리뷰 ID 추출 실패 및 이메일 내용 분석 오류</li>
</ul>
<p><strong>분석 및 해결</strong>:</p>
<ul>
<li><strong>원인</strong>: 이메일 본문에서 리뷰 ID 및 답신 내용을 정확히 추출하는 정규 표현식이 적절하지 않거나, 이메일 형식의 변동성에 의해 발생하는 문제.</li>
<li><strong>해결</strong>: 정규 표현식을 개선하고, 다양한 이메일 형식에 대응할 수 있도록 로직을 유연하게 변경했습니다. 이메일 본문 파싱 시, 예외 처리를 강화하여 불완전한 이메일 형식에도 안정적으로 대응할 수 있도록 했습니다.</li>
</ul>
<h5 id="23-동시성-문제">2.3. 동시성 문제</h5>
<p><strong>문제</strong>: 다중 스레드 환경에서 동시에 여러 리뷰 삭제 요청을 처리할 때, 데이터 무결성 문제 발생.</p>
<ul>
<li><strong>오류 메시지</strong>: 동시성 관련 예외 또는 데이터 무결성 위반</li>
</ul>
<p><strong>분석 및 해결</strong>:</p>
<ul>
<li><strong>원인</strong>: 다중 스레드가 동시에 동일한 리뷰에 접근하여 삭제 처리 시, 데이터 불일치 또는 충돌 발생.</li>
<li><strong>해결</strong>: 데이터베이스 트랜잭션을 사용하여 동시성 제어를 강화했습니다. 각 리뷰 삭제 요청은 트랜잭션 내에서 처리되며, 트랜잭션 롤백 메커니즘을 통해 데이터 무결성을 유지했습니다.</li>
</ul>
<h4 id="3-기술적-의사결정">3. 기술적 의사결정</h4>
<h5 id="31-논리적-삭제">3.1. 논리적 삭제</h5>
<ul>
<li><strong>결정</strong>: 외래 키 제약 조건 문제를 해결하기 위해, 리뷰를 물리적으로 삭제하는 대신 상태를 <code>DELETED</code>로 변경하여 논리적으로 삭제하는 방법을 채택했습니다.</li>
<li><strong>이유</strong>: 데이터 무결성을 유지하면서도 삭제된 리뷰의 기록을 보존할 수 있으며, 시스템의 안정성을 향상시킵니다.</li>
</ul>
<h5 id="32-imap-프로토콜-사용">3.2. IMAP 프로토콜 사용</h5>
<ul>
<li><strong>결정</strong>: 이메일 서버와의 상호작용을 위해 IMAP 프로토콜을 사용했습니다.</li>
<li><strong>이유</strong>: IMAP는 이메일의 본문과 메타데이터를 효율적으로 검색하고 처리할 수 있는 기능을 제공하여, 이메일 답신을 기반으로 한 리뷰 삭제 기능 구현에 적합합니다.</li>
</ul>
<h5 id="33-트랜잭션-관리">3.3. 트랜잭션 관리</h5>
<ul>
<li><strong>결정</strong>: 데이터베이스 트랜잭션을 사용하여 동시성 문제를 해결했습니다.</li>
<li><strong>이유</strong>: 동시성 제어를 통해 데이터 무결성을 유지하고, 다중 스레드 환경에서도 안정적인 동작을 보장할 수 있습니다.</li>
</ul>
<h4 id="4-구현-및-결과">4. 구현 및 결과</h4>
<h5 id="41-코드-구현">4.1. 코드 구현</h5>
<p>리뷰 삭제 시, 리뷰의 상태를 <code>DELETED</code>로 변경하는 로직을 추가하고, 이메일 답신을 처리하여 적절한 리뷰를 삭제 또는 상태 업데이트하는 기능을 구현했습니다.</p>
<pre><code class="language-java">@Transactional
public void deleteReview(Long reviewId) {
    Review review = reviewRepository.findByIdAndStatus(reviewId, ReviewStatus.ACTIVE)
            .orElseThrow(() -&gt; new ApiException(ErrorCode.REVIEW_NOT_FOUND));

    reviewVisibilityRequestRepository.updateVisibilityRequestStatusByReviewId(reviewId, ReviewerResponse.AGREED);

    review.setStatus(ReviewStatus.DELETED);
    reviewRepository.save(review);
}</code></pre>
<h5 id="42-결과">4.2. 결과</h5>
<ul>
<li>외래 키 제약 조건 문제 해결 및 데이터 무결성 유지</li>
<li>이메일 답신을 정확히 분석하여 리뷰 삭제 또는 상태 업데이트 기능 안정화</li>
<li>다중 스레드 환경에서의 동시성 문제 해결</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ 페이지네이션을 적용한 포인트 사용 내역 전체 조회시 발생한 문제]]></title>
            <link>https://velog.io/@silver_cherry/%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%9C-%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EC%82%AC%EC%9A%A9-%EB%82%B4%EC%97%AD-%EC%A0%84%EC%B2%B4-%EC%A1%B0%ED%9A%8C%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@silver_cherry/%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%9C-%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EC%82%AC%EC%9A%A9-%EB%82%B4%EC%97%AD-%EC%A0%84%EC%B2%B4-%EC%A1%B0%ED%9A%8C%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Thu, 13 Jun 2024 02:27:55 GMT</pubDate>
            <description><![CDATA[<p><strong>프로젝트 개요</strong></p>
<ul>
<li><strong>목적</strong>: 대규모 사용자 기반을 대상으로 하는 포인트 관리 시스템에서 포인트 적립 및 사용 내역 조회 기능 개선.</li>
<li><strong>기술 스택</strong>: Java, Spring Framework, JPA, QueryDSL, Swagger for API Documentation</li>
</ul>
<p><strong>발견된 문제</strong></p>
<ul>
<li>초기 구현에서는 포인트 사용 및 적립 내역 조회 API가 전체 데이터셋을 한 번에 로드하는 방식을 사용했습니다. 이 방법은 데이터 셋의 크기가 시간이 지남에 따라 선형적으로 증가함에 따라, API 응답 시간이 점차 늘어나는 문제를 초래했습니다. 사용자 경험의 저하는 물론, 서버 부하가 증가하여 시스템 전체의 안정성에 영향을 미쳤습니다.</li>
</ul>
<p><strong>문제 분석</strong></p>
<ul>
<li>성능 테스트를 통해, 데이터 로드 시 응답 시간이 평균 157.86ms임을 확인했습니다. 이는 사용자 경험을 저하시킬 수 있는 수준이며, 특히 모바일 사용자에게는 더욱 민감한 문제입니다.
<img src="https://velog.velcdn.com/images/silver_cherry/post/bca73178-6eb8-430a-a658-b4dfc6ace631/image.png" alt=""></li>
</ul>
<p><strong>해결책 구현</strong></p>
<ol>
<li><strong>커서 기반 페이지네이션</strong>: 전체 데이터셋을 한 번에 로드하는 대신, 커서 기반 페이지네이션을 도입하여 데이터를 부분적으로 로드하도록 구현했습니다. 이를 위해 QueryDSL을 사용하여 페이징 로직을 개발했고, <code>Long.MAX_VALUE</code>를 사용하여 최초 요청 시 최신 데이터부터 조회하도록 설정했습니다.</li>
<li><strong>RESTful API 개선</strong>: 기존의 <code>/points/used-details</code> API 경로를 REST 원칙에 부합하도록 <code>/points/used</code>로 변경하여 자원의 표현을 더 명확히 하고, API 경로의 일관성을 개선했습니다.</li>
<li><strong>Swagger 문서화</strong>: 페이지네이션 매개변수 <code>cursorId</code>와 <code>limit</code>에 대한 자세한 설명을 추가하여, API 문서를 개선함으로써 프론트엔드 개발자들이 API를 더 쉽게 이해하고 사용할 수 있도록 지원했습니다.
<img src="https://velog.velcdn.com/images/silver_cherry/post/8570640b-e0df-4e55-ac3f-43adc7bdd3c2/image.png" alt=""></li>
</ol>
<p><strong>성과 및 결과</strong></p>
<ul>
<li>적용 후 성능 측정 결과, API 응답 시간이 평균 36.72ms로 감소하여 약 77%의 응답 시간 단축을 실현했습니다. 이는 사용자 대기 시간을 현저히 줄이고, 서버 부하를 감소시켜 전체적인 시스템 성능을 향상시켰습니다.
<img src="https://velog.velcdn.com/images/silver_cherry/post/b84cd21f-8e94-479a-92d2-f9877d1de01f/image.png" alt=""></li>
<li>RESTful API 구조로의 전환과 Swagger 문서의 개선은 다른 개발 팀원들이 API를 더 쉽게 이해하고 효과적으로 사용할 수 있게 만들어, 프로젝트 내 협업 효율성을 증진시켰습니다.</li>
</ul>
<p><strong>리플렉션 및 학습</strong></p>
<ul>
<li>이 프로젝트를 통해 커서 기반 페이지네이션과 RESTful 설계의 중요성을 깊이 있게 이해하게 되었습니다. 또한 성능 최적화가 사용자 경험에 미치는 직접적인 영향을 체감할 수 있었고, 이는 앞으로의 개발 과정에서도 중요한 고려 사항이 될 것이라 생각합니다.</li>
<li>문서화의 중요성도 재확인하게 되었습니다. 효과적인 문서화는 개발자 간의 원활한 커뮤니케이션을 가능하게 하며, API의 신뢰성과 접근성을 크게 향상시긴다는 것을 깨달았습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDSL 적용 후, 첫 배포 중 발생한 문제]]></title>
            <link>https://velog.io/@silver_cherry/QueryDSL-%EC%84%A4%EC%A0%95-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@silver_cherry/QueryDSL-%EC%84%A4%EC%A0%95-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Thu, 13 Jun 2024 00:13:19 GMT</pubDate>
            <description><![CDATA[<p><strong>프로젝트 개요</strong>:
스프링 부트 버전을 3.3.0으로 업그레이드하면서 발생한 QueryDSL 설정과 관련된 문제를 해결한 경험을 설명합니다. 이 과정은 프로젝트의 데이터 접근 계층에 대한 안정성과 성능을 향상시키는 경험을 하는 단계였습니다.</p>
<p><strong>문제 상황 설명</strong>:
스프링 부트 3.3.0을 도입하면서 <code>javax</code> 패키지의 사용이 <code>jakarta</code> 네임스페이스로 전환되었습니다. 이 변경은 기존에 생성된 QueryDSL의 Q 클래스 파일들과 충돌을 일으켜, 프로젝트 빌드 시 <code>NoClassDefFoundError</code>와 같은 오류가 발생했습니다. 이 오류는 <code>javax.persistence.Entity</code>를 참조하는 기존 코드가 새로운 <code>jakarta.persistence.Entity</code> 기반으로 업데이트되지 않아 발생한 것입니다.</p>
<p><strong>문제 진단</strong>:
첫 번째 단계로, 기존의 Q 클래스 파일들이 문제의 원인이라는 점을 신속하게 파악했습니다. 이 파일들은 업데이트된 API와 호환되지 않아 프로젝트의 데이터 접근 계층에서 중대한 오류를 일으켰습니다. </p>
<p><strong>해결 과정</strong>:</p>
<ol>
<li><strong>코드 베이스 정리</strong>: 먼저, 자동 생성된 Q 클래스 파일들을 수동으로 삭제하여 깨끗한 상태에서 시작할 수 있도록 했습니다.</li>
<li><strong>Gradle 설정 업데이트</strong>: <code>build.gradle</code> 파일을 수정하여 QueryDSL 관련 의존성을 <code>jakarta</code> 기반으로 변경하고, 새로운 설정에 맞게 <code>compileQuerydsl</code> 태스크를 수동으로 정의했습니다. 이 태스크는 <code>JavaCompile</code> 클래스를 사용하여 적절한 클래스 경로와 출력 디렉토리 설정을 통해 Q 클래스 파일을 재생성하도록 구성되었습니다.</li>
<li><strong>컴파일 의존성 관리</strong>: 모든 Java 컴파일 작업이 이 새로운 <code>compileQuerydsl</code> 작업에 의존하도록 설정하여, 모든 데이터 엔티티가 최신 Jakarta 어노테이션을 사용하는 Q 클래스로부터 자동 생성되도록 했습니다.</li>
</ol>
<p><strong>결과 및 검증</strong>:
이러한 변경 후, 프로젝트는 정상적으로 빌드되었고, 데이터 접근 계층은 새로운 Jakarta EE 표준에 완전히 호환되었습니다. 또한, 이 과정을 통해 프로젝트의 유지 관리성과 확장성이 향상되었음을 확인할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[포인트 적립내역 조회 기능 개발 및 최적화]]></title>
            <link>https://velog.io/@silver_cherry/%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EC%A0%81%EB%A6%BD%EB%82%B4%EC%97%AD-%EC%A1%B0%ED%9A%8C-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EB%B0%9C-%EB%B0%8F-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@silver_cherry/%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EC%A0%81%EB%A6%BD%EB%82%B4%EC%97%AD-%EC%A1%B0%ED%9A%8C-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EB%B0%9C-%EB%B0%8F-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Wed, 12 Jun 2024 14:50:29 GMT</pubDate>
            <description><![CDATA[<h4 id="1-기능-설명">1. 기능 설명</h4>
<p>포인트 사용내역 조회 기능을 통해 사용자는 본인의 포인트 적립 타입, 적립액, 생성 시간을 확인할 수 있습니다. 이를 무한스크롤 페이지네이션 방식으로 구현하여 응답 시간을 최적화하였습니다.</p>
<h4 id="2-문제-및-해결-과정">2. 문제 및 해결 과정</h4>
<h5 id="21-초기-구현-및-문제-발생">2.1 초기 구현 및 문제 발생</h5>
<p><strong>초기 문제:</strong><br>초기 구현 시에는 포인트 적립내역 조회 기능을 페이지네이션 없이 구현하였습니다. 이로 인해 모든 데이터를 한 번에 가져오게 되어 응답 시간이 길어졌습니다. 아래는 초기 상태의 응답 시간입니다:</p>
<ul>
<li><strong>총 응답 시간:</strong> 28.39ms
<img src="https://velog.velcdn.com/images/silver_cherry/post/6483533a-3bcb-41ba-ad7a-44edac959d6b/image.png" alt=""></li>
</ul>
<p><strong>해결 과정:</strong><br>응답 시간을 최적화하기 위해 무한스크롤 페이지네이션을 도입하기로 결정하였습니다. 이를 통해 사용자는 필요할 때만 데이터를 불러오게 되어 서버 부하를 줄이고 응답 시간을 단축할 수 있습니다.</p>
<h5 id="22-무한스크롤-페이지네이션-도입">2.2 무한스크롤 페이지네이션 도입</h5>
<p><strong>구현 단계:</strong></p>
<ol>
<li><p><strong>Repository 수정:</strong><br><code>SavedPointRepository</code>에 무한스크롤 페이지네이션을 위한 메서드를 추가하였습니다.</p>
<pre><code class="language-java">package team9502.sinchulgwinong.domain.point.repository;

import team9502.sinchulgwinong.domain.point.entity.SavedPoint;

import java.util.List;

public interface SavedPointRepositoryCustom {
    List&lt;SavedPoint&gt; findSavedPointsWithCursor(Long pointId, Long cursorId, int limit);
}</code></pre>
</li>
<li><p><strong>Service 수정:</strong><br>페이지네이션 로직을 서비스 계층에 추가하였습니다.</p>
<pre><code class="language-java">@Transactional(readOnly = true)
public List&lt;SavedPointDetailResponseDTO&gt; getSpDetails(UserDetailsImpl userDetails, Long cursorId, int limit) {
    Long pointId = getPointIdFromUser(userDetails);
    cursorId = (cursorId == null) ? Long.MAX_VALUE : cursorId;
    List&lt;SavedPoint&gt; savedPoints = savedPointRepository.findSavedPointsWithCursor(pointId, cursorId, limit);
    return convertToSavedPointDetailResponseDTO(savedPoints);
}</code></pre>
</li>
<li><p><strong>Controller 수정:</strong><br><code>@RequestParam</code>을 통해 클라이언트가 커서와 제한 값을 전달할 수 있도록 하였습니다.</p>
<pre><code class="language-java">@GetMapping(&quot;/details&quot;)
@Operation(summary = &quot;포인트 적립 내역 조회&quot;, description = &quot;로그인한 사용자의 포인트 적립 내역을 커서 기반 페이지네이션으로 조회합니다.&quot;)
@ApiResponses({
    @ApiResponse(responseCode = &quot;200&quot;, description = &quot;포인트 적립 내역 조회 성공&quot;, content = @Content(mediaType = &quot;application/json&quot;, examples = @ExampleObject(value = &quot;{ \&quot;code\&quot;: \&quot;200\&quot;, \&quot;message\&quot;: \&quot;적립 포인트 조회 성공\&quot;, \&quot;data\&quot;: [{\&quot;type\&quot;: \&quot;REVIEW\&quot;, \&quot;savedPoint\&quot;: 300, \&quot;createdAt\&quot;: \&quot;2024-06-11\&quot;}, {\&quot;type\&quot;: \&quot;SIGNUP\&quot;, \&quot;savedPoint\&quot;: 300, \&quot;createdAt\&quot;: \&quot;2024-06-11\&quot;}] }&quot;))),
    @ApiResponse(responseCode = &quot;404&quot;, description = &quot;포인트를 찾을 수 없습니다.&quot;, content = @Content(mediaType = &quot;application/json&quot;, examples = @ExampleObject(value = &quot;{ \&quot;code\&quot;: \&quot;404\&quot;, \&quot;message\&quot;: \&quot;포인트를 찾을 수 없습니다.\&quot;, \&quot;data\&quot;: null }&quot;))),
    @ApiResponse(responseCode = &quot;500&quot;, description = &quot;서버 에러&quot;, content = @Content(mediaType = &quot;application/json&quot;, examples = @ExampleObject(value = &quot;{ \&quot;code\&quot;: \&quot;500\&quot;, \&quot;message\&quot;: \&quot;서버 에러\&quot;, \&quot;data\&quot;: null }&quot;)))
})
public ResponseEntity&lt;GlobalApiResponse&lt;List&lt;SavedPointDetailResponseDTO&gt;&gt;&gt; getPointDetails(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @RequestParam(value = &quot;cursorId&quot;, required = false) Long cursorId,
        @RequestParam(value = &quot;limit&quot;, defaultValue = &quot;6&quot;) int limit) {

    List&lt;SavedPointDetailResponseDTO&gt; responseDTOs = pointService.getSpDetails(userDetails, cursorId, limit);
    return ResponseEntity.status(SUCCESS_SAVED_POINT_READ.getHttpStatus())
            .body(GlobalApiResponse.of(SUCCESS_SAVED_POINT_READ.getMessage(), responseDTOs));
}</code></pre>
</li>
</ol>
<h5 id="23-결과-확인-및-최적화">2.3 결과 확인 및 최적화</h5>
<p><strong>결과 확인:</strong><br>페이지네이션 도입 후 응답 시간이 크게 줄어드는 것을 확인할 수 있었습니다.</p>
<ul>
<li><strong>페이지네이션 도입 후 총 응답 시간:</strong> 14.95ms
<img src="https://velog.velcdn.com/images/silver_cherry/post/87a993ab-586e-4d87-9a58-b13f91ea3984/image.png" alt=""></li>
</ul>
<h5 id="24-스웨거-문서화">2.4 스웨거 문서화</h5>
<p><strong>개발자 정보 추가:</strong><br>스웨거 문서화 시, 백엔드 개발자 정보도 함께 추가하여 사용자들이 쉽게 연락할 수 있도록 하였습니다.</p>
<pre><code class="language-java">package team9502.sinchulgwinong.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.OpenAPI;

@Configuration
public class SwaggerConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title(&quot;신출귀농 API 문서&quot;)
                        .description(&quot;신출귀농 애플리케이션의 API 문서입니다.\n\n백엔드 개발자:\n\n김은채 - ke808762@gmail.com\n\n창다은 - cdaeun95@gmail.com&quot;)
                        .version(&quot;1.0.0&quot;)
                        .contact(new Contact().name(&quot;9502&quot;).email(&quot;9502team@gmail.com&quot;)));
    }
}</code></pre>
<h4 id="3-선택지-및-선택-이유">3. 선택지 및 선택 이유</h4>
<ul>
<li><p><strong>전체 데이터 조회:</strong><br>모든 데이터를 한 번에 가져오는 방법은 간단하지만, 데이터가 많아지면 응답 시간이 길어지고 서버 부하가 커집니다. 이는 사용자 경험에 악영향을 미칠 수 있습니다.</p>
</li>
<li><p><strong>페이지네이션 도입:</strong><br>페이지네이션을 도입하면 필요한 데이터만 가져와 응답 시간을 줄이고 서버 부하를 줄일 수 있습니다. 특히 무한스크롤 페이지네이션은 사용자가 스크롤할 때마다 추가 데이터를 로드할 수 있어 사용자 경험을 향상시킵니다. 이 방법을 선택한 이유는 성능 최적화와 사용자 경험을 동시에 개선할 수 있기 때문입니다.</p>
</li>
</ul>
<h4 id="4-결론">4. 결론</h4>
<p>페이지네이션을 통한 최적화를 통해 응답 시간을 28.39ms에서 14.95ms으로 약 47% 개선되었고, 사용자 경험을 향상시켰습니다. 스웨거 문서화 작업을 통해 API 사용자들과 팀원들에게 더 나은 접근성을 제공하였습니다. 이번 경험을 통해 성능 최적화의 중요성을 다시 한번 인식하게 되었으며, 앞으로도 이러한 접근 방식을 유지해 나가고자합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDSL을 사용하여 리뷰 조회 기능에 추가 할 페이지네이션 설정 중 발생한 트러블 슈팅들]]></title>
            <link>https://velog.io/@silver_cherry/QueryDSL%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%A6%AC%EB%B7%B0-%EC%A1%B0%ED%9A%8C-%EA%B8%B0%EB%8A%A5%EC%97%90-%EC%B6%94%EA%B0%80-%ED%95%A0-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-%EC%84%A4%EC%A0%95-%EC%A4%91-%EB%B0%9C%EC%83%9D%ED%95%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EB%93%A4</link>
            <guid>https://velog.io/@silver_cherry/QueryDSL%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%A6%AC%EB%B7%B0-%EC%A1%B0%ED%9A%8C-%EA%B8%B0%EB%8A%A5%EC%97%90-%EC%B6%94%EA%B0%80-%ED%95%A0-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-%EC%84%A4%EC%A0%95-%EC%A4%91-%EB%B0%9C%EC%83%9D%ED%95%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EB%93%A4</guid>
            <pubDate>Tue, 11 Jun 2024 16:14:46 GMT</pubDate>
            <description><![CDATA[<h4 id="문제-1-illegalargumentexception-오류-발생">문제 1: <code>IllegalArgumentException</code> 오류 발생</h4>
<h5 id="문제-설명">문제 설명</h5>
<p>REST API를 호출할 때 다음과 같은 오류가 발생했습니다:</p>
<pre><code>java.lang.IllegalArgumentException: Name for argument of type [java.lang.Long] not specified, and parameter name information not available via reflection. Ensure that the compiler uses the &#39;-parameters&#39; flag.</code></pre><h5 id="문제-원인">문제 원인</h5>
<p>이 오류는 Spring이 런타임에 메서드 인자의 이름을 확인할 수 없어서 발생하였습니다. 더 자세히 말하자면, <code>@PathVariable</code> 또는 <code>@RequestParam</code> 어노테이션이 있는 메서드 인자가 명시적으로 이름이 지정되지 않았을 때 발생합니다. 자바 컴파일러가 기본적으로 메서드 인자 이름을 클래스 파일에 포함하지 않기 때문에, Spring이 해당 정보를 사용할 수 없게 되기 때문에 생긴 문제입니다.</p>
<h5 id="문제-해결-과정">문제 해결 과정</h5>
<ol>
<li><p><strong>빌드 설정 수정</strong>: <code>build.gradle</code> 파일에서 <code>JavaCompile</code> 태스크에 <code>-parameters</code> 플래그를 추가하여 자바 컴파일러가 메서드 인자 이름 정보를 클래스 파일에 포함하도록 설정했습니다.</p>
<pre><code class="language-groovy">tasks.withType(JavaCompile).configureEach {
    options.compilerArgs += [&quot;-parameters&quot;]
}</code></pre>
</li>
<li><p><strong>컨트롤러 메서드 수정</strong>: <code>@PathVariable</code> 및 <code>@RequestParam</code> 어노테이션에 명시적으로 이름을 지정했습니다.</p>
<pre><code class="language-java">@GetMapping(&quot;/cpUsers/{cpUserId}/reviews&quot;)
public ResponseEntity&lt;GlobalApiResponse&lt;UserReviewListResponseDTO&gt;&gt; getReviewsWithVisibility(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable(&quot;cpUserId&quot;) Long cpUserId,
        @RequestParam(&quot;page&quot;) int page,
        @RequestParam(&quot;size&quot;) int size) {
    // 메서드 구현
}

@PostMapping(&quot;/reviews/{reviewId}/view&quot;)
public ResponseEntity&lt;GlobalApiResponse&lt;ReviewResponseDTO&gt;&gt; viewReview(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable(&quot;reviewId&quot;) Long reviewId) {
    // 메서드 구현
}</code></pre>
</li>
</ol>
<h5 id="결과">결과</h5>
<p>이러한 수정 후, <code>IllegalArgumentException</code> 오류가 발생하지 않고, API가 정상적으로 작동했습니다. 이를 통해 컴파일러 플래그와 메서드 인자 이름의 중요성을 이해하게 되었습니다.</p>
<hr>
<h4 id="문제-2-querydsl-설정-문제">문제 2: QueryDSL 설정 문제</h4>
<h5 id="문제-설명-1">문제 설명</h5>
<p>복잡한 쿼리를 작성하기 위해 QueryDSL을 사용하려고 했으나, 설정 문제로 인해 제대로 작동하지 않았습니다.</p>
<h5 id="문제-원인-1">문제 원인</h5>
<p>QueryDSL은 JPA와 함께 사용될 때, 적절한 설정과 의존성이 필요합니다. 특히, 컴파일 타임에 QueryDSL 관련 클래스를 생성하기 위해서는 빌드 설정이 올바르게 구성되어 있어야 합니다. 초기 설정에서는 이러한 부분이 누락되어 문제가 발생했습니다.</p>
<h5 id="문제-해결-과정-1">문제 해결 과정</h5>
<ol>
<li><p><strong>QueryDSL 의존성 추가</strong>: <code>build.gradle</code> 파일에 QueryDSL 관련 의존성을 추가했습니다.</p>
<pre><code class="language-groovy">dependencies {
    implementation &#39;com.querydsl:querydsl-jpa:5.0.0:jakarta&#39;
    annotationProcessor &quot;com.querydsl:querydsl-apt:${dependencyManagement.importedProperties[&#39;querydsl.version&#39;]}:jakarta&quot;
    annotationProcessor &#39;jakarta.annotation:jakarta.annotation-api&#39;
    annotationProcessor &#39;jakarta.persistence:jakarta.persistence-api&#39;
}</code></pre>
</li>
<li><p><strong>빌드 설정 수정</strong>: QueryDSL의 소스 생성 디렉토리를 설정하고, 컴파일러가 이를 인식하도록 빌드 설정을 수정했습니다.</p>
<pre><code class="language-groovy">def generated = &#39;src/main/generated&#39;

sourceSets {
    main {
        java {
            srcDirs = [&#39;src/main/java&#39;, generated]
        }
    }
}

tasks.named(&#39;clean&#39;) {
    delete generated
}

tasks.withType(JavaCompile).configureEach {
    options.annotationProcessorPath = configurations.querydsl
    options.generatedSourceOutputDirectory = file(generated)
}

querydsl {
    library = &quot;com.querydsl:querydsl-apt&quot;
    jpa = true
    querydslSourcesDir = generated
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

tasks.withType(JavaCompile).matching { task -&gt; task.name != &#39;compileQuerydsl&#39; }.configureEach {
    dependsOn compileQuerydsl
}</code></pre>
</li>
</ol>
<h5 id="결과-1">결과</h5>
<p>QueryDSL 설정 후, 복잡한 쿼리를 문제없이 작성하고 실행할 수 있었습니다. 이를 통해 빌드 설정의 중요성과 QueryDSL의 유용성을 다시 한 번 확인할 수 있었습니다.</p>
<hr>
<h4 id="문제-3-페이지네이션-문제">문제 3: 페이지네이션 문제</h4>
<h5 id="문제-설명-2">문제 설명</h5>
<p>대량의 데이터를 처리할 때, 페이지네이션이 제대로 작동하지 않는 문제를 겪었습니다. 이는 성능 문제로 이어질 수 있었습니다.</p>
<h5 id="문제-원인-2">문제 원인</h5>
<p>페이지네이션을 구현하기 위해 <code>Pageable</code> 및 <code>PageRequest</code>를 사용하지 않았거나, 잘못 사용하여 문제가 발생했습니다.</p>
<h5 id="문제-해결-과정-2">문제 해결 과정</h5>
<ol>
<li><p><strong>페이지네이션 구현</strong>: 서비스 레이어에서 <code>Pageable</code> 및 <code>PageRequest</code>를 사용하여 데이터를 페이징 처리하도록 수정했습니다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ReviewService {
    private final ReviewRepository reviewRepository;
    private final UserReviewStatusRepository userReviewStatusRepository;
    private final UserRepository userRepository;

    @Transactional(readOnly = true)
    public UserReviewListResponseDTO getReviewsWithVisibility(Long cpUserId, Long userId, int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        Page&lt;Review&gt; reviews = reviewRepository.findReviewsByCpUser_CpUserId(cpUserId, pageable);
        List&lt;UserReviewStatus&gt; statuses = userReviewStatusRepository.findByUserAndReviewIn(userRepository.findById(userId)
                .orElseThrow(() -&gt; new ApiException(ErrorCode.USER_NOT_FOUND)), reviews.getContent());

        return new UserReviewListResponseDTO(reviews, statuses);
    }
}</code></pre>
</li>
<li><p><strong>컨트롤러 수정</strong>: 페이지네이션을 지원하도록 컨트롤러 메서드를 수정했습니다.</p>
<pre><code class="language-java">@GetMapping(&quot;/cpUsers/{cpUserId}/reviews&quot;)
public ResponseEntity&lt;GlobalApiResponse&lt;UserReviewListResponseDTO&gt;&gt; getReviewsWithVisibility(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable(&quot;cpUserId&quot;) Long cpUserId,
        @RequestParam(&quot;page&quot;) int page,
        @RequestParam(&quot;size&quot;) int size) {
    Long userId = userDetails.getUserId();
    UserReviewListResponseDTO responseDTO = reviewService.getReviewsWithVisibility(cpUserId, userId, page, size);

    return ResponseEntity.status(SUCCESS_CP_USER_REVIEW_READ.getHttpStatus())
            .body(
                    GlobalApiResponse.of(
                            SUCCESS_CP_USER_REVIEW_READ.getMessage(),
                            responseDTO
                    )
            );
}</code></pre>
</li>
</ol>
<h5 id="결과-2">결과</h5>
<p>페이지네이션 기능이 정상적으로 작동하여 대량의 데이터를 효율적으로 처리할 수 있었습니다. 이를 통해 페이지네이션의 중요성을 실감할 수 있었으며, 대규모 데이터를 처리할 때 발생할 수 있는 성능 문제를 해결할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리뷰 관리 시스템 개선]]></title>
            <link>https://velog.io/@silver_cherry/%EB%A6%AC%EB%B7%B0-%EA%B4%80%EB%A6%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@silver_cherry/%EB%A6%AC%EB%B7%B0-%EA%B4%80%EB%A6%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Mon, 10 Jun 2024 16:19:46 GMT</pubDate>
            <description><![CDATA[<p>기존 리뷰 관리 시스템의 기능을 개선하여 RESTful 원칙에 더욱 부합하도록 설계하고, 효과적인 데이터 검증 및 예외 처리 로직을 구현하는 것을 목표로 했습니다. 주요 개선 사항으로는 API 경로의 RESTful 재설계, 입력 데이터 유효성 검증, 사용자별 리뷰 작성 제한 등이 있습니다.</p>
<h4 id="문제-정의"><strong>문제 정의</strong></h4>
<ol>
<li><strong>비RESTful API 경로</strong>: 초기 API 설계에서 <code>POST /reviews/create</code>는 REST 원칙을 완벽히 따르지 않았습니다. API 경로에 동작(create)이 명시되어 있어, 리소스 지향적인 URL 설계 원칙에 어긋났습니다.</li>
<li><strong>데이터 유효성 검증 부재</strong>: 리뷰 제목, 내용 및 평점에 대한 클라이언트 측 입력이 서버 측에서 충분히 검증되지 않아, 부적절한 데이터가 시스템에 저장될 위험이 있었습니다.</li>
<li><strong>리뷰 중복 문제</strong>: 사용자가 동일한 기업 회원에 대해 여러 리뷰를 작성할 수 있는 문제가 있어, 데이터의 일관성과 신뢰성을 저해했습니다.</li>
</ol>
<h4 id="해결-방법"><strong>해결 방법</strong></h4>
<ol>
<li><p><strong>RESTful API 재설계</strong></p>
<ul>
<li><code>POST /reviews/create</code>를 <code>POST /reviews</code>로 변경하여 리소스 생성에 대한 표준 REST 방식을 채택했습니다. 이 변경은 API의 이해를 돕고, 표준에 맞는 설계를 적용하는 데 도움이 되었습니다.</li>
</ul>
</li>
<li><p><strong>입력 데이터 검증 로직 추가</strong></p>
<ul>
<li>Jakarta Bean Validation을 활용하여 <code>ReviewCreationRequestDTO</code>에서 리뷰 제목은 최대 100자, 내용은 최대 1000자를 초과할 수 없도록 했습니다. 또한, 평점은 1에서 5 사이의 정수만 허용되도록 검증 로직을 추가했습니다.</li>
<li>이 유효성 검사는 잘못된 데이터가 데이터베이스에 저장되는 것을 방지하고, 사용자에게 명확한 피드백을 제공하여 사용자 경험을 개선합니다.</li>
</ul>
</li>
<li><p><strong>사용자별 리뷰 작성 제한 로직 구현</strong></p>
<ul>
<li><code>ReviewService</code>에 로직을 추가하여 사용자가 특정 기업 회원에 대해 이미 리뷰를 작성했는지 확인합니다. 이미 리뷰를 작성한 경우, 추가적인 리뷰 작성을 방지하는 예외를 발생시킵니다.</li>
<li>이 기능은 데이터의 중복을 방지하고, 각 리뷰의 유니크함과 가치를 유지하는 데 기여합니다.</li>
</ul>
</li>
</ol>
<h4 id="결과-및-영향"><strong>결과 및 영향</strong></h4>
<p>이러한 개선을 통해 리뷰 관리 시스템은 RESTful 원칙을 더욱 잘 따르며, 입력 데이터의 정확성이 보장됩니다. 또한, 사용자가 동일 기업에 대해 중복 리뷰를 작성하는 것을 방지함으로써 데이터의 일관성과 품질이 향상되었습니다. 이러한 변경은 시스템의 안정성과 신뢰성을 높이며, 최종 사용자의 만족도를 개선하는 데 기여했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[포인트 시스템 리팩토링]]></title>
            <link>https://velog.io/@silver_cherry/%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@silver_cherry/%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</guid>
            <pubDate>Mon, 10 Jun 2024 11:38:50 GMT</pubDate>
            <description><![CDATA[<p>사용자와 기업 사용자가 다양한 활동(예: 리뷰 작성, 회원가입 등)을 통해 포인트를 적립할 수 있도록 하는 포인트 시스템을 구현했습니다. 기존 시스템에서는 포인트 적립 로직이 여러 서비스 클래스(<code>AuthService</code>, <code>ReviewService</code> 등)에 분산되어 있어 코드 중복이 발생하고 유지보수성이 떨어졌습니다. 이러한 문제를 해결하기 위해 단일 책임 원칙(Single Responsibility Principle, SRP)과 관심사 분리(Separation of Concerns, SoC)를 적용하여 포인트 시스템을 리팩토링했습니다.</p>
<h4 id="문제점">문제점</h4>
<ol>
<li><strong>코드 중복</strong>: 포인트 적립 로직이 여러 서비스 클래스에 분산되어 있어 동일한 코드가 여러 곳에서 중복되고 있었습니다.</li>
<li><strong>유지보수 어려움</strong>: 포인트 로직이 여러 클래스에 분산되어 있어, 포인트 관련 로직을 수정하거나 추가할 때 여러 클래스를 수정해야 했습니다.</li>
<li><strong>확장성 부족</strong>: 새로운 유형의 사용자(예: 다른 종류의 회사 사용자)가 추가될 때마다 각 서비스 클래스에 포인트 적립 로직을 추가해야 했습니다.</li>
</ol>
<h4 id="목표">목표</h4>
<ul>
<li>단일 책임 원칙(SRP)을 적용하여 포인트 적립 로직을 하나의 클래스에 집중시킵니다.</li>
<li>관심사 분리(SoC)를 통해 서비스 클래스들이 각각의 주요 책임에만 집중할 수 있도록 합니다.</li>
<li>포인트 시스템의 유지보수성과 확장성을 향상시킵니다.</li>
</ul>
<h4 id="해결책">해결책</h4>
<ol>
<li><p><strong>공통 인터페이스 정의</strong>: <code>User</code>와 <code>CompanyUser</code>가 공통으로 구현할 <code>CommonPoint</code> 인터페이스를 정의했습니다.</p>
<pre><code class="language-java"> public interface CommonPoint {
     Point getPoint();
     void setPoint(Point point);
 }</code></pre>
</li>
<li><p><strong>인터페이스 구현</strong>: <code>User</code>와 <code>CompanyUser</code> 클래스가 <code>CommonPoint</code> 인터페이스를 구현하도록 수정했습니다.</p>
<pre><code class="language-java"> public class User implements CommonPoint {
     private Point point;
     @Override
     public Point getPoint() {
         return point;
     }
     @Override
     public void setPoint(Point point) {
         this.point = point;
     }
 }

 public class CompanyUser implements CommonPoint {
     private Point point;
     @Override
     public Point getPoint() {
         return point;
     }
     @Override
     public void setPoint(Point point) {
         this.point = point;
     }
 }</code></pre>
</li>
<li><p><strong>포인트 서비스 리팩토링</strong>: 포인트 관련 로직을 <code>PointService</code> 클래스에 집중시켰습니다.</p>
<pre><code class="language-java"> @Service
 @RequiredArgsConstructor
 public class PointService {

     private final PointRepository pointRepository;
     private final SavedPointRepository savedPointRepository;

     @Transactional
     public void earnPoints(CommonPoint holder, SpType spType) {
         int points = getPointsByType(spType);
         Point point = holder.getPoint();
         if (point == null) {
             point = new Point(points);
         } else {
             point.setPoint(point.getPoint() + points);
         }
         pointRepository.save(point);
         holder.setPoint(point);

         SavedPoint savedPoint = SavedPoint.builder()
             .point(point)
             .spAmount(points)
             .spBalance(point.getPoint())
             .spType(spType)
             .build();
         savedPointRepository.save(savedPoint);
     }

     private int getPointsByType(SpType spType) {
         return switch (spType) {
             case REVIEW, BOARD -&gt; 100;
             case SIGNUP -&gt; 500;
             default -&gt; 0;
         };
     }
 }</code></pre>
</li>
<li><p><strong>서비스 클래스 리팩토링</strong>: <code>AuthService</code>와 <code>ReviewService</code>에서 포인트 관련 로직을 제거하고, 대신 <code>PointService</code>를 사용하도록 수정했습니다.</p>
<pre><code class="language-java"> @Service
 @RequiredArgsConstructor
 public class AuthService {

     private final UserRepository userRepository;
     private final CompanyUserRepository companyUserRepository;
     private final PointService pointService;

     @Transactional
     public void signup(UserSignupRequestDTO signupRequest) {
         validateUserSignupRequest(signupRequest.getEmail(), signupRequest.getPassword(),
                 signupRequest.getConfirmPassword(), signupRequest.isAgreeToTerms());

         User user = User.builder()
                 .username(signupRequest.getUsername())
                 .nickname(signupRequest.getNickname())
                 .email(signupRequest.getEmail())
                 .password(passwordEncoder.encode(signupRequest.getPassword()))
                 .loginType(signupRequest.getLoginType())
                 .build();

         userRepository.save(user);
         pointService.earnPoints(user, SpType.SIGNUP);
     }

     @Transactional
     public void cpSignup(CpUserSignupRequestDTO requestDTO) {
         validateCpSignupRequest(requestDTO.getCpEmail(), requestDTO.getCpPassword(),
                 requestDTO.getCpConfirmPassword(), requestDTO.isAgreeToTerms());

         CompanyUser companyUser = CompanyUser.builder()
                 .hiringStatus(requestDTO.getHiringStatus())
                 .employeeCount(requestDTO.getEmployeeCount())
                 .foundationDate(requestDTO.getFoundationDate())
                 .description(requestDTO.getDescription())
                 .cpNum(encryptionService.encryptCpNum(requestDTO.getCpNum()))
                 .cpName(requestDTO.getCpName())
                 .cpUsername(requestDTO.getCpUsername())
                 .cpEmail(requestDTO.getCpEmail())
                 .cpPhoneNumber(requestDTO.getCpPhoneNumber())
                 .cpPassword(passwordEncoder.encode(requestDTO.getCpPassword()))
                 .build();

         companyUserRepository.save(companyUser);
         pointService.earnPoints(companyUser, SpType.SIGNUP);
     }
 }

 @Service
 @RequiredArgsConstructor
 public class ReviewService {

     private final ReviewRepository reviewRepository;
     private final CompanyUserRepository companyUserRepository;
     private final UserRepository userRepository;
     private final PointService pointService;

     @Transactional
     public ReviewCreationResponseDTO createReview(Long userId, Long cpUserId, ReviewCreationRequestDTO requestDto) {

         User user = userRepository.findById(userId)
                 .orElseThrow(() -&gt; new ApiException(ErrorCode.USER_NOT_FOUND));
         CompanyUser companyUser = companyUserRepository.findById(cpUserId)
                 .orElseThrow(() -&gt; new ApiException(ErrorCode.COMPANY_USER_NOT_FOUND));

         Review review = Review.builder()
                 .user(user)
                 .cpUser(companyUser)
                 .reviewTitle(requestDto.getReviewTitle())
                 .reviewContent(requestDto.getReviewContent())
                 .rating(requestDto.getRating())
                 .isPrivate(true)
                 .build();
         review = reviewRepository.save(review);

         pointService.earnPoints(user, SpType.REVIEW);

         return new ReviewCreationResponseDTO(
                 review.getReviewId(),
                 requestDto.getCpUserId(),
                 review.getReviewTitle(),
                 review.getReviewContent(),
                 review.getRating(),
                 review.getIsPrivate()
         );
     }
 }</code></pre>
</li>
</ol>
<h3 id="결과">결과</h3>
<ul>
<li><strong>코드 중복 제거</strong>: 포인트 관련 로직이 <code>PointService</code>에 집중되어 코드 중복이 제거되었습니다.</li>
<li><strong>유지보수성 향상</strong>: 포인트 관련 로직이 한 곳에 모여 있어 수정이나 확장이 용이해졌습니다.</li>
<li><strong>확장성 확보</strong>: 새로운 유형의 사용자나 포인트 적립 이벤트가 추가될 때, <code>PointService</code>만 수정하면 되므로 확장성이 향상되었습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[기업회원 사용자와 일반 사용자의 통합 인증 시스템 구현 및 문제 해결]]></title>
            <link>https://velog.io/@silver_cherry/%EA%B8%B0%EC%97%85%ED%9A%8C%EC%9B%90-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%99%80-%EC%9D%BC%EB%B0%98-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%ED%86%B5%ED%95%A9-%EC%9D%B8%EC%A6%9D-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84-%EB%B0%8F-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@silver_cherry/%EA%B8%B0%EC%97%85%ED%9A%8C%EC%9B%90-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%99%80-%EC%9D%BC%EB%B0%98-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%ED%86%B5%ED%95%A9-%EC%9D%B8%EC%A6%9D-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84-%EB%B0%8F-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sun, 09 Jun 2024 14:16:17 GMT</pubDate>
            <description><![CDATA[<p>저희 시스템에서는 사용자와 기업 사용자 간의 인증 절차를 다루는데, <code>/auth/login</code> 경로를 통한 일반 사용자 로그인은 잘 작동하였으나, <code>/auth/cp-login</code> 경로를 통한 기업 사용자 로그인 시 JWT 토큰이 헤더에 포함되지 않는 문제가 발생했습니다. 이로 인해 기업 사용자는 시스템 리소스에 접근할 수 없는 문제가 있었습니다.
<img src="https://velog.velcdn.com/images/silver_cherry/post/9effea6f-f9d6-42e0-94a1-a5e63044fc98/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/silver_cherry/post/45a80a18-219e-4e5c-a2ed-c1878c2bdfa0/image.png" alt=""></p>
<p><strong>해결 과정</strong></p>
<ol>
<li><strong>문제 진단</strong>: 기존 <code>JwtAuthenticationFilter</code> 클래스를 분석한 결과, 필터가 <code>/auth/login</code> 경로에만 적용되어 있었음을 확인했습니다.</li>
<li><strong>코드 수정</strong>: <code>JwtAuthenticationFilter</code>의 <code>requiresAuthentication</code> 메서드를 오버라이드하여 <code>/auth/login</code>과 <code>/auth/cp-login</code> 두 경로 모두에서 필터가 작동하도록 조건을 추가했습니다.</li>
<li><strong>테스트 및 검증</strong>: 수정 후 로컬 및 스테이징 환경에서 경로별 인증 흐름을 테스트하여 모든 경로에서 JWT 토큰이 정상적으로 헤더에 포함되는 것을 확인했습니다.</li>
</ol>
<p><strong>기술적 성과</strong></p>
<p>이 변경으로 인해, 시스템의 인증 로직이 한결 간결해졌으며, 보안 레이어의 일관성이 향상되었습니다. 또한, 다양한 인증 경로에 대한 지원이 가능해져 시스템의 확장성과 유지관리가 용이해졌습니다. 이 경험을 통해 스프링 시큐리티와 JWT 기반 인증 시스템에 대한 깊은 이해를 가지게 되었음을 보여주며, 실제 운영 환경에서 발생할 수 있는 문제를 신속하게 해결할 수 있는 능력을 강조합니다.</p>
<p><strong>결론</strong></p>
<p>이 프로젝트를 통해 저는 복잡한 인증 시스템에서의 문제를 식별하고 해결함으로써, 안정적인 사용자 인증 흐름을 보장하는 것의 중요성을 다시 한번 깨달았습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[
기업회원 로그인 트러블슈팅]]></title>
            <link>https://velog.io/@silver_cherry/%EA%B8%B0%EC%97%85%ED%9A%8C%EC%9B%90-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@silver_cherry/%EA%B8%B0%EC%97%85%ED%9A%8C%EC%9B%90-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</guid>
            <pubDate>Sat, 08 Jun 2024 10:54:42 GMT</pubDate>
            <description><![CDATA[<p>프로젝트 진행 중 기업회원 로그인 기능을 추가하는 과정에서 다양한 이슈가 발생했습니다. 이 문서는 그 트러블슈팅 과정과 해결 방법을 상세히 기록한 것입니다. 이를 통해 발생할 수 있는 잠재적인 문제를 사전에 파악하고, 유사한 상황에서 효과적으로 대응할 수 있도록 돕기 위해 작성되었습니다.</p>
<h4 id="문제-정의">문제 정의</h4>
<p>기존 시스템에서는 일반 사용자 로그인 기능만을 제공하고 있었으며, 기업회원 로그인 기능이 추가됨에 따라 다음과 같은 문제들이 발생했습니다:</p>
<ol>
<li><strong>기업회원과 일반 사용자의 구분 문제</strong>: 기업회원과 일반 사용자를 어떻게 구분하여 인증하고, 각 사용자 유형에 맞는 권한을 어떻게 부여할 것인가.</li>
<li><strong>Security Configuration 문제</strong>: Spring Security 설정에서 기업회원과 일반 사용자의 접근 권한을 구분하여 설정하는 방법.</li>
<li><strong>UserDetailsService 문제</strong>: 기업회원과 일반 사용자의 정보를 어떻게 로드할 것인가.</li>
</ol>
<h4 id="문제-해결-과정">문제 해결 과정</h4>
<h5 id="1-데이터베이스-스키마-수정">1. 데이터베이스 스키마 수정</h5>
<p>먼저, 기업회원과 일반 사용자를 구분할 수 있는 데이터베이스 스키마 수정이 필요했습니다. 이를 위해 <code>User</code> 테이블과 별도로 <code>CompanyUser</code> 테이블을 생성하고, 각각의 사용자 정보를 저장할 수 있도록 구성했습니다.</p>
<h5 id="2-userdetails-구현-수정">2. UserDetails 구현 수정</h5>
<p>기존의 <code>UserDetailsImpl</code> 클래스는 일반 사용자만을 고려한 구조였습니다. 이를 수정하여 기업회원의 정보를 포함하도록 변경했습니다.</p>
<pre><code class="language-java">package team9502.sinchulgwinong.global.security;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class UserDetailsImpl implements UserDetails {

    private final String email;
    private final String password;
    private final Collection&lt;? extends GrantedAuthority&gt; authorities;
    private final String userType; // 추가된 필드

    public UserDetailsImpl(String email, String password, Collection&lt;? extends GrantedAuthority&gt; authorities, String userType) {
        this.email = email;
        this.password = password;
        this.authorities = authorities;
        this.userType = userType;
    }

    // 기존 메서드들...

    public String getUserType() {
        return userType;
    }
}</code></pre>
<h5 id="3-userdetailsservice-수정">3. UserDetailsService 수정</h5>
<p><code>UserDetailsServiceImpl</code> 클래스를 수정하여 기업회원과 일반 사용자를 구분하여 로드할 수 있도록 했습니다.</p>
<pre><code class="language-java">package team9502.sinchulgwinong.global.security;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import team9502.sinchulgwinong.domain.user.entity.User;
import team9502.sinchulgwinong.domain.user.repository.UserRepository;
import team9502.sinchulgwinong.domain.companyuser.repository.CompanyUserRepository;

import java.util.Collections;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;
    private final CompanyUserRepository companyUserRepository;

    public UserDetailsServiceImpl(UserRepository userRepository, CompanyUserRepository companyUserRepository) {
        this.userRepository = userRepository;
        this.companyUserRepository = companyUserRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email).orElse(null);
        if (user != null) {
            return new UserDetailsImpl(user.getEmail(), user.getPassword(), Collections.emptyList(), &quot;USER&quot;);
        }

        CompanyUser companyUser = companyUserRepository.findByEmail(email)
                .orElseThrow(() -&gt; new UsernameNotFoundException(email + &quot;으로 등록된 사용자를 찾을 수 없습니다.&quot;));
        return new UserDetailsImpl(companyUser.getEmail(), companyUser.getPassword(), Collections.emptyList(), &quot;COMPANY_USER&quot;);
    }
}</code></pre>
<h5 id="4-spring-security-설정-수정">4. Spring Security 설정 수정</h5>
<p>Spring Security 설정을 통해 기업회원과 일반 사용자의 접근 권한을 구분했습니다.</p>
<pre><code class="language-java">package team9502.sinchulgwinong.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -&gt; auth
                        .requestMatchers(&quot;/&quot;, &quot;/home&quot;, &quot;/swagger-ui.html&quot;, &quot;/v3/api-docs/**&quot;, &quot;/swagger-ui/**&quot;).permitAll()
                        .requestMatchers(&quot;/auth/signup&quot;, &quot;auth/login&quot;, &quot;/auth/cp-signup&quot;, &quot;/auth/cp-login&quot;).permitAll()
                        .requestMatchers(&quot;/business/status&quot;, &quot;business/verify&quot;).permitAll()
                        .anyRequest().authenticated())
                .formLogin(AbstractAuthenticationFilterConfigurer::disable)
                .logout(LogoutConfigurer::permitAll);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }
}</code></pre>
<h4 id="최종-테스트-및-결과">최종 테스트 및 결과</h4>
<p>위의 변경사항을 적용한 후, 기업회원과 일반 사용자가 각각 로그인을 시도했을 때 올바르게 인증 및 권한 부여가 이루어지는 것을 확인했습니다. 각 사용자 유형에 맞는 페이지 접근이 정상적으로 이루어졌으며, 이를 통해 보안성이 향상되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀 생성 기능의 트러블 슈팅]]></title>
            <link>https://velog.io/@silver_cherry/%ED%8C%80-%EC%83%9D%EC%84%B1-%EA%B8%B0%EB%8A%A5%EC%9D%98-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@silver_cherry/%ED%8C%80-%EC%83%9D%EC%84%B1-%EA%B8%B0%EB%8A%A5%EC%9D%98-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Fri, 07 Jun 2024 05:04:08 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 팀 생성 기능을 구현하는 과정에서, 사용자가 팀을 생성할 때 여러 예외 상황이 발생하였습니다. 주요 문제로는 데이터베이스 연동 오류, 유효하지 않은 입력 처리, 그리고 동시성 문제가 있었습니다.</p>
<h4 id="진단-과정">진단 과정</h4>
<ol>
<li><p><strong>데이터베이스 연동 오류</strong></p>
<ul>
<li><strong>문제</strong>: 팀 정보를 데이터베이스에 저장할 때 <code>ConstraintViolationException</code>이 발생하였습니다.</li>
<li><strong>원인</strong>: 팀 이름에 대한 유니크 제약 조건이 설정되어 있었으나, 동일한 팀 이름으로 여러 요청이 들어올 경우 이를 사전에 필터링하지 못했습니다.</li>
<li><strong>해결</strong>: 서비스 레이어에서 팀 이름의 중복 검사 로직을 추가하여, 이미 존재하는 이름인 경우 <code>CustomException</code>을 발생시키도록 처리했습니다.</li>
</ul>
</li>
<li><p><strong>유효하지 않은 입력 처리</strong></p>
<ul>
<li><strong>문제</strong>: 사용자로부터 입력 받은 데이터 중 일부가 유효하지 않은 경우가 간헐적으로 발생했습니다.</li>
<li><strong>원인</strong>: 프론트엔드에서의 입력 검증 부재와 백엔드에서의 예외 처리 미흡이 원인으로 파악되었습니다.</li>
<li><strong>해결</strong>: <code>@Valid</code> 어노테이션을 사용하여 입력 데이터의 검증을 강화하고, 프론트엔드에서도 입력 검증 로직을 추가하여 두 번의 검증 과정을 거치도록 구현했습니다.</li>
</ul>
</li>
<li><p><strong>동시성 문제</strong></p>
<ul>
<li><strong>문제</strong>: 고도의 동시 요청 상황에서 동일한 사용자가 여러 팀을 생성하는 문제가 발견되었습니다.</li>
<li><strong>원인</strong>: 트랜잭션 관리가 적절히 이루어지지 않아 발생한 문제로 추정되었습니다.</li>
<li><strong>해결</strong>: 서비스 메소드에 <code>@Transactional</code> 어노테이션을 적용하여 메소드 전체를 하나의 트랜잭션으로 관리하도록 설정하고, 필요한 경우에는 트랜잭션 격리 수준을 조정하여 문제를 해결했습니다.</li>
</ul>
</li>
</ol>
<h4 id="결과-및-효과">결과 및 효과</h4>
<p>위와 같은 트러블 슈팅을 통해 팀 생성 기능의 안정성을 높일 수 있었습니다. 데이터베이스 연동 오류와 유효하지 않은 입력 처리의 개선으로 데이터 무결성을 확보했으며, 동시성 문제 해결로 서비스의 신뢰성을 강화했습니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[config 구성 파일 방법과 목적 [수정 예정]]]></title>
            <link>https://velog.io/@silver_cherry/config-%EA%B5%AC%EC%84%B1-%ED%8C%8C%EC%9D%BC-%EB%B0%A9%EB%B2%95%EA%B3%BC-%EB%AA%A9%EC%A0%81-%EC%88%98%EC%A0%95-%EC%98%88%EC%A0%95</link>
            <guid>https://velog.io/@silver_cherry/config-%EA%B5%AC%EC%84%B1-%ED%8C%8C%EC%9D%BC-%EB%B0%A9%EB%B2%95%EA%B3%BC-%EB%AA%A9%EC%A0%81-%EC%88%98%EC%A0%95-%EC%98%88%EC%A0%95</guid>
            <pubDate>Sat, 01 Jun 2024 13:02:04 GMT</pubDate>
            <description><![CDATA[<p>애플리케이션의 구성 파일을 세분화하는 접근 방식은 각 구성의 관리를 용이하게 하고, 각 설정의 책임을 명확히 하는 데 도움이 됩니다. 제시하신 <code>CorsConfig</code>, <code>JpaConfig</code>, <code>RestTemplateConfig</code>, <code>SwaggerConfig</code>, <code>WebSecurityConfig</code> 등으로 구성 파일을 분리하는 것은 좋은 전략입니다. 각각의 구성 파일이 특정 기능에 집중하게 함으로써 설정의 복잡성을 줄이고, 필요한 부분만 수정할 수 있게 됩니다. 아래는 각 구성 파일의 역할에 대해 간단히 설명하겠습니다:</p>
<h3 id="1-corsconfig">1. <code>CorsConfig</code></h3>
<ul>
<li><strong>목적</strong>: 애플리케이션의 Cross-Origin Resource Sharing (CORS) 정책을 설정합니다. 이는 다른 도메인에서 애플리케이션의 리소스를 안전하게 요청할 수 있도록 허용하는 규칙을 정의합니다.</li>
</ul>
<h3 id="2-jpaconfig">2. <code>JpaConfig</code></h3>
<ul>
<li><strong>목적</strong>: Java Persistence API (JPA) 관련 설정을 구성합니다. 데이터베이스 엔티티 관리, 트랜잭션 설정, EntityManager 설정 등 데이터 접근 계층의 설정을 담당합니다.</li>
</ul>
<h3 id="3-resttemplateconfig">3. <code>RestTemplateConfig</code></h3>
<ul>
<li><strong>목적</strong>: <code>RestTemplate</code>의 빈(Bean)을 설정하여 외부 HTTP 호출을 수행할 수 있도록 합니다. 커스텀 인터셉터, 오류 핸들러, 타임아웃 설정 등을 구성할 수 있습니다.</li>
<li><blockquote>
<p>소셜 로그인시 사용</p>
</blockquote>
</li>
</ul>
<h3 id="4-swaggerconfig">4. <code>SwaggerConfig</code></h3>
<ul>
<li><strong>목적</strong>: API 문서화를 위한 Swagger 설정을 구성합니다. API 엔드포인트에 대한 메타데이터를 제공하고, Swagger UI를 통해 API를 시각적으로 탐색할 수 있게 합니다.</li>
</ul>
<h3 id="5-websecurityconfig">5. <code>WebSecurityConfig</code></h3>
<ul>
<li><strong>목적</strong>: Spring Security 설정을 관리합니다. 인증 및 권한 부여, CSRF 보호, 세션 관리 등 보안 관련 설정을 구현합니다.</li>
</ul>
<h3 id="전략의-장점">전략의 장점</h3>
<ul>
<li><strong>관리 용이성</strong>: 각 설정 파일이 특정 기능에 초점을 맞추고 있기 때문에, 관련 설정을 찾고 수정하기가 더 쉬워집니다.</li>
<li><strong>결합도 감소</strong>: 각 설정 파일이 서로 독립적으로 관리됨으로써, 한 설정의 변경이 다른 설정에 영향을 미치는 일을 줄일 수 있습니다.</li>
<li><strong>재사용성 향상</strong>: 특정 구성 요소(예: <code>RestTemplate</code>)가 다른 애플리케이션에서도 필요할 경우 해당 설정 클래스를 쉽게 재사용할 수 있습니다.</li>
</ul>
<h3 id="전략의-단점">전략의 단점</h3>
<ul>
<li><strong>파일 수 증가</strong>: 설정이 세분화됨에 따라 관리해야 할 파일 수가 증가할 수 있습니다. 이는 프로젝트의 복잡성을 증가시킬 수 있으나, 각 파일이 명확한 책임을 가지므로 이해하기는 더 쉬워집니다.</li>
</ul>
<p>결론적으로, 설정을 세분화하는 전략은 프로젝트의 유지 관리와 확장성 측면에서 많은 이점을 제공합니다. 프로젝트의 요구 사항과 팀의 선호에 따라 적절히 채택할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[트랙 참여자 업데이트 메소드 리팩터링: 안정성 및 성능 개선을 위한 트랙 식별자 변경]]></title>
            <link>https://velog.io/@silver_cherry/%ED%8A%B8%EB%9E%99-%EC%B0%B8%EC%97%AC%EC%9E%90-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EB%A9%94%EC%86%8C%EB%93%9C-%EB%B0%8F-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@silver_cherry/%ED%8A%B8%EB%9E%99-%EC%B0%B8%EC%97%AC%EC%9E%90-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EB%A9%94%EC%86%8C%EB%93%9C-%EB%B0%8F-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Thu, 30 May 2024 07:21:04 GMT</pubDate>
            <description><![CDATA[<h3 id="서론">서론:</h3>
<p>두 가지 주요 문제에 직면했습니다. 첫 번째 문제는 <code>TrackParticipantsService</code> 클래스의 <code>updateParticipantTrack</code> 메소드가 처음에 새 트랙의 이름을 매개변수로 받았던 것과 관련이 있습니다. 두 번째 문제는 <code>TrackParticipants</code> 테이블이 제대로 업데이트되지 않는 문제였습니다. 이 두 문제 모두 리팩터링이 필요했습니다.</p>
<h3 id="문제점">문제점:</h3>
<p><code>updateParticipantTrack</code> 메소드의 초기 구현은 새 트랙의 이름을 매개변수로 받았습니다. 이 접근법은 여러 잠재적 문제점을 가지고 있었습니다:</p>
<ol>
<li><strong>불안정성</strong>: 트랙 이름은 변경될 수 있으며, 요청이 이루어진 시간과 처리 시간 사이에 이름이 변경되면 일관성에 문제가 발생할 수 있습니다.</li>
<li><strong>성능</strong>: 데이터베이스에 많은 트랙이 있는 경우, 이름으로 트랙을 검색하는 것은 ID로 검색하는 것보다 느릴 수 있습니다.</li>
</ol>
<p>또한, <code>TrackParticipants</code> 테이블이 제대로 업데이트되지 않아 데이터에 일관성이 없게 되어 애플리케이션에서 잠재적인 오류가 발생할 수 있음을 발견했습니다.</p>
<h3 id="해결책">해결책:</h3>
<p>이 문제들을 해결하기 위해, 새 트랙의 이름 대신 ID를 받아 처리하도록 <code>updateParticipantTrack</code> 메소드를 리팩터링하기로 결정했습니다. 이 접근법은 여러 가지 이점을 제공합니다:</p>
<ol>
<li><strong>안정성</strong>: 트랙 ID는 고유하며 변경할 수 없어 트랙 이름보다 더 안정적인 식별자입니다.</li>
<li><strong>성능</strong>: 이름으로 검색 시 평균 응답 시간이 300ms였으나 ID로 검색 시 100ms로 개선되었습니다.
 <img src="https://velog.velcdn.com/images/silver_cherry/post/5ce27f7a-195c-4f12-9978-da9ba43b20b6/image.png" alt=""></li>
</ol>
<p>테이블 업데이트 문제를 해결하기 위해서는 <code>TrackParticipants</code> 엔티티가 Hibernate 세션에 의해 올바르게 관리되고 있음을 확인했습니다. 또한 업데이트 작업 후에 트랜잭션이 제대로 커밋되고 있는지 확인했습니다.</p>
<h3 id="구현">구현:</h3>
<p>리팩터링 과정은 여러 단계를 포함했습니다:</p>
<ol>
<li><code>TrackUpdateRequestDTO</code> 클래스를 수정하여 <code>newTrackId</code> 필드를 포함시켰습니다.</li>
<li><code>TrackParticipantsController</code> 클래스의 <code>updateParticipantTrack</code> 메소드를 리팩터링하여 <code>TrackUpdateRequestDTO</code> 객체를 받도록 변경했습니다.</li>
<li><code>TrackParticipantsService</code> 클래스의 <code>updateParticipantTrack</code> 메소드를 리팩터링하여 새 트랙의 ID를 사용하여 트랙 엔티티를 찾도록 했습니다.</li>
<li>더 이상 필요하지 않은 <code>TrackRepository</code> 인터페이스에서 <code>findByTrackName</code> 메소드를 제거했습니다.</li>
<li><code>TrackParticipants</code> 엔티티가 Hibernate 세션에 의해 올바르게 관리되고 있음을 확인했습니다.</li>
<li>업데이트 작업 후에 트랜잭션이 제대로 커밋되고 있는지 확인했습니다.</li>
</ol>
<h3 id="결과">결과:</h3>
<p>이러한 변경을 구현한 후, 새 트랙의 ID를 포함하는 요청을 보내어 <code>updateParticipantTrack</code> 메소드를 테스트했습니다. 메소드는 예상대로 작동하여 제공된 ID를 기반으로 참가자의 트랙을 성공적으로 업데이트했습니다. 애플리케이션 로그를 통해 메소드가 이제 데이터베이스 쿼리에 새 트랙의 ID를 사용하고 있음을 확인했으며, 메소드의 성능이 약 3배 향상되었습니다.</p>
<p>또한 <code>TrackParticipants</code> 테이블이 올바르게 업데이트되고 있는지 확인했습니다. 테이블의 데이터는 일관되었으며 테이블 업데이트와 관련된 오류는 발생하지 않았습니다.</p>
<h3 id="결론">결론:</h3>
<p>이 트러블슈팅을 통해 데이터베이스의 엔티티에 대한 올바른 식별자를 선택하는 것의 중요성과 데이터베이스 트랜잭션을 올바르게 관리하는 것이 얼마나 중요한지 깨달았습니다. 이름을 사용한다면 더 인간 친화적일 수 있지만 변경될 수 있으며 고유하지 않을 수도 있어 문제를 일으킬 가능성이 존재합니다. 반면에 ID는 고유하고 변경할 수 없어 엔티티를 식별하는 데 더 안정적이고 효율적인 선택입니다. 데이터베이스 트랜잭션을 올바르게 관리하는 것도 데이터의 일관성을 보장하고 오류를 방지하기 위해 필수적임을 다시 한 번 느꼈습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CORS 설정 문제 해결]]></title>
            <link>https://velog.io/@silver_cherry/CORS-%EC%84%A4%EC%A0%95-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@silver_cherry/CORS-%EC%84%A4%EC%A0%95-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 29 May 2024 07:28:36 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-상황">문제 상황</h3>
<p>웹 애플리케이션에서 서버에 요청을 보낼 때, CORS(Cross-Origin Resource Sharing) 정책에 의해 발생하는 에러가 있었습니다. 이 에러는 서버에서 응답을 보낼 때 <code>Access-Control-Allow-Origin</code> 헤더를 포함하지 않아서 발생하는 문제였습니다. 이 헤더는 해당 응답을 받을 수 있는 웹 페이지의 출처(origin)를 지정합니다.</p>
<h3 id="문제-해결-과정">문제 해결 과정</h3>
<ol>
<li>먼저, <code>WebSecurityConfig.java</code> 파일에서 <code>corsConfiguration</code> 메서드를 확인했습니다. 이 메서드에서는 허용되는 출처(origin), 메서드, 헤더 등을 설정하고 있었습니다.</li>
</ol>
<pre><code class="language-java">@Bean
public CorsConfiguration corsConfiguration() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList(&quot;https://spaghetticoding.shop&quot;, &quot;http://43.202.186.51:8080&quot;, &quot;http://localhost:3000/&quot;, &quot;http://43.202.186.51:3000&quot;, &quot;http://localhost:3000&quot;));
    configuration.setAllowedMethods(Arrays.asList(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;PATCH&quot;, &quot;DELETE&quot;, &quot;OPTIONS&quot;));
    configuration.setAllowedHeaders(Arrays.asList(&quot;Origin&quot;, &quot;Content-Type&quot;, &quot;Accept&quot;, &quot;Authorization&quot;));
    configuration.setAllowCredentials(true);
    configuration.setExposedHeaders(List.of(&quot;Authorization&quot;));

    return configuration;
}</code></pre>
<ol start="2">
<li><p>그러나 실제 요청이 <code>&quot;http://localhost:3000/&quot;</code>에서 발생했음에도 불구하고 CORS 에러가 발생하였습니다. 이는 서버가 이 출처를 허용하도록 설정되어 있지 않거나, 설정이 제대로 적용되지 않았을 가능성이 있었습니다.</p>
</li>
<li><p>이 문제를 해결하기 위해, <code>WebSecurityConfig.java</code> 파일에서 <code>securityFilterChain</code> 메서드를 수정하여 CORS 설정을 적용하였습니다. 아래는 수정된 코드입니다:</p>
</li>
</ol>
<pre><code class="language-java">@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(AbstractHttpConfigurer::disable);

    http.sessionManagement((sessionManagement) -&gt;
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    );

    http.authorizeHttpRequests((authorizeHttpRequests) -&gt;
            authorizeHttpRequests
                    .requestMatchers(HttpMethod.OPTIONS, &quot;/**&quot;).permitAll() // preflight 요청 허용 설정
                    .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                    .requestMatchers(&quot;/api/auths/**&quot;).permitAll() // &#39;/api/auths/&#39;로 시작하는 요청 모두 접근 허가
                    .requestMatchers(&quot;/tracks&quot;).permitAll() // &#39;/tracks&#39; 요청 접근 허가 (트랙 전체 조회)
                    .requestMatchers(&quot;/spaghettiiii&quot;).permitAll() // aws 테스트를 위함
                    .requestMatchers(&quot;/&quot;).permitAll() // 메인 페이지 요청 허가
                    .anyRequest().authenticated() // 그 외 모든 요청 인증처리
    );

    http.cors(cors -&gt; cors.configurationSource(request -&gt; corsConfiguration())); // CORS 설정 적용

    http.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);

    return http.build();
}</code></pre>
<ol start="4">
<li>이렇게 수정하면, <code>securityFilterChain</code> 메서드에서 CORS 설정이 적용되어, 서버 응답에 <code>Access-Control-Allow-Origin</code> 헤더가 포함되어야 합니다. 이 변경을 적용한 후에는 서버를 재시작해야 합니다.</li>
</ol>
<h3 id="결과">결과</h3>
<p>위의 수정 후, 서버를 재시작하고 API를 다시 호출했을 때, <code>Access-Control-Allow-Origin</code> 헤더가 적절하게 설정되어 있어 CORS 에러가 발생하지 않았습니다. 이로써, 웹 브라우저의 CORS 정책에 의해 발생하는 에러를 방지할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2 환경변수 설정 문제 해결(CodeDeploy 환경변수)]]></title>
            <link>https://velog.io/@silver_cherry/AWS-EC2-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0CodeDeploy-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98</link>
            <guid>https://velog.io/@silver_cherry/AWS-EC2-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0CodeDeploy-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98</guid>
            <pubDate>Tue, 14 May 2024 03:22:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>AWS EC2 인스턴스에 배포된 Spring Boot 애플리케이션에서 AWS Systems Manager Parameter Store를 사용하여 환경변수를 관리하던 중 환경변수를 불러오지 못하는 상황이 생겼습니다.</p>
</blockquote>
<hr>
<h4 id="문제-상황">문제 상황</h4>
<p>AWS EC2 인스턴스에서 Spring Boot를 실행시킬 때, AWS Systems Manager Parameter Store에서 환경변수 값을 불러오지 못하는 문제가 발생했습니다. 이로 인해 데이터베이스 연결 정보와 JWT 시크릿키 등 시크릿값들을 사용할 수 없었습니다.</p>
<hr>
<h4 id="가능한-원인-분석">가능한 원인 분석</h4>
<ol>
<li><strong>AWS CLI 미설치</strong>: EC2 인스턴스에 AWS CLI가 설치되지 않아 Parameter Store에 접근할 수 없었습니다.</li>
<li><strong>IAM 역할 및 정책 설정</strong>: EC2 인스턴스에 할당된 IAM 역할에 필요한 권한이 부여되지 않았을 가능성이 있습니다.</li>
<li><strong>환경변수 추출 스크립트 오류</strong>: 환경변수 값을 추출하는 스크립트에 오류가 있거나 실행되지 않았을 가능성이 있습니다.</li>
</ol>
<hr>
<h4 id="해결-과정">해결 과정</h4>
<ol>
<li><strong>AWS CLI 설치</strong>: EC2 인스턴스에 AWS CLI를 설치하여 AWS 서비스에 접근할 수 있도록 하였습니다.<pre><code>curl &quot;https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip&quot; -o &quot;awscliv2.zip&quot;
unzip awscliv2.zip
sudo ./aws/install
</code></pre></li>
</ol>
<pre><code>
2. **IAM 역할 및 정책 검토**: EC2 인스턴스에 할당된 IAM 역할에 Parameter Store 접근을 위한 권한이 포함되어 있는지 확인하고 필요한 정책을 추가하였습니다.
![](https://velog.velcdn.com/images/silver_cherry/post/8a527f1f-c215-4ee1-bc10-c7a19ea58142/image.png)

3. **환경변수 추출 스크립트 수정**: start.sh 스크립트에서 환경변수 값을 제대로 불러오지 못하는 부분을 수정하였습니다. Parameter Store에서 값을 제대로 불러올 수 있도록 쿼리 및 출력 형식을 조정하였습니다.</code></pre><p>#!/bin/bash</p>
<p>PROJECT_ROOT=&quot;/home/ubuntu/app&quot;
JAR_FILE=&quot;$PROJECT_ROOT/kims-spaghetti.jar&quot;</p>
<p>APP_LOG=&quot;$PROJECT_ROOT/application.log&quot;
ERROR_LOG=&quot;$PROJECT_ROOT/error.log&quot;
DEPLOY_LOG=&quot;$PROJECT_ROOT/deploy.log&quot;</p>
<h1 id="aws-cli를-사용하여-aws-systems-manager-parameter-store에서-파라미터-값을-불러옵니다">AWS CLI를 사용하여 AWS Systems Manager Parameter Store에서 파라미터 값을 불러옵니다</h1>
<p>export PROD_DB_URL=$(aws ssm get-parameter --name &quot;/prod/spaghetti-app/db/url&quot; --with-decryption --query &quot;Parameter.Value&quot; --output text)
export PROD_DB_USER=$(aws ssm get-parameter --name &quot;/prod/spaghetti-app/db/user&quot; --with-decryption --query &quot;Parameter.Value&quot; --output text)
export PROD_DB_PASSWORD=$(aws ssm get-parameter --name &quot;/prod/spaghetti-app/db/password&quot; --with-decryption --query &quot;Parameter.Value&quot; --output text)
export JWT_SECRET_KEY=$(aws ssm get-parameter --name &quot;/prod/spaghetti-app/jwt/secret&quot; --with-decryption --query &quot;Parameter.Value&quot; --output text)</p>
<p>TIME_NOW=$(date +%c)</p>
<h1 id="build-파일-복사">build 파일 복사</h1>
<p>echo &quot;$TIME_NOW &gt; $JAR_FILE 파일 복사&quot; &gt;&gt; $DEPLOY_LOG
cp $PROJECT_ROOT/build/libs/*.jar $JAR_FILE</p>
<h1 id="jar-파일-실행">jar 파일 실행</h1>
<p>echo &quot;$TIME_NOW &gt; $JAR_FILE 파일 실행&quot; &gt;&gt; $DEPLOY_LOG
nohup java -jar $JAR_FILE &gt; $APP_LOG 2&gt; $ERROR_LOG &amp;</p>
<h1 id="현재-실행중인-프로세스-id-출력">현재 실행중인 프로세스 ID 출력</h1>
<p>CURRENT_PID=$(pgrep -f $JAR_FILE)
echo &quot;$TIME_NOW &gt; 실행된 프로세스 아이디 $CURRENT_PID 입니다.&quot; &gt;&gt; $DEPLOY_LOG</p>
<p>```</p>
<ol start="4">
<li><strong>테스트 및 확인</strong>: 수정된 설정과 스크립트를 테스트하여 모든 환경변수가 올바르게 불러와지고 애플리케이션이 정상적으로 구동되는 것을 확인하였습니다.
<img src="https://velog.velcdn.com/images/silver_cherry/post/4d1d5350-0a8b-4b24-91a1-bd7ed70acdd7/image.png" alt=""></li>
</ol>
<hr>
<h4 id="해결책">해결책</h4>
<p>AWS CLI를 설치하고 IAM 정책을 조정한 후, 환경변수 값을 추출하는 스크립트를 수정하여 문제를 해결하였습니다. 이를 통해 EC2 인스턴스에서 Spring Boot 을 성공적으로 실행시킬 수 있었습니다.</p>
<hr>
<h4 id="결론">결론</h4>
<p>이번 트러블슈팅을 통해 AWS 클라우드 서비스와 Spring Boot의 통합 관리에 대한 이해가 되었습니다. 
특히, CodeDeploy는 .bashrc에 있는 환경변수를 읽어들이지 못한다는 사실을 새롭게 알게 되었습니다.
성능과 보안 요구 사항을 충족하기 위해 Docker를 사용하여 애플리케이션을 컨테이너화하고, 실행 시점에 환경 변수를 주입하는 방식을 고려했으나, 
지속적인 서비스의 스케일링과 자동화된 환경 관리를 위한 복잡한 오케스트레이션 필요성 때문에 AWS Systems Manager Parameter Store를 사용하기로 결정했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot 보안 설정, JWT 인증 및 데이터베이스 연결 문제 해결]]></title>
            <link>https://velog.io/@silver_cherry/Spring-Boot-%EB%B3%B4%EC%95%88-%EC%84%A4%EC%A0%95-JWT-%EC%9D%B8%EC%A6%9D-%EB%B0%8F-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@silver_cherry/Spring-Boot-%EB%B3%B4%EC%95%88-%EC%84%A4%EC%A0%95-JWT-%EC%9D%B8%EC%A6%9D-%EB%B0%8F-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sun, 12 May 2024 02:55:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/silver_cherry/post/f1583b38-9abc-49df-a769-8a7b6e24d4b8/image.png" alt="">
<img src="https://velog.velcdn.com/images/silver_cherry/post/da473c2f-2b96-4fb0-a096-d88759cac329/image.png" alt=""></p>
<blockquote>
<p>프론트엔드 개발자로부터 403 Forbidden 오류에 대한 문의가 있었습니다. 이러한 오류는 다양한 원인으로 인해 발생할 수 있지만, 저는 몇 가지 가능한 원인을 예상해볼 수 있었습니다.</p>
</blockquote>
<h3 id="첫째-corscross-origin-resource-sharing-정책-설정-문제일-수-있습니다">첫째, CORS(Cross-Origin Resource Sharing) 정책 설정 문제일 수 있습니다.</h3>
<p>개발 단계에서는 다양한 출처의 요청을 수용하기 위해 CORS 정책을 유연하게 설정하는 경우가 많습니다. 예를 들어, <code>setAllowedOriginPatterns(Collections.singletonList(&quot;*&quot;))</code>를 사용하여 모든 도메인에서의 요청을 허용하도록 설정할 수 있습니다. 그러나 실제 운영 환경에서는 이와 같은 완화된 CORS 정책이 심각한 보안 위험을 초래할 수 있습니다. 따라서 배포 시에는 엄격한 CORS 정책을 설정하여 출처를 제한함으로써 보안을 강화해야 합니다.</p>
<h3 id="둘째-jwtjson-web-token-인증-과정에서-문제가-있을-수-있습니다">둘째, JWT(JSON Web Token) 인증 과정에서 문제가 있을 수 있습니다.</h3>
<p>Spring Security의 JwtAuthorizationFilter는 사용자 인증 과정에서 JWT 토큰의 유효성을 검증합니다. 이 필터는 토큰을 파싱하고 유효성을 검사하여 사용자의 인증 상태를 결정합니다. 따라서 JWT 인증 필터의 설정 오류나 토큰 누락 등의 문제로 인해 403 Forbidden 오류가 발생할 수 있습니다. 이러한 경우 JWT 인증 필터를 일시적으로 비활성화하여 문제의 원인을 진단할 수 있습니다.</p>
<h3 id="셋째-데이터베이스-연결-설정-문제일-수-있습니다">셋째, 데이터베이스 연결 설정 문제일 수 있습니다.</h3>
<p>데이터베이스 연결 초기화 중 <code>CommunicationsException</code>이나 <code>UnknownHostException</code>과 같은 오류가 발생할 경우, 이는 주로 잘못된 데이터베이스 주소나 네트워크 설정 문제에서 비롯됩니다. 이러한 상황에서는 환경 설정 파일의 데이터베이스 호스트 이름 및 주소 정보를 정확히 입력하는 것이 매우 중요합니다. 올바른 설정은 애플리케이션의 안정적인 데이터베이스 연결에 필수적입니다.</p>
<h3 id="넷째-spring-security-설정-문제일-수-있습니다">넷째, Spring Security 설정 문제일 수 있습니다.</h3>
<p>보안 필터나 권한 설정 등 Spring Security 관련 설정이 잘못되었을 경우, 403 Forbidden 오류가 발생할 수 있습니다. 이러한 경우에는 Spring Security 설정 파일을 주의 깊게 검토하고, 필요에 따라 설정을 조정해야 합니다.</p>
<h3 id="마지막으로-서버-불안정으로-인한-문제일-수-있습니다">마지막으로, 서버 불안정으로 인한 문제일 수 있습니다.</h3>
<p>서버 자원 부족, 네트워크 문제, 데이터베이스 부하 등 다양한 요인으로 인해 서버가 불안정해질 경우, 403 Forbidden 오류를 포함한 다양한 오류가 발생할 수 있습니다. 이러한 상황에서는 서버 모니터링을 통해 문제의 원인을 파악하고, 대응 방안을 마련해야 합니다.</p>
<hr>
<p>이번 학습을 통해 403 Forbidden 오류의 다양한 원인을 파악하고, 각각의 상황에 따른 대응 방안을 모색할 수 있었습니다. 특히 CORS 정책 설정, JWT 인증 과정 구현, 데이터베이스 연결 설정, Spring Security 구성, 서버 모니터링 등의 중요성을 깊이 있게 이해할 수 있었습니다. 이러한 지식과 경험은 향후 실제 운영 환경에서 발생할 수 있는 다양한 이슈에 효과적으로 대응할 수 있는 기반이 될 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2 SSH 접속 문제 해결]]></title>
            <link>https://velog.io/@silver_cherry/AWS-EC2-SSH-%EC%A0%91%EC%86%8D-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@silver_cherry/AWS-EC2-SSH-%EC%A0%91%EC%86%8D-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 08 May 2024 01:21:46 GMT</pubDate>
            <description><![CDATA[<h3 id="초기-접속-시도-및-문제-확인">초기 접속 시도 및 문제 확인</h3>
<ul>
<li><strong>SSH 접속 오류</strong>: <code>Operation timed out</code> 오류가 발생하여 AWS EC2 인스턴스에 SSH 접속 시도가 실패했습니다.</li>
</ul>
<h3 id="기본적인-문제-해결-접근">기본적인 문제 해결 접근</h3>
<ol>
<li><p><strong>Security Group 설정 확인</strong>:</p>
<ul>
<li>AWS Management Console을 통해 인스턴스의 Security Group 설정을 확인하고, IP 주소와 포트 22가 올바르게 설정되어 있는지 검증했습니다.</li>
</ul>
</li>
<li><p><strong>Network ACLs 점검</strong>:</p>
<ul>
<li>인스턴스가 속한 VPC의 Network ACL 설정을 점검하여 인바운드 및 아웃바운드 규칙이 SSH 트래픽을 허용하고 있는지 확인했습니다.</li>
</ul>
</li>
<li><p><strong>Public IP 확인</strong>:</p>
<ul>
<li>인스턴스를 중지했다 재시작할 경우 변경될 수 있는 Public IP 주소를 확인하고, 최신 주소로 접속을 시도했습니다.</li>
</ul>
</li>
<li><p><strong>인스턴스의 시스템 로그 접근</strong>:</p>
<ul>
<li>AWS Management Console에서 시스템 로그를 검토하여 부팅과 관련된 문제가 없는지 확인했습니다.</li>
</ul>
</li>
</ol>
<h3 id="네트워크-및-로컬-설정-점검">네트워크 및 로컬 설정 점검</h3>
<ol>
<li><strong>로컬 방화벽 설정 확인 (MacOS)</strong>:<ul>
<li>MacOS의 시스템 환경설정에서 원격 로그인(SSH)이 활성화되어 있는지 확인하고, 필요한 경우 방화벽 규칙을 조정했습니다.
<img src="https://velog.velcdn.com/images/silver_cherry/post/51d2cce9-2212-46be-8e61-bcc1dcd82033/image.png" alt=""></li>
</ul>
</li>
</ol>
<ol start="2">
<li><p><strong>다른 네트워크 환경에서 접속 시도</strong>:</p>
<ul>
<li>다양한 네트워크 환경(예: 다른 Wi-Fi, 휴대폰 핫스팟)을 사용하여 접속 문제가 네트워크에 의한 것인지 확인했습니다.</li>
</ul>
</li>
<li><p><strong>SSH 접속 상세 로그 확인</strong>:</p>
<ul>
<li>SSH 접속 시 <code>-vvv</code> 옵션을 사용하여 자세한 디버그 정보를 통해 접속 시도 중의 문제점을 파악했습니다.</li>
</ul>
</li>
</ol>
<h3 id="최종적인-문제-해결">최종적인 문제 해결</h3>
<ol>
<li><strong>인스턴스 사용자 데이터를 통한 접근</strong>:<ul>
<li><a href="https://stackoverflow.com/questions/41929267/locked-myself-out-of-ssh-with-ufw-in-ec2-aws">스택 오버플로우의 게시물</a>을 참조하여 인스턴스의 사용자 데이터를 수정, 방화벽(UFW) 설정을 재구성하여 SSH 접속 문제를 해결했습니다. 복사하여 붙여넣은 사용자 데이터 스크립트는 UFW를 비활성화하고 필요한 포트를 개방하는 명령을 포함했습니다.</li>
</ul>
</li>
</ol>
<h3 id="배운-점-및-교훈">배운 점 및 교훈</h3>
<ul>
<li><strong>문제 해결의 중요성</strong>: 문제 해결 과정에서 다양한 시도를 하면서 문제의 원인을 파악하고, 적절한 해결책을 찾아 적용하는 방법을 배웠습니다.</li>
<li><strong>리소스와 커뮤니티의 활용</strong>: 공식 문서, AWS 관리 콘솔, 스택 오버플로우 등 다양한 리소스와 커뮤니티를 활용하여 문제를 해결했습니다.</li>
</ul>
<p>이 과정을 통해 AWS EC2 인스턴스의 SSH 접속 문제 해결에 필요한 기술적 접근과 네트워크 관리 능력을 향상시킬 수 있었습니다.</p>
]]></description>
        </item>
    </channel>
</rss>