<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>star_pooh.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 14 Jan 2025 12:25:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. star_pooh.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/star_pooh" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[20250114 회고]]></title>
            <link>https://velog.io/@star_pooh/20250114-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@star_pooh/20250114-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 14 Jan 2025 12:25:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p>✅ 알고리즘 &amp; SQL 코드 카타
💡 오랜만에 코드 카타를 풀었는데 그래도 다행히 쉬운 문제여서 금방 풀 수 있었다. (알고리즘 문제에서 시간 초과 때문에 코드를 한 번 갈아 엎었지만...) SQL도 아직은 문제 유형이 반복적이라서 문제를 못 읽어서 못 풀지는 않는 것 같다. (예시를 보고 이해한 적도 더러 있지만...)</p>
<blockquote>
</blockquote>
<p>✅ 미니 프로젝트 성능 개선
💡 미니 프로젝트를 진행했을 때 성능 개선을 하고 싶었던 찜찜한 부분이 있었는데, 그 부분을 해결했다. 내가 100을 한 건 아니지만 같이 의견 제시하고 유의미한 결과를 도출해내서 좋은 경험이 되었던 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[트러블 슈팅_성능 개선(쿼리 수정)]]></title>
            <link>https://velog.io/@star_pooh/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EC%BF%BC%EB%A6%AC-%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@star_pooh/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EC%BF%BC%EB%A6%AC-%EC%88%98%EC%A0%95</guid>
            <pubDate>Tue, 14 Jan 2025 09:18:45 GMT</pubDate>
            <description><![CDATA[<p>미니 프로젝트를 진행하면서 조회 API의 쿼리를 수정하여 성능 개선을 이룬 부분에 대해서 적어보려고 한다.</p>
<h2 id="전제-조건">전제 조건</h2>
<p>우리가 진행한 미니 프로젝트는 배달앱을 모티브로 하였으며, 1건의 주문에 1건의 리뷰(일반 사용자)를 달 수 있고, 
작성된 리뷰 1건에 1건의 코멘트(사장님)를 달 수 있는 전제 조건을 가지고 있다. 즉, 리뷰와 코멘트는 일대일 연관관계를 가지고 있다.</p>
<img src="https://velog.velcdn.com/images/star_pooh/post/65eb1127-1bd2-4154-a225-73a42bb5c7fc/image.png" width=1000>

<h2 id="성능-개선의-필요성-인식">성능 개선의 필요성 인식</h2>
<h3 id="db에-너무-많은-요청을-보내는데">DB에 너무 많은 요청을 보내는데..?</h3>
<blockquote>
</blockquote>
<pre><code class="language-java">// Dto
@Getter
public class ReviewListDto {
  // 리뷰 정보
  private Long reviewId;
  private Long purchaseId;
  private Long userId;
  private Long shopId;
  private Long starPoint;
  private String reviewContent;
  private LocalDateTime createdAt;
  private LocalDateTime updatedAt;
  // 코멘트 정보
  private CommentReviewDto comment;
&gt;  
  // ... 생략
}
&gt;
@Getter
@AllArgsConstructor
public class CommentReviewDto {
&gt;
  private Long reviewId;
  private Long commentId;
  private Long userId;
  private String commentContent;
  private LocalDateTime createdAt;
  private LocalDateTime updatedAt;
&gt;
  // ... 생략
  }
}</code></pre>
<blockquote>
</blockquote>
<pre><code class="language-java">// Controller
@GetMapping(&quot;/shops/{shopId}&quot;)
public ResponseEntity&lt;ApiResponse&lt;List&lt;ReviewListDto&gt;&gt;&gt; getShopReview(
    @PathVariable Long shopId,
    @RequestParam(defaultValue = &quot;1&quot;) Long minStarPoint,
    @RequestParam(defaultValue = &quot;5&quot;) Long maxStarPoint) {
&gt;
    List&lt;ReviewListDto&gt; reviewDtos = reviewService.getShopReview(shopId, minStarPoint, maxStarPoint);
    ApiResponse&lt;List&lt;ReviewListDto&gt;&gt; apiResponse = ApiResponse.success(HttpStatus.OK, &quot;가게 리뷰 조회 성공&quot;, reviewDtos);
&gt;
    return new ResponseEntity&lt;&gt;(apiResponse, HttpStatus.OK);
}</code></pre>
<blockquote>
</blockquote>
<pre><code class="language-java">// Service
public List&lt;ReviewListDto&gt; getShopReview(Long shopId, Long minStarPoint, Long maxStarPoint) {
    // ... 생략
&gt;
    List&lt;Review&gt; reviewList = reviewRepository.findAllByShopShopIdAndStarPointBetweenOrderByCreatedAtDesc(
        shopId, minStarPoint, maxStarPoint);
&gt;   
    return reviewList.stream().map(
        review -&gt; ReviewListDto.builder()
            .userId(review.getUser().getUserId())
            .createdAt(review.getCreatedAt()).updatedAt(review.getLastModifiedAt())
            .starPoint(review.getStarPoint()).reviewContent(review.getReviewContent())
            .shopId(review.getShop().getShopId()).reviewId(review.getReviewId())
            .purchaseId(review.getPurchase().getPurchaseId()).comment(
                Optional.ofNullable(
                    // 리뷰 1건의 데이터를 조회할 때마다 작성된 코멘트의 데이터도 조회
                    commentRepository.findByReviewReviewId(review.getReviewId()))
                    .map(CommentReviewDto::convertDto).orElse(null))
            .build()).toList();
  }</code></pre>
<p>리뷰 조회시 코멘트에 대한 정보도 함께 출력해줘야 했으며, 위의 코드는 리뷰 조회 API의 일부 코드이다. 
리뷰와 코멘트를 함께 출력해줘야 한다는 요구사항에 맞게 Dto 내부에 코멘트 Dto도 포함되어 있으며, 서비스에서는 조회한 리뷰 데이터마다 코멘트 데이터를 조회하고 있다. 프로그램 실행에 문제는 없었지만 이렇게 작성된 코드를 보니 DB에 보내는 요청을 너무나도 줄이고 싶었다. </p>
<p>지금 상태라면 <strong><code>n건</code></strong>의 리뷰 데이터가 있다고 가정할 때 <strong><code>2n번</code></strong>의 DB 접근이 필요하다. 
그리고 <code>commentContent</code>라는 데이터 이외에는 필요 없는 데이터이기 때문에 힘들게 가져온 데이터 중에서도 상당 부분이 필요없는 데이터이다. </p>
<h3 id="필요한-데이터만-최소한의-db-접근으로-가져오자">필요한 데이터만 최소한의 DB 접근으로 가져오자</h3>
<p>그래서 우리는 DB 접근 횟수를 최소한으로 줄이면서 <code>commentContent</code> 만 가져올 수 있도록 수정을 시작했다.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">// Dto
@Data
public class ReviewWithCommentDto {
&gt;
  private Long reviewId;
  private Long userId;
  private LocalDateTime createdAt;
  private LocalDateTime lastModifiedAt;
  private Long starPoint;
  private String reviewContent;
  private Long shopId;
  private Long purchaseId;
  private String commentContent;
&gt;  
  // ... 생략
  }
}</code></pre>
<blockquote>
</blockquote>
<pre><code class="language-java">// Controller
@GetMapping(&quot;/shops/{shopId}&quot;)
public ResponseEntity&lt;ApiResponse&lt;List&lt;ReviewWithCommentDto&gt;&gt;&gt; getShopReview(
    @PathVariable Long shopId,
    @RequestParam(defaultValue = &quot;1&quot;) Long minStarPoint,
    @RequestParam(defaultValue = &quot;5&quot;) Long maxStarPoint) {
&gt;
    List&lt;ReviewWithCommentDto&gt; reviewDtos = reviewService.getShopReview(shopId, minStarPoint, maxStarPoint);
    ApiResponse&lt;List&lt;ReviewWithCommentDto&gt;&gt; apiResponse = ApiResponse.success(HttpStatus.OK, &quot;가게 리뷰 조회 성공&quot;, reviewDtos);
&gt;
    return new ResponseEntity&lt;&gt;(apiResponse, HttpStatus.OK);
  }</code></pre>
<blockquote>
</blockquote>
<pre><code class="language-java">// Service
public List&lt;ReviewWithCommentDto&gt; getShopReview(Long shopId, Long minStarPoint,
      Long maxStarPoint) {
    // ... 생략
&gt;
    List&lt;ReviewWithCommentDto&gt; results = reviewRepository.findReviewsWithComment(shopId, minStarPoint, maxStarPoint);
&gt;
    return results.stream()
        .map(dto -&gt; ReviewWithCommentDto.builder()
            .reviewId(dto.getReviewId())
            .userId(dto.getUserId())
            .createdAt(dto.getCreatedAt())
            .lastModifiedAt(dto.getLastModifiedAt())
            .starPoint(dto.getStarPoint())
            .reviewContent(dto.getReviewContent())
            .shopId(dto.getShopId())
            .purchaseId(dto.getPurchaseId())
            .commentContent(dto.getCommentContent())
            .build())
        .toList();
  }</code></pre>
<blockquote>
</blockquote>
<pre><code class="language-java">// Repository
public interface ReviewRepository extends JpaRepository&lt;Review, Long&gt; {
  @Query(&quot;&quot;&quot;
          SELECT
              new Not.Delivered.review.domain.Dto.ReviewWithCommentDto(
                r.reviewId,
                r.user.userId,
                r.createdAt,
                r.lastModifiedAt,
                r.starPoint,
                r.reviewContent,
                r.shop.shopId,
                r.purchase.purchaseId,
                (SELECT c.commentContent FROM Comment c WHERE c.review.reviewId = r.reviewId)
              )
          FROM Review r
          WHERE r.shop.shopId = :shopId
          AND r.starPoint BETWEEN :minStarPoint AND :maxStarPoint
          ORDER BY r.createdAt DESC
      &quot;&quot;&quot;)
  List&lt;ReviewWithCommentDto&gt; findReviewsWithComment(Long shopId, Long minStarPoint,
      Long maxStarPoint);
}</code></pre>
<p>가장 크게 바뀐 것은 <strong><code>Dto</code></strong>와 <strong><code>Repository</code></strong>이다.
<strong><code>Dto</code></strong>에서는 Comment의 모든 필드가 아닌 꼭 필요한 <code>commentContent</code>만 포함하도록 수정했다.</p>
<p>이에 따라 <strong><code>Repository</code></strong>에서도 바뀐 Dto인 <code>ReviewWithCommentDto</code>의 필드에 맞게 리뷰 테이블과 코멘트 테이블을 한 번에 조회하도록 수정했다. 
이제 리뷰 테이블과 코멘트 테이블을 따로 조회하지 않아도 되는 것이다.</p>
<h2 id="그래서-결과는">그래서 결과는..?</h2>
<p>API를 반복적으로 호출하면 캐싱과 같은 부가적인 요소 때문에 시간이 다르게 측정될 수 있음을 고려하여, 각 API 호출 전에 애플리케이션을 재시작하였다. 데이터 수와 테스트 수 모두 많지 않지만 유의미한 결과를 볼 수 있었다.</p>
<h3 id="1--데이터-수--리뷰-3건-코멘트-3건-→">1.  데이터 수 : 리뷰 3건, 코멘트 3건 →</h3>
<img src="https://velog.velcdn.com/images/star_pooh/post/063fbe3b-2bb0-45d7-899c-c9e38d755365/image.png" width=1100>

<blockquote>
</blockquote>
<p>성능 개선 이전</p>
<ul>
<li>DB 접근 횟수(쿼리 발생 횟수) : 6회</li>
<li>처리 시간 : 141 ms<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/6ec688c7-c9ff-4ce8-b759-701c1c76c90c/image.png" width=1100>

</li>
</ul>
<blockquote>
</blockquote>
<p>성능 개선 이후</p>
<ul>
<li>DB 접근 횟수(쿼리 발생 횟수) : 2회</li>
<li>처리 시간 : 88 ms<img src="https://velog.velcdn.com/images/star_pooh/post/7e82506a-ce3f-4dab-b578-6b3445824625/image.png" width=1100>

</li>
</ul>
<h3 id="2-데이터-수--리뷰-20건-코멘트-20건">2. 데이터 수 : 리뷰 20건, 코멘트 20건</h3>
<img src="https://velog.velcdn.com/images/star_pooh/post/a36e31f2-70ce-45dd-b4dc-3bdc850d296f/image.png" width=1100>

<blockquote>
</blockquote>
<p>성능 개선 이전</p>
<ul>
<li>DB 접근 횟수(쿼리 발생 횟수) : 40회</li>
<li>처리 시간 : 162 ms<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/dbf2868d-ffdf-43b9-9f8d-702cd1392b74/image.png" width=1100>

</li>
</ul>
<blockquote>
</blockquote>
<p>성능 개선 이후</p>
<ul>
<li>DB 접근 횟수(쿼리 발생 횟수) : 2회</li>
<li>처리 시간 : 95 ms<img src="https://velog.velcdn.com/images/star_pooh/post/96bfde21-2282-47f6-aac5-573d76d77dc4/image.png" width=1100>

</li>
</ul>
<h3 id="3-데이터-수--리뷰-50건-코멘트-50건">3. 데이터 수 : 리뷰 50건, 코멘트 50건</h3>
<img src="https://velog.velcdn.com/images/star_pooh/post/7d52a693-0647-48c6-8076-fd26d7cbab1f/image.png" width=1100>

<blockquote>
</blockquote>
<p>성능 개선 이전</p>
<ul>
<li>DB 접근 횟수(쿼리 발생 횟수) : 100회</li>
<li>처리 시간 : 233 ms<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/4664df3a-7486-43e9-bb3f-817860bbb883/image.png" width=1100>

</li>
</ul>
<blockquote>
</blockquote>
<p>성능 개선 이후</p>
<ul>
<li>DB 접근 횟수(쿼리 발생 횟수) : 2회</li>
<li>처리 시간 : 110 ms<img src="https://velog.velcdn.com/images/star_pooh/post/d08be69c-7860-4d09-8293-0e4c53166699/image.png" width=1100>

</li>
</ul>
<h2 id="정리">정리</h2>
<p>무분별한 쿼리메소드 사용은 불필요한 데이터까지 얻어오고, 잘못된 로직 구성은 꽤나 큰 리소스 낭비로 이어진다는 것을 알 수 있었다.
앞으로도 최적화, 개선에 초점을 맞춰서 개발할 수 있는 자세를 가져야겠다는 생각을 가지게 되었다.</p>
<blockquote>
<p>✅ 테스트 내용 요약
#1 
DB 접근 횟수(쿼리 발생 횟수) : 6회 → 2회
처리 시간 : 141ms → 88ms
(약 1.6배의 처리 속도 증가 == 약 37.59%의 효율 증가)</p>
</blockquote>
<p>#2 
DB 접근 횟수(쿼리 발생 횟수) : 40회 → 2회
처리 시간 : 162ms → 95ms
(약 1.71배의 처리 속도 증가 == 약 41.36%의 효율 증가)</p>
<blockquote>
</blockquote>
<p>#3 
DB 접근 횟수(쿼리 발생 횟수) : 100회 → 2회
처리 시간 : 233ms → 110ms
(약 2.12배의 처리 속도 증가 == 약 52.79%의 효율 증가)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250113 회고]]></title>
            <link>https://velog.io/@star_pooh/20250113-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@star_pooh/20250113-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 13 Jan 2025 11:34:05 GMT</pubDate>
            <description><![CDATA[<h3 id="1-한-일">1. 한 일</h3>
<ul>
<li><p>프로젝트 명: 404 Not Delivered</p>
</li>
<li><p>팀 작업 내용</p>
<ul>
<li>배달의 민족을 모방한 배달 애플리케이션 아웃소싱</li>
</ul>
</li>
</ul>
<h3 id="2-keep">2. Keep</h3>
<h4 id="1-협업-내용-정하기">1) 협업 내용 정하기</h4>
<pre><code>IDE 세팅을 비롯한 다양한 코딩 컨벤션(변수 및 메소드명, 인덴트 사이즈 등....)과
깃 커밋 메시지, 브랜치 전략 등을 사전에 정하고 프로젝트를 진행해서 일관성 있게 
진행할 수 있었던 것 같다.</code></pre><h4 id="2-원활한-의사소통">2) 원활한 의사소통</h4>
<pre><code>의사소통하는데에 약 이틀을 사용할 정도로 많은 의사소통을 해서 시간이 모자를 줄 알았는데,
오히려 프로젝트 후반에 강점을 발휘해서 빨리 끝낼 수 있던 요소로 작용한 것 같다.</code></pre><h4 id="3-pr을-자주-작성하기">3) PR을 자주 작성하기</h4>
<pre><code>브랜치 전략은 작은 기능 단위로 가져가면서 PR 및 코드리뷰를 할 상황이 많이 발생했는데,
리뷰어의 시간이 적게 소모되고 기능 사용이 익숙해져서 좋았다.</code></pre><h4 id="4-팀원-간-구현-내용-공유">4) 팀원 간 구현 내용 공유</h4>
<pre><code>각자 맡은 부분만 구현한 다음에 합치고 끝!이 아니라 각자 구현한 부분을 서로에게
설명하고 질문을 하는 식으로 프로젝트 전체의 흐름에 대해서 파악할 수 있었던 점이 좋았다.</code></pre><h3 id="3-problem">3. Problem</h3>
<h4 id="1-패키지-네이밍-실수">1) 패키지 네이밍 실수</h4>
<pre><code>프로젝트를 생성할 때 잘못 만들어서 추후에 리팩토링을 하려고 했더니
예상치 못한 부분에 많은 에러가 발생했다. 처음부터 신경써서 제대로 만들어야겠다.</code></pre><h4 id="2-성능-개선-미반영">2) 성능 개선 미반영</h4>
<pre><code>중복 코드 및 미사용 코드, DB에서의 데이터 조회 등에 대한 코드들이 성능 개선을 
할 수 있는 부분이 남아있는 채로 마무리 했다. 조금 더 효율적이면서 빠른 프로그램이 
될 수 있도록 고민을 할 필요가 있을 것 같다. </code></pre><h3 id="4-try">4. Try</h3>
<h4 id="1-docker를-사용한-환경설정-제공">1) Docker를 사용한 환경설정 제공</h4>
<pre><code>환경설정을 통일할 수 있고, 예기치 못한 상황이 발생해서 환경이 꼬이더라도
재설정하는데 큰 리소스가 필요하지 않기 때문에 사용해보면 좋을 것 같다. </code></pre><h4 id="2-aop를-통한-중복코드-제거">2) AOP를 통한 중복코드 제거</h4>
<pre><code>AOP와 커스텀 어노테이션을 사용한다면 인가, 로깅과 같은 중복이 발생할 수 있는 부분에
대해서 관리할 수 있기 때문에 가독성 및 유지보수 측면에서 효과를 기대할 수 있을 것 같다.</code></pre><h4 id="3-cicd-적용하기">3) CI/CD 적용하기</h4>
<pre><code>이번 프로젝트에서는 서버 담당자가 직접 배포를 진행해서 공백이 발생할 수 있는 부분이 있었다.
그래서 코드 컨벤션 및 테스트 정상 작동, 서버 자동 배포 등 리소스를 아낄 수 있는 CI/CD를 적용해 보면 좋을 것 같다.</code></pre><h3 id="5-feel">5. Feel</h3>
<ul>
<li><p>강세민
 팀장으로서 부족했던 모습이 많았을 것 같은데 팀원 분들이 모두 좋은 분들이고
 각자 맡은 바를 생각 이상으로 너무 잘들 해주셔서 즐겁게 프로젝트를 진행할 수 있었다.</p>
</li>
<li><p>김형준
 서버 배포를 준비하느라 전체적인 작업 흐름에 개입을 하지 못했던 게 아쉬웠다. 
 API 문서를 짜며 팀원들의 코드 자체와 각각의 흐름을 눈에 익힐 수 있었지만, 이를 어느 정도 늦은 타이밍에 알아챘기에 개입할 여지가 적었다.</p>
<p> Github Actions를 통한 CD를 구현은 했지만 적용해보지 못한 게 아쉬웠다.
 그래도 의사소통에 좀 더 적극적으로 참여해봤던 점, 서버 캐시를 사용하는 방식들에 대해 이해할 수 있었던 점, 팀원들의 코드를 이해할 확실한 방법을 익혀봤던 점이 좋았다고 생각한다. </p>
</li>
<li><p>임희현</p>
<pre><code>  프로젝트를 진행하면서 어려운 점이 있을 때마다 팀원들과 의견 교환 및 공유를 통해 많이 배울 수 있어서 좋은 경험이었다.</code></pre></li>
<li><p>최원준</p>
<pre><code>  동료들이 참조할 엔티티와 기능을 빠르게 만들고 코드의 일관성 유지와 품질 향상을 위해 노력했다. 
  협업 도구와 함께 메모장과 스티커 노트 등으로 체계적으로 기록하고 관리하는 것이 작업 효율성을 높이는 데 중요하다는 것을 깨달았다. 
  새로운 기술은 빠르게 쓰고, 기존 도구와 기술은 꼼꼼하게 관리하도록 해야겠다.</code></pre></li>
<li><p>홍은기</p>
<pre><code>프로젝트를 진행하는데 큰 어려움이 없었던 가장 큰 이유는 원활한 의사소통이라고 생각한다. 
의사소통을 나누며 모자란 부분, 더 알게 된 부분들이 많기 때문에 다시 한 번 의사소통의 중요성에 대해 알게 되었다.</code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[트러블 슈팅_API 로깅(AOP)]]></title>
            <link>https://velog.io/@star_pooh/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85API-%EB%A1%9C%EA%B9%85AOP</link>
            <guid>https://velog.io/@star_pooh/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85API-%EB%A1%9C%EA%B9%85AOP</guid>
            <pubDate>Mon, 06 Jan 2025 11:09:55 GMT</pubDate>
            <description><![CDATA[<p>과제를 진행하면서 구현했던 API 로깅에 대해서 어려웠던 부분과 알게 된 부분을 적어보려고 한다.</p>
<h3 id="interceptor-vs-aop">Interceptor VS AOP</h3>
<p>과제에 제시된 구현 방법은 인터셉터와 AOP 두 가지였는데 나는 AOP를 선택하였다. Admin 사용자만 접근할 수 있는 API의 실행 전후에 로깅을 한다는 세부 구현의 내용이 좀 더 주제에 맞는 것 같다고 생각했다. 또한, 인터셉터를 사용할 경우엔 <code>SecurityContextHolder</code> 또는 <code>HttpServletRequest</code>에서 사용자의 인증 정보를 가져올 수 있는데 나도 모르게 스프링 시큐리티를 사용하고 있을 것 같다는 생각이 들어서 인터셉터를 배제한 이유도 있다.</p>
<h3 id="aop로-구현하기">AOP로 구현하기</h3>
<p>횡단 관심사라는 것을 나타내기 위해 <code>@Aspect</code>를 사용하고, 메소드 호출 전 / 후에 로깅을 해야하기 때문에 <code>@Around</code>를 사용하였다. 그리고 Admin 사용자만 접근할 수 있는 API라는 조건이 주어졌기 때문에 <code>@Pointcut</code>을 사용해서 Aspect가 언제 적용될 지 정의하였다.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AdminApiLogging {
&gt;
    private final ObjectMapper objectMapper;
&gt;
    @Around(&quot;adminMethodA() || adminMethodB()&quot;)
    public Object adminApiLogging(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
&gt;
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
&gt;
        String requestBody = getRequestBody(wrappedRequest);
        Long userId = (Long) request.getAttribute(&quot;userId&quot;);
&gt;
        log.info(&quot;API Request Log - User ID : {}, Request Time : {}, URL : {}, RequestBody : {}&quot;,
                userId, LocalDateTime.now(), request.getRequestURL(), requestBody);
&gt;
        // 원래 메소드 실행
        Object result = joinPoint.proceed();
&gt;
        String responseBody = getResponseBody(wrappedResponse);
        log.info(&quot;API Response Log - User ID : {}, ResponseBody : {}&quot;, userId, responseBody);
&gt;
        return result;
    }
&gt;
    // 로깅에 Json 형식으로 출력하기 위해 requestBody / responseBody를 Json으로 변환하는 로직
    private String getRequestBody(ContentCachingRequestWrapper request) throws IOException {
        // 생략
    }
&gt;
    private String getResponseBody(ContentCachingResponseWrapper response) throws IOException {
        // 생략
    }
&gt;
    @Pointcut(&quot;execution(* org.example.domain.user.controller.UserAdminController.adminMethodA(..))&quot;)
    private void adminMethodA() {
    }
&gt;
    @Pointcut(&quot;execution(* org.example.domain.comment.controller.CommentAdminController.adminMethodB(..))&quot;)
    private void adminMethodB() {
    }
}</code></pre>
<img src="https://velog.velcdn.com/images/star_pooh/post/b912316b-c961-4026-b76f-f71c88f34309/image.png" width=1300>

<p>Admin 사용자만 접근 가능한 API를 실행하면 전후에 로깅은 문제 없이 출력됐다. 그런데 <code>RequestBody</code>가 빈 값으로 출력되는 문제가 있었다.(반환 타입이 <code>void</code>인 API이기 때문에 <code>ResponseBody</code>는 빈 값으로 출력되는게 정상이다.)</p>
<h3 id="requestbody-값은-왜-비어있을까">RequestBody 값은 왜 비어있을까?</h3>
<p>API를 실행하면 Postman에서도 200 응답이기 때문에 API가 요청 / 응답 자체에 문제가 있는 것은 아닌 것 같았다. 그래서 요청을 보냈을 때부터 API가 실행되기 직전(현재 설정해놓은 AOP의 적용 시점이 API 호출 전 / 후이기 때문에)까지의 과정을 알아볼 필요가 있었다. <code>HttpSevletRequest</code>는 <code>ServletRequest</code>를 상속 받고 있는데 <code>ServletRequest</code>에 <code>getInputStream()</code>이라는 메소드가 있었다.</p>
<img src="https://velog.velcdn.com/images/star_pooh/post/07c15eed-9e62-4b3f-8967-9ca61273b39f/image.png" width=700>

<p><code>getInputStream()</code>은 <strong>ServletInputStream</strong> 타입인데 <a href="https://tomcat.apache.org/tomcat-11.0-doc/servletapi/jakarta/servlet/ServletInputStream.html">톰캣의 공식 문서</a>를 확인해보면 다음과 같은 내용이 있다. </p>
<blockquote>
</blockquote>
<pre><code>Provides an input stream for reading binary data from a client request, including an efficient readLine method for reading data one line at a time. 
With some protocols, such as HTTP POST and PUT, a ServletInputStream object can be used to read data sent from the client.
A ServletInputStream object is normally retrieved via the ServletRequest.getInputStream() method.
&gt;
This is an abstract class that a servlet container implements. Subclasses of this class must implement the java.io.InputStream.read() method.</code></pre><p>클라이언트의 요청을 읽기 위해 제공 되는 <strong>InputStream</strong>이라는 것인데, InputStream은 데이터를 읽어들이는 통로로서 <strong>한 번 사용하면 없어진다.</strong> 그러면 위에서 작성한 AOP에 도달하기 전에 어디선가 요청을 읽어들였다는 의미가 된다. Json 형태로 요청을 보냈다고 가정한다면 다음과 같은 처리 과정을 거치게 된다. </p>
<blockquote>
</blockquote>
<pre><code class="language-java">DispatcherServlet
     👇
HandlerAdapter
     👇
HttpMessageConverter(요청 소비)
     👇
AOP(예시에선 AdminApiLogging)
     👇
Controller</code></pre>
<img src="https://velog.velcdn.com/images/star_pooh/post/620c0bdf-d206-43b7-af39-4a0027b3e94f/image.png" width=700>

<p>그림엔 나타나 있지 않지만 빨간 네모로 표시된 부분에서 <strong>HttpMessageConverter</strong>가 <a href="https://docs.spring.io/spring-framework/reference/web/webmvc/message-converters.html">Json 요청을 자바 객체로 변환</a>하기 위해 <strong>InputStream</strong>을 사용한다. 지금은 요청이 Json 형태라고 가정했기 때문에 <code>MappingJackson2HttpMessageConverter</code>가 InputStream을 사용하게 된다. 그리고나서 컨트롤러 호출 직전에 AOP가 실행되는데 이미 사용되고 없는 요청을 읽으니 빈 값이 출력되고 있었던 것이다.</p>
<h3 id="어떻게-해야-여러-번-쓸-수-있을까">어떻게 해야 여러 번 쓸 수 있을까?</h3>
<p>로그에 빈 값이 출력되고 있는 원인을 알았으니 해결책을 찾아야 하는데 이미 스프링에 답이 있었다. <strong><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/ContentCachingRequestWrapper.html"><code>ContentCachingRequestWrapper</code></a></strong>와 <strong><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/ContentCachingResponseWrapper.html"><code>ContentCachingResponseWrapper</code></a></strong>인데, 내용을 살펴보면 다음과 같다.</p>
<blockquote>
</blockquote>
<pre><code>HttpServletRequest wrapper that caches all content read from the input stream and reader, and allows this content to be retrieved via a byte array.
This class acts as an interceptor that only caches content as it is being read but otherwise does not cause content to be read. 
That means if the request content is not consumed, then the content is not cached, and cannot be retrieved via getContentAsByteArray().
&gt;
Used, for example, by AbstractRequestLoggingFilter.</code></pre><p><code>getContentAsByteArray()</code>와 같은 메소드를 통해 InputStream을 소비하면 내부의 byte 배열에 캐싱하는데, 이렇게 캐싱된 요청 데이터는 몇 번이고 사용할 수 있게 되는 것이다.</p>
<h3 id="그럼-어디에서-캐싱해야-할까">그럼 어디에서 캐싱해야 할까?</h3>
<p>요청 데이터는 스프링 내부에서 계속 사용될테니 클라이언트와의 연결점에서 가장 최전선에 있는 필터에서 캐싱을 하는 것이 좋다고 생각했다. 그래서 아래와 같이 요청과 응답을 캐싱하는 필터를 추가주었다.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">@Component
public class ContentCachingFilter implements Filter {
&gt;
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
&gt;
        HttpServletRequest wrappedRequest = new ContentCachingRequestWrapper((HttpServletRequest) servletRequest);
        HttpServletResponse wrappedResponse = new ContentCachingResponseWrapper((HttpServletResponse) servletResponse);
&gt;
        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            ((ContentCachingResponseWrapper) wrappedResponse).copyBodyToResponse();
        }
    }
}</code></pre>
<p>필터를 추가하면 다음과 같은 처리 과정을 거치게 된다.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">ContentCachingFilter(요청을 소비하고 캐싱)
     👇
DispatcherServlet
     👇
HandlerAdapter
     👇
HttpMessageConverter
     👇
AOP(예시에선 AdminApiLogging)
     👇
Controller</code></pre>
<h3 id="api-로깅에-다시-출력해보자">API 로깅에 다시 출력해보자</h3>
<p>사용자가 보낸 요청을 필터에서 사용하고 캐싱했기 때문에 이후의 처리 과정에서는 캐싱한 요청 데이터를 사용하면 된다. 지금 작성해 놓은 API 로깅 코드에서는 캐싱한 요청 데이터를 사용하고 있지 않기 때문에 수정이 필요하다.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AdminApiLogging {
&gt;
    private final ObjectMapper objectMapper;
&gt;
    @Around(&quot;adminMethodA() || adminMethodB()&quot;)
    public Object adminApiLogging(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
&gt;
        String requestBody = getRequestBody((ContentCachingRequestWrapper) request);
        Long userId = (Long) request.getAttribute(&quot;userId&quot;);
&gt;
        log.info(&quot;API Request Log - User ID : {}, Request Time : {}, URL : {}, RequestBody : {}&quot;,
                userId, LocalDateTime.now(), request.getRequestURL(), requestBody);
&gt;
        // 원래 메소드 실행
        Object result = joinPoint.proceed();
&gt;
        String responseBody = getResponseBody((ContentCachingResponseWrapper) response);
        log.info(&quot;API Response Log - User ID : {}, ResponseBody : {}&quot;, userId, responseBody);
&gt;
        return result;
    }
&gt;
    // 로깅에 Json 형식으로 출력하기 위해 requestBody / responseBody를 Json으로 변환하는 로직
    private String getRequestBody(ContentCachingRequestWrapper request) throws IOException {
        // 생략
    }
&gt;
    private String getResponseBody(ContentCachingResponseWrapper response) throws IOException {
        // 생략
    }
&gt;
    @Pointcut(&quot;execution(* org.example.domain.user.controller.UserAdminController.adminMethodA(..))&quot;)
    private void adminMethodA() {
    }
&gt;
    @Pointcut(&quot;execution(* org.example.domain.comment.controller.CommentAdminController.adminMethodB(..))&quot;)
    private void adminMethodB() {
    }
}</code></pre>
<img src="https://velog.velcdn.com/images/star_pooh/post/3dbd5ba7-99cc-4db4-aa0c-ce41a2af94a8/image.png" width=1500>

<p>RequestBody도 문제 없이 출력되는 것을 확인할 수 있다. 지금은 Json 형태의 요청이라고 가정하며 만들었기 때문에 Json 이외의 요청, 에러 상황 등에 대해서는 제대로 동작하지 않을 수 있다. 하지만 RequestBody가 왜 빈 값이 출력되는지 알아보는 과정에서 스프링의 구조적인 부분에 대해 공부가 됐기 때문에 추가적인 구현이 필요하다고 해도 조금 더 수월하게 진행할 수 있지 않을까 생각한다.</p>
<br>

<blockquote>
<p>✅ 출처
Spring AOP란?
<a href="https://velog.io/@wnguswn7/Spring-AOPAspect-Oriented-Programming%EB%9E%80">https://velog.io/@wnguswn7/Spring-AOPAspect-Oriented-Programming%EB%9E%80</a></p>
</blockquote>
<p>HttpServletRequest 여러 번 읽기
<a href="https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times">https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times</a></p>
<blockquote>
</blockquote>
<p>AOP를 사용해서 로깅하기
<a href="https://velog.io/@wnguswn7/Project-Spring-AOP%EB%A1%9C-%EB%A1%9C%EA%B7%B8-%EC%95%8C%EB%A6%BC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">https://velog.io/@wnguswn7/Project-Spring-AOP%EB%A1%9C-%EB%A1%9C%EA%B7%B8-%EC%95%8C%EB%A6%BC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Entity, 연관관계]]></title>
            <link>https://velog.io/@star_pooh/Entity-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@star_pooh/Entity-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84</guid>
            <pubDate>Fri, 03 Jan 2025 11:31:41 GMT</pubDate>
            <description><![CDATA[<h3 id="entity">Entity</h3>
<p>JPA가 관리하는 Entity로 만들기 위해서는 해당 클래스에 어노테이션을 활용해야함.</p>
<blockquote>
</blockquote>
<ul>
<li>@Entity<ul>
<li>JPA가 관리할 객체라는 것을 명시</li>
<li>PK 값을 나타내는 <code>@Id</code>는 반드시 필요</li>
<li>기본 생성자 필수</li>
<li><code>final</code>, <code>enum</code>, <code>interface</code>, <code>inner</code> 클래스에는 사용 불가</li>
<li>필드에 <code>final</code> 키워드 사용 불가<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>@Table<ul>
<li>엔티티와 매핑할 테이블을 지정</li>
<li><code>name</code> 속성을 사용하여 테이블 이름을 지정<pre><code class="language-java">@Entity
@Table(name = &quot;car&quot;)
public class Car {
// PK
@Id
private Long id;
&gt;
// 필드
private String maker;
&gt;   
private String model;
&gt;
// 기본 생성자
public Tutor() {
}
&gt;
public Tutor(Long id, String maker, String model) {
  this.id = id;
  this.maker = maker;
  this.model = model;
}
}</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="기본키">기본키</h3>
<blockquote>
</blockquote>
<ul>
<li>@Id<ul>
<li>엔티티를 생성할 때 필수로 존재해야 하며, 단독으로 사용시 DB에 데이터를 저장할 때 ID값을 수동으로 설정해줘야 함</li>
<li>가장 권장되는 방식은 <code>Long</code> 타입의 기본키를 사용하는 것</li>
</ul>
</li>
<li>@GeneratedValue<ul>
<li>ID값을 자동으로 생성해주며, <code>strategy</code> 속성을 사용하여 다양한 설정 가능<ul>
<li>GenerationType<ul>
<li>IDENTITY : MySQL, PostgreSQL에서 사용하며, PK 자동 생성</li>
<li>SEQUENCE : Oracle에서 사용하며, <code>@SequenceGenerator</code>와 함께 사용</li>
<li>TABLE : 키 생성용 테이블을 사용하며, <code>@TableGenerator</code>와 함께 사용</li>
<li>AUTO : DB 종류에 따라 자동 지정하며 <code>GenerationType</code>의 기본값</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="필드-매핑">필드 매핑</h3>
<p>엔티티의 필드는 테이블의 컬럼과 매핑되며, 다양한 설정 가능</p>
<blockquote>
</blockquote>
<ul>
<li>@Column<ul>
<li>생략해도 DB 컬럼으로 매핑이 가능하지만, 다양한 옵션을 사용하기 위해 설정<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th align="left">속성</th>
<th align="left">설명</th>
<th align="left">기본값</th>
</tr>
</thead>
<tbody><tr>
<td align="left">name</td>
<td align="left">객체 필드와 매핑할 테이블의 컬럼 이름 설정</td>
<td align="left">객체 필드 이름</td>
</tr>
<tr>
<td align="left">nullable</td>
<td align="left">DDL 생성 시 null 값의 허용 여부 설정</td>
<td align="left">true(허용)</td>
</tr>
<tr>
<td align="left">unique</td>
<td align="left">DDL 생성 시 유니크 제약 조건 설정</td>
<td align="left">-</td>
</tr>
<tr>
<td align="left">columnDefinition</td>
<td align="left">DDL 생성 시 컬럼 정보 설정</td>
<td align="left">-</td>
</tr>
<tr>
<td align="left">length</td>
<td align="left">DDL 생성 시 문자 길이 설정(String만 가능)</td>
<td align="left">255</td>
</tr>
<tr>
<td align="left">insertable</td>
<td align="left">설정된 컬럼의 INSERT 가능 여부</td>
<td align="left">true</td>
</tr>
<tr>
<td align="left">updatable</td>
<td align="left">설정된 컬럼의 UPDATE 가능 여부</td>
<td align="left">true</td>
</tr>
</tbody></table>
<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>@Temporal<ul>
<li>날짜 타입을 설정<ul>
<li>객체 필드의 데이터 타입이 <code>LocalDate</code>, <code>LocalDateTime</code>인 경우에는 생략 가능<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li>@Enumerated<ul>
<li>enum 값을 사용하기 위해 설정(DB에는 enum 타입이 없음)<ul>
<li>속성으로 <code>value</code>를 사용하며, 값으로는 enum의 이름을 저장하는 <code>EnumType.STRING</code>을 사용</li>
<li>값이 추가될 때마다 순서가 변하기 때문에 enum의 순서를 저장하는 <code>EnumType.ORDINAL</code>은 사용하지 않음<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li>@Transient<ul>
<li>DB 컬럼과 매핑하지 않을 때 사용<blockquote>
</blockquote>
<pre><code class="language-java">@Entity
@Table(name = &quot;board&quot;)
public class Board {
@Id
private Long id;
&gt;
// @Column을 사용하지 않아도 DB 컬럼으로 매핑
private Integer view;
&gt;
// 객체 필드 이름과 DB 컬럼 이름을 다르게 설정 가능
@Column(name = &quot;title&quot;)
private String bigTitle;
&gt;
@Enumerated(EnumType.STRING)
private BoardType boardType;
&gt;
// longtext를 설정하면 VARCHAR()를 넘어서는 큰 용량의 문자열 저장 가능
@Column(columnDefinition = &quot;longtext&quot;)
private String contents;
&gt;
// 날짜 타입 (DATE, TIME, TIMESTAMP) 사용 가능
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
&gt;
// LocalDate를 사용했기 때문에 @Temporal 생략 가능
private LocalDate lastModifiedDate;
&gt;   
// 객체에선 필요하지만 DB에는 필요 없은 필드
@Transient
private int count;
&gt;
public Board() {
}
}</code></pre>
<img src="https://velog.velcdn.com/images/star_pooh/post/1c9d44ff-f496-43a5-9266-1017da990267/image.png" width=300>
>
>> 💡 hibearnate.hbm2ddl.auto
애플리케이션 로딩 시점에 JPA가 DDL을 생성하기 위해 필요한 설정 값.
>>
|설정값|설명|
|:--|:--|
|create|기존 테이블을 삭제(DROP)한 후 다시 생성(CREATE)|
|create-drop|기존 테이블을 삭제(DROP)한 후 다시 생성(CREATE)하고 애플리케이션 종료 시점에 테이블을 삭제(DROP)|
|update|엔티티에서 변경된 사항만 DDL에 반영(데이터 타입, 컬럼명 등)|
|validate|엔티티와 테이블이 정상적으로 매핑 되었는지 확인하며, 실패 시 예외 발생|
|none|속성을 사용하지 않는 것으로서 아무 변화 없음|
```java
// application.properties에서의 사용 예시
hibernate.hbm2ddl.auto=create
```

</li>
</ul>
</li>
</ul>
<h3 id="연관관계">연관관계</h3>
<h4 id="단방향">단방향</h4>
<p>객체 간의 관계가 한쪽에서만 참조될 수 있는 관계. 설정이 단순하고 유지 관리가 쉬우며 불필요한 데이터 접근 방지 가능.</p>
<img src="https://velog.velcdn.com/images/star_pooh/post/ca3eda90-5f89-4e07-a3aa-14148b159113/image.png" width=400>

<blockquote>
</blockquote>
<ul>
<li>객체가 다른 객체를 참조</li>
<li>N:1 관계일 경우, <code>@ManyToOne</code>, <code>@JoinColume</code>을 사용</li>
<li>N에 해당하는 테이블(Tutor)의 외래키와 1에 해당하는 테이블(Company)의 PK를 <code>@JoinColumn</code>으로 매핑<pre><code class="language-java">@Entity
@Table(name = &quot;tutor&quot;)
public class Tutor {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
&gt;        
  private String name;
  // N:1 단방향 연관관계 설정
  @ManyToOne
  @JoinColumn(name = &quot;company_id&quot;)
  private Company company;
&gt;
  // 기본 생성자, getter/setter
}</code></pre>
</li>
</ul>
<h4 id="양방향">양방향</h4>
<p>객체 간의 관계가 양쪽에서 서로를 참조할 수 있는 관계. 양쪽에서 데이터에 쉽게 접근할 수 있지만, 관계를 관리할 때는 한쪽에서만 연관관계를 설정하거나 삭제하지 않도록 주의 필요.</p>
<img src="https://velog.velcdn.com/images/star_pooh/post/090e0653-2ca0-40b9-96c8-45144dfb2713/image.png" width=400>

<blockquote>
</blockquote>
<ul>
<li>DB 관점에서는 연관관계에 방향의 개념이 없음</li>
<li>따라서 테이블에 변화는 없음</li>
<li>양방향 연관관계 설정을 위해 <code>mappedBy</code> 속성을 설정<ul>
<li>Tutor → Company N:1 연관관계 (단방향)</li>
<li>Company → Tutor 1:N 연관관계 (단방향)</li>
<li><strong>단방향 연관관계 두 개</strong>로 양방향을 설정</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Entity
@Table(name = &quot;tutor&quot;)
public class Tutor {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
&gt;    
    private String name;
    // N:1 단방향 연관관계 설정
    @ManyToOne
    @JoinColumn(name = &quot;company_id&quot;)
    private Company company;
&gt;        
    // 기본 생성자, getter/setter
}</code></pre>
<pre><code class="language-java">@Entity
@Table(name = &quot;company&quot;)
public class Company {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
&gt;
    private String name;
&gt;        
    // null을 방지하기 위해 ArrayList로 초기화
    // Tutor 클래스의 company 필드와 매핑
    @OneToMany(mappedBy = &quot;company&quot;)
    private List&lt;Tutor&gt; tutors = new ArrayList&lt;&gt;();
&gt;        
    // 기본 생성자, getter/setter
}</code></pre>
<h4 id="양방향-연관관계의-주인">양방향 연관관계의 주인</h4>
<p><code>mappedBy</code>는 양방향 연관관계 설정 시 연관관계의 주인이 아닌 쪽에 선언. 이를 통해 외래 키 관리 책임을 주인 엔티티에 두고 매핑이 중복되지 않도록 함.</p>
<img src="https://velog.velcdn.com/images/star_pooh/post/9248716e-250f-4043-ab98-ba58a333491a/image.png" width=350>

<blockquote>
</blockquote>
<ul>
<li>Tutor의 Company를 수정할 때 FK 수정</li>
<li>Company의 Tutor를 수정할 때 FK 수정</li>
<li>Tutor가 새로운 Company를 간다면<ul>
<li>Tutor가 참조하는 Company 수정</li>
<li>Company가 참조하는 List&lt;Tutor&gt; 수정</li>
<li>DB 입장에서는 FK만 수정되면 되기 때문에 한 쪽에서만 FK를 관리해야함</li>
</ul>
</li>
</ul>
<img src="https://velog.velcdn.com/images/star_pooh/post/ec176640-3243-48a7-a9b3-a9e61f09e690/image.png" width=350>

<blockquote>
</blockquote>
<ul>
<li>양방향 연관관계 규칙<ol>
<li>두 개의 엔티티 중 하나를 연관관계의 주인으로 설정해야 함</li>
<li>연관관계의 주인은 <code>mappedBy</code> 속성을 사용하지 않음</li>
<li>연관관계의 주인이 아니면 <code>mappedBy</code> 속성을 사용</li>
<li>연관관계의 주인이 아니면 조회만 가능</li>
<li>연관관계의 주인만 외래키를 관리(등록, 수정)<blockquote>
</blockquote>
</li>
</ol>
</li>
<li>연관관계의 주인 선정 기준<ul>
<li>외래키가 있는 곳을 연관관계의 주인으로 지정</li>
<li>Company가 주인인 경우<ul>
<li>Company를 수정할 때 Tutor를 수정하는 SQL이 실행되기 때문에 두 번의 SQL이 실행되어야 하므로 혼동되기 쉬움</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA]]></title>
            <link>https://velog.io/@star_pooh/JPA</link>
            <guid>https://velog.io/@star_pooh/JPA</guid>
            <pubDate>Thu, 02 Jan 2025 02:55:26 GMT</pubDate>
            <description><![CDATA[<h3 id="객체와-관계형-데이터베이스rdb의-패러다임-불일치">객체와 관계형 데이터베이스(RDB)의 패러다임 불일치</h3>
<blockquote>
</blockquote>
<ul>
<li>상속<ul>
<li>DB에는 상속 관계가 없음</li>
<li>각각의 객체별로 데이터를 저장해야하고, JOIN을 사용해서 데이터를 조회해야 함</li>
<li>데이터를 저장하고 조회하기 까다롭기 때문에 DB에 저장할 객체는 상속 관계를 사용하지 않음<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>연관관계<ul>
<li>DB는 외래키를 사용하여 표현</li>
<li>객체는 <strong>참조</strong>를 사용하여 표현<pre><code class="language-java">// 데이터베이스 중심 객체 설계
public class Tutor {
  private Long id; // PK
  private Long companyId; // FK
  private String name;
}
&gt;
public class Company {
  private Long id; // PK
  private String name;
}</code></pre>
<pre><code class="language-java">// 객체 중심 설계
public class Tutor {
  private Long id; // PK
  private Company company; // 참조 연관관계
  private String name;
&gt;        
  public Company getCompany() {
          return company;
  }
&gt;        
  public Company setCompany(Company company) {
          this.company = company;
  }
}
&gt;
public Company {
  private Long id; // PK
  private String name;
}</code></pre>
</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/0801bc28-7dfb-4440-a7bd-83de8218ac72/image.png" width=400>
>
```sql
SELECT p.*, c.* FROM product p JOIN category c ON p.category_id = c.id;
```
```java
product.getCategory(); // 가능
product.getOrder(); // 불가능
```
>
- 객체 그래프
    - 객체는 연관된 객체를 탐색할 수 있어야 하지만, 실행된 SQL만큼만 탐색 가능해짐
        - Entity의 신뢰성에 문제 발생
    - 연관 객체를 한꺼번에 조회하면 신뢰성은 생기지만,
        - SQL 쿼리가 무거워짐
        - 진정한 의미의 계층 분할이 어려움
        - 필요 없는 데이터도 항상 함께 조회

<blockquote>
</blockquote>
<pre><code class="language-java">Product product1 = productRepository.findById(productId);
Product product2 = productRepository.findById(productId);
&gt;
product1 == product2; // false</code></pre>
<ul>
<li>객체의 비교<ul>
<li>데이터는 같지만, 새로운 인스턴스이기 때문에 주소값이 다름</li>
<li>Collection에 저장하면 문제를 해결할 수 있음</li>
</ul>
</li>
</ul>
<h3 id="jpa">JPA</h3>
<p>객체 지향 프로그래밍 언어인 Java와 관계형 데이터베이스 간의 패러다임 불일치 문제를 해결하여 데이터베이스 작업을 객체 지향적으로 수행할 수 있도록 지원. 대표적으로 <strong>Hibernate</strong>를 사용.</p>
<h4 id="ormobject-relational-mapping">ORM(Object-Relational Mapping)</h4>
<blockquote>
</blockquote>
<ul>
<li>객체와 관계형 DB를 자동으로 매핑하여 패러다임 불일치 문제를 해결</li>
<li>개발자 대신 데이터베이스와 상호작용하는 역할을 수행</li>
</ul>
<img src="https://velog.velcdn.com/images/star_pooh/post/2b9ffc7a-f1ff-4621-87cd-9597a0ee44e2/image.png" width=400>
<img src="https://velog.velcdn.com/images/star_pooh/post/8d12f4f7-9d50-4050-b9e5-33e63dadf7c0/image.png" width=400>

<h4 id="jpa를-사용하는-이유">JPA를 사용하는 이유</h4>
<blockquote>
</blockquote>
<ul>
<li>생산성</li>
<li>유지보수성</li>
<li>패러다임 불일치 문제 해결<ul>
<li>SQL 중심적인 개발에서 객체 중심으로 개발하기 때문에</li>
</ul>
</li>
<li>성능 향상<ul>
<li>1차 캐시</li>
<li>쓰기 지연</li>
<li>지연 로딩</li>
<li>즉시 로딩</li>
</ul>
</li>
</ul>
<h3 id="영속성-컨텍스트">영속성 컨텍스트</h3>
<p>Entity 객체를 영속성 상태로 관리하는 일종의 캐시 역할을 하는 공간으로 여기에 저장된 Entity는 데이터베이스와 자동으로 동기화되며, 같은 트랜잭션 내에서는 동일한 객체가 유지됨.</p>
<img src="https://velog.velcdn.com/images/star_pooh/post/2dc19e9d-fdff-4662-a60c-802474efec30/image.png" width=700>

<h4 id="entity">Entity</h4>
<blockquote>
</blockquote>
<ul>
<li>데이터베이스에서 엔티티란 저장할 수 있는 데이터의 집합을 의미</li>
<li>JPA에서 엔티티란 데이터베이스의 테이블을 나타내는 클래스를 의미</li>
</ul>
<h4 id="entity-생명주기">Entity 생명주기</h4>
<img src="https://velog.velcdn.com/images/star_pooh/post/e86e617a-809b-4b2e-9329-8da7baf8d80b/image.png" width=700>

<h4 id="entity-상태">Entity 상태</h4>
<blockquote>
</blockquote>
<ul>
<li>비영속(new/transient)<ul>
<li>영속성 컨텍스트가 모르는 새로운 상태</li>
<li>데이터베이스와 전혀 연관이 없는 객체<img src="https://velog.velcdn.com/images/star_pooh/post/b535ec4c-2b35-4e93-a794-144ae5a33d4c/image.png" width=350>
>
></li>
</ul>
</li>
<li>영속(managed)<ul>
<li>영속성 컨텍스트에 저장되고 관리되고 있는 상태</li>
<li>데이터베이스와 동기화되는 상태<img src="https://velog.velcdn.com/images/star_pooh/post/498969df-e60d-4223-a7f2-9f4704ad7a30/image.png" width=350>    
>
></li>
</ul>
</li>
<li>준영속(detached)<ul>
<li>영속성 컨텍스트에 저장되었다가 분리되어 더 이상 기억하지 않는 상태</li>
<li>영속성 컨텍스트가 제공하는 기능을 사용하지 못함<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>삭제(removed)<ul>
<li>영속성 컨텍스트에 의해 삭제로 표시된 상태</li>
<li>트랜잭션이 끝나면 데이터베이스에서 제거</li>
</ul>
</li>
</ul>
<h4 id="1차-캐시">1차 캐시</h4>
<blockquote>
</blockquote>
<ul>
<li>엔티티를 영속성 컨텍스트에 저장할 때 생성되는 메모리 내 캐시</li>
<li>엔티티는 1차 캐시에 먼저 저장 <ul>
<li>이후 같은 엔티티를 요청하면 DB를 조회하지 않음</li>
<li>1차 캐시에서 데이터를 반환하여 성능을 높임</li>
</ul>
</li>
<li><strong>동일한 트랜잭션</strong> 안에서만 사용 가능</li>
<li>트랜잭션이 종료되면 영속성 컨텍스트는 삭제됨<blockquote>
</blockquote>
</li>
<li>새로운 엔티티를 영속시키면 1차 캐시에 저장됨<img src="https://velog.velcdn.com/images/star_pooh/post/847e8b62-e9f2-4820-bf4b-24c3abddf49c/image.png" width=400>
>
></li>
<li>id가 1번인 엔티티를 조회하면, DB가 아닌 1차 캐시에서 데이터를 반환함<img src="https://velog.velcdn.com/images/star_pooh/post/4437e7a1-dc88-419a-9b05-750d9f58e9c6/image.png" width=400>
>
></li>
<li>id가 2번인 엔티티를 조회하면, 1차 캐시에 해당 데이터가 없으므로 <img src="https://velog.velcdn.com/images/star_pooh/post/80c9e033-a0d7-46b1-aabe-e215e227d6d6/image.png" width=400>
>
></li>
<li>DB를 조회하고, 1차 캐시에 저장한 후 데이터를 반환함<img src="https://velog.velcdn.com/images/star_pooh/post/044c9148-85cf-40f1-9c15-f4be98d4cc0f/image.png" width=400>

</li>
</ul>
<h4 id="동일성-보장">동일성 보장</h4>
<blockquote>
</blockquote>
<ul>
<li><strong>동일한 트랜잭션</strong> 안에서 특정 엔티티를 여러 번 조회해도 항상 같은 객체 인스턴스를 반환</li>
<li>1차 캐시를 사용하여 동일한 객체를 참조하게 하기 때문에 일관성 유지 가능<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/59230e22-1aa1-4f2a-8d15-fa97be14be5f/image.png" width=400>

</li>
</ul>
<h4 id="쓰기-지연">쓰기 지연</h4>
<blockquote>
</blockquote>
<ul>
<li><strong>동일한 트랜잭션</strong> 내에서 생성된 SQL들을 Commit 시점에 한꺼번에 반영<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/ca086b36-3bbc-4753-92b3-e783f1d91a04/image.png" width=400>
>
>
<img src="https://velog.velcdn.com/images/star_pooh/post/ce3fb294-1bd9-4632-8e60-aeb57adbdb73/image.png" width=400>
>
>
<img src="https://velog.velcdn.com/images/star_pooh/post/f453793e-67f2-45af-81a8-82ca70c8584b/image.png" width=400>

</li>
</ul>
<h4 id="변경-감지dirty-checking">변경 감지(Dirty Checking)</h4>
<blockquote>
</blockquote>
<ul>
<li>영속성 컨텍스트가 엔티티의 초기 상태를 저장하고 커밋 시점에 현재 상태와 비교하여 변경 사항이 있는지 확인하는 기능<blockquote>
</blockquote>
</li>
<li>DB에서 조회한 데이터에 대해 스냅샷(snapshot) 저장<img src="https://velog.velcdn.com/images/star_pooh/post/a33a3c4d-1b97-462c-b489-164a9cf2e5d6/image.png" width=400>
>
></li>
<li>커밋 시점에 엔티티와 스냅샷을 비교</li>
<li>변경 사항이 있다면 UPDATE 쿼리문을 생성하여 반영<img src="https://velog.velcdn.com/images/star_pooh/post/ea272167-c875-4d7d-a88c-995f34b95ee5/image.png" width=400>

</li>
</ul>
<h4 id="flush">flush</h4>
<blockquote>
</blockquote>
<ul>
<li>영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 기능</li>
<li>변경된 엔티티 정보를 SQL로 변환해 데이터베이스에 동기화함</li>
<li>트랜잭션 커밋 시 자동으로 실행되지만, 특정 시점에 데이터베이스 반영이 필요하다면 수동으로 호출도 가능</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20241227 회고]]></title>
            <link>https://velog.io/@star_pooh/20241227-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@star_pooh/20241227-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 27 Dec 2024 11:45:46 GMT</pubDate>
            <description><![CDATA[<p>그동안 미니 프로젝트를 진행한다고 회고를 전혀 하지 못했다. 오늘은 진행했던 미니 프로젝트를 가지고 KPT 회고를 해보려고 한다. </p>
<h3 id="keep">KEEP</h3>
<blockquote>
</blockquote>
<ul>
<li>Git 컨벤션 및 브랜치 전략<ul>
<li>프로젝트를 시작하기에 앞서 Git에 관련된 컨벤션(커밋 메시지, 브랜치 이름 정하기 등) 및 브랜치 전략(main / develop / 작업 브랜치)을 정했더니 작업하는데 있어 효율적이었던 것 같다.<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>페어 코드 리뷰<ul>
<li>내가 모르고 있던 내용이나 시야에 대해서 공유를 할 수 있기 때문에 페어를 정해서 코드 리뷰를 했던건 좋았던 것 같다. 짧은 일정이었지만 중간에 페어를 바꿨던 것도 좋았던 것 같다.<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>AI 코드 리뷰어<ul>
<li>리뷰를 해준다는 것은 페어가 그만큼 시간과 에너지를 들여야한다는 것이기 때문에 최소한의 시간과 에너지만 쓸 수 있도록 사전에 AI가 코드를 리뷰를 해주는 것도 좋았던 것 같다. 전체적인 내용보다는 파일 단위로 리뷰를 해주기 때문에 반복적인 내용이 있고 적용하면 안되는 내용도 섞여있지만, 오타, 중복 코드, 변수 네이밍 등 쉽게 놓칠 수 있는 부분은 꽤나 잘 찾아주기 때문에 잘 사용하면 유용할 것 같다.</li>
</ul>
</li>
</ul>
<h3 id="problem">PROBLEM</h3>
<blockquote>
</blockquote>
<ul>
<li>코드 컨벤션<ul>
<li>Git에 대해서는 컨벤션을 정했지만 코드에 대해서는 컨벤션을 정하지 않아서 코드를 합친 후에 리팩토링을 해야하는 등 리소스가 추가로 투입되었다. 각자의 코드 스타일이 다르기 때문에 사전에 디테일한 부분에 대해 컨벤션을 정했더라면 코드의 품질은 올라가면서 리소스는 적게 필요했을 것 같다는 생각이 들었다.<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>브랜치 전략<ul>
<li>프로젝트 시작 전에 브랜치 전략을 정했지만 프로젝트를 진행하면서 제대로 지켜지지 않은 부분이 있었다. 하나의 브랜치에서 너무 많은 기능을 구현한다거나, develop 브랜치에 반영하는 타이밍을 제대로 잡지 못해서 각각의 브랜치를 서로 pull 당겨서 작업을 한다던가 하는 문제가 있었다.<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>적절한 역할 분담<ul>
<li>각자의 진도 상황, 참여하고 있는 수준별 학습반 등 여러 부분에서 차이가 있었는데 그것을 고려하고 역할 분담을 나눴음에도 프로젝트를 진행하면서 일정에 대한 압박과 함께 적절하게 역할 분담이 되고 있는가에 대한 생각을 하게 되었다.</li>
</ul>
</li>
</ul>
<h3 id="try">TRY</h3>
<blockquote>
</blockquote>
<ul>
<li>코드 컨벤션<ul>
<li>다음 프로젝트에는 코드의 품질은 올리고 투입되는 리소스는 줄일 수 있도록 사전에 코드 컨벤션을 정할 것이다. 얼마만큼 디테일하게 정해야하는지 감이 오지 않기 때문에 이번에 코드 컨벤션을 적용한 조의 멤버와 튜터님들에게 조언을 구해서 진행하면 좋을 것 같다.<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>브랜치 전략<ul>
<li>이번 프로젝트에서 진행 했던 브랜치 전략은 처음에는 그럴싸 했지만 마지막에는 그럴싸하지 못했다. 끝까지 그럴싸하지 못했다는 것은 운용법이 잘못 되었을 수 있다는 뜻이기도 하기 때문에 튜터님들에게 조언을 구해서 끝까지 잘 지킬 수 있는 전략을 시도해봐야 할 것 같다.<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>프로젝트 디렉토리 관리<ul>
<li>이번 프로젝트는 계층, 설정 등으로 디렉토리를 관리했는데 기능이 많지 않았음에도 불구하고 깔끔한 모양이 아니었다. 발표회에서 좋은 평가를 받았던 도메인을 기준으로 관리하는 방법을 다음에는 사용해야 할 것 같다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[트러블 슈팅_미니 프로젝트(뉴스 피드) (2)]]></title>
            <link>https://velog.io/@star_pooh/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EB%AF%B8%EB%8B%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%89%B4%EC%8A%A4-%ED%94%BC%EB%93%9C-nrajnptn</link>
            <guid>https://velog.io/@star_pooh/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EB%AF%B8%EB%8B%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%89%B4%EC%8A%A4-%ED%94%BC%EB%93%9C-nrajnptn</guid>
            <pubDate>Thu, 26 Dec 2024 13:20:14 GMT</pubDate>
            <description><![CDATA[<p>미니 프로젝트를 진행하는 과정에서 팀원이 git에 익숙하지 않아서 develop 브랜치를 덮어쓰는 실수가 있었는데 그것을 해결한 방법에 대해서 적어보려고 한다.</p>
<h3 id="현재-브랜치-상황은">현재 브랜치 상황은?</h3>
<blockquote>
<blockquote>
</blockquote>
</blockquote>
<pre><code class="language-java">main (최종 브랜치)
┗ develop (개발 브랜치)
  ┣ post-api (뉴스피드 기능 브랜치)
  ┃ ┣ feature/xxxxx
  ┃ ┗ feature/xxxxx
  ┃
  ┗ user-api (사용자 기능 브랜치)
    ┣ feature/xxxxx
    ┗ feature/xxxxx</code></pre>
<blockquote>
</blockquote>
<p>우리 조는 위의 예시처럼 브랜치를 운영하기로 정했다. 그래서 기능을 구현하기 전에는 해당하는 브랜치를 생성한 후 작업을 했어야 했다. 하지만 이렇게 git으로 작업하시는게 처음이시라 브랜치를 만들어야 한다는 것을 잊어버리셔서 git clone한 상태 그대로 develop 브랜치에 작업을 하시고 push까지 하셔서 develop 브랜치가 그대로 덮어써졌다. 그래도 다행인 점은 작업량이 많지 않은 시점이었다는 것이었다.</p>
<h4 id="develop-브랜치-복구-방법은">develop 브랜치 복구 방법은?</h4>
<p>이미 push까지 진행된 상태였기 때문에 작업 내용을 임시로 저장할 수 있는 <code>git stash</code>를 사용할 수 없어서 아래와 같은 순서로 복구 작업을 진행했다.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">// 현재까지의 작업 내용을 저장할 별도의 브랜치 생성
git branch &lt;브랜치 이름&gt;
&gt;
// git log를 통해 이전 커밋의 해시 확인
git log
&gt;
// git reset을 통해 이전 커밋으로 되돌리기
git reset --hard &lt;이전 커밋 해시&gt;
&gt;
// 원격 저장소에 강제로 push
git push origin develop --force</code></pre>
<p>프로젝트를 하게 되면 git에서의 실수가 발생할 것이라고 생각은 했는데 이렇게 바로 생길 줄은 몰랐어서 얘기를 듣고서는 당황을 좀 했던 것 같다. 그래도 작업량이 적었던 극초반에 발생해서 매를 먼저 맞는다는 생각으로 해결했던 것 같다. 작업 하기 전에 무조건 브랜치부터 만들어야 한다는 것을 잊지 않을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[트러블 슈팅_미니 프로젝트(뉴스 피드) (1)]]></title>
            <link>https://velog.io/@star_pooh/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EB%AF%B8%EB%8B%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%89%B4%EC%8A%A4-%ED%94%BC%EB%93%9C</link>
            <guid>https://velog.io/@star_pooh/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EB%AF%B8%EB%8B%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%89%B4%EC%8A%A4-%ED%94%BC%EB%93%9C</guid>
            <pubDate>Thu, 26 Dec 2024 11:40:00 GMT</pubDate>
            <description><![CDATA[<p>미니 프로젝트를 진행하는 과정에서 로그인 기능을 JWT를 이용하여 구현하게 되었는데, 그 때 만났던 에러에 대해서 정리를 하려고 한다.</p>
<h3 id="jwt">JWT</h3>
<h4 id="왜-spring-security를-선택했나">왜 Spring Security를 선택했나?</h4>
<p>인증/인가에 대해서 튜터님들의 세션을 들을 때마다 <code>Spring Security</code>는 투자하는 노력에 비해 가성비가 좋지 않으니 지금은 신경 쓰지 않아도 된다라는 것이었다. 하지만 Spring Security를 사용하지 않고 구현하는 방법이 머릿 속에 그려지지 않아서 Spring Security를 사용하는 쪽으로 결정했다. 3~4년 전에 복붙 수준으로 한 번 만들어봤던 것이 전부였기 때문에 참고할 자료가 많았으면 좋겠다라고 생각을 했는데 대부분 Spring Security를 사용했기 때문에 정한 것도 이유 중에 하나다.</p>
<h4 id="어떻게-만들-것인가">어떻게 만들 것인가?</h4>
<p>JWT의 특징들에 대해서만 알고 있었기 때문에 예제 코드나 구현 내용을 설명해주는 강의 같은게 필요했는데, 인프런에서 튜토리얼을 다루는 <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt/dashboard">강의</a>를 찾게 되었고 하나씩 차근차근 따라해 나갔다. 그런데...</p>
<h4 id="생각치-못한-버전-업그레이드">생각치 못한 버전 업그레이드</h4>
<p>강의를 따라서 튜토리얼 코드를 구현 중에 하위 메소드가 나오지 않는 것이었다. 검색해보니 버전이 변경 되면서 <code>Deprecated</code> 된 메소드들이었던 것이다. <a href="https://docs.spring.io/spring-security/reference/getting-spring-security.html">Spring Security</a>와 <a href="https://github.com/jwtk/jjwt">jjwt</a>(JWT를 구현하기 위해 사용한 라이브러리)에서 버전을 확인하고 버전에 따른 구현 방법을 찾았는데, 잘 찾아지지 않아서 JWT를 사용하지 않아도 되는지 상담까지 생각했었다. 마지막이라는 생각으로 강의에 최신 코드가 반영된 부분이 있을까 하고 찾아봤는데 강사님께서 버전에 맞춰서 작성해주신 예제 코드가 <a href="https://github.com/SilverNine/spring-boot-jwt-tutorial/tree/master">깃허브</a>에 있었다!</p>
<h4 id="잘-진행된-코드-작성과-그렇지-못한-실행-결과">잘 진행된 코드 작성과 그렇지 못한 실행 결과</h4>
<p>어떻게 동작하는지에 대한 이해는 잠시 제쳐두고 깃허브에 있는 내용을 기반으로 코드 작성을 했다. 예제 코드가 꼼꼼하게 작성되어 있었기 때문에 코드를 작성하는데는 문제가 없었다. 다만, Spring Security가 들어가다보니 작성해야 할 양이 이전에 구현했던 Session과 비교해서 몇 배는 많았다. 코드 작성을 다 끝낸후 떨리는 마음으로 실행을 했는데 문제 없이 동작했다. 남은 일은 로그인이 성공적으로 이루어지고 생성된 토큰 값이 확인되는 것이었는데 다음과 같은 에러가 발생하였다. 참고로 진행 중인 프로젝트에서는 <strong>이메일</strong>과 비밀번호를 이용해서 로그인을 한다.  </p>
<blockquote>
</blockquote>
<pre><code class="language-java">[newsfeed] [nio-8080-exec-1] o.t.n.exception.GlobalExceptionHandler   : [BadCredentialsException] 500 INTERNAL_SERVER_ERROR : 자격 증명에 실패하였습니다.
&gt;
// 상세 에러 내용
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:141)
org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182)
org.team14.newsfeed.controller.AuthController.login(AuthController.java:38)</code></pre>
<p>위의 에러 내용을 기반으로 검색해보니 토큰을 만들때 사용자가 요청으로 전달한 비밀번호가 아닌 암호화된 비밀번호를 전달할 경우 발생하기 때문에 암호화하기 이전의 비밀번호를 사용하면 해결된다고 했는데, 나는 이미 암호화하기 이전의 비밀번호를 사용하기 있었기 때문에 해당하는 내용이 아니었다. </p>
<p><img src="https://velog.velcdn.com/images/star_pooh/post/08755f5f-7f21-43dd-9f24-dcb1439d4ff7/image.png" alt=""></p>
<p>그렇게 원인을 찾아 헤매고 있었는데 <a href="https://yoonsys.tistory.com/33">같은 에러를 겪으신 분</a>과 <a href="https://ming412.tistory.com/302">해결 방법을 찾으신 분</a>이 있어서 참고하며 해결했다. </p>
<h4 id="badcredentialsexception의-원인">BadCredentialsException의 원인</h4>
<blockquote>
</blockquote>
<p>토큰이 제대로 생성되지 않았기 때문에 상세 에러 내용에 적혀있는대로 토큰을 생성해주는 메소드부터 하나씩 따라가보려고 한다.
<img src="https://velog.velcdn.com/images/star_pooh/post/2ffc7083-ad61-49d4-bc6f-d3bc211feb7b/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>토큰을 생성해주는 메소드로 들어가보면 인증을 관리하는 AuthenticationManager 인터페이스를 만나게 된다.
<img src="https://velog.velcdn.com/images/star_pooh/post/d6bb0a4f-3be2-4562-b680-429587f6f457/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>ProviderManager의 authenticate의 구현체로 이동해서 다시 한번 authenticate 메소드로 이동하면
<img src="https://velog.velcdn.com/images/star_pooh/post/c730a052-c7c1-4b4b-96e1-295c8de82889/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>AuthenticationProvider 인터페이스를 만나게 된다.</p>
<blockquote>
<blockquote>
<p>💡 ProviderManager는 <code>AuthenticationManager</code>, <code>MessageSourceAware</code>, <code>InitializingBean</code> 구현체이다. </p>
</blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/star_pooh/post/3c01e49c-0002-4bac-8a47-c60bcb89d7a5/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>AbstractUserDetailsAuthenticationProvider의 구현체로 이동하게 되면 에러 내용에서 봤던 <code>BadCredentialsException</code>을 만나게 된다! 하지만 아직 끝이 아니다. 에러가 발생한 지점을 찾았으니 원인을 찾아야한다. catch문에서 예외가 처리되고 있기 때문에 try문의 retrieveUser 메소드로 이동하면
<img src="https://velog.velcdn.com/images/star_pooh/post/19d11a77-6f07-4a50-a5b8-a147c00d759c/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>이렇게 추상 클래스를 만나게 되고
<img src="https://velog.velcdn.com/images/star_pooh/post/b30bbe00-3a12-4822-8d6e-29b86be50f0c/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>구현체로 이동하게 되면 사용자 이름으로 사용자 정보를 조회하는 loadByUsername이라는 메소드를 만나게 된다.
<img src="https://velog.velcdn.com/images/star_pooh/post/c2367ef2-fd21-4d1c-951b-b9124e0471df/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>그런데 위에서 지나치듯 언급했지만, 우리는 이메일로 로그인을 하고 싶은데 username으로 사용자 정보를 조회하기 때문에 에러가 발생한 것 같았다. 직접 코드를 실행시켜서 확인해보니 우려하던 결과가 실제로 눈앞에 보여지게 됐다.
<img src="https://velog.velcdn.com/images/star_pooh/post/5e6fdcf1-0bd3-4158-b739-eb4f88542822/image.png" alt=""></p>
<h4 id="badcredentialsexception의-해결-방법">BadCredentialsException의 해결 방법</h4>
<blockquote>
</blockquote>
<p>이메일로 로그인 하고 싶었는데 username으로 조회하는 것이 원인이라는 것까지는 알았는데 어떻게 해결해야하나 고민이 되었다. 그런데 다행인 것은 <code>loadUserByUsername</code> 메소드가 <strong>인터페이스</strong>라는 것이다.
<img src="https://velog.velcdn.com/images/star_pooh/post/1583e49f-7306-48b8-8b0f-33101707dd43/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>그렇다는 것은 UserDetailService를 implements하여 <code>loadUserByUsername</code> 메소드를 오버라이딩 할 수 있다는 것이다. 
<img src="https://velog.velcdn.com/images/star_pooh/post/b067184f-6219-4b9a-94c5-53de262fac32/image.png" alt=""></p>
<h4 id="그런데-어떻게-spring-security랑-연결해주지">그런데 어떻게 Spring Security랑 연결해주지..?</h4>
<p>제목과 같은 고민을 잠깐 했었는데 Spring Security에서는 <code>SecurityAutoConfiguration</code> 클래스에 의해 구성을 설정하는데, 사용자가 정의한 <code>UserDetailsService</code>가 있다면 우선적으로 사용하도록 한다고 한다. 그러니까 <code>CustomUserDetailService</code>를 만들어서 Bean으로 등록한 순간 우리의 역할은 끝난 것이다!</p>
<h4 id="결과는">결과는?</h4>
<p>원하던 결과를 문제없이 얻을 수 있었고, 이번 트러블 슈팅으로 인해서 왜 Spring Security를 사용하려면 많은 투자가 필요하다고 했는지 느끼게 됐다. 기회가 된다면 Spring Security 없이 JWT를 구현하는 것을 연습해 보면 좋을 것 같다고 생각했다.
<img src="https://velog.velcdn.com/images/star_pooh/post/bcba6f50-d223-4766-a882-a485d7b88208/image.png" alt=""></p>
<h3 id="그런데-말입니다">그런데 말입니다</h3>
<p><code>자격 증명에 실패했다</code>는 에러를 해결할 수 있는 방법 중에 하나로 <code>암호화하기 이전의 비밀번호(사용자가 입력한 비밀번호)</code>를 사용하는 것이었는데 이것은 왜 그래야하는건지 궁금해졌다.</p>
<h4 id="다시-이동해보자">다시 이동해보자</h4>
<blockquote>
</blockquote>
<p>위의 메소드 파고들기 여정에서 AbstractUserDetailsAuthenticationProvider의 authenticate까지는 동일하다. loadUserByUsername에서 에러가 발생하지 않았다면, 그 이후의 코드가 진행될 것이기 때문에 그 부분을 살펴볼 것이다.
<img src="https://velog.velcdn.com/images/star_pooh/post/acfaf17a-582d-456e-8b28-8b18e12fd15d/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>이동하게 되면 추상 클래스인 AbstractUserDetailsAuthenticationProvider를 만나게 된다. 
<img src="https://velog.velcdn.com/images/star_pooh/post/2ca07944-5d9d-439c-b331-6e59e0f72dac/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>구현체로 이동하게 되면 익숙한 BadCredentialsException을 만나게 된다. BadCredentialsException이 발생하게 되는 조건을 자세히 보면 사용자가 입력한 비밀번호와 조회해온 사용자 정보의 비밀번호가 일치하는지 확인한다. 그렇기 때문에 사용자가 입력한 비밀번호를 그대로 전해주어야 했던 것이었다.
<img src="https://velog.velcdn.com/images/star_pooh/post/3a7e9b73-9618-4680-b21c-4db328213eec/image.png" alt=""></p>
<br>
<br>

<blockquote>
</blockquote>
<p>✅ 출처</p>
<ul>
<li>인프런 JWT 튜토리얼 강의
<a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt/dashboard">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt/dashboard</a><blockquote>
</blockquote>
</li>
<li>JWT 예제 코드
<a href="https://github.com/SilverNine/spring-boot-jwt-tutorial/tree/master">https://github.com/SilverNine/spring-boot-jwt-tutorial/tree/master</a><blockquote>
</blockquote>
</li>
<li>JJWT 라이브러리
<a href="https://github.com/jwtk/jjwt">https://github.com/jwtk/jjwt</a><blockquote>
</blockquote>
</li>
<li>Spring Security
<a href="https://docs.spring.io/spring-security/reference/getting-spring-security.html">https://docs.spring.io/spring-security/reference/getting-spring-security.html</a>
<a href="https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/user-details-service.html">https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/user-details-service.html</a>
<a href="https://youtu.be/q3gT4198RKU">https://youtu.be/q3gT4198RKU</a><blockquote>
</blockquote>
</li>
<li>참고 블로그
<a href="https://yoonsys.tistory.com/33">https://yoonsys.tistory.com/33</a>
<a href="https://ming412.tistory.com/302">https://ming412.tistory.com/302</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Token, Filter]]></title>
            <link>https://velog.io/@star_pooh/Token-Filter</link>
            <guid>https://velog.io/@star_pooh/Token-Filter</guid>
            <pubDate>Mon, 23 Dec 2024 11:53:12 GMT</pubDate>
            <description><![CDATA[<h3 id="token">Token</h3>
<h4 id="token-1">Token</h4>
<p>Web Application이나 API에서 인증(Authentication)과 인가(Authorization) 과정에서 사용되며 사용자 또는 시스템의 신원과 권한을 증명하고 요청의 유효성을 검증하는 데 사용되는 디지털 문자열.</p>
<blockquote>
</blockquote>
<ul>
<li>Token의 장점<ul>
<li>Token은 서버가 아닌 클라이언트에 저장되어 서버의 부담이 적음</li>
<li>Cookie는 웹 브라우저에만 존재하기 때문에 모바일 앱 등의 다양한 클라이언트에서 인증 처리 불가</li>
<li>Token 방식은 Stateless 기반이기 때문에 확장성이 뛰어남</li>
<li>인증된 사용자임을 확인하기 위한 고유한 서명을 포함하여 위조된 요청인지 확인 가능</li>
</ul>
</li>
<li>Token의 단점<ul>
<li>Cookie/Session 방식보다 많은 데이터 용량<ul>
<li>요청이 많아질 시, 트래픽 증가</li>
</ul>
</li>
<li>Payload(전송되는 데이터)는 암호화되지 않아서 중요한 데이터를 담기에 어려움</li>
<li>Token을 탈취당하면 대처하기 어려움<ul>
<li>만료 시간(기본값 : 30분)을 설정</li>
</ul>
</li>
</ul>
</li>
<li>Token의 동작 순서<ul>
<li>Token 생성 시 사용자의 고유한 정보 포함</li>
<li>데이터베이스에 접근하지 않고 Token의 유효성만 검증<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/bb3b697b-e0a3-4286-a20c-db8d353959fd/image.png" width=800>

</li>
</ul>
</li>
</ul>
<h4 id="jwt">JWT</h4>
<p>인증에 필요한 정보들을 암호화시킨 JSON 형태의 Token을 의미. JSON 데이터 포맷을 사용하여 정보를 효율적으로 저장하고 암호화로 서버의 보안성 향상.</p>
<blockquote>
</blockquote>
<ul>
<li><a href="https://jwt.io/">JWT Encoding / Decoding</a></li>
<li>JWT 구조<pre><code class="language-java">XXXXXX.YYYYYY.ZZZZZZ
(Header).(Payload).(Signature)</code></pre>
<blockquote>
</blockquote>
</li>
<li>Header<ul>
<li>토큰의 타입과 해싱 알고리즘 정의<pre><code class="language-json">{
&quot;alg&quot;: &quot;HS256&quot;,
&quot;typ&quot;: &quot;JWT&quot;
}</code></pre>
<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>Payload<ul>
<li>실제로 인증과 관련된 데이터(Claims) 포함</li>
<li>Claims의 종료<ul>
<li>Registered Claims : 미리 정의된 Claims<ul>
<li>iss(issuer) : 발행자</li>
<li>exp(expiration time) : 만료시간</li>
<li>sub(subject) :  제목</li>
<li>iat(issued At) : 발행 시간</li>
<li>jti(JWT ID) : 토큰의 고유 식별자</li>
</ul>
</li>
<li>Public Claims : 사용자가 정의할 수 있는 Claims이며, 공개용 정보 전달이 목적</li>
<li>Private Claims : 사용자 지정 Claims, 당사자들 간의 정보를 공유하기 위한 목적<pre><code class="language-json">{
&quot;sub&quot;: &quot;1234567890&quot;,
&quot;name&quot;: &quot;Sparta&quot;,
&quot;exp&quot;: 1682563600
}</code></pre>
<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li>Signature<ul>
<li>Header와 Payload를 서버의 <code>Secret Key</code>로 서명하여 암호화</li>
<li>암호화는 Header에서 정의한 알고리즘을 활용</li>
<li>서버는 서명을 통해 Token의 변조 여부를 확인 가능<pre><code class="language-json">HMACSHA256(
base64UrlEncode(header) + &quot;.&quot; +
base64UrlEncode(payload),
secret
)</code></pre>
<blockquote>
<blockquote>
<p>💡 base64UrlEncode는 URL에서 사용할 수 있도록 <code>+</code>, <code>/</code> 값을 각각 <code>-</code>, <code>_</code>로 표기</p>
</blockquote>
<p>💡 Header와 Payload는 Encoding된 값이기 때문에 복호화 혹은 값을 수정할 수 있지만 Signature는 서버에서 관리하는 값이기 때문에 Secret Key가 유출되지 않는 이상 복호화 불가능</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<h4 id="jwt-인증">JWT 인증</h4>
<p>JWT는 Base64로 인코딩되어 쉽게 복호화 할 수 있고, Payload가 그대로 노출되기 때문에 비밀번호나 민감한 정보를 저장하지 않음.</p>
<blockquote>
</blockquote>
<ul>
<li>JWT 인증 과정<ul>
<li>클라이언트의 로그인 요청</li>
<li>로그인에 성공하면 Signature 생성<ul>
<li>이후 Base64로 Encoding 실행</li>
<li>일반적으로 Cookie에 담아서 클라이언트에게 발급</li>
</ul>
</li>
<li>서버에 요청을 보낼 때 <code>Authorization</code> Header에 발급 받은 JWT를 포함</li>
<li>서버에서 JWT의 유효성 검사를 통과하면 요청 처리<ul>
<li>JWT 만료, 위변조 여부 검사<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/1f035bfe-a951-4b86-85af-db11430943db/image.png" width=700>
>
></li>
</ul>
</li>
</ul>
</li>
<li>JWT의 유효성 검사<ul>
<li>A의 JWT를 B가 탈취</li>
<li>B가 탈취한 JWT를 임의로 수정</li>
<li>B가 수정한 JWT로 Server에 요청</li>
<li>서버는 Signature를 사용하여 유효성 검사<ul>
<li>Header, Payload, 서버의 Secret Key값으로 만든 Signature와 비교하기 때문에 조작된 데이터 판별 가능<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/238e6645-fff4-436b-99db-6c9f3bc77ea7/image.png" width=700>
>
>>💡 JWT의 목적은 정보 보호가 아닌, **위조 방지**

</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="jwt-장단점">JWT 장단점</h4>
<blockquote>
</blockquote>
<ul>
<li>JWT 장점<ul>
<li>Signature로 서버의 보안성 증가</li>
<li>Token에 필요한 정보(유저 및 검증 정보)가 모두 존재</li>
<li>서버는 인증 정보와 관련된 별도의 저장소를 사용하지 않음</li>
<li>서버의 수평 확장성(Scale Out) 증가</li>
<li>Cookie가 없는 다른 환경에서도 인증/인가 적용 가능</li>
<li>DB를 조회하지 않아도 됨</li>
</ul>
</li>
<li>JWT 단점<ul>
<li>Payload는 암호화 되어 있지 않기 때문에 민감한 정보를 다루지 못함</li>
<li>Token의 길이가 길어서 트래픽이 증가하면 네트워크 부하가 증가함</li>
<li>클라이언트 측에서 Token을 관리하기 때문에 탈취당하면 대처가 힘듦</li>
</ul>
</li>
</ul>
<h4 id="access-token-refresh-token">Access Token, Refresh Token</h4>
<p>Token은 클라이언트에서 관리하여 탈취당할 위험성이 높기 때문에 만료 시간 설정이 필요. 이 때 발생하는 단점을 극복하기 위해 Access Token과 Refresh Token 사용.</p>
<blockquote>
</blockquote>
<ul>
<li>Access Token<ul>
<li>사용자 인증 후 서버가 발급하는 유저 정보가 담긴 토큰</li>
<li>유효 기간 동안 API나 리소스에 접근할 때 사용</li>
<li>보안을 위해 짧은 수명을 가짐</li>
</ul>
</li>
<li>Refresh Token<ul>
<li>Access Token이 만료된 경우 재발급 받기위해 사용</li>
<li>주로 데이터베이스에 유저 정보와 같이 저장</li>
</ul>
</li>
<li>Access Token, Refresh Token 인증<ul>
<li>클라이언트의 로그인 요청</li>
<li>로그인에 성공하면 Signature 생성</li>
<li>서버에 요청할 때 <code>Authorization</code> Header에 JWT(<code>Access Token</code>)를 포함</li>
<li>서버에서 JWT의 유효성 검사를 통과하면 요청 처리</li>
<li><code>Access Token</code>이 만료 되었다면 <code>Refresh Token</code> 으로 토큰 재발급 요청</li>
<li>서버로부터 <code>Access Token</code>을 재발급 실행<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/310e00c6-8120-44ab-9b15-7a827c8bad3e/image.png" width=700>
>
>>💡 JWT를 Access Token만을 사용하여 인증한다면 탈취되었을 때 보안에 취약. 유효 시간을 부여하여 문제를 해결하지만 유효 시간이 짧은 경우, 로그인을 자주 해야하기 때문에 Refresh Token 적용.

</li>
</ul>
</li>
</ul>
<h3 id="filter">Filter</h3>
<h4 id="공통-관심-사항cross-cutting-concerns">공통 관심 사항(cross-cutting concerns)</h4>
<p>횡단 관심사라고도 하며 여러 위치에서 공통적으로 사용되는 부가 기능을 의미.</p>
<blockquote>
</blockquote>
<ul>
<li>핵심 기능(비즈니스 로직)<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/7e6bb92f-9c7e-4274-b7bb-c80659b410c5/image.png" width=500>
></li>
<li>부가 기능(비즈니스 로직과는 별개로 동작하는 기능)<img src="https://velog.velcdn.com/images/star_pooh/post/dc3d0f6c-5ddd-4f4f-8b00-7fa56b058a07/image.png" width=500>
>
></li>
<li>Spring AOP 활용 가능</li>
<li>Web과 관련된 공통 관심사는 Servlet Filter나 Spring Interceptor를 사용<ul>
<li><code>HttpServletRequest</code> 객체를 제공하기 때문에 HTTP 정보나 URL 정보에 접근이 용이<blockquote>
<blockquote>
<p>💡 AOP(Aspect-Oriented Programmin, 관점 지향 프로그래밍)란?
핵심 관심사(Core Concerns)와 횡단 관심사(Cross-cutting Concerns)를 분리하여 가독성과 모듈성을 증진시킬 수 있는 프로그래밍 패러다임</p>
</blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
<h4 id="servlet-filter">Servlet Filter</h4>
<p>Servlet Filter는 보안, 로깅, 인코딩, 인증/인가 등 다양한 작업을 처리하기 위해 사용.</p>
<blockquote>
</blockquote>
<ul>
<li>Servlet Filter 특징<ul>
<li>공통 관심사 로직 처리<ul>
<li>공통된 로직을 중앙 집중적으로 구현하여 재사용성이 높고 유지보수가 쉬움</li>
<li>모든 요청이 하나의 입구를 통해 처리되어 일관성 유지</li>
</ul>
</li>
<li>HTTP 요청 및 응답 필터링</li>
<li>Filter Chain<ul>
<li>여러 개의 필터를 순차적으로 적용 가능</li>
</ul>
</li>
<li>doFilter()<ul>
<li>실제 필터링 작업을 수행하는 주요 메소드</li>
<li>다음 필터로 제어를 넘길지 결정<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li>Servlet Filter 적용<ul>
<li>Filter를 적용하면 Servlet이 호출되기 이전에 항상 Filter를 거치게 됨</li>
<li>공통 관심사를 필터에만 적용하면 모든 요청 / 응답에 적용됨</li>
<li>특정 URL Pattern에 적용 가능</li>
<li>Spring에서 Servlet은 <code>Dispatcher Servlet</code>을 의미</li>
<li>필터를 통과하지 못하면 컨트롤러를 호출하지 않음<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/d09256d0-2d02-41f1-9863-521607f68099/image.png" width=600>
></li>
</ul>
</li>
<li>Filter Chain <ul>
<li>Filter는 Chain 형식으로 구성</li>
<li>개발자가 자유롭게 추가할 수 있으며, 순서도 지정 가능<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/4c18f15d-930c-4b1f-a6e6-9723c4393ea6/image.png" width=600>

</li>
</ul>
</li>
</ul>
<h4 id="filter-interface">Filter Interface</h4>
<p>Java Servlet에서 HTTP 요청과 응답을 가로채고, 이를 기반으로 다양한 처리 작업을 수행하는 데 사용되는 인터페이스.</p>
<blockquote>
</blockquote>
<ul>
<li>jakarta.servlet.Filter<ul>
<li>Filter Interface를 Implements하여 구현하고 Bean으로 등록하여 사용<ul>
<li>Servlet Container가 Filter를 Singleton 객체로 생성 및 관리</li>
</ul>
</li>
</ul>
</li>
<li>주요 메소드<ul>
<li><code>init()</code><ul>
<li>필터를 초기화하며, Servlet Container가 생성될 때 호출</li>
<li><code>default</code> 메소드이기 때문에 구현하지 않아도 됨</li>
</ul>
</li>
<li><code>doFilter()</code><ul>
<li>클라이언트에서 요청이 올 때 마다 호출<ul>
<li><code>doFilter()</code> 내부에 필터 로직(공통 관심사 로직)을 구현</li>
</ul>
</li>
<li>WAS에서 <code>doFilter()</code> 를 호출해주고 하나의 필터의 <code>doFilter()</code>가 통과된다면, 필터 체인의 순서에 따라서  <code>doFilter()</code> 를 호출</li>
<li>더이상 호출할 필터가 없으면 Servlet 호출</li>
</ul>
</li>
<li><code>destroy()</code><ul>
<li>필터를 종료하는 메소드로서 Servlet Container가 종료될 때 호출</li>
<li><code>default</code> 메소드이기 때문에 구현하지 않아도 됨<blockquote>
<blockquote>
<p>💡 <code>ServletRequest</code>는 기능이 별로 없기 때문에 대부분 기능이 많은 <code>HttpServletRequest</code>를 다운 캐스팅하여 사용</p>
</blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20241220 회고]]></title>
            <link>https://velog.io/@star_pooh/20241220-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@star_pooh/20241220-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 20 Dec 2024 12:26:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✅ 알고리즘 &amp; SQL 코드카타
💡 오늘 알고리즘 문제는 조합에 관련된 문제였다. 조합이라는 것을 들었을 때 생각나는 것과 조금 다른 패턴이어서 로직을 생각해내는데 어려웠던 것 같다. 비슷한 유형을 많이 풀어보면서 다음에 나오면 바로 생각해 낼 수 있도록 해야할 것 같다. SQL에서도 서브쿼리를 활용한 계산, 조인을 활용한 계산 등이 많이 나오고 있어서 익숙해질 수 있도록 해야할 것 같다.</p>
</blockquote>
<blockquote>
<p>✅ 미니 프로젝트 1일차
💡 새로운 팀원들과 프로젝트 진행에 관해서 많은 얘기를 나눴다. 다들 적극적으로 의견을 내고 반응해서 잘 진행될 수 있었던 것 같다. 양해를 구하고 평소에 해보고 싶었던 Git ↔ Slack 연동과 Github Action을 통한 AI Code Reviewer를 설정했다. Slack과의 연동은 제대로 된 것을 확인했는데, AI Code Reviewer는 에러는 해결했지만 제대로 동작하는지는 진행하면서 살펴봐야할 것 같다. 튜터님께서도 사용해보는 것에 긍정적인 답변을 주셨는데 2가지를 당부하셨다. 첫 번째는 AI Code Reviewer의 결과를 공유하는 것, 두 번째는 팀원들끼리도 코드 리뷰를 하는 것이다. 코드 리뷰를 통해 얻어갈 수 있는게 많기 때문에 말씀하신 것 같다. 또, 언제 어떻게 발생할 지 모르기 때문에 트러블 슈팅을 잘 해놓으려고 한다. 그래서 이번에는 나한테서 발생한 트러블에 한정하지 않고, 팀 전체로 확장하려고 한다. </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[20241219 회고]]></title>
            <link>https://velog.io/@star_pooh/20241219-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@star_pooh/20241219-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 19 Dec 2024 12:37:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✅ 스프링 과제 리팩토링 및 제출
💡 수준별 학습반 세션을 들으면서 이건 과제에 적용해야겠다라고 메모해 놨던 부분들을 수정했다. 개인적인 욕심으로 API 명세서를 레벨별로 작성하느라 시간이 오래걸리긴 했지만 하고자 했던 부분들을 시간 안에 마무리해서 만족스러웠다.  </p>
</blockquote>
<blockquote>
<p>✅ 세션 및 발제 참여
💡 과제 제출 후 과제 해설 세션, 미니 세션(쿠키 / 세션), 출결 관리 관련 발제에 참여했더니 오후 시간이 사라져 있었다.....</p>
</blockquote>
<blockquote>
<p>✅ 셀프 피드백 적용
💡 과제 해설 세션을 보면서 저렇게 할 수도 있구나 하는 부분이 몇몇 있었지만, 바로 적용하고 싶었던 부분은 프로젝트 구조였다. 내 과제는 계층별로 구분되어 있어서 요구 사항이 하나 추가될때마다 패키지가 늘어나서 뭐가 엄청 많았는데, 튜터님은 기능별로 구분하셔서 엄청 깔끔했기 때문이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[트러블 슈팅_스프링 과제(일정 관리 앱 develop ver.)]]></title>
            <link>https://velog.io/@star_pooh/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-d368i9kd</link>
            <guid>https://velog.io/@star_pooh/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-d368i9kd</guid>
            <pubDate>Thu, 19 Dec 2024 02:06:13 GMT</pubDate>
            <description><![CDATA[<p>이번 스프링 과제(일정 관리 앱 develop version)를 진행하는데 있어서 발생했던 에러와 해결방법, 학습하게 된 내용에 대해 정리하려고 한다.</p>
<h3 id="에러1">에러(1)</h3>
<h4 id="mapping-miss">Mapping Miss</h4>
<p>코드를 짜고 스프링을 실행시켰는데 아래와 같은 에러가 발생했다.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">Error starting ApplicationContext. To display the condition evaluation report re-run your application with &#39;debug&#39; enabled.
2024-12-19T00:08:38.141+09:00 ERROR 26196 --- [develop_todo] [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 
&gt;
***************************
APPLICATION FAILED TO START
***************************
&gt;
Description:
&gt;
Invalid mapping pattern detected:
/{/id}
  ^
Expected close capture character after variable name }
&gt;
Action:
&gt;
Fix this pattern in your application or switch to the legacy parser implementation with &#39;spring.mvc.pathmatch.matching-strategy=ant_path_matcher&#39;.
&gt;
Disconnected from the target VM, address: &#39;127.0.0.1:62780&#39;, transport: &#39;socket&#39;
&gt;
Process finished with exit code 1</code></pre>
<p>처음에는 <code>Expected close capture character after variable name }</code> 내용만 보고 괄호 문제인줄 알고 코드 정렬을 했다. 에러가 발생한 파일명도 없어서 모든 파일에 대해서 했다. 하지만 그래도 해결되지 않았다.
에러를 다시 자세히 살펴보니 유효하지 않은 매핑 패턴이 있다는 것이다. 친절히 알려준 <code>{/id}</code>로 검색해보니 HttpMethod 매핑 부분에 오타가 있었다. 오타를 고치고 나니 문제 없이 실행되었다.</p>
<pre><code class="language-java">@GetMapping(&quot;{/id}&quot;)
public ResponseEntity&lt;Map&lt;String, TodoResponseDto&gt;&gt; findById(@PathVariable Long id) {
    ...
}
&gt;
👇 오타 수정 후
&gt;
@GetMapping(&quot;/{id}&quot;)
public ResponseEntity&lt;Map&lt;String, TodoResponseDto&gt;&gt; findById(@PathVariable Long id) {
    ...
}</code></pre>
<h3 id="에러2">에러(2)</h3>
<h4 id="annotation-miss">Annotation Miss</h4>
<p>Validation을 추가하고 제대로 동작하는지 Postman으로 확인하는데 아래와 같은 에러가 발생했다.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">2024-12-19T00:21:33.577+09:00 ERROR 1460 --- [develop_todo] [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw 
exception [Request processing failed: jakarta.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint &#39;jakarta.validation.constraints.NotBlank&#39; validating type &#39;java.lang
.Long&#39;. Check configuration for &#39;userId&#39;] with root cause
&gt;
jakarta.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint &#39;jakarta.validation.constraints.NotBlank&#39; validating type &#39;java.lang.Long&#39;. Check 
configuration for &#39;userId&#39;
    at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:105) ~[spring-context-6.2.0.jar:6.2.0]
    at org.springframework.boot.autoconfigure.validation.ValidatorAdapter.validate(ValidatorAdapter.java:67) ~[spring-boot-autoconfigure-3.4.0.jar:3.4.0]
    at org.springframework.validation.DataBinder.validate(DataBinder.java:1358) ~[spring-context-6.2.0.jar:6.2.0]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.33.jar:6.0]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.0.jar:6.2.0]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.33.jar:6.0]
    at org.example.develop_todo.lv8.filter.LoginFilter.doFilter(LoginFilter.java:41) ~[main/:na]</code></pre>
<p><code>userId</code>에 설정한 <code>@NotBlank</code>가 제대로 동작하지 않는다는 것이었다. <code>userId</code>는 Long 타입이었는데, <code>@NotBlank</code>는 문자열(CharSequence)만 허용하기 때문이었다. <code>userId</code>에 null값이 들어오는지 확인하기 위해서는 <code>@NotNull</code>을 사용해야했다. <code>@NotNull</code>은 모든 타입을 허용한다.</p>
<h3 id="학습하게-된-내용">학습하게 된 내용</h3>
<h4 id="jpa-페이징">JPA 페이징</h4>
<p>JPA를 사용하여 페이징을 간단히 설정할 수 있는데, 사용법을 정리해놓으려고 한다.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">// Controller
// 페이징에 필요한 페이지 번호와 페이지 크기를 QueryParam으로 요청 받음
@GetMapping(&quot;/cars&quot;)
public ResponseEntity&lt;List&lt;CarPagingDto&gt;&gt; findAllCar(
        @RequestParam(defaultValue = &quot;0&quot;) int pageNum, // 페이지 번호
        @RequestParam(defaultValue = &quot;10&quot;) int pageSize // 페이지 크기
) {
    Page&lt;CarDto&gt; carPagingDtoList = carService.findAllCar(pageNum, pageSize);
    return new ResponseEntity&lt;&gt;(carPagingDtoList, HttpStatus.OK);
}</code></pre>
<blockquote>
</blockquote>
<pre><code class="language-java">// Serivce
public List&lt;CarPagingDto&gt; findAllCar(int pageNum, int pageSize) {
    Pageable pageable = PageRequest.of(pageNum, pageSize); // pageable 객체 생성
    Page&lt;Car&gt; foundAllCarList = carRepository.findAllByOrderByCreatedAtDesc(pageable); 
&gt;
    return foundAllCarList.stream().map(CarPagingDto::toDto).toList(); // Dto로 매핑해서 반환
}</code></pre>
<blockquote>
</blockquote>
<pre><code class="language-java">// Repository
@Repository
public interface CarRepository extends JpaRepository&lt;Car, Long&gt; {
    // 페이징을 적용할 쿼리 메소드 추가
    // 생성일을 내림차순으로 갖는 전체 목록 조회
    Page&lt;Car&gt; findAllByOrderByCreatedAtDesc(Pageable pageable); 
}</code></pre>
<br>

<blockquote>
<p>✅ 이번에는 큼직큼직한 에러보다는 잔실수로 인한 에러가 많았던 것 같다. 오타가 나지 않도록 조금 더 신경써야할 것 같고, 어노테이션을 사용할 때는 사용법에 대해서 좀 더 확실하게 숙지한 후에 사용해야겠다는 생각을 하게 되었다. </p>
</blockquote>
<blockquote>
<p>✅ 출처 및 참고
<a href="https://docs.jboss.org/hibernate/validator/8.0/reference/en-US/html_single/#section-builtin-constraints">https://docs.jboss.org/hibernate/validator/8.0/reference/en-US/html_single/#section-builtin-constraints</a>
<a href="https://chaeyami.tistory.com/245">https://chaeyami.tistory.com/245</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Cookie, Session]]></title>
            <link>https://velog.io/@star_pooh/Cookie-Session</link>
            <guid>https://velog.io/@star_pooh/Cookie-Session</guid>
            <pubDate>Wed, 18 Dec 2024 11:23:54 GMT</pubDate>
            <description><![CDATA[<h3 id="인증과-인가">인증과 인가</h3>
<img src="https://velog.velcdn.com/images/star_pooh/post/92ac198b-131e-40f5-9fde-7adf50e9b008/image.png" width=600>

<blockquote>
</blockquote>
<ul>
<li>인증(Authentication)<ul>
<li>사용자가 누구인지 확인하는 과정
ex) 로그인</li>
</ul>
</li>
<li>인가(Authorization)<ul>
<li>사용자가 어떤 권한을 가지고 있는지 결정하는 과정</li>
<li><strong>인증 선행 필수</strong>
ex) 회원만 조회 가능한 게시글, 본인이 작성한 게시글 수정</li>
</ul>
</li>
</ul>
<h3 id="cookie">Cookie</h3>
<p>사용자의 웹 브라우저에 저장되는 정보로 사용자의 상태 혹은 세션을 유지하거나 사용자 경험을 개선하기 위해 사용. 사용자 세션 관리(로그인, 장바구니, 접속 시간), 광고 트래킹(사용자 행동)등이 포함됨.</p>
<h4 id="cookie-1">Cookie</h4>
<blockquote>
</blockquote>
<ul>
<li>쿠키를 사용하는 이유<ul>
<li>HTTP는 Stateless, Connectionless 특성을 가지고 있기 때문에 Client가 재요청시 Server는 이전 요청에 대한 정보를 기억하지 못함</li>
<li>로그인과 같이 상태를 유지해야 하는 경우 발생</li>
<li>Request에 사용자 정보를 포함하면 해결<ul>
<li>로그인 후에는 사용자 정보와 관련된 값이 저장되어 있어야 함</li>
</ul>
</li>
<li>브라우저를 완전히 종료한 뒤 다시 열어도 사용자 정보가 유지되어야 함<blockquote>
<blockquote>
<p>💡 쿠키의 위치
브라우저 개발자 도구(F12) → Application → Cookies</p>
</blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>로그인 성공시 응답<ul>
<li>Set-Cookie</li>
<li>로그인시 전달된 ID, Password로 User 테이블 조회하여 일치여부 확인</li>
<li>일치한다면 Set-Cookie를 활용해 Cookie에 사용할 값 저장</li>
</ul>
</li>
<li>로그인 이후 요청 <ul>
<li>요청 헤더 <code>Cookie : 사용자 정보</code></li>
<li>로그인 이후의 모든 요청에는 Request Header에 항상 Cookie 값을 추가<ul>
<li>네트워크 트래픽이 추가적으로 발생</li>
<li>최소한의 정보만 사용 해야 함</li>
</ul>
</li>
<li>Cookie에 담겨있는 값으로 인증/인가를 진행</li>
</ul>
</li>
</ul>
<h4 id="cookie-header">Cookie Header</h4>
<p>서버에서는 HTTP 응답 헤더에 Set-Cookie 속성을 사용해 생성하고 설정할 수 있음.</p>
<blockquote>
</blockquote>
<ul>
<li>Cookie Header <ul>
<li>Set-cookie<ul>
<li>Server에서 Client로 Cookie 전달(Response Header)</li>
</ul>
</li>
<li>Cookie<ul>
<li>Client가 Cookie를 저장하고 HTTP 요청시 Server로 전달(Request Header)</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<pre><code class="language-java">// Response 알아보기
set-cookie: 
sessionId=abcd; 
expires=Sat, 11-Dec-2024 00:00:00 GMT;
path=/; 
domain=abcde.com;
Secure</code></pre>
<ul>
<li>Cookie의 생명주기<ul>
<li>세션 Cookie<ul>
<li>만료 날짜(<code>expires</code>, <code>max-age</code>)를 생략하면 브라우저 완전 종료시 까지만 유지(<code>Default</code>)</li>
<li>브라우저를 완전 종료 후 다시 페이지를 방문했을 때 재로그인 필요</li>
</ul>
</li>
<li>영속 Cookie<ul>
<li>만료 날짜를 입력하면 해당 날짜까지 유지<ul>
<li><code>expires=Sat, 11-Dec-2024 00:00:00 GMT;</code> (해당 만료일이 되면 쿠키가 삭제됨)</li>
<li><code>max-age=3600</code> (초단위, 0이 되거나 음수를 지정하면 쿠기가 삭제됨)    </li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Cookie의 도메인<ul>
<li>쿠키가 아무 사이트에서나 생기고 동작하면 안되기 때문에 설정<ul>
<li>필요없는 값 전송, 트래픽 문제 등이 발생</li>
<li><code>domain=abcde.com</code>를 지정하여 쿠키를 저장</li>
<li><code>dev.abcde.com</code>와 같은 서브 도메인에서도 쿠키에 접근</li>
</ul>
</li>
<li>domain을 생략하면 현재 문서 기준 도메인만 적용</li>
</ul>
</li>
<li>Cookie의 경로<ul>
<li>1차적으로 도메인으로 필터링 후 Path 적용</li>
<li>일반적으로 <code>path=/</code> 루트(전체)로 지정</li>
<li>위 경로를 포함한 하위 경로 페이지만 쿠키에 접근<ul>
<li><code>path=/api</code> 지정<ul>
<li><code>path=/api/example</code> 가능</li>
<li><code>path=/example</code> 불가능</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Cookie 보안<ul>
<li><code>Secure</code><ul>
<li>기본적으로 Cookie는 http, https 구분하지 않고 전송</li>
<li>Secure를 적용하면 https인 경우에만 전송</li>
</ul>
</li>
<li><code>HttpOnly</code><ul>
<li>XSS(Cross-site Scripting) 공격 방지<ul>
<li>악성 스크립트를 웹 페이지에 삽입하여 다른 사용자의 브라우저에서 실행되도록 하는 공격</li>
</ul>
</li>
<li>자바스크립트에서 Cookie 접근 불가</li>
<li>HTTP 요청시 사용</li>
</ul>
</li>
<li><code>SameSite</code><ul>
<li>브라우저 지원 여부 확인필요</li>
<li>CSRF(Cross-Site Request Forgery) 공격 방지<ul>
<li>사용자가 의도하지 않은 상태에서 특정 요청을 서버에 전송하게 하여 사용자 계정에서 원치 않는 행동을 하게 만듦</li>
</ul>
</li>
<li>요청 도메인과 쿠키에 설정된 도메인이 같은 경우에만 쿠키 전송</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="cookie의-문제점">Cookie의 문제점</h4>
<blockquote>
</blockquote>
<ul>
<li>쿠키 값을 임의로 변경 가능<ul>
<li>Client가 임의로 쿠키의 값을 변경하면 서버는 다른 유저로 인식<ul>
<li>브라우저 개발자도구(F12) → Application → Cookies → 값 수정 가능</li>
</ul>
</li>
</ul>
</li>
<li>Cookie에 저장된 Data는 탈취되기 쉬움<ul>
<li>네트워크 전송 구간에서 탈취될 확률 매우 높음<ul>
<li>HTTPS를 사용하는 이유 중 하나</li>
<li>민감한 정보를 저장하면 안됨</li>
</ul>
</li>
<li>한번 탈취된 정보는 변경이 없다면 반영구적으로 사용 가능<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>대처 방법<ul>
<li>쿠키에 중요한값을 저장하지 않음</li>
<li>일반 유저나 해커들이 알아보지 못하는 값을 노출<ul>
<li>일반적으로 암호화된 <code>Token</code>을 쿠키에 저장</li>
<li>서버에서 암호화된 <code>Token</code>과 사용자를 매핑해서 인식</li>
<li>Token은 서버에서 관리</li>
</ul>
</li>
<li>토큰은 해커가 임의의 값을 넣어도 동작하지 않도록 만들어야 함</li>
<li>토큰이 탈취 당해도 사용할 수 없도록 토큰 만료시간을 짧게 설정</li>
<li>탈취가 의심되는 경우 해당 토큰을 강제로 만료시킴<ul>
<li>접속기기 혹은 IP가 다른 경우 등</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="session">Session</h3>
<h4 id="session-1">Session</h4>
<p>Cookie를 사용한 방식은 여러가지 보안 문제가 있기 때문에 중요한 정보는 모두 서버에서 저장. Client와 서버는 예측 불가능한 임의의 값으로 연결.</p>
<blockquote>
</blockquote>
<ul>
<li>Session 생성 순서 <ul>
<li>로그인에 성공하면 Server에서 임의로 만든 Session ID를 생성<ul>
<li>Session ID는 예측 불가능 해야 함(ex. UUID)</li>
</ul>
</li>
<li>생성된 Session ID와 조회한 User 인스턴스를 서버의 Session 저장소에 저장<ul>
<li>서버에 유저와 관련된 중요한 정보를 저장</li>
</ul>
</li>
</ul>
</li>
<li>Session 동작 순서<ul>
<li>로그인<ul>
<li>상태유지를 위해 Cookie 사용<ul>
<li>서버는 클라이언트에 <code>Set-Cookie: SessionId=임의생성값</code> 을 전달</li>
<li>클라이언트는 Cookie 저장소에 전달받은 <code>SessionId</code> 값을 저장</li>
</ul>
</li>
</ul>
</li>
<li>로그인 이후 요청<ul>
<li>클라이언트는 모든 요청에 Cookie의 SessionId를 전달</li>
<li>서버에서는 전달된 SessionId로 Session 저장소를 조회</li>
<li>로그인 시 저장하였던 Session 정보를 서버에서 사용<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li>Session 특징<ul>
<li>Session을 사용하여 서버에 민감한 정보를 저장<ul>
<li>예측이 불가능한 세션 ID를 사용하여 쿠키값을 변조해도 문제 없음</li>
<li>세션 ID에 중요한 정보는 포함되어 있지 않음</li>
<li>시간이 지나면 세션이 만료되도록 설정</li>
<li>해킹이 의심되는 경우 해당 세션을 제거</li>
</ul>
</li>
<li>데이터를 저장하는 곳이 클라이언트가 아닌 서버라는 것이 쿠키와 다른 점</li>
<li>Servlet은 Session을 자체적으로 지원</li>
</ul>
</li>
</ul>
<h4 id="spring의-session">Spring의 Session</h4>
<blockquote>
</blockquote>
<ul>
<li>@SessioinAttribute<ul>
<li>세션을 새로 생성하는 기능은 없음</li>
<li>이미 로그인이 완료된 사용자를 찾는 경우, 즉 세션이 있는 경우에 사용<pre><code class="language-java">@Controller
@Requiredargsconstructor
public class SessionHomeController {
private UserService userService;
&gt;
@GetMapping(&quot;/v2/session-home&quot;)
public String homeV2(
      // 로그인 여부를 확인해야하므로 required = false
      @SessionAttribute(name = Const.LOGIN_USER, required = false) UserResponseDto loginUser,
      Model model) {
  // session에 loginUser가 없으면 Login 페이지로 이동
  if (loginUser == null) {
      return &quot;session-login&quot;;
  }
  // Session이 정상적으로 조회되면 로그인된것으로 간주
  model.addAttribute(&quot;loginUser&quot;, loginUser);
  // home 화면으로 이동
  return &quot;session-home&quot;;
}
}</code></pre>
</li>
</ul>
</li>
<li>HttpSession<ul>
<li>세션을 간편하게 사용할 수 있도록 다양한 기능 지원<pre><code class="language-java">@Slf4j
@RestController
public class SessionController {
@GetMapping(&quot;/session&quot;)
public String session(HttpServletRequest request) {
  HttpSession session = request.getSession(false);
&gt;
  if (session == null) {
      return &quot;세션이 없습니다.&quot;;
  }
&gt;
  // jsessionId 값 조회
  log.info(&quot;session.getId()={}&quot;, session.getId());
  // 세션의 유효 시간(초단위, 기본값은 1800)
  log.info(&quot;session.getMaxInactiveInterval()={}&quot;, session.getMaxInactiveInterval());
  // 세션 생성 시간
  log.info(&quot;session.getCreationTime()={}&quot;, session.getCreationTime());
  // 해당 세션에 마지막으로 접근한 시간
  log.info(&quot;session.getLastAccessedTime()={}&quot;, session.getLastAccessedTime());
  // 새로 생성된 세션인지 확인
  log.info(&quot;session.isNew()={}&quot;, session.isNew());
  return &quot;세션 조회 성공!&quot;;
}
}</code></pre>
</li>
</ul>
</li>
</ul>
<h4 id="session-timeout">Session TimeOut</h4>
<p>Session은 logout 기능을 사용하여 <code>session.invalidate();</code> 가 되어야 삭제되지만 대부분의 사용자들은 로그아웃을 굳이 하지않고, 브라우저를 종료함.</p>
<blockquote>
</blockquote>
<ul>
<li>Session의 문제점<ul>
<li>HTTP는 Connectionless 특성을 가지고 있어서 서버가 브라우저의 종료 여부 판별 불가</li>
<li>서버에서 언제 세션을 삭제해야 하는지 판단 불가</li>
<li>JSESSIONID의 값을 탈취 당한 경우 해당 값으로 악의적인 요청 가능</li>
<li>세션은 서버 메모리에 생성되고 자원은 한정적이기 때문에 꼭 필요한 경우만 생성 필요<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>Session 생명 주기<ul>
<li>기본적으로 30분을 기준으로 삭제</li>
<li>실제 로그인 후 30분 이상의 시간동안 사용중인 사용자의 세션도 삭제됨<ul>
<li>재로그인 해야 하는 경우 발생<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li>HttpSession 사용<ul>
<li>세션 생성 시점으로부터 30분이 아닌 서버에 최근 Session을 요청한 시간을 기준으로 30분 유지</li>
</ul>
</li>
</ul>
<h4 id="session의-한계">Session의 한계</h4>
<p>Session은 서버의 메모리를 사용하여 확장성이 제한됨.</p>
<blockquote>
</blockquote>
<ul>
<li>서버가 DB 혹은 메모리에 저장된 세션 정보를 매번 조회하여 오버헤드 발생</li>
<li>서버가 상태를 유지해야 하므로 사용자 수가 많아질수록 부담 증가</li>
<li>Cookie는 웹 브라우저에만 존재하여 모바일 앱 등의 다양한 클라이언트에서 인증 처리 불가</li>
<li>Scale Out(수평적 확장)에서 서버간 세션 공유가 여려움<img src="https://velog.velcdn.com/images/star_pooh/post/e59dda2d-6598-46ee-ba5d-fb30cde01e41/image.png" width=600>
>
>>💡 오버헤드(Overhead)란?
어떤 처리를 하기 위해 들어가는 간접적인 처리 시간, 메모리 등을 의미한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20241218 회고]]></title>
            <link>https://velog.io/@star_pooh/20241218-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@star_pooh/20241218-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 18 Dec 2024 11:23:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✅ 알고리즘 &amp; SQL 코드카타
💡 요즘 알고리즘 문제가 아니라 수학 문제를 푸는 것 같다. 문제에 설명이 없어서 검색해서 풀었다. SQL은 JOIN을 쓰는 문제가 계속 나오고 있다. </p>
</blockquote>
<blockquote>
<p>✅ 스프링 숙련 2주차 강의 정리
💡 쿠키와 세션에 대해서 정리했고, 어제 녹화본으로 시청한 베이직반 세션과 과제에서 구현한 로그인 기능에 사용했던 부분이라 다시 한 번 정리되는 느낌이었다.</p>
</blockquote>
<blockquote>
<p>✅ 스프링 과제
💡 Lv8까지 구현은 끝났다. API 명세서와 ERD 등 문서화 작업이 남아있다. 레벨별로 바뀐 점이 많아서 다 나눠서 작성하려니 생각보다 양이 많아서 오래 걸릴 것 같다. 리팩토링도 하고 싶은데 가능할지 모르겠다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[20241217 회고]]></title>
            <link>https://velog.io/@star_pooh/20241217-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@star_pooh/20241217-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 17 Dec 2024 12:26:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✅ 알고리즘 &amp; SQL 코드카타
💡 알고리즘 문제는 어렵지는 않았는데 디테일한 부분을 놓쳐서 한 번에 제출을 하지 못했다. 주어진 입출력 말고도 테스트 케이스를 추가하는 것도 생각해봐야 할 것 같다. SQL은 지문이 조금씩 복잡해지고 있다. 다른 풀이법을 보고 여러 방법에 대해 알게 되는 것 같다. 시간이 생길지…는 모르겠지만 정리하면 좋을 것 같다.</p>
</blockquote>
<blockquote>
<p>✅ 스프링 숙련 1주차 강의 정리
💡 어제 정리하고 남았던 Spring Bean과 Validation에 대해서 정리했다. 마침 오늘 스프링 과제에서 Validation을 했어야 해서 사전에 복습한게 도움이 되었다.</p>
</blockquote>
<blockquote>
<p>✅ 스프링 과제
💡 어제에 이어서 Lv3~5까지 완료하고 Lv6을 막 시작했다. 요구 사항은 하나 늘어나지만 테스트가 필요한 부분은 여러 군데라서 확인하는데 시간이 들고 있다. 프로젝트 때 바로 적용이 가능할지는 모르겠지만 내가 구현한 부분이라도 테스트 코드를 만들어야 하나 고민 중에 있다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Bean, Validation]]></title>
            <link>https://velog.io/@star_pooh/Spring-Bean-Validation</link>
            <guid>https://velog.io/@star_pooh/Spring-Bean-Validation</guid>
            <pubDate>Tue, 17 Dec 2024 12:12:48 GMT</pubDate>
            <description><![CDATA[<h3 id="spring-bean">Spring Bean</h3>
<h4 id="spring-bean-등록1">Spring Bean 등록(1)</h4>
<blockquote>
</blockquote>
<ul>
<li>@ComponentScan(자동 등록)<ul>
<li>특정 패키지 내에서 @Component 어노테이션이 붙은 클래스를 자동으로 찾아서 Bean으로 등록<ul>
<li>@Service, @Repository, @Controller 같은 어노테이션도 @Component가 포함되어 있음</li>
</ul>
</li>
<li>스캐닝 범위는 주로 어플리케이션의 루트(최상위) 패키지<ul>
<li><code>basePackages</code> 옵션으로 스캔할 패키지 선택 가능</li>
</ul>
</li>
<li>@SpringBootApplication에 포함되어 있음<ul>
<li>스프링 프로젝트를 생성하면 메인 메소드가 있는 클래스 상단에 @SpringBootApplication이 존재</li>
</ul>
</li>
</ul>
</li>
<li>@Configuration &amp; @Bean(수동 등록)<ul>
<li>@Configuration이 있는 클래스를 Bean으로 등록하고 해당 클래스 파싱</li>
<li>@Bean이 있는 메소드를 찾아 Bean을 생성하며, 해당 메소드명이 Bean의 이름이 됨<pre><code class="language-java">@Configuration
public class AppConfig {
@Bean
public TestService testService() {
  ...
}
}</code></pre>
</li>
</ul>
</li>
<li>Bean 충돌<ul>
<li>이름이 같은 Bean이 등록되려고 하는 경우, 충돌 발생<ul>
<li>자동 등록 vs 자동 등록에서 충돌이 발생하는 경우, <code>ConflictingBeanDefinitionException</code>가 발생</li>
<li>수동 등록 vs 자동 등록에서 충돌이 발생하는 경우, 수동 등록이 자동 등록을 오버라이딩 해서 우선권을 가짐</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="의존관계-주입">의존관계 주입</h4>
<blockquote>
</blockquote>
<ul>
<li><strong>생성자 주입</strong><ul>
<li>생성자를 통해 의존성을 주입하는 방법</li>
<li>최초에 한 번 생성된 후 값이 수정되지 못함(불변, 필수)</li>
<li>애플리케이션 실행과 동시에 의존성이 주입되기 때문에 시스템 안정성이 향상됨</li>
<li><a href="https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html">스프링 팀에서 권장하는 방법</a><pre><code class="language-java">@Component
public class Car {
private final Engine engine;
&gt;
public Car(Engine engine) {
  this.engine = engine;
}
&gt;
public void start() {
 ...
}
}</code></pre>
<blockquote>
<blockquote>
<p>💡 @RequiredArgsConstructor</p>
</blockquote>
</blockquote>
</li>
</ul>
</li>
<li>생성자 주입 방식에서 반복되는 코드를 편하게 작성하기 위해 Lombok에서 제공되는 어노테이션</li>
<li><code>final</code> 필드를 모아서 생성자를 자동으로 만들어주는 역할</li>
<li>컴파일 시점에 자동으로 생성자 코드를 생성<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class MyApp {
  // 필드에 final 키워드 필수
  private final MyService myService;
&gt;&gt;
  // Annotation Processor가 만들어 주는 코드
  // public MyApp(MyService myService) {
  //     this.myService = myService;
  // }
&gt;&gt;
  public void run() {
      myService.doSomething();
  }
}</code></pre>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>Setter 주입<ul>
<li>Setter 메소드를 통해 의존성을 주입하는 방법</li>
<li>선택하거나 변경 가능한 의존관계에 사용</li>
<li>클래스 안에 의존성이 숨겨져 있어서 유지보수 어려움</li>
<li>의존성이 변경될 가능성이 있기 때문에 시스템 안정성이 떨어짐<pre><code class="language-java">@Component
public class Car {
private Engine engine;
&gt;
@Autowired // 세터 주입
public void setEngine(Engine engine) {
  this.engine = engine;
}
&gt;
public void start() {
 ...
}
}</code></pre>
</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>필드 주입<ul>
<li>필드에 직접적으로 주입하는 방법</li>
<li>코드는 간결하지만 스프링이 없으면 동작하지 않음</li>
<li>클래스 안에 의존성이 숨겨져 있어서 유지보수 어려움</li>
<li>의존성이 변경될 가능성이 있기 때문에 시스템 안정성이 떨어짐<pre><code class="language-java">@Component
public class Car {
@Autowired // 필드 주입
private Engine engine;
&gt;
public void start() {
 ...
}
}</code></pre>
</li>
</ul>
</li>
</ul>
<h4 id="spring-bean-등록2">Spring Bean 등록(2)</h4>
<p>같은 타입의 Bean이 충돌했을 경우</p>
<blockquote>
</blockquote>
<ul>
<li>@Qualifier<ul>
<li>Bean 등록시 추가 구분자를 붙여줌<pre><code class="language-java">@Component
@Qualifier(&quot;firstService&quot;)
public class MyServiceImplV1 implements MyService { ... }
&gt;
@Component
@Qualifier(&quot;secondService&quot;)
public class MyServiceImplV2 implements MyService { ... }
&gt;
@Component
public class ConflictApp {
private MyService myService;
&gt;
// 생성자 주입에 구분자 추가
@Autowired
public ConflictApp(@Qualifier(&quot;firstService&quot;) MyService myService) {
  this.myService = myService;
}
}        </code></pre>
</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>@Primary<ul>
<li>지정된 Bean이 우선 순위<pre><code class="language-java">@Component
public class MyServiceImplV1 implements MyService { ... }
&gt;
@Component
@Primary
public class MyServiceImplV2 implements MyService { ... }
&gt;
@Component
public class ConflictApp {
  private MyService myService;
&gt;
  @Autowired
  public ConflictApp(MyService myService) {
          this.myService = myService;
  }
...
}</code></pre>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 실제 적용 사례</p>
</blockquote>
<ul>
<li>Database가 (메인 MySQL, 보조 Oracle) 두 개 존재하는 경우<ul>
<li>기본적으로 MySQL을 사용할 때 <code>@Primary</code>를 사용</li>
<li>필요할 때 <code>@Qualifier</code>로 Oracle을 사용</li>
<li>동시에 사용되는 경우 <code>@Qualifier</code> 의 우선 순위가 높음</li>
</ul>
</li>
</ul>
<h3 id="validataion검증">Validataion(검증)</h3>
<h4 id="validation">Validation</h4>
<p>특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인하는 단계.</p>
<blockquote>
</blockquote>
<ul>
<li>검증의 역할<ul>
<li>검증을 통해 오류 발생 시 유저에게 적절한 메시지 제공</li>
<li>검증 오류로 인한 비정상적인 동작 방지</li>
<li>사용자가 입력한 데이터 유지</li>
</ul>
</li>
<li>검증의 종류<ul>
<li>프론트 검증<ul>
<li>유저가 조작할 수 있어서 보안에 취약</li>
<li>하지만 유저 사용성을 위해 필요
ex) 비밀번호에 특수문자가 포함되어야 한다면 즉각적인 alert 가능 → 유저 사용성 증가</li>
</ul>
</li>
<li>서버 검증<ul>
<li>프론트 검증 없이 서버에서만 검증한다면 유저 사용성이 저하됨</li>
<li>API 명세서의 Response에 검증 오류를 적어야함<ul>
<li>그에 맞는 대응 가능</li>
</ul>
</li>
<li>서버 검증은 선택이 아닌 <strong>필수</strong></li>
</ul>
</li>
<li>데이터베이스 검증</li>
<li>Not Null, Default와 같은 제약조건 설정</li>
<li>최종 방어선 역할</li>
</ul>
</li>
</ul>
<h4 id="bindingresult">BindingResult</h4>
<p>Spring에서 기본적으로 제공되는 검증 오류를 보관하는 객체. 주로 사용자 입력 폼을 검증할 때 많이 쓰이고 Field Error와 ObjectError를 보관.</p>
<blockquote>
</blockquote>
<ul>
<li>BindResult<ul>
<li>Errors 인터페이스를 상속받은 인터페이스</li>
<li>Errors 인터페이스는 에러의 저장과 조회 기능 제공</li>
<li>BindingResult는 <code>addError()</code> 와 같은 추가적인 기능 제공</li>
<li><code>BeanPropertyBindingResult</code>는 Spring이 기본적으로 사용하는 구현체<pre><code class="language-java">@Data
public class MemberCreateRequestDto {
private Long point;
private String name;
private Integer age;
}
&gt;
@Controller
public class BingdingResultController {
@PostMapping(&quot;/v1/member&quot;)
public String createMemberV1(@ModelAttribute MemberCreateRequestDto request, Model model) {
  System.out.println(&quot;/V1/member API가 호출되었습니다.&quot;);
  model.addAttribute(&quot;point&quot;, request.getPoint());
  model.addAttribute(&quot;name&quot;, request.getName());
  model.addAttribute(&quot;age&quot;, request.getAge());
  return &quot;complete&quot;;
}
}</code></pre>
<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>파라미터에 BindingResult가 없을 때 잘못된 요청을 보낸 경우<ul>
<li>검증 오류(400 Bad Request)가 발생하고 Controller가 호출 되지 않음<pre><code class="language-java">@Controller
public class BindingResultController {
@PostMapping(&quot;/v2/member&quot;)
public String createMemberV2(
  // @ModelAttribute 뒤에 BindingResult 위치
  @ModelAttribute MemberCreateRequestDto request,
  BindingResult bindingResult,
  Model model) {
  System.out.println(&quot;/V2/member API가 호출되었습니다.&quot;);
&gt;
  // BindingResult의 에러 출력
  List&lt;ObjectError&gt; allErrors = bindingResult.getAllErrors();
  System.out.println(&quot;allErrors = &quot; + allErrors);
&gt;
  model.addAttribute(&quot;point&quot;, request.getPoint());
  model.addAttribute(&quot;name&quot;, request.getName());
  model.addAttribute(&quot;age&quot;, request.getAge());
  return &quot;complete&quot;;
}
}</code></pre>
</li>
</ul>
</li>
<li>파라미터에 BindingResult가 있을 때 잘못된 요청을 보낸 경우<ul>
<li><strong>BindingResult는 검증 대상 파라미터 뒤에 위치해야 함</strong></li>
<li>@ModelAttrbute 필드 또는 객체에 파라미터 바인딩 오류 발생<ul>
<li>BindingResult에 오류가 보관되고 Controller가 호출됨</li>
<li>@ModelAttribute는 파라미터를 필드 하나하나에 바인딩하기 때문에 어떤 필드에 오류가 발생할 경우, 해당 필드를 제외하고 나머지 필드들만 바인딩 된 후 Controller가 호출됨 </li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="bean-validation">Bean Validation</h4>
<blockquote>
</blockquote>
<ul>
<li>파라미터에 대한 검증을 Conroller에서 하게 되면, Controller가 너무 커지며, 단일 책임 원칙(SRP)를 위배</li>
<li>객체의 필드나 메소드에 제약 조건을 설정하여, 올바른 값인지 검증하는 표준화된 방법<ul>
<li>Bean Validation은 기술 표준 인터페이스</li>
<li>다양한 어노테이션과 인터페이스로 구성<ul>
<li>Bean Validation 구현체인 <a href="https://hibernate.org/validator/"><strong>Hibernate Validator</strong></a> 사용<pre><code class="language-java">@Getter
public class SignUpRequestDto {
@NotBlank
private String name;
&gt;
@NotNull
@Range(min = 1, max = 120)
private Integer age;
}</code></pre>
<a href="https://docs.jboss.org/hibernate/validator/8.0/reference/en-US/html_single/#section-builtin-constraints"><strong>어노테이션 정보(공식문서)</strong></a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="validator">Validator</h4>
<p>어노테이션만 선언해도 검증이 완료되는 이유는 Validator가 존재하기 때문인데, validation 라이브러리를 설정하면 <code>&#39;org.springframework.boot:spring-boot-starter-validation&#39;</code> 자동으로 Bean Validator를 Spring에 통합되도록 설정해줌.</p>
<blockquote>
</blockquote>
<ul>
<li>동작 순서<ul>
<li>Spring Boot Application 실행 시 자동으로 Bean Validator 통합<ul>
<li><code>LocalValidatorFactoryBean</code> 을 Global Validator로 등록</li>
<li>Global Validator가 Default로 적용되어 있으니 <code>@Valid</code>, <code>@Validated</code> 적용</li>
<li>Bean Validation Annotation(@NotNull, @NotBlank, @Max ...)에 대한 검증 수행</li>
<li>Validation Error가 발생하면 <code>FieldError</code>, <code>ObjectError</code>를 생성하여 <code>BindingResult</code>에 보관<blockquote>
<blockquote>
<p>💡 LocalValidatorFactoryBean의 클래스 다이어그램
<img src="https://velog.velcdn.com/images/star_pooh/post/8bf8ff82-b800-4cf5-b719-6a7b48ff60d9/image.png" alt=""></p>
</blockquote>
<p>💥 Global Validator를 수동으로 등록하면 <code>LocalValidatorFactoryBean</code>를 등록하지 않는다.</p>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li>@Valid, @Validated 차이점<ul>
<li><code>@Valid</code> 는 Java 표준이고 <code>@Validated</code> 는 Spring에서 제공</li>
<li><code>@Validated</code>를 통해 Group Validation 또는 Controller 이외 계층에서 Validation 가능</li>
<li><code>@Valid</code>는 <code>MethodArgumentNotValidException</code>를 예외로 발생</li>
<li><code>@Validated</code>는 <code>ConstraintViolationException</code>를 예외로 발생</li>
</ul>
</li>
</ul>
<h4 id="에러-메시지">에러 메시지</h4>
<p>Spring의 Bean Validation은 디폴트로 제공하는 메시지가 존재하며, 임의로 수정 가능</p>
<blockquote>
</blockquote>
<ul>
<li>BindingResult에 등록된 검증 오류에는 어노테이션 이름으로 오류가 등록되어 있음</li>
<li>어노테이션의 <code>message</code> 속성을 사용해서 메시지 수정 가능<pre><code class="language-java">@Data
public class TestDto {
  @NotNull(message = &quot;메시지 수정 가능&quot;)
  private String stringField;
}</code></pre>
<pre><code>Field error in object &#39;testDto&#39; on field &#39;integerField&#39;: rejected value [null]; codes [NotNull.testDto.integerField,NotNull.integerField,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDto.integerField,integerField]; arguments []; default message [integerField]]; default message [널이어서는 안됩니다]</code></pre>👇 메시지 수정 후<pre><code>Field error in object &#39;testDto&#39; on field &#39;integerField&#39;: rejected value [null]; codes [NotNull.testDto.integerField,NotNull.integerField,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDto.integerField,integerField]; arguments []; default message [integerField]]; default message [메시지 수정 가능]</code></pre></li>
</ul>
<h4 id="bean-validtaion의-충돌">Bean Validtaion의 충돌</h4>
<blockquote>
</blockquote>
<ul>
<li>요구사항<ul>
<li>상품<ul>
<li>id (식별자)</li>
<li>name (이름)</li>
<li>price (가격)</li>
<li>count (재고)</li>
</ul>
</li>
<li>상품 등록 API<ul>
<li><strong>식별자 값은 필수가 아니다.</strong></li>
<li>name은 null, “”, “ “을 허용하지 않는다.</li>
<li><strong>price는 10 ~ 10000 사이의 숫자로 생성한다.</strong></li>
<li>count는 1 ~ 999 사이의 숫자로 생성한다.</li>
</ul>
</li>
<li>상품 수정 API<ul>
<li><strong>식별자 값이 필수이다.</strong></li>
<li>name은 null, “”, “ “을 허용하지 않는다.</li>
<li><strong>price는 무제한으로 허용한다.</strong></li>
<li>count는 1 ~ 999 사이의 숫자로 생성한다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>등록과 수정API에 공통된 RequestDto를 사용할 수 없음<ul>
<li>SaveRequestDto, UpdateRequestDto를 따로 사용(주로 사용하는 방법)</li>
<li>Bean Validation의 groups 기능 사용</li>
</ul>
</li>
</ul>
<h4 id="groups">groups</h4>
<p>동일한 객체에 대한 검증을 상황에 따라 다르게 적용하고 싶을 때 활용.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">// 저장용 group
public interface SaveCheck {
}
&gt;
// 수정용 group
public interface UpdateCheck {
}
&gt;
@Data
public class ProductRequestDtoV2 {
    // 저장, 수정 모두 적용
    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
    private String name;
&gt;
    // 사용하는 모든 곳에 적용
    @NotNull
    // 저장만 적용
    @Range(min = 10, max = 10000, groups = SaveCheck.class)
    private Integer price;
&gt;
    @NotNull
    @Range(min = 1, max = 999)
    private Integer count;
}
&gt;
@RestController
public class ProductController {
    @PostMapping(&quot;/v2/product&quot;)
    public String save(
            // 저장 속성값 설정
            @Validated(SaveCheck.class) @ModelAttribute ProductRequestDtoV2 requestDtoV2) {
        return &quot;상품 생성이 완료되었습니다&quot;;
    }
&gt;
    @PutMapping(&quot;/v2/product/{id}&quot;)
    public String update(
            @PathVariable Long id,
            // 수정 속성값 설정
            @Validated(UpdateCheck.class) @ModelAttribute ProductRequestDto test) {
        return &quot;상품 수정이 완료되었습니다.&quot;;
    }
}</code></pre>
<ul>
<li>groups 속성을 사용하면 각각 다르게 검증 적용 가능<ul>
<li>가독성이 떨어지고 코드 복잡도 증가</li>
<li><code>@Validated</code>만 지원</li>
</ul>
</li>
</ul>
<h4 id="requestbody">@RequestBody</h4>
<p>@Valid, @Validated는 @ModelAttribute뿐만 아니라 @RequestBody에도 적용 가능. @ModelAttribute는 요청 파라미터 혹은 Form Data(x-www-urlencoded)를 다룰 때 사용하고 @RequestBody 는 HTTP Body Data를 Object로 변환할 때 사용.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">@Data
public class ExampleRequestDto {
    @NotBlank
    private String field1;
&gt;
    @NotNull
    @Range(min = 1, max = 150)
    private Integer field2;
}
&gt;
@Slf4j
@RestController
public class RequestBodyController {
    @PostMapping(&quot;/example&quot;)
    public Object save(
        @Validated @RequestBody ExampleRequestDto dto, BindingResult bindingResult) {
        log.info(&quot;RequestBody Controller 호출&quot;);
&gt;
        if(bindingResult.hasErrors()) {
            log.info(&quot;validation errors={}&quot;, bindingResult);
            // Field, Object Error 모두 Json으로 반환
            return bindingResult.getAllErrors();
        }
        return dto;
    }
}</code></pre>
<ul>
<li>RestAPI 요청에 따른 결과<ul>
<li>성공 요청<ul>
<li>Controller 정상 호출</li>
<li>응답 반환</li>
</ul>
</li>
<li>실패 요청 : Json 객체를 변환하는 것 자체가 실패<ul>
<li>Controller 호출 되지 않음</li>
<li>반드시 Json → 객체 변환이 되어야 검증 진행</li>
</ul>
</li>
<li>검증 오류 요청 : Json 객체로 변환하는 것은 성공했지만, 검증에서 실패<ul>
<li><code>bindingResult.getAllErrors()</code>가 MessageConverter에 의해 Json으로 변환되어 반환</li>
<li>Controller 호출</li>
<li>log로 작성한 bindingResult 에러가 콘솔에 출력</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[202241216 회고]]></title>
            <link>https://velog.io/@star_pooh/202241216-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@star_pooh/202241216-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 16 Dec 2024 12:16:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✅ 알고리즘 &amp; SQL 코드카타
💡 알고리즘은 문제가 어렵진 않았는데 스택을 처음 써봐서 앞으로는 사용할 수 있다면 자주 사용해서 익숙해지도록 해야 할 것 같다. SQL은 해설이 있다는 것을 알게 되었는데 푼 방법과 다른 방법은 무엇이 있는지 살펴볼 수 있어서 좋은 것 같다.</p>
</blockquote>
<blockquote>
<p>✅ 스프링 숙련 1주차 강의 정리
💡 반정도 정리를 마쳤고, SOLID 원칙과 Spring Container에 대해서 복습할 수 있었다.</p>
</blockquote>
<blockquote>
<p>✅ 스프링 과제
💡 요구 사항 정의에 대해 궁금했던 부분을 튜터님께 물어보고 과제를 진행했다. Lv2까지 완료했고 Lv3를 시작하기 전에 인증/인가에 대한 세션 녹화본을 보고 있다. 내일 세션이 있긴하지만 Lv5까지는 끝낼 수 있지 않을까 생각하고 있다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[SOLID 원칙, Spring Container]]></title>
            <link>https://velog.io/@star_pooh/SOLID-%EC%9B%90%EC%B9%99-Spring-Container</link>
            <guid>https://velog.io/@star_pooh/SOLID-%EC%9B%90%EC%B9%99-Spring-Container</guid>
            <pubDate>Mon, 16 Dec 2024 12:15:15 GMT</pubDate>
            <description><![CDATA[<h3 id="solid-원칙">SOLID 원칙</h3>
<p>객체 지향 설계의 5가지 기본 원칙으로서 소프트웨어 설계에서 유지보수성, 확장성, 유연성을 높이기 위한 지침.</p>
<h4 id="단일-책임-원칙single-responsibility-principle">단일 책임 원칙(Single Responsibility Principle)</h4>
<p><code>하나의 클래스는 하나의 책임만 가져야 한다.</code></p>
<blockquote>
</blockquote>
<pre><code class="language-java">// User 클래스는 사용자 정보 관리, 로그인 및 데이터베이스 저장 책임을 동시에 가지고 있음
public class User {
    private String name; // 사용자 정보
    public void login() { /* 로그인 기능 */ }
    public void saveUser() { /* 데이터베이스 저장 기능 */ }
}
&gt;
👇 단일 책임 원칙 적용 후
&gt;
public class User { 
    // 사용자 정보 관리
}
&gt;
public class AuthService {
    public void login(User user) {
        // 로그인 기능
    }
}
&gt;
public class UserRepository {
    public void saveUser(User user) { 
        // 데이터베이스 저장
    }
}</code></pre>
<ul>
<li>클래스는 한 가지 기능에 집중해야 하며, 그 외의 기능을 담당하지 않아야 함</li>
<li>클래스가 변경될 때 파급 효과가 작아야 함</li>
<li>상황에 따라 책임의 크기가 달라질 수 있음</li>
</ul>
<h4 id="개방-폐쇄-원칙open-closed-principle">개방 폐쇄 원칙(Open Closed Principle)</h4>
<p><code>소프트웨어 요소는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.</code></p>
<blockquote>
</blockquote>
<pre><code class="language-java">public class Shape {
    public String type;
}
&gt;
public class AreaCalculator {
    public double calculate(Shape shape) {
        if (shape.type.equals(&quot;circle&quot;)) {
            return 원의 넓이 계산;
        } else if (shape.type.equals(&quot;square&quot;)) {
            return 사각형의 넓이 계산;
        }
    }
}
&gt;
👇 개방 폐쇄 원칙 적용 후
&gt;
public interface Shape {
    double calculateArea();
}
&gt;
public class Circle implements Shape {
    public double calculateArea() { 
        return 원의 넓이 계산; 
    }
}
&gt;
public class Square implements Shape {
    public double calculateArea() { 
        return 사각형의 넓이 계산; 
    }
}
&gt;
public class AreaCalculator {
    public double calculate(Shape shape) {
        return shape.calculateArea();
    }
}</code></pre>
<ul>
<li>새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있도록 설계해야 함<ul>
<li><strong>다형성</strong>을 활용하여 해결</li>
<li>OCP 적용 전에는 새로운 도형이 추가될 때마다 AreaCalculator 클래스를 수정해야함</li>
<li>OCP 적용 후에는 새로운 도형이 추가되더라도 Shape 인터페이스만 구현하면 AreaCalculator는 수정할 필요가 없음</li>
</ul>
</li>
</ul>
<blockquote>
<p>💥 문제점</p>
</blockquote>
<pre><code class="language-java">// Circle을 계산하는 경우
public class Main {
    public static void main(String[]) {
        AreaCalculator areaCalculator = new AreaCalculator();
        Circle circle = new Circle();
        areaCalculator.calculate(circle);
    }
}
&gt;
// Square를 계산하는 경우
public class Main {
    public static void main(String[]) {
        AreaCalculator areaCalculator = new AreaCalculator();
        // Circle circle = new Circle();
        Square square = new Square();
        areaCalculator.calculate(square);
    }
}</code></pre>
<ul>
<li>구현 객체를 변경하기 위해서는 해당 코드를 사용하는 클라이언트측의 코드를 변경해야 함</li>
<li>객체의 생성, 사용 등을 자동으로 설정해주는 <strong>무엇인가</strong>가 필요해짐<ul>
<li>Spring Container의 역할</li>
</ul>
</li>
</ul>
<h4 id="리스코프-치환-원칙liskov-substitution-principle">리스코프 치환 원칙(Liskov Substitution Principle)</h4>
<p><code>자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.</code></p>
<blockquote>
</blockquote>
<pre><code class="language-java">class Car {
    public void accelerate() {
        System.out.println(&quot;자동차가 휘발유로 가속합니다.&quot;);
    }
}
&gt;
class ElectricCar extends Car {
    @Override
    public void accelerate() {
        throw new UnsupportedOperationException(&quot;전기차는 이 방식으로 가속하지 않습니다.&quot;);
    }
}
&gt;
public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.accelerate(); // &quot;자동차가 가속합니다.&quot;
&gt;
        Car electricCar = new ElectricCar();
        electricCar.accelerate(); // UnsupportedOperationException 발생
    }
}
&gt;
👇 리스코프 치환 원칙 적용 후
&gt;
interface Acceleratable {
    void accelerate();
}
&gt;
class Car implements Acceleratable {
    @Override
    public void accelerate() {
        System.out.println(&quot;내연기관 자동차가 가속합니다.&quot;);
    }
}
&gt;
class ElectricCar implements Acceleratable {
    @Override
    public void accelerate() {
        System.out.println(&quot;전기차가 배터리로 가속합니다.&quot;);
    }
}
&gt;
public class Main {
    public static void main(String[] args) {
        Acceleratable car = new Car();
        car.accelerate(); // &quot;내연기관 자동차가 가속합니다.&quot;
&gt;
        Acceleratable electricCar = new ElectricCar();
        electricCar.accelerate(); // &quot;전기차가 배터리로 가속합니다.&quot;
    }
}</code></pre>
<ul>
<li>부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램의 동작에 문제가 없어야 함<ul>
<li>ElectricCar는 Car 클래스를 상속 받았지만, accelerate() 를 사용할 수 없음</li>
<li>인터페이스를 구현한 구현체를 믿고 사용할 수 있도록 만들어줌</li>
</ul>
</li>
</ul>
<h4 id="인터페이스-분리-원칙interface-segregation-principle">인터페이스 분리 원칙(Interface Segregation Principle)</h4>
<p><code>특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.</code></p>
<blockquote>
</blockquote>
<pre><code class="language-java">public interface Animal {
    void fly();
    void run();
    void swim();
}
&gt;
public class Dog implements Animal {
    public void fly() { /* 사용하지 않음 */ }
    public void run() { /* 달리기 */ }
    public void swim() { /* 수영 */ }
}
&gt;
👇 인터페이스 분리 원칙 적용 후
&gt;
public interface Runnable {
    void run();
}
&gt;
public interface Swimmable {
    void swim();
}
&gt;
public class Dog implements Runnable, Swimmable {
    public void run() { /* 달리기 */ }
    public void swim() { /* 수영 */ }
}</code></pre>
<ul>
<li>클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 함<ul>
<li>Dog 클래스는 fly() 메소드를 사용하지 않지만 구현 해야함</li>
<li>하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스로 분리해야 함<ul>
<li>인터페이스가 명확해짐</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="의존관계-역전-원칙dependency-inversion-principle">의존관계 역전 원칙(Dependency Inversion Principle)</h4>
<p><code>구체적인 클래스에 의존하지 말고, 인터페이스나 추상 클래스에 의존하도록 설계해야 한다.</code></p>
<blockquote>
</blockquote>
<pre><code class="language-java">class EmailNotifier {
    public void sendEmail(String message) {
        System.out.println(&quot;Email 알림: &quot; + message);
    }
}
&gt;
class NotificationService {
    private EmailNotifier emailNotifier;
&gt;
    public NotificationService() {
        // 구체적인 클래스인 EmailNotifier에 의존
        this.emailNotifier = new EmailNotifier();
    }
&gt;
    public void sendNotification(String message) {
        emailNotifier.sendEmail(message);
    }
}
&gt;
public class Main {
    public static void main(String[] args) {
        NotificationService service = new NotificationService();
        service.sendNotification(&quot;안녕하세요! 이메일 알림입니다.&quot;);
    }
}
&gt;
👇 의존관계 역전 원칙 적용 후
&gt;
interface Notifier {
    void send(String message);
}
&gt;
class EmailNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println(&quot;Email 알림: &quot; + message);
    }
}
&gt;
class SMSNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println(&quot;SMS 알림: &quot; + message);
    }
}
&gt;
class NotificationService {
    private Notifier notifier;
&gt;
    public NotificationService(Notifier notifier) {
        this.notifier = notifier;
    }
&gt;
    public void sendNotification(String message) {
        notifier.send(message);
    }
}
&gt;
public class Main {
    public static void main(String[] args) {
        // Email 알림을 사용
        Notifier emailNotifier = new EmailNotifier();
        NotificationService emailService = new NotificationService(emailNotifier);
        emailService.sendNotification(&quot;안녕하세요! 이메일 알림입니다.&quot;);
&gt;
        // SMS 알림을 사용
        Notifier smsNotifier = new SMSNotifier();
        NotificationService smsService = new NotificationService(smsNotifier);
        smsService.sendNotification(&quot;안녕하세요! SMS 알림입니다.&quot;);
    }
}</code></pre>
<ul>
<li>추상화된 Notifier 인터페이스에만 의존<ul>
<li>DIP 적용 전에는 SMS 알림과 같은 기능이 추가 되면 NotificationService가 수정 되어야 함</li>
<li>DIP 적용 후에는 NotificationService이 수정되지 않아도 됨 </li>
</ul>
</li>
<li>필요한 Notifier 객체를 외부에서 주입받음<ul>
<li>NotificationService는 어떤 알림 방식을 사용할지에 대한 세부 사항을 몰라도 되므로, 의존성이 약해짐</li>
</ul>
</li>
<li>모듈간의 결합도를 낮추고 유연성과 확장성을 높일 수 있음</li>
<li>서로의 변경 사항에 독립적이어서 변경에 유연함</li>
</ul>
<h4 id="spring과-객체지향">Spring과 객체지향</h4>
<p>객체 지향의 핵심은 다형성에 있지만, 다형성만으로는 OCP, DIP를 지킬 수 없음. Spring은 IOC(Inversion Of Control), DI(Dependency Injection)을 통해 OCP, DIP가 가능하도록 만들어줌</p>
<blockquote>
</blockquote>
<ul>
<li><p>Spring의 역할</p>
<ul>
<li>OCP, DIP 원칙을 지킬 수 있도록 도와줌</li>
<li>코드의 변경 없이 기능을 확장할 수 있도록 만들어줌</li>
<li>개발자가 원하는 구성 요소를 손쉽게 교체하고 결합할 수 있도록 만들어줌</li>
</ul>
</li>
<li><p>개발자의 역할</p>
<ul>
<li>이상적으로는 모든 설계를 인터페이스로 만들어야 코드가 유연하게 변경이 가능해짐<ul>
<li>정해진 비지니스 로직이나 사용할 기술이 없는 상황에서도 개발할 수 있는 장점을 가짐</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="spring-container">Spring Container</h3>
<h4 id="spring-container-1">Spring Container</h4>
<p>Spring으로 구성된 애플리케이션에서 객체(Bean)를 생성, 관리, 소멸하는 역할을 담당. 애플리케이션 시작 시, 설정 파일이나 Annotation을 읽어 Bean을 생성하고 주입하는 모든 과정을 담당
<img src="https://velog.velcdn.com/images/star_pooh/post/afb12384-9fe1-4119-915c-25988e802ef5/image.png" width=400 align=left>
<img src="https://velog.velcdn.com/images/star_pooh/post/943106ad-4841-4c4b-83f6-19d9423cc2e3/image.png" width=400></p>
<blockquote>
</blockquote>
<ul>
<li>객체를 직접 생성하는 경우, 객체 간의 의존성 및 결합도가 높아짐<ul>
<li>OCP, DIP 위반</li>
</ul>
</li>
<li>Spring Container를 사용하면 인터페이스에만 의존하는 설계가 가능해짐<ul>
<li>OCP, DIP 준수<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/b0319a27-b51f-4992-80ee-2d24f8dc45f7/image.png" alt=""></li>
</ul>
</li>
<li>Spring Container의 종류<ul>
<li>BeanFactory<ul>
<li>Spring Container의 최상위 인터페이스</li>
<li>Spring Bean을 관리하고 조회함</li>
</ul>
</li>
<li>ApplicationContext<ul>
<li>BeanFactory의 확장된 형태(implements)</li>
<li>Application 개발에 필요한 다양한 기능을 추가적으로 제공<ul>
<li>국제화, 환경변수 분리, 이벤트, 리소스 조회<blockquote>
<blockquote>
<p>💡 일반적으로 ApplicationContext를 사용하기 때문에 ApplicationContext를 Spring Container라 표현함</p>
</blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="spring-bean">Spring Bean</h4>
<p>스프링 컨테이너가 관리하는 객체를 의미. 자바 객체 자체는 특별하지 않지만, 스프링이 객체를 관리하는 순간부터 Bean이 됨. Spring은 Bean을 생성, 초기화, 의존성 주입 등을 통해 관리.</p>
<blockquote>
</blockquote>
<ul>
<li>Spring Bean의 특징<ul>
<li>스프링 컨테이너에 의해 생성되고 관리됨</li>
<li>기본적으로 <strong>Singleton</strong>으로 설정</li>
<li>의존성 주입(DI)을 통해 다른 객체들과 의존 관계를 맺을 수 있음</li>
<li>생성, 초기화, 사용, 소멸의 생명주기를 가짐</li>
</ul>
</li>
</ul>
<h4 id="ioc제어의-역전-inversion-of-control">IOC(제어의 역전, Inversion Of Control)</h4>
<p>객체의 생성과 관리 권한을 개발자가 아닌 스프링 컨테이너가 담당하는 것. 스프링 컨테이너가 객체의 생성, 주입, 소멸을 관리하며, 객체 간의 결합도는 낮추기 때문에 유연한 코드가 됨.</p>
<img src="https://velog.velcdn.com/images/star_pooh/post/d2453804-7b0b-4af0-86f4-6318bf141f66/image.png" width=400>

<h4 id="di의존성-주입-dependency-injection">DI(의존성 주입, Dependency Injection)</h4>
<p>스프링이 객체 간의 의존성을 자동으로 주입해주는 것. 객체가 다른 객체를 사용할 때, 해당 객체를 직접 생성하지 않고 Spring이 주입해주는 방식이며, IOC를 구현하는 방식 중 하나.</p>
<img src="https://velog.velcdn.com/images/star_pooh/post/b6def0d7-4d78-4ad3-b47c-8dd3547e72b6/image.png" width=400>

<h4 id="싱글톤-패턴singleton-patter">싱글톤 패턴(Singleton Patter)</h4>
<p>클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 디자인 패턴.</p>
<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/afac1dc6-7ad7-4049-9da2-55bbd1992a96/image.png" width=400>
>
- 요청을 할 때 마다 객체가 새로 생성되며, 처리가 완료되면 소멸
- 메모리 낭비가 심함
>
<img src="https://velog.velcdn.com/images/star_pooh/post/233a8eb4-a4ed-471d-9b29-35993be973c8/image.png" width=400>
>
- 객체가 한번만 생성되어 리소스 절약 가능

<blockquote>
</blockquote>
<ul>
<li>싱글톤 패턴 생성 예제<pre><code class="language-java">public interface Singleton {
  void showMessage();
}
&gt;
public class SingletonImpl implements Singleton {
  private static SingletonImpl instance;
&gt;
  // private 생성자를 통해 외부에서 객체 생성을 방지
  private SingletonImpl() {}
&gt;
  // public으로 설정하여 인스턴스가 필요하면
  // getInstance 메서드를 통해 인스턴스에 접근하도록 만듦
  public static SingletonImpl getInstance() {
      // 인스턴스가 없을 때만 생성
      if (instance == null) {
          instance = new SingletonImpl();
      }
      return instance;
  }
&gt;
  @Override
  public void showMessage() {
      // 인스턴스 주소값 출력
      System.out.println(instance.toString());
  }
}</code></pre>
💥 싱글톤 패턴의 문제점</li>
<li>싱글톤 패턴을 구현하기 위한 코드의 양이 많음</li>
<li>구현 클래스에 의존(DIP, OCP 위반)<ul>
<li>유연성이 떨어지기 때문에 안티 패턴이라고도 불림</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li>스프링의 싱글톤 컨테이너<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/star_pooh/post/4b87e0dd-d8bc-4ef1-a757-d1c573bc42bc/image.png" width=400>
></li>
<li>스프링 컨테이너는 싱글톤 패턴의 문제점들을 해결하면서 객체를 싱글톤으로 관리</li>
</ul>
<h4 id="싱글톤-패턴의-주의점">싱글톤 패턴의 주의점</h4>
<p>객체의 인스턴스를 하나만 생성하여 공유하기 때문에 싱글톤 패턴의 객체는 상태를 유지(stateful)하면 안됨.</p>
<blockquote>
</blockquote>
<pre><code class="language-java">public class StatefulSingleton {
    private static StatefulSingleton instance;
    private int value;
&gt;
    private StatefulSingleton() {}
&gt;
    public static StatefulSingleton getInstance() {
        if (instance == null) {
            instance = new StatefulSingleton();
        }
        return instance;
    }
&gt;
    public void setValue(int value) {
        this.value = value;
    }
&gt;
    public int getValue() {
        return this.value;
    }
}
&gt;
public class MainApp {
    public static void main(String[] args) {
        // 클라이언트 1: 싱글톤 인스턴스를 가져와서 상태를 설정
        StatefulSingleton client1 = StatefulSingleton.getInstance();
        client1.setValue(42);
        System.out.println(&quot;클라이언트 1이 설정한 값: &quot; + client1.getValue());
&gt;
        // 클라이언트 2: 동일한 싱글톤 인스턴스를 사용해 상태를 변경
        StatefulSingleton client2 = StatefulSingleton.getInstance();
        client2.setValue(100);
        System.out.println(&quot;클라이언트 2가 설정한 값: &quot; + client2.getValue());
&gt;
        // 클라이언트 1이 다시 값을 확인
        System.out.println(&quot;클라이언트 1이 다시 확인한 값: &quot; + client1.getValue());
    }
}</code></pre>
<pre><code>클라이언트 1이 설정한 값: 42
클라이언트 2가 설정한 값: 100
클라이언트 1이 다시 확인한 값: 100</code></pre><ul>
<li>상태 유지(stateful)의 문제점<ul>
<li>데이터의 불일치나 동시성 문제가 발생할 수 있음</li>
<li><strong>Spring Bean은 항상 무상태(stateless)로 설계해야 함</strong><ul>
<li>특정 클라이언트에 의존적인 필드가 있거나 변경할 수 있으면 안됨</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 스프링 컨테이너가 해결한 싱글톤 패턴의 문제점
<strong>1. 코드의 복잡성 및 테스트 어려움</strong></p>
</blockquote>
<ul>
<li>문제점: 싱글톤 패턴에서는 싱글톤을 구현하기 위해 <code>private</code> 생성자와 <code>static</code> 메서드 등을 사용해야 한다. 이러한 코드 작성은 복잡하고 테스트를 어렵게 만든다. 특히, 전역 상태를 가지는 싱글톤 객체는 테스트 중 다른 테스트에 영향을 줄 수 있다.<blockquote>
</blockquote>
</li>
<li>스프링의 해결: 스프링 컨테이너가 객체의 생명 주기를 관리하므로, 개발자가 직접 싱글톤 구현을 작성할 필요가 없다. 또한 스프링은 객체를 DI로 주입하기 때문에 테스트 환경에서 다른 객체로 쉽게 대체 가능하여 테스트 용이성을 높인다.<blockquote>
</blockquote>
</li>
<li><em>2. 멀티쓰레드 환경에서의 동기화 문제*</em></li>
<li>문제점: 싱글톤 패턴에서는 멀티쓰레드 환경에서 안전하게 동작하도록 동기화 코드를 작성해야 한다. 그러나 동기화 코드는 성능 저하를 유발할 수 있다.<blockquote>
</blockquote>
</li>
<li>스프링의 해결: 스프링 컨테이너는 내부적으로 안전하게 싱글톤 객체를 관리하므로, 개발자가 동기화 문제를 신경 쓰지 않아도 된다.<blockquote>
</blockquote>
</li>
<li><em>3. 객체 생명 주기 관리의 어려움*</em></li>
<li>문제점: 싱글톤 패턴으로 생성한 객체는 어플리케이션이 종료될 때까지 유지되므로, 사용하지 않더라도 메모리를 점유할 수 있다. 또한 명시적으로 해제하는 코드가 필요할 수도 있다.<blockquote>
</blockquote>
</li>
<li>스프링의 해결: 스프링 컨테이너는 싱글톤 객체를 생성, 초기화, 사용, 소멸 단계까지 관리한다. <code>@PostConstruct</code>와 <code>@PreDestroy</code> 같은 어노테이션을 통해 초기화 및 소멸 작업을 간편하게 처리할 수 있다.<blockquote>
</blockquote>
</li>
<li><em>4. 전역 상태 문제*</em></li>
<li>문제점: 싱글톤 객체는 전역 상태를 가질 수 있기 때문에, 한 곳에서 상태를 변경하면 다른 곳에서 의도치 않은 부작용이 발생할 수 있다.<blockquote>
</blockquote>
</li>
<li>스프링의 해결: 스프링은 필요한 경우 프로토타입 스코프(<code>@Scope(&quot;prototype&quot;)</code>)나 요청 스코프(<code>@Scope(&quot;request&quot;)</code>) 등 다양한 스코프를 지원하여 전역 상태 문제를 완화한다. 또한, 상태를 가지는 빈 대신 상태 없는 빈(stateless bean)을 권장한다.<blockquote>
</blockquote>
</li>
<li><em>5. DI 지원 부족*</em></li>
<li>문제점: 싱글톤에서는 객체 간의 의존성을 관리하기 어렵다. 의존성이 많아질수록 코드가 복잡해지고 결합도가 높아진다.<blockquote>
</blockquote>
</li>
<li>스프링의 해결: 스프링은 DI를 통해 객체 간 의존성을 관리하므로, 결합도를 낮추고 코드의 유연성과 재사용성을 높인다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20241213 회고]]></title>
            <link>https://velog.io/@star_pooh/20241213-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@star_pooh/20241213-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 13 Dec 2024 12:17:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✅ 알고리즘 &amp; SQL 코드카타
💡 오늘은 특별히 어려운 것이 없었다. 알고리즘은 문제 풀기 전에 생각을 정리하니까 잘 풀리는 것 같고, SQL도 영어 지문이 아직까지는 그렇게 복잡하지 않아서 할 만 하다.</p>
</blockquote>
<blockquote>
<p>✅ 스프링 강의 입문 6주차 강의 정리
💡 생각보다 양이 많아서 정리하는데 시간이 오래 걸렸다. 조금만 더 하면 끝날거 같은데 하다보니 시간을 많이 썼다. 하지만 쌓인게 많아서 천천히 할 수 없기는하다…</p>
</blockquote>
<blockquote>
<p>✅ 스프링 과제
💡 강의 정리에 시간을 많이 써서 이제 Lv2 구현 중에 있다. SQL을 안 쓰고 JPA로 하려니 헷갈리는 부분이 좀 있어서 잘 잡아야 할 것 같다. Lv7에서 테이블이 늘어나기 때문에 지금 안 잡아놓으면 그 때 가서도 헷갈려 할 것 같다.</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>