<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>code.log</title>
        <link>https://velog.io/</link>
        <description>일상</description>
        <lastBuildDate>Wed, 26 Aug 2026 06:20:06 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>code.log</title>
            <url>https://velog.velcdn.com/images/seungho7-1/profile/722028e8-34ca-4449-ba99-16fa45e827de/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. code.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/seungho7-1" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[트러블슈팅]]></title>
            <link>https://velog.io/@seungho7-1/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@seungho7-1/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</guid>
            <pubDate>Wed, 26 Aug 2026 06:20:06 GMT</pubDate>
            <description><![CDATA[<h2 id="관리자-기능-확장에-따른-controller·service-구조-개선">관리자 기능 확장에 따른 Controller·Service 구조 개선</h2>
<h3 id="문제-상황">문제 상황</h3>
<p>초기 관리자 기능은 하나의 <code>AdminController</code>와 <code>AdminService</code>에서 처리하도록 구현하였다.</p>
<pre><code class="language-text">AdminController
      ↓
AdminService
 ├── 대시보드
 ├── 신고 관리
 ├── 회원 관리
 ├── 판매자 신청 관리
 └── 상품 관리</code></pre>
<p>관리자 기능이 추가되면서 하나의 Controller와 Service에 여러 도메인의 API와 비즈니스 로직이 집중되었다.</p>
<p>그 결과 특정 기능을 수정하기 위해 관련 없는 코드까지 함께 확인해야 했으며, 새로운 관리자 기능을 추가할 때 기존 클래스의 코드가 계속 증가하는 문제가 발생했다. 또한 Controller에서 여러 Service를 직접 의존하게 되면서 클래스 간 결합도 역시 높아졌다.</p>
<h3 id="원인">원인</h3>
<p>기능의 구분 없이 관리자 기능이라는 큰 범위만을 기준으로 Controller와 Service를 구성한 것이 원인이었다.</p>
<p>특히 각 기능이 서로 다른 책임을 가지고 있음에도 하나의 Service에서 회원, 신고, 상품 등의 비즈니스 로직을 모두 처리하고 있었다.</p>
<h3 id="해결">해결</h3>
<p>관리자 기능을 도메인별로 분리하여 각 기능이 독립적인 책임을 갖도록 구조를 변경하였다.</p>
<pre><code class="language-text">AdminMemberController  → AdminMemberService
AdminReportController  → AdminReportService
AdminSellerController  → AdminSellerService
AdminProductController  → AdminProductService</code></pre>
<p>대시보드처럼 여러 도메인의 데이터를 하나의 응답으로 조합해야 하는 기능은 각 Service를 Controller에서 직접 호출하지 않고 <code>AdminDashboardService</code>가 조합하도록 구성하였다.</p>
<pre><code class="language-text">AdminDashboardController
          ↓
AdminDashboardService
     ┌────┼────┬────┐
     ↓    ↓    ↓    ↓
  Member Report Seller Product
  Service Service Service Service</code></pre>
<p>이를 통해 Controller는 HTTP 요청과 응답 처리에 집중하고, 각 도메인의 비즈니스 로직은 해당 Service에서 담당하도록 책임을 분리하였다.</p>
<h3 id="결과">결과</h3>
<ul>
<li>관리자 기능별 책임이 명확해져 코드 탐색과 유지보수가 쉬워졌다.</li>
<li>특정 기능을 수정할 때 관련 없는 도메인의 코드를 확인해야 하는 범위를 줄였다.</li>
<li>새로운 관리자 기능을 추가할 때 기존 Controller와 Service의 코드 증가를 최소화할 수 있었다.</li>
<li>Controller가 여러 도메인의 Service를 직접 의존하지 않도록 하여 의존 관계를 단순화하였다.</li>
<li>여러 도메인의 기능을 조합해야 하는 대시보드는 상위 Service에서 조합하도록 하여 각 도메인 Service의 책임을 유지할 수 있었다.</li>
</ul>
<h3 id="개선-후-구조">개선 후 구조</h3>
<pre><code class="language-text">admin
├── dashboard
│   ├── AdminDashboardController
│   └── AdminDashboardService
│
├── member
│   ├── AdminMemberController
│   └── AdminMemberService
│
├── report
│   ├── AdminReportController
│   └── AdminReportService
│
├── seller
│   ├── AdminSellerController
│   └── AdminSellerService
│
└── product
    ├── AdminProductController
    └── AdminProductService</code></pre>
<p>이번 개선을 통해 단순히 클래스를 분리하는 것에 그치지 않고, <strong>각 기능의 책임과 의존 관계를 기준으로 구조를 재설계하여 기능 확장에 대응하기 쉬운 구조로 개선하였다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 : 원형큐]]></title>
            <link>https://velog.io/@seungho7-1/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9B%90%ED%98%95%ED%81%90</link>
            <guid>https://velog.io/@seungho7-1/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9B%90%ED%98%95%ED%81%90</guid>
            <pubDate>Wed, 05 Aug 2026 04:30:00 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-java">import java.util.Arrays;
import java.util.Scanner;

public class js {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println(&quot;저장할 큐 사이즈를 입력해주세요: &quot;);
        int n = sc.nextInt();
        CircularQueue circularQueue = new CircularQueue(n);
        for (int i = 0; i &lt; n-1; i++) {
            System.out.println(&quot;원형 큐에 넣을 사이즈를 입력해보세요&quot;);
            circularQueue.enqueue(sc.nextInt());
        }
        System.out.println(&quot;현재 원형 큐: &quot;+circularQueue.toString());
    }
}
class CircularQueue {

    private int[] queue;
    private int front;
    private int rear;


    public CircularQueue(int size) {
        queue = new int[size];
        front = 0;
        rear = 0;
    }


    public boolean isFull() {
        return (rear + 1) % queue.length == front;
    }


    public boolean isEmpty() {
        return front == rear;
    }


    public void enqueue(int data) {

        if(isFull()) {
            throw new RuntimeException(&quot;Queue Full&quot;);
        }

        queue[rear] = data;
        rear = (rear + 1) % queue.length;
    }


    public int dequeue() {

        if(isEmpty()) {
            throw new RuntimeException(&quot;Queue Empty&quot;);
        }

        int data = queue[front];

        front = (front + 1) % queue.length;

        return data;
    }


    public int peek() {

        if(isEmpty()) {
            throw new RuntimeException(&quot;Queue Empty&quot;);
        }

        return queue[front];
    }
    @Override
    public String toString() {

        StringBuilder sb = new StringBuilder();

        int index = front;

        while(index != rear) {
            sb.append(queue[index]).append(&quot; &quot;);
            index = (index + 1) % queue.length;
        }

        return sb.toString();
    }
}

</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[게시물 목록 조회 N+1 문제(getPosts)]]></title>
            <link>https://velog.io/@seungho7-1/%EA%B2%8C%EC%8B%9C%EB%AC%BC-%EB%AA%A9%EB%A1%9D-%EC%A1%B0%ED%9A%8C-N1-%EB%AC%B8%EC%A0%9CgetPosts</link>
            <guid>https://velog.io/@seungho7-1/%EA%B2%8C%EC%8B%9C%EB%AC%BC-%EB%AA%A9%EB%A1%9D-%EC%A1%B0%ED%9A%8C-N1-%EB%AC%B8%EC%A0%9CgetPosts</guid>
            <pubDate>Fri, 24 Jul 2026 19:15:57 GMT</pubDate>
            <description><![CDATA[<h3 id="게시글-조회시-n--1-문제-발생">게시글 조회시 N + 1 문제 발생</h3>
<pre><code>Hibernate: select distinct p1_0.id,p1_0.board_type,p1_0.content,p1_0.created_at,p1_0.image_url,p1_0.is_hidden,p1_0.like_count,p1_0.member_id,m1_0.id,m1_0.created_at,m1_0.email,m1_0.last_login_at,m1_0.nickname,m1_0.oauth_access_token,m1_0.oauth_provider,m1_0.oauth_refresh_token,m1_0.onboarded,m1_0.password,m1_0.profile_image_url,m1_0.role,m1_0.shop_name,m1_0.shop_url,m1_0.sns_urls,m1_0.status,m1_0.username,p1_0.title,p1_0.view_count from post p1_0 left join comment c1_0 on c1_0.post_id=p1_0.id join member m1_0 on m1_0.id=p1_0.member_id where p1_0.board_type in (?,?,?,?) and p1_0.is_hidden=0 and (? is null or ?=&#39;&#39; or lower(p1_0.title) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\\&#39;,&#39;\\\\&#39;) or lower(p1_0.content) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\\&#39;,&#39;\\\\&#39;) or lower(c1_0.content) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\\&#39;,&#39;\\\\&#39;)) order by p1_0.created_at desc limit ?
Hibernate: select count(distinct p1_0.id) from post p1_0 left join comment c1_0 on c1_0.post_id=p1_0.id where p1_0.board_type in (?,?,?,?) and p1_0.is_hidden=0 and (? is null or ?=&#39;&#39; or lower(p1_0.title) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\\&#39;,&#39;\\\\&#39;) or lower(p1_0.content) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\\&#39;,&#39;\\\\&#39;) or lower(c1_0.content) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\\&#39;,&#39;\\\\&#39;))
Hibernate: select m1_0.id,m1_0.created_at,m1_0.email,m1_0.last_login_at,m1_0.nickname,m1_0.oauth_access_token,m1_0.oauth_provider,m1_0.oauth_refresh_token,m1_0.onboarded,m1_0.password,m1_0.profile_image_url,m1_0.role,m1_0.shop_name,m1_0.shop_url,m1_0.sns_urls,m1_0.status,m1_0.username from member m1_0 where m1_0.username=?
Hibernate: select pl1_0.id,pl1_0.member_id,pl1_0.post_id from post_like pl1_0 where pl1_0.member_id=? and pl1_0.post_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)
Hibernate: select count(c1_0.id) from comment c1_0 where c1_0.post_id=? and not(c1_0.is_hidden)</code></pre><ul>
<li><p>24번의 쿼리가 실행.</p>
</li>
<li><p>1.게시글 목록 조회 (1번): select distinct p1_0.id ... from post p1_0 ... limit ?</p>
</li>
<li><p>2.전체 게시글 수 카운트 (1번): select count(distinct p1_0.id) from post p1_0 ... (페이징 처리를 위해 전체 개수를 세는 쿼리)</p>
</li>
<li><p>3.로그인 회원 정보 조회 (1번): select ... from member m1_0 where m1_0.username=?</p>
</li>
<li><p>4.좋아요 누른 목록 조회 (1번): select ... from post_like pl1_0 where pl1_0.member_id=? and pl1_0.post_id in (...)</p>
</li>
<li><p>5.개별 댓글 개수 조회 (20번): select count(c1_0.id) from comment c1_0 where c1_0.post_id=? ... (이 부분이 게시글 개수만큼 반복되어 총 20번 실행됨)</p>
</li>
<li><p>--&gt; N+1문제 발생. 여기도 1번발 발생해서 총 5번만 발생되게 해야한다.
총계: 1 + 1 + 1 + 1 + 20 = 총 24번</p>
</li>
</ul>
<p>&#39;&#39;&#39;
commentRepository.countByPostIdAndIsHiddenFalse(p.getId())에서 문제 발생.</p>
<p>int countByPostIdAndIsHiddenFalse(Long postId); //게시글 하나당 댓글 개수를 조회하는 메서드 ---&gt; 게시글이 20개면 20번 호출하니까 N+1이 발생하는 것
&#39;&#39;&#39;</p>
<ul>
<li>posts.map()이 게시글 개수만큼 반복 실행되면서 게시글마다 댓글 개수를 조회하는 SQL이 추가로 발생하였다.<h3 id="해결방법">해결방법</h3>
</li>
<li>GROUP BY로 한 번에 조회</li>
<li>댓글 개수를 게시글마다 조회하지 않고, GROUP BY와 IN을 이용하여 모든 게시글의 댓글 개수를 한 번의 집계 쿼리로 조회하도록 변경.</li>
</ul>
<p>&#39;&#39;&#39;
SELECT
    post_id,
    COUNT(*)
FROM comment
WHERE post_id IN (...)
AND is_hidden = false
GROUP BY post_id;
&#39;&#39;&#39;</p>
<ul>
<li>조회한 결과를 Map&lt;PostId, CommentCount&gt; 형태로 저장한 뒤, PostResponse 생성 시 Map에서 댓글 개수를 조회하도록 수정</li>
</ul>
<p>&#39;&#39;&#39;
List<Long> postIds = posts.stream()
        .map(Post::getId)
        .toList();</p>
<p>Map&lt;Long, Integer&gt; commentCountMap =
        commentRepository.countCommentsByPostIds(postIds)
                .stream()
                .collect(Collectors.toMap(
                        row -&gt; (Long) row[0],
                        row -&gt; ((Long) row[1]).intValue()
                ));</p>
<p>final List<Long> finalLikedPostIds = likedPostIds;</p>
<p>return posts.map(p -&gt; new PostResponse(
        p.getId(),
        p.getTitle(),
        p.getContent(),
        p.getBoardType().name(),
        p.getImageUrl(),
        p.getMember().getNickname(),
        p.getViewCount() + postRedisService.getCachedViewCount(p.getId()),
        p.getLikeCount(),
        p.getCreatedAt(),
        p.getMember().getProfileImageUrl(),
        finalLikedPostIds.contains(p.getId()),
        commentCountMap.getOrDefault(p.getId(), 0)
));
&#39;&#39;&#39;
Hibernate: select distinct p1_0.id,p1_0.board_type,p1_0.content,p1_0.created_at,p1_0.image_url,p1_0.is_hidden,p1_0.like_count,p1_0.member_id,m1_0.id,m1_0.created_at,m1_0.email,m1_0.last_login_at,m1_0.nickname,m1_0.oauth_access_token,m1_0.oauth_provider,m1_0.oauth_refresh_token,m1_0.onboarded,m1_0.password,m1_0.profile_image_url,m1_0.role,m1_0.shop_name,m1_0.shop_url,m1_0.sns_urls,m1_0.status,m1_0.username,p1_0.title,p1_0.view_count from post p1_0 left join comment c1_0 on c1_0.post_id=p1_0.id join member m1_0 on m1_0.id=p1_0.member_id where p1_0.board_type in (?,?,?,?) and p1_0.is_hidden=0 and (? is null or ?=&#39;&#39; or lower(p1_0.title) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\&#39;,&#39;\\&#39;) or lower(p1_0.content) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\&#39;,&#39;\\&#39;) or lower(c1_0.content) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\&#39;,&#39;\\&#39;)) order by p1_0.created_at desc limit ?
Hibernate: select count(distinct p1_0.id) from post p1_0 left join comment c1_0 on c1_0.post_id=p1_0.id where p1_0.board_type in (?,?,?,?) and p1_0.is_hidden=0 and (? is null or ?=&#39;&#39; or lower(p1_0.title) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\&#39;,&#39;\\&#39;) or lower(p1_0.content) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\&#39;,&#39;\\&#39;) or lower(c1_0.content) like replace(lower(concat(&#39;%&#39;,?,&#39;%&#39;)),&#39;\&#39;,&#39;\\&#39;))
Hibernate: select m1_0.id,m1_0.created_at,m1_0.email,m1_0.last_login_at,m1_0.nickname,m1_0.oauth_access_token,m1_0.oauth_provider,m1_0.oauth_refresh_token,m1_0.onboarded,m1_0.password,m1_0.profile_image_url,m1_0.role,m1_0.shop_name,m1_0.shop_url,m1_0.sns_urls,m1_0.status,m1_0.username from member m1_0 where m1_0.username=?
Hibernate: select pl1_0.id,pl1_0.member_id,pl1_0.post_id from post_like pl1_0 where pl1_0.member_id=? and pl1_0.post_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Hibernate: select c1_0.post_id,count(c1_0.id) from comment c1_0 where c1_0.post_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) and c1_0.is_hidden=0 group by c1_0.post_id
&#39;&#39;&#39; --&gt; sql문이 5번만 실행.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[reids]]></title>
            <link>https://velog.io/@seungho7-1/reids</link>
            <guid>https://velog.io/@seungho7-1/reids</guid>
            <pubDate>Sun, 19 Jul 2026 17:15:54 GMT</pubDate>
            <description><![CDATA[<p>[Spring Boot] 게시글 조회수 및 좋아요 동시성 문제와 성능 개선 (Redis 도입기)
🚨 문제 인식 (Troubleshooting)
현재 개발 중인 커뮤니티 플랫폼에서 게시글 상세 조회와 좋아요 기능을 구현했습니다. 기능적으로는 정상 작동하는 MVP(Minimum Viable Product) 상태였지만, <strong>&quot;대용량 트래픽이 발생한다면 이 코드가 버틸 수 있을까?&quot;</strong>라는 의문이 들었습니다.</p>
<p>기존 코드를 분석해 본 결과, 실무 환경에서는 치명적일 수 있는 두 가지 문제점을 발견했습니다.</p>
<ol>
<li>조회수(Views) 무한 새로고침 어뷰징 및 DB 부하
기존의 조회수 증가 로직은 다음과 같이 아주 단순했습니다.</li>
</ol>
<p>java</p>
<p>// As-Is: 기존 BoardService 로직
@Transactional
public PostResponse incrementViewCount(Long postId) {
    Post post = postRepository.findById(postId)
            .orElseThrow(() -&gt; new IllegalArgumentException(&quot;게시글을 찾을 수 없습니다.&quot;));</p>
<pre><code>post.incrementViewCount(); // DB의 조회수 + 1

return new PostResponse(...);</code></pre><p>}
문제점:</p>
<p>한 명의 유저가 F5(새로고침)를 100번 누르면 조회수가 그대로 100이 올라가는 어뷰징에 무방비했습니다.
유저가 게시글을 클릭할 때마다 무조건 DB에 UPDATE 쿼리가 날아갑니다. 트래픽이 몰릴 경우 DB 커넥션이 고갈되고 엄청난 병목 현상이 발생할 수 있습니다.
2. 좋아요(Likes) 동시성 문제 및 DB Lock
좋아요 기능 역시 PostLike라는 매핑 테이블을 두어 정석대로 구현했습니다.</p>
<p>java</p>
<p>// As-Is: 기존 BoardService 로직
@Transactional
public boolean toggleLike(Long postId, String username) {
    Post post = postRepository.findById(postId)...
    Member member = memberRepository.findByUsername(username)...
    Optional<PostLike> existingLike = postLikeRepository.findByMemberAndPost(member, post);</p>
<pre><code>if (existingLike.isPresent()) {
    postLikeRepository.delete(existingLike.get());
    post.decrementLikeCount(); // 취소 시 감소
} else {
    postLikeRepository.save(new PostLike(member, post));
    post.incrementLikeCount(); // 클릭 시 증가
}</code></pre><p>}
문제점: 로직 자체는 흠잡을 데가 없지만, 동시성(Concurrency) 측면에서 위험합니다. 유명인의 게시물에 수백 명이 동시에 좋아요를 누르게 되면, 1번 게시글(Row)에 수많은 쓰레드가 동시에 UPDATE 쿼리를 날리게 됩니다. 이는 심각한 DB Lock 경합을 유발하여 서버 장애로 이어질 수 있습니다.</p>
<p>🛠 해결 방안: Redis 인메모리 캐시 도입 (To-Be)
관계형 데이터베이스(RDBMS)에 직접 쿼리를 때리는 기존 방식을 버리고, 응답 속도가 매우 빠른 <strong>Redis(인메모리 데이터 저장소)</strong>를 활용한 아키텍처로 전면 개편하기로 결정했습니다.</p>
<ol>
<li>조회수 개편 (중복 방지 및 스케줄러 동기화)
중복 방지: 사용자의 식별자(username 또는 IP)를 조합하여 view:post:{postId}:user:{identifier}라는 키를 생성합니다. 이 키에 TTL(만료시간)을 24시간으로 걸어두어, 하루 동안은 같은 글을 여러 번 봐도 조회수가 오르지 않도록 방어했습니다.
Write-Back 패턴 (Batch): 조회수를 올릴 때 DB에 UPDATE를 치지 않고, Redis의 post:views:{postId} 카운터만 INCR 시킵니다. 그리고 Spring Scheduler를 이용해 5분마다 한 번씩 Redis에 쌓인 조회수를 긁어모아 DB에 벌크 업데이트(Bulk Update)를 치도록 설계했습니다. DB 부하가 획기적으로 줄어듭니다.</li>
<li>좋아요 개편 (Set 자료구조 활용)
Redis Set 활용: 좋아요를 누른 유저 목록을 Redis의 Set 자료구조(post:likes:{postId})로 관리합니다. SISMEMBER 명령어로 중복 여부를 O(1) 속도로 파악하고, SADD/SREM으로 토글 처리를 합니다.
실시간 렌더링 + 비동기 처리: 사용자가 버튼을 누르면 Redis에서 즉시 카운트(SCARD)를 올려 프론트엔드에 빠르게 응답(200 OK)을 내려줍니다. 그리고 무거운 DB Insert/Delete 작업은 @Async를 활용하여 백그라운드 스레드에서 비동기로 처리하여 사용자 경험(UX)을 극대화했습니다.
💡 회고 및 결론
MVP 단계에서는 &quot;일단 돌아가는 코드&quot;를 짰다면, 이번 리팩토링을 통해 &quot;실무에서 트래픽을 견딜 수 있는 튼튼한 아키텍처&quot;에 대해 깊게 고민해 볼 수 있었습니다.</li>
</ol>
<p>단순히 비즈니스 로직을 구현하는 것을 넘어, DB 트랜잭션을 최소화하고 캐시 서버를 적재적소에 활용하는 것이 백엔드 개발자의 진짜 핵심 역량이라는 것을 깨닫는 귀중한 트러블슈팅 경험이었습니다. 앞으로 남은 동기화 스케줄러 로직 구현과 프론트 연동 테스트도 꼼꼼히 진행할 예정입니다! 🚀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security + JWT 로그인]]></title>
            <link>https://velog.io/@seungho7-1/Spring-Security-JWT-%EB%A1%9C%EA%B7%B8%EC%9D%B8</link>
            <guid>https://velog.io/@seungho7-1/Spring-Security-JWT-%EB%A1%9C%EA%B7%B8%EC%9D%B8</guid>
            <pubDate>Tue, 14 Jul 2026 03:54:57 GMT</pubDate>
            <description><![CDATA[<h2 id="jwt-인증이란">JWT 인증이란?</h2>
<p>JWT(JSON Web Token)는 <strong>로그인 후 서버가 사용자에게 발급하는 인증 토큰</strong>이다.</p>
<p>기존 Session 방식은 서버가 로그인 정보를 저장하지만,</p>
<p>JWT 방식은 <strong>사용자 정보와 권한을 토큰에 담아 클라이언트가 보관</strong>한다.</p>
<p>이후 요청마다 JWT를 보내면 서버는 토큰만 검증하여 로그인 여부를 판단한다.</p>
<hr>
<h1 id="전체-흐름">전체 흐름</h1>
<pre><code class="language-text">로그인 요청
      ↓
Controller
      ↓
Service
      ↓
DB에서 회원 조회
      ↓
비밀번호 검증
      ↓
JWT 생성
      ↓
클라이언트에게 JWT 반환
      ↓
클라이언트가 JWT 저장
      ↓
이후 모든 요청에 JWT 포함
      ↓
JwtFilter가 JWT 검증
      ↓
SecurityContext에 인증 정보 저장
      ↓
Controller / Service 실행</code></pre>
<hr>
<h1 id="1-로그인-요청">1. 로그인 요청</h1>
<p>사용자가 로그인 버튼을 누르면</p>
<pre><code class="language-http">POST /api/auth/login</code></pre>
<p>으로 요청이 들어온다.</p>
<p>Controller</p>
<pre><code class="language-java">@PostMapping(&quot;/login&quot;)
public ResponseEntity&lt;LoginResponse&gt; login(
        @Valid @RequestBody LoginRequest request) {

    LoginResponse loginResult = memberService.login(request);
    return ResponseEntity.ok(loginResult);
}</code></pre>
<p>Controller의 역할은 매우 단순하다.</p>
<ul>
<li>로그인 요청 받기</li>
<li>Service 호출</li>
<li>결과 반환</li>
</ul>
<p>실제 로그인 로직은 모두 Service에서 처리한다.</p>
<hr>
<h1 id="2-회원-조회">2. 회원 조회</h1>
<p>Service에서는 먼저 username이 존재하는지 확인한다.</p>
<pre><code class="language-java">Member member = memberRepository.findByUsername(loginRequest.getUsername())
        .orElseThrow(() -&gt; new IllegalArgumentException(&quot;존재하지 않는 유저&quot;));</code></pre>
<p>실행 순서</p>
<pre><code class="language-text">입력한 username
        ↓
MemberRepository
        ↓
DB 조회
        ↓
Member 반환</code></pre>
<p>회원이 없다면 로그인은 실패한다.</p>
<hr>
<h1 id="3-비밀번호-검증">3. 비밀번호 검증</h1>
<p>DB에는 평문 비밀번호가 저장되지 않는다.</p>
<p>예를 들어 사용자가 입력한 비밀번호가</p>
<pre><code class="language-text">1234</code></pre>
<p>라고 해도,</p>
<p>DB에는 BCrypt로 암호화된 값만 저장된다.</p>
<pre><code class="language-text">$2a$10$......</code></pre>
<p>비밀번호 비교는</p>
<pre><code class="language-java">passwordEncoder.matches(
        loginRequest.getPassword(),
        member.getPassword()
);</code></pre>
<p>가 수행한다.</p>
<p>Spring Security가</p>
<pre><code class="language-text">입력한 비밀번호
        ↓
BCrypt 암호화
        ↓
DB의 암호화된 비밀번호와 비교</code></pre>
<p>를 자동으로 수행한다.</p>
<p>일치하지 않으면</p>
<pre><code class="language-java">throw new IllegalArgumentException(&quot;비밀번호 불일치&quot;);</code></pre>
<p>가 발생한다.</p>
<hr>
<h1 id="4-jwt-생성">4. JWT 생성</h1>
<p>로그인이 성공하면 JWT를 생성한다.</p>
<pre><code class="language-java">String token = jwtUtil.generateToken(
        member.getUsername(),
        member.getRole().name()
);</code></pre>
<p>예를 들어</p>
<pre><code class="language-text">username = sleepy
role = SELLER</code></pre>
<p>이라면</p>
<pre><code class="language-java">generateToken(&quot;sleepy&quot;, &quot;SELLER&quot;);</code></pre>
<p>가 호출된다.</p>
<hr>
<h1 id="5-jwtutil에서-토큰-생성">5. JwtUtil에서 토큰 생성</h1>
<p>토큰 생성 메서드</p>
<pre><code class="language-java">public String generateToken(String username, String role)</code></pre>
<p>에서 JWT를 만든다.</p>
<h3 id="현재-시간-저장">현재 시간 저장</h3>
<pre><code class="language-java">Date now = new Date();</code></pre>
<p>토큰 발급 시간을 저장한다.</p>
<h3 id="subject-저장">Subject 저장</h3>
<pre><code class="language-java">.setSubject(username)</code></pre>
<p>내 프로젝트에서는 username을 저장한다.</p>
<pre><code class="language-text">subject = sleepy</code></pre>
<h3 id="권한-저장">권한 저장</h3>
<pre><code class="language-java">.claim(&quot;role&quot;, role)</code></pre>
<pre><code class="language-text">role = SELLER</code></pre>
<h3 id="발급-시간">발급 시간</h3>
<pre><code class="language-java">.setIssuedAt(now)</code></pre>
<h3 id="만료-시간">만료 시간</h3>
<pre><code class="language-java">.setExpiration(...)</code></pre>
<p>설정한 시간이 지나면 토큰은 사용할 수 없다.</p>
<h3 id="signature-생성">Signature 생성</h3>
<pre><code class="language-java">.signWith(secretKey, SignatureAlgorithm.HS256)</code></pre>
<p>SECRET_KEY로 서명(Signature)을 만든다.</p>
<p>이 서명이 있기 때문에</p>
<ul>
<li>username 변경</li>
<li>role 변경</li>
<li>토큰 위조</li>
</ul>
<p>를 하면 검증에 실패한다.</p>
<h3 id="jwt-완성">JWT 완성</h3>
<pre><code class="language-java">.compact();</code></pre>
<p>실행 후</p>
<pre><code class="language-text">eyJhbGciOiJIUzI1NiJ9
.
eyJzdWIiOiJzbGVlcHkiLCJyb2xlIjoiU0VMTEVSIn0
.
xxxxxxxxxxxxxxxx</code></pre>
<p>와 같은 JWT가 생성된다.</p>
<hr>
<h1 id="6-로그인-응답">6. 로그인 응답</h1>
<p>Service는</p>
<pre><code class="language-java">return LoginResponse.builder()
        .memberId(member.getId())
        .accessToken(token)
        .username(member.getUsername())
        .email(member.getEmail())
        .nickname(member.getNickname())
        .role(member.getRole())
        .build();</code></pre>
<p>를 반환한다.</p>
<p>Controller는 JSON으로 변환하여 응답한다.</p>
<pre><code class="language-json">{
  &quot;memberId&quot;: 1,
  &quot;username&quot;: &quot;sleepy&quot;,
  &quot;nickname&quot;: &quot;슬리피&quot;,
  &quot;role&quot;: &quot;SELLER&quot;,
  &quot;accessToken&quot;: &quot;eyJhbGc...&quot;
}</code></pre>
<hr>
<h1 id="7-클라이언트가-jwt-저장">7. 클라이언트가 JWT 저장</h1>
<p>Flutter에서는</p>
<ul>
<li>SecureStorage</li>
<li>SharedPreferences</li>
</ul>
<p>등에 저장한다.</p>
<p>로그인은 여기서 끝난다.</p>
<hr>
<h1 id="8-이후-모든-요청">8. 이후 모든 요청</h1>
<p>상품 등록 요청을 한다고 가정하면</p>
<pre><code class="language-http">POST /api/products</code></pre>
<p>Header에 JWT를 함께 보낸다.</p>
<pre><code class="language-http">Authorization: Bearer eyJhbGc...</code></pre>
<p>JWT가 <strong>신분증</strong> 역할을 한다.</p>
<hr>
<h1 id="9-jwtfilter-실행">9. JwtFilter 실행</h1>
<p>Spring Security는 Controller보다 먼저 JwtFilter를 실행한다.</p>
<p>Authorization 헤더를 가져온다.</p>
<pre><code class="language-java">String header = request.getHeader(&quot;Authorization&quot;);</code></pre>
<p>값은</p>
<pre><code class="language-text">Bearer eyJhbGc...</code></pre>
<p>Bearer를 제거한다.</p>
<pre><code class="language-java">String token = header.substring(7);</code></pre>
<p>최종적으로</p>
<pre><code class="language-text">eyJhbGc...</code></pre>
<p>토큰만 남는다.</p>
<hr>
<h1 id="10-jwt-검증">10. JWT 검증</h1>
<pre><code class="language-java">Claims claims = Jwts.parserBuilder()
        .setSigningKey(secretKey)
        .build()
        .parseClaimsJws(token)
        .getBody();</code></pre>
<p>이 과정에서</p>
<ul>
<li>토큰 서명 검증</li>
<li>SECRET_KEY 검증</li>
<li>토큰 위조 여부 확인</li>
<li>만료 시간 확인</li>
<li>JWT 형식 확인</li>
</ul>
<p>을 모두 수행한다.</p>
<p>검증이 성공하면</p>
<pre><code class="language-java">String username = claims.getSubject();
String role = claims.get(&quot;role&quot;, String.class);</code></pre>
<p>를 통해 사용자 정보를 가져온다.</p>
<hr>
<h1 id="11-securitycontext에-저장">11. SecurityContext에 저장</h1>
<pre><code class="language-java">List&lt;GrantedAuthority&gt; authorities =
        List.of(new SimpleGrantedAuthority(&quot;ROLE_&quot; + role));

UsernamePasswordAuthenticationToken auth =
        new UsernamePasswordAuthenticationToken(
                username,
                null,
                authorities
        );

SecurityContextHolder.getContext().setAuthentication(auth);</code></pre>
<p>이 순간부터 Spring Security는</p>
<pre><code class="language-text">현재 요청은

username = sleepy
ROLE = SELLER</code></pre>
<p>인 사용자가 로그인한 요청이라고 인식한다.</p>
<hr>
<h1 id="12-controller-실행">12. Controller 실행</h1>
<pre><code class="language-java">@PostMapping
public void create(Authentication authentication) {

    String username = authentication.getName();
}</code></pre>
<p><code>authentication.getName()</code>으로 현재 로그인한 사용자의 username을 얻을 수 있다.</p>
<p>또한</p>
<pre><code class="language-java">@PreAuthorize(&quot;hasRole(&#39;SELLER&#39;)&quot;)</code></pre>
<p>가 있다면</p>
<p>SecurityContext에 저장된 권한을 검사하여</p>
<pre><code class="language-text">ROLE_SELLER</code></pre>
<p>가 있을 때만 메서드를 실행한다.</p>
<p>없다면</p>
<pre><code class="language-text">403 Forbidden</code></pre>
<p>을 반환한다.</p>
<hr>
<h1 id="최종-흐름">최종 흐름</h1>
<pre><code class="language-text">사용자
   │
   │ POST /login
   ▼
Controller
   │
   ▼
MemberService.login()
   │
   ├── DB에서 회원 조회
   ├── BCrypt 비밀번호 검증
   ├── JwtUtil.generateToken()
   │      ├── username 저장
   │      ├── role 저장
   │      ├── 발급 시간 저장
   │      ├── 만료 시간 저장
   │      └── SECRET_KEY로 서명
   │
   ▼
JWT 생성
   │
   ▼
클라이언트에 반환
   │
   ├── SecureStorage / SharedPreferences 저장
   │
   ▼
이후 모든 요청
Authorization: Bearer JWT
   │
   ▼
JwtFilter
   │
   ├── Authorization 헤더 확인
   ├── Bearer 제거
   ├── JWT 서명 검증
   ├── 만료 시간 확인
   ├── username 추출
   ├── role 추출
   └── SecurityContextHolder에 Authentication 저장
   │
   ▼
Spring Security
   │
   ├── @PreAuthorize 권한 검사
   └── Authentication 객체 제공
   │
   ▼
Controller → Service → Repository → DB</code></pre>
<h1 id="정리">정리</h1>
<ul>
<li><strong>Controller</strong> : 로그인 요청을 받아 Service를 호출한다.</li>
<li><strong>Service</strong> : 회원 조회, 비밀번호 검증, JWT 생성까지 담당한다.</li>
<li><strong>JwtUtil</strong> : JWT 생성과 검증을 담당한다.</li>
<li><strong>JwtFilter</strong> : 요청마다 JWT를 검사하고 SecurityContext에 인증 정보를 저장한다.</li>
<li><strong>SecurityContextHolder</strong> : 현재 요청의 로그인 정보를 저장한다.</li>
<li><strong>Spring Security</strong> : SecurityContext를 이용하여 인증과 권한 검사를 수행한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Builder 패턴]]></title>
            <link>https://velog.io/@seungho7-1/Builder-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@seungho7-1/Builder-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Tue, 14 Jul 2026 03:08:50 GMT</pubDate>
            <description><![CDATA[<p>Builder Pattern은 <strong>객체를 단계적으로 생성하는 디자인 패턴</strong>이다.</p>
<p>Spring Boot와 JPA에서는 엔티티(Entity)나 DTO 객체를 생성할 때 가장 많이 사용하는 패턴 중 하나이다.</p>
<hr>
<h1 id="builder-pattern을-사용하는-이유">Builder Pattern을 사용하는 이유</h1>
<p>객체의 필드가 많아질수록 생성자(Constructor)나 Setter를 사용하는 방식에는 여러 문제가 발생한다.</p>
<p>Builder Pattern은 이러한 문제를 해결하기 위해 사용된다.</p>
<hr>
<h1 id="1-생성자의-문제점">1. 생성자의 문제점</h1>
<p>예를 들어 <code>Member</code> 객체를 생성한다고 가정해보자.</p>
<pre><code class="language-java">Member member = new Member(
    &quot;kim&quot;,
    &quot;1234&quot;,
    &quot;김승호&quot;,
    &quot;kim@test.com&quot;,
    Role.BUYER,
    LocalDateTime.now()
);</code></pre>
<p>이 코드를 보면 <code>&quot;1234&quot;</code>가 비밀번호인지, <code>&quot;김승호&quot;</code>가 닉네임인지 한눈에 알아보기 어렵다.</p>
<p>또한 같은 타입(String)이 여러 개라면 순서를 잘못 입력해도 컴파일 오류가 발생하지 않을 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-java">Member member = new Member(
    &quot;kim&quot;,
    &quot;김승호&quot;,   // password 자리에 nickname이 들어감
    &quot;1234&quot;,
    &quot;kim@test.com&quot;,
    Role.BUYER,
    LocalDateTime.now()
);</code></pre>
<p>이처럼 매개변수의 순서를 실수해도 잘못된 객체가 생성될 수 있다.</p>
<hr>
<h1 id="2-setter의-문제점">2. Setter의 문제점</h1>
<p>Setter를 사용하면 다음과 같이 객체를 생성할 수 있다.</p>
<pre><code class="language-java">Member member = new Member();

member.setUsername(&quot;kim&quot;);
member.setPassword(&quot;1234&quot;);
member.setNickname(&quot;김승호&quot;);</code></pre>
<p>하지만 객체가 생성되는 도중 예외가 발생하면</p>
<pre><code class="language-java">Member member = new Member();

member.setUsername(&quot;kim&quot;);

// 예외 발생</code></pre>
<p>비밀번호도 없고 닉네임도 없는 <strong>불완전한 객체</strong>가 만들어질 수 있다.</p>
<p>또한 JPA에서는 Entity의 Setter를 무분별하게 사용하는 것을 권장하지 않는다.</p>
<hr>
<h1 id="3-builder-pattern-사용">3. Builder Pattern 사용</h1>
<p>Builder Pattern을 사용하면 다음과 같이 객체를 생성할 수 있다.</p>
<pre><code class="language-java">Member member = Member.builder()
        .username(&quot;kim&quot;)
        .password(&quot;1234&quot;)
        .nickname(&quot;김승호&quot;)
        .email(&quot;kim@test.com&quot;)
        .role(Role.BUYER)
        .build();</code></pre>
<p>각 값이 어떤 필드에 들어가는지 코드만 봐도 바로 알 수 있다.</p>
<p>또한 필드의 순서를 자유롭게 작성할 수 있다.</p>
<pre><code class="language-java">Member member = Member.builder()
        .role(Role.BUYER)
        .nickname(&quot;김승호&quot;)
        .username(&quot;kim&quot;)
        .password(&quot;1234&quot;)
        .email(&quot;kim@test.com&quot;)
        .build();</code></pre>
<p>순서를 바꿔도 동일한 객체가 생성된다.</p>
<hr>
<h1 id="build는-무슨-역할을-할까">build()는 무슨 역할을 할까?</h1>
<p>Builder는 객체를 바로 생성하지 않는다.</p>
<pre><code class="language-java">Member.builder()</code></pre>
<p>를 호출하면 Builder 객체가 생성된다.</p>
<pre><code class="language-java">.username(&quot;kim&quot;)
.password(&quot;1234&quot;)
.nickname(&quot;김승호&quot;)</code></pre>
<p>를 호출하면서 Builder 내부에 값만 저장한다.</p>
<p>마지막에</p>
<pre><code class="language-java">.build()</code></pre>
<p>를 호출하면 저장된 값들을 이용해 <code>Member</code> 객체를 생성한다.</p>
<p>즉,</p>
<pre><code>builder()
    ↓
Builder 객체 생성
    ↓
필드 값 저장
    ↓
build()
    ↓
Member 객체 생성</code></pre><p>이라는 순서로 동작한다.</p>
<hr>
<h1 id="lombok의-builder">Lombok의 @Builder</h1>
<p>Lombok의 <code>@Builder</code>를 사용하면 Builder 클래스를 직접 작성하지 않아도 자동으로 생성해준다.</p>
<p>예를 들어</p>
<pre><code class="language-java">@Builder
public class Member {

    private String username;
    private String password;
}</code></pre>
<p>만 작성하면 내부적으로 Builder 클래스가 자동 생성되어</p>
<pre><code class="language-java">Member.builder()
        .username(&quot;kim&quot;)
        .password(&quot;1234&quot;)
        .build();</code></pre>
<p>와 같이 사용할 수 있다.</p>
<hr>
<h1 id="실제-프로젝트-예시">실제 프로젝트 예시</h1>
<p>회원가입에서 <code>Member</code> 객체를 생성하는 코드이다.</p>
<pre><code class="language-java">Member member = Member.builder()
        .username(request.getUsername())
        .email(email)
        .password(passwordEncoder.encode(request.getPassword()))
        .nickname(request.getNickname())
        .role(Role.BUYER)
        .createdAt(LocalDateTime.now())
        .onboarded(true)
        .build();

memberRepository.save(member);</code></pre>
<p>동작 순서는 다음과 같다.</p>
<pre><code>Member.builder()
      ↓
Builder 생성
      ↓
username 저장
      ↓
email 저장
      ↓
password 저장
      ↓
nickname 저장
      ↓
role 저장
      ↓
build()
      ↓
Member 객체 생성
      ↓
memberRepository.save()
      ↓
DB 저장</code></pre><hr>
<h1 id="builder-pattern의-장점">Builder Pattern의 장점</h1>
<ul>
<li>코드의 가독성이 좋아진다.</li>
<li>어떤 값이 어떤 필드에 들어가는지 한눈에 알 수 있다.</li>
<li>생성자의 매개변수 순서를 신경 쓰지 않아도 된다.</li>
<li>Setter를 남발하지 않아도 된다.</li>
<li>불완전한 객체 생성을 방지할 수 있다.</li>
<li>필드가 많아질수록 유지보수가 쉬워진다.</li>
</ul>
<hr>
<h1 id="정리">정리</h1>
<p>Spring Boot와 JPA에서는 Entity나 DTO를 생성할 때 Builder Pattern을 많이 사용한다.</p>
<p>Builder Pattern은 객체를 단계적으로 생성하여 <strong>가독성</strong>, <strong>안정성</strong>, <strong>유지보수성</strong>을 높여주는 패턴이며, 실무에서도 가장 많이 사용하는 객체 생성 방식 중 하나이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA Entity 생명주기(Entity Lifecycle)
]]></title>
            <link>https://velog.io/@seungho7-1/JPA-Entity-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0Entity-Lifecycle</link>
            <guid>https://velog.io/@seungho7-1/JPA-Entity-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0Entity-Lifecycle</guid>
            <pubDate>Tue, 14 Jul 2026 02:12:31 GMT</pubDate>
            <description><![CDATA[<p>JPA는 엔티티(Entity)를 <strong>영속성 컨텍스트(Persistence Context)</strong> 를 통해 관리한다.</p>
<p>엔티티는 생성부터 삭제까지 다음과 같은 4가지 상태를 가진다.</p>
<pre><code>비영속(Transient)
        ↓
영속(Persistent)
        ↓
준영속(Detached)
        ↓
삭제(Removed)</code></pre><hr>
<h1 id="1-비영속transient">1. 비영속(Transient)</h1>
<p>비영속 상태는 <strong>영속성 컨텍스트가 관리하지 않는 새로운 객체</strong>이다.</p>
<pre><code class="language-java">Product product = new Product();</code></pre>
<p>이 객체는 단순히 JVM 메모리에만 존재하며 DB와는 아무런 관계가 없다.</p>
<h3 id="특징">특징</h3>
<ul>
<li>영속성 컨텍스트가 관리하지 않는다.</li>
<li>DB에 저장되지 않는다.</li>
<li>Dirty Checking이 동작하지 않는다.</li>
</ul>
<hr>
<h1 id="2-영속persistent">2. 영속(Persistent)</h1>
<p>영속 상태는 <strong>영속성 컨텍스트가 엔티티를 관리하는 상태</strong>이다.</p>
<pre><code class="language-java">Product product = productRepository.findById(1L).get();</code></pre>
<p>또는</p>
<pre><code class="language-java">productRepository.save(product);</code></pre>
<p>이후부터 해당 엔티티는 영속성 컨텍스트의 관리 대상이 된다.</p>
<h3 id="특징-1">특징</h3>
<ul>
<li>1차 캐시를 사용할 수 있다.</li>
<li>Dirty Checking이 동작한다.</li>
<li>쓰기 지연(Write Behind)이 적용된다.</li>
<li>트랜잭션 종료 시 변경 사항이 자동으로 DB에 반영된다.</li>
</ul>
<p>예를 들어</p>
<pre><code class="language-java">@Transactional
public void update() {

    Product product = productRepository.findById(1L).get();

    product.setPrice(30000);

}</code></pre>
<p><code>save()</code>를 호출하지 않아도 Dirty Checking으로 인해 UPDATE SQL이 자동으로 실행된다.</p>
<hr>
<h1 id="3-준영속detached">3. 준영속(Detached)</h1>
<p>준영속 상태는 <strong>영속성 컨텍스트가 더 이상 엔티티를 관리하지 않는 상태</strong>이다.</p>
<p>예를 들어</p>
<pre><code class="language-java">entityManager.detach(product);</code></pre>
<p>를 호출하면 해당 엔티티는 준영속 상태가 된다.</p>
<h3 id="특징-2">특징</h3>
<ul>
<li>영속성 컨텍스트의 관리 대상이 아니다.</li>
<li>Dirty Checking이 동작하지 않는다.</li>
<li>객체를 수정해도 DB에 반영되지 않는다.</li>
</ul>
<hr>
<h1 id="4-삭제removed">4. 삭제(Removed)</h1>
<p>삭제 상태는 <strong>삭제가 예약된 상태</strong>이다.</p>
<pre><code class="language-java">productRepository.delete(product);</code></pre>
<p>또는</p>
<pre><code class="language-java">entityManager.remove(product);</code></pre>
<p>를 호출하면 삭제 상태가 되며,</p>
<p>트랜잭션이 Commit되면 DELETE SQL이 실행된다.</p>
<pre><code class="language-sql">DELETE
FROM product
WHERE id = 1;</code></pre>
<hr>
<h1 id="entity-생명주기-정리">Entity 생명주기 정리</h1>
<table>
<thead>
<tr>
<th>상태</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>비영속(Transient)</td>
<td>단순히 생성된 객체로 영속성 컨텍스트가 관리하지 않는다.</td>
</tr>
<tr>
<td>영속(Persistent)</td>
<td>영속성 컨텍스트가 관리하는 상태이며 Dirty Checking, 1차 캐시 등을 사용할 수 있다.</td>
</tr>
<tr>
<td>준영속(Detached)</td>
<td>영속성 컨텍스트가 더 이상 관리하지 않는 상태이다.</td>
</tr>
<tr>
<td>삭제(Removed)</td>
<td>삭제가 예약된 상태이며 Commit 시 DELETE SQL이 실행된다.</td>
</tr>
</tbody></table>
<hr>
<h1 id="한-줄-요약">한 줄 요약</h1>
<ul>
<li><strong>비영속</strong> : <code>new</code>로 생성한 객체</li>
<li><strong>영속</strong> : JPA가 관리하는 객체</li>
<li><strong>준영속</strong> : JPA가 더 이상 관리하지 않는 객체</li>
<li><strong>삭제</strong> : 삭제가 예약된 객체</li>
</ul>
<hr>
<h1 id="jpa-전체-흐름">JPA 전체 흐름</h1>
<pre><code>JPA
    ↓
Hibernate
    ↓
@Transactional
    ↓
영속성 컨텍스트
    ↓
Entity(영속 상태)
    ↓
1차 캐시
    ↓
Dirty Checking
    ↓
Write Behind
    ↓
Flush
    ↓
Commit
    ↓
Database</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 영속성 컨텍스트(Persistence Context) 정리]]></title>
            <link>https://velog.io/@seungho7-1/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8Persistence-Context-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@seungho7-1/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8Persistence-Context-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 14 Jul 2026 01:41:20 GMT</pubDate>
            <description><![CDATA[<h2 id="영속성-컨텍스트란">영속성 컨텍스트란?</h2>
<p>영속성 컨텍스트(Persistence Context)는 <strong>JPA가 엔티티(Entity)를 관리하는 메모리 공간</strong>이다.</p>
<p>개발자가 엔티티를 조회하거나 저장하면 DB와 바로 통신하는 것이 아니라, 먼저 영속성 컨텍스트에서 엔티티를 관리한 후 필요할 때 DB와 동기화한다.</p>
<pre><code>Application
    ↓
JPA(Hibernate)
    ↓
영속성 컨텍스트
    ↓
Database</code></pre><hr>
<h1 id="1-1차-캐시first-level-cache">1. 1차 캐시(First Level Cache)</h1>
<p>영속성 컨텍스트는 조회한 엔티티를 메모리에 저장한다.</p>
<p>예를 들어</p>
<pre><code class="language-java">Product p1 = productRepository.findById(1L).get();
Product p2 = productRepository.findById(1L).get();</code></pre>
<p>처음 조회할 때만 SQL이 실행된다.</p>
<pre><code class="language-sql">SELECT * FROM product WHERE id = 1;</code></pre>
<p>두 번째 조회는 영속성 컨텍스트의 1차 캐시에서 가져오기 때문에 SQL이 실행되지 않는다.</p>
<h3 id="장점">장점</h3>
<ul>
<li>불필요한 DB 조회를 줄일 수 있다.</li>
<li>같은 트랜잭션 내에서 조회 성능이 향상된다.</li>
</ul>
<hr>
<h1 id="2-dirty-checking변경-감지">2. Dirty Checking(변경 감지)</h1>
<p>JPA는 조회한 엔티티의 변경 사항을 자동으로 감지한다.</p>
<pre><code class="language-java">@Transactional
public void updatePrice() {

    Product product = productRepository.findById(1L).get();

    product.setPrice(30000);

}</code></pre>
<p><code>save()</code>를 호출하지 않아도 트랜잭션이 종료될 때 JPA가 변경 내용을 감지하여 UPDATE SQL을 실행한다.</p>
<pre><code class="language-sql">UPDATE product
SET price = 30000
WHERE id = 1;</code></pre>
<h3 id="장점-1">장점</h3>
<ul>
<li>개발자가 직접 UPDATE SQL을 작성하지 않아도 된다.</li>
<li>코드가 간결해지고 객체 중심 개발이 가능하다.</li>
</ul>
<hr>
<h1 id="3-쓰기-지연write-behind">3. 쓰기 지연(Write Behind)</h1>
<p><code>save()</code>를 호출했다고 해서 바로 INSERT SQL이 실행되는 것은 아니다.</p>
<pre><code class="language-java">productRepository.save(product);</code></pre>
<p>먼저 영속성 컨텍스트에 저장한 뒤,</p>
<p>트랜잭션이 Commit 되는 시점에 SQL을 한꺼번에 실행한다.</p>
<pre><code>save()

↓

영속성 컨텍스트 저장

↓

Commit

↓

Flush

↓

INSERT / UPDATE / DELETE 실행</code></pre><h3 id="장점-2">장점</h3>
<ul>
<li>SQL을 모아서 실행하므로 성능을 향상시킬 수 있다.</li>
<li>트랜잭션 단위로 데이터를 일관성 있게 관리할 수 있다.</li>
</ul>
<hr>
<h1 id="4-flush">4. Flush</h1>
<p>Flush는 <strong>영속성 컨텍스트의 변경 내용을 데이터베이스와 동기화하는 작업</strong>이다.</p>
<p>Flush가 발생하면</p>
<ul>
<li>INSERT</li>
<li>UPDATE</li>
<li>DELETE</li>
</ul>
<p>SQL이 실행된다.</p>
<p>하지만 <strong>Flush는 Commit과 다르다.</strong></p>
<ul>
<li><strong>Flush</strong> : DB와 동기화</li>
<li><strong>Commit</strong> : 실제 트랜잭션 확정</li>
</ul>
<hr>
<h1 id="정리">정리</h1>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>영속성 컨텍스트</td>
<td>엔티티를 관리하는 메모리 공간</td>
</tr>
<tr>
<td>1차 캐시</td>
<td>같은 엔티티를 다시 조회하면 DB 대신 메모리에서 반환</td>
</tr>
<tr>
<td>Dirty Checking</td>
<td>엔티티 변경을 감지하여 UPDATE SQL을 자동 실행</td>
</tr>
<tr>
<td>쓰기 지연(Write Behind)</td>
<td>SQL을 바로 실행하지 않고 Commit 시점까지 모아둠</td>
</tr>
<tr>
<td>Flush</td>
<td>영속성 컨텍스트의 변경 내용을 DB와 동기화</td>
</tr>
</tbody></table>
<hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<ul>
<li><strong>영속성 컨텍스트</strong>는 JPA가 엔티티를 관리하는 메모리 공간이다.</li>
<li><strong>1차 캐시</strong>를 통해 같은 엔티티를 반복 조회해도 DB를 다시 조회하지 않는다.</li>
<li><strong>Dirty Checking</strong>을 통해 엔티티 변경을 자동으로 감지하여 UPDATE SQL을 생성한다.</li>
<li><strong>쓰기 지연</strong>을 통해 SQL을 즉시 실행하지 않고 Commit 시점에 한꺼번에 실행한다.</li>
<li><strong>Flush</strong>는 영속성 컨텍스트의 변경 내용을 DB와 동기화하는 과정이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA N+1 문제와 Fetch Join]]></title>
            <link>https://velog.io/@seungho7-1/JPA-N1-%EB%AC%B8%EC%A0%9C%EC%99%80-Fetch-Join</link>
            <guid>https://velog.io/@seungho7-1/JPA-N1-%EB%AC%B8%EC%A0%9C%EC%99%80-Fetch-Join</guid>
            <pubDate>Tue, 14 Jul 2026 01:08:12 GMT</pubDate>
            <description><![CDATA[<h2 id="fetchtype">FetchType</h2>
<p>JPA에서 연관 엔티티를 언제 조회할지 결정하는 옵션이다.</p>
<h3 id="1-lazy-지연-로딩">1. LAZY (지연 로딩)</h3>
<p>연관 엔티티를 <strong>실제로 사용할 때</strong> 조회한다.</p>
<pre><code class="language-java">@ManyToOne(fetch = FetchType.LAZY)
private Seller seller;</code></pre>
<p>예를 들어 상품을 조회하면 상품만 가져오고,</p>
<pre><code class="language-java">Product product = productRepository.findById(1L).get();</code></pre>
<p>이 시점에는 <code>Seller</code>를 조회하지 않는다.</p>
<p>하지만</p>
<pre><code class="language-java">product.getSeller().getName();</code></pre>
<p>을 호출하는 순간 판매자를 조회하는 SQL이 실행된다.</p>
<p><strong>장점</strong></p>
<ul>
<li>필요한 데이터만 조회하여 성능이 좋다.</li>
<li>실무에서 기본적으로 많이 사용된다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>잘못 사용하면 N+1 문제가 발생할 수 있다.</li>
</ul>
<hr>
<h3 id="2-eager-즉시-로딩">2. EAGER (즉시 로딩)</h3>
<p>부모 엔티티를 조회할 때 연관 엔티티도 함께 조회한다.</p>
<pre><code class="language-java">@ManyToOne(fetch = FetchType.EAGER)
private Seller seller;</code></pre>
<p>상품을 조회하면 판매자를 사용하지 않아도 함께 조회한다.</p>
<p><strong>장점</strong></p>
<ul>
<li>연관 엔티티를 바로 사용할 수 있다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>필요 없는 데이터까지 조회할 수 있어 성능이 저하될 수 있다.</li>
<li>연관관계가 많아질수록 예상하지 못한 SQL이 실행될 수 있다.</li>
</ul>
<blockquote>
<p><code>@ManyToOne</code>과 <code>@OneToOne</code>의 기본 FetchType은 <code>EAGER</code>이다.
실무에서는 대부분 <code>LAZY</code>로 변경하여 사용한다.</p>
</blockquote>
<hr>
<h2 id="n1-문제란">N+1 문제란?</h2>
<p><code>LAZY</code> 상태에서 부모 엔티티를 조회한 후 연관 엔티티를 반복해서 접근하면 추가 SQL이 계속 실행되는 문제이다.</p>
<h3 id="예시">예시</h3>
<pre><code class="language-java">List&lt;Product&gt; products = productRepository.findAll();

for (Product product : products) {
    System.out.println(product.getSeller().getName());
}</code></pre>
<p>실행되는 SQL</p>
<pre><code class="language-sql">-- 1번
SELECT * FROM product;

-- 상품 개수만큼 반복
SELECT * FROM seller WHERE id = 1;
SELECT * FROM seller WHERE id = 2;
...</code></pre>
<p>상품이 100개라면</p>
<ul>
<li>상품 조회 : <strong>1번</strong></li>
<li>판매자 조회 : <strong>100번</strong></li>
</ul>
<p>➡️ <strong>총 101번의 SQL이 실행된다.</strong></p>
<p>이를 <strong>N+1 문제</strong>라고 한다.</p>
<hr>
<h2 id="fetch-join">Fetch Join</h2>
<p><code>Fetch Join</code>은 연관 엔티티를 부모 엔티티와 함께 한 번에 조회하는 방법이다.</p>
<pre><code class="language-java">@Query(&quot;&quot;&quot;
select p
from Product p
join fetch p.seller
&quot;&quot;&quot;)
List&lt;Product&gt; findAllWithSeller();</code></pre>
<p>실행되는 SQL</p>
<pre><code class="language-sql">SELECT *
FROM product p
JOIN seller s
ON p.seller_id = s.id;</code></pre>
<p>이후</p>
<pre><code class="language-java">product.getSeller().getName();</code></pre>
<p>을 호출해도 추가 SQL이 발생하지 않는다.</p>
<p>즉,</p>
<ul>
<li>상품 조회 + 판매자 조회를 <strong>한 번의 SQL</strong>로 처리한다.</li>
<li>N+1 문제를 해결할 수 있다.</li>
</ul>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>LAZY</strong></td>
<td>연관 엔티티를 실제 사용할 때 조회한다.</td>
</tr>
<tr>
<td><strong>EAGER</strong></td>
<td>부모를 조회할 때 연관 엔티티도 함께 조회한다.</td>
</tr>
<tr>
<td><strong>N+1</strong></td>
<td>부모 조회 1번 + 연관 엔티티 조회 N번이 발생하는 성능 문제이다.</td>
</tr>
<tr>
<td><strong>Fetch Join</strong></td>
<td>부모와 연관 엔티티를 한 번의 SQL로 조회하여 N+1 문제를 해결한다.</td>
</tr>
</tbody></table>
<h2 id="사용-방식">사용 방식</h2>
<ul>
<li>기본은 <strong>LAZY</strong>를 사용한다.</li>
<li>연관 엔티티가 필요한 조회에서는 <strong>Fetch Join</strong> 또는 <strong><code>@EntityGraph</code></strong>를 사용한다.</li>
<li><strong>EAGER는 예기치 않은 성능 문제를 유발할 수 있어 일반적으로 권장되지 않는다.</strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java -260703(ArrayList)]]></title>
            <link>https://velog.io/@seungho7-1/Java-260703ArrayList</link>
            <guid>https://velog.io/@seungho7-1/Java-260703ArrayList</guid>
            <pubDate>Thu, 02 Jul 2026 15:29:32 GMT</pubDate>
            <description><![CDATA[<h1 id="java-arraylist-정리">Java ArrayList 정리</h1>
<h2 id="1-arraylist란">1. ArrayList란?</h2>
<p><code>ArrayList</code>는 Java에서 가장 많이 사용하는 컬렉션(Collection) 중 하나로, <strong>크기가 자동으로 늘어나고 줄어드는 동적 배열(Dynamic Array)</strong> 이다.</p>
<p>일반 배열(Array)은 크기를 한 번 정하면 변경할 수 없지만, <code>ArrayList</code>는 데이터를 추가하거나 삭제할 때 내부적으로 크기를 자동으로 조절해 준다.</p>
<p>예를 들어 일반 배열은 다음과 같이 크기를 미리 지정해야 한다.</p>
<pre><code class="language-java">int[] arr = new int[5];</code></pre>
<p>반면 <code>ArrayList</code>는 크기를 지정하지 않아도 된다.</p>
<pre><code class="language-java">ArrayList&lt;Integer&gt; list = new ArrayList&lt;&gt;();</code></pre>
<p>데이터를 계속 추가하면 내부적으로 더 큰 배열을 생성하고 기존 데이터를 복사하여 저장 공간을 늘려준다.</p>
<hr>
<h1 id="2-왜-arraylist를-사용할까">2. 왜 ArrayList를 사용할까?</h1>
<p>예를 들어 회원 정보를 저장한다고 가정해보자.</p>
<p>배열을 사용하면</p>
<pre><code class="language-java">String[] users = new String[100];</code></pre>
<p>100명을 초과하면 더 이상 저장할 수 없다.</p>
<p>하지만 ArrayList는</p>
<pre><code class="language-java">ArrayList&lt;String&gt; users = new ArrayList&lt;&gt;();</code></pre>
<p>회원이 계속 추가되어도 자동으로 크기가 증가한다.</p>
<p>즉, <strong>데이터 개수를 미리 알 수 없는 경우 ArrayList가 매우 유용하다.</strong></p>
<hr>
<h1 id="3-선언-방법">3. 선언 방법</h1>
<p>정수 저장</p>
<pre><code class="language-java">ArrayList&lt;Integer&gt; list = new ArrayList&lt;&gt;();</code></pre>
<p>문자열 저장</p>
<pre><code class="language-java">ArrayList&lt;String&gt; list = new ArrayList&lt;&gt;();</code></pre>
<p>사용자 객체 저장</p>
<pre><code class="language-java">ArrayList&lt;User&gt; users = new ArrayList&lt;&gt;();</code></pre>
<hr>
<h1 id="4-주요-메서드">4. 주요 메서드</h1>
<h2 id="1-add">1) add()</h2>
<p>데이터를 추가한다.</p>
<pre><code class="language-java">ArrayList&lt;String&gt; list = new ArrayList&lt;&gt;();

list.add(&quot;Java&quot;);
list.add(&quot;Spring&quot;);
list.add(&quot;MySQL&quot;);</code></pre>
<p>결과</p>
<pre><code>[Java, Spring, MySQL]</code></pre><p>특정 위치에 추가</p>
<pre><code class="language-java">list.add(1, &quot;Python&quot;);</code></pre>
<p>결과</p>
<pre><code>[Java, Python, Spring, MySQL]</code></pre><hr>
<h2 id="2-get">2) get()</h2>
<p>특정 위치의 데이터를 가져온다.</p>
<pre><code class="language-java">System.out.println(list.get(0));</code></pre>
<p>출력</p>
<pre><code>Java</code></pre><hr>
<h2 id="3-set">3) set()</h2>
<p>특정 위치의 데이터를 수정한다.</p>
<pre><code class="language-java">list.set(1, &quot;JavaScript&quot;);</code></pre>
<p>결과</p>
<pre><code>[Java, JavaScript, Spring, MySQL]</code></pre><hr>
<h2 id="4-remove">4) remove()</h2>
<p>인덱스로 삭제</p>
<pre><code class="language-java">list.remove(1);</code></pre>
<p>값으로 삭제</p>
<pre><code class="language-java">list.remove(&quot;Spring&quot;);</code></pre>
<hr>
<h2 id="5-contains">5) contains()</h2>
<p>데이터가 존재하는지 확인한다.</p>
<pre><code class="language-java">list.contains(&quot;Java&quot;);</code></pre>
<p>결과</p>
<pre><code>true</code></pre><hr>
<h2 id="6-size">6) size()</h2>
<p>저장된 데이터의 개수를 반환한다.</p>
<pre><code class="language-java">System.out.println(list.size());</code></pre>
<hr>
<h1 id="5-반복문으로-출력하기">5. 반복문으로 출력하기</h1>
<h3 id="일반-for문">일반 for문</h3>
<pre><code class="language-java">for (int i = 0; i &lt; list.size(); i++) {
    System.out.println(list.get(i));
}</code></pre>
<h3 id="향상된-for문">향상된 for문</h3>
<pre><code class="language-java">for (String language : list) {
    System.out.println(language);
}</code></pre>
<hr>
<h1 id="6-arraylist의-내부-동작">6. ArrayList의 내부 동작</h1>
<p>ArrayList는 내부적으로 <strong>배열(Array)</strong> 을 사용한다.</p>
<p>예를 들어</p>
<pre><code class="language-java">list.add(&quot;A&quot;);
list.add(&quot;B&quot;);
list.add(&quot;C&quot;);</code></pre>
<p>메모리 구조는 다음과 같다.</p>
<pre><code>Index
 0    1    2
+----+----+----+
| A  | B  | C  |
+----+----+----+</code></pre><p>만약 저장 공간이 모두 찼는데 새로운 데이터를 추가하면</p>
<pre><code>기존 배열

[A][B][C]

↓

더 큰 배열 생성

[A][B][C][ ][ ][ ]</code></pre><p>새로운 배열을 생성한 뒤 기존 데이터를 복사하고, 마지막에 새로운 데이터를 추가한다.</p>
<hr>
<h1 id="7-시간복잡도">7. 시간복잡도</h1>
<table>
<thead>
<tr>
<th>기능</th>
<th>시간복잡도</th>
</tr>
</thead>
<tbody><tr>
<td>get()</td>
<td>O(1)</td>
</tr>
<tr>
<td>set()</td>
<td>O(1)</td>
</tr>
<tr>
<td>add(맨 뒤)</td>
<td>평균 O(1)</td>
</tr>
<tr>
<td>add(중간)</td>
<td>O(N)</td>
</tr>
<tr>
<td>remove(중간)</td>
<td>O(N)</td>
</tr>
<tr>
<td>contains()</td>
<td>O(N)</td>
</tr>
</tbody></table>
<h3 id="왜-get은-o1일까">왜 get()은 O(1)일까?</h3>
<p>ArrayList는 내부적으로 배열을 사용한다.</p>
<p>배열은 메모리 공간에 연속적으로 저장되기 때문에 원하는 위치를 바로 계산하여 접근할 수 있다.</p>
<pre><code class="language-java">list.get(3);</code></pre>
<p>처럼 특정 인덱스를 바로 조회할 수 있으므로 시간복잡도는 <strong>O(1)</strong> 이다.</p>
<hr>
<h3 id="왜-add중간은-on일까">왜 add(중간)은 O(N)일까?</h3>
<p>예를 들어</p>
<pre><code>[10][20][30][40]</code></pre><p>여기에 15를 두 번째 위치에 삽입하면</p>
<pre><code>[10][15][20][30][40]</code></pre><p>이 되어야 한다.</p>
<p>이를 위해 기존의</p>
<ul>
<li>20</li>
<li>30</li>
<li>40</li>
</ul>
<p>을 한 칸씩 뒤로 이동해야 한다.</p>
<p>데이터를 이동하는 작업이 발생하기 때문에 시간복잡도는 <strong>O(N)</strong> 이다.</p>
<hr>
<h3 id="왜-remove중간도-on일까">왜 remove(중간)도 O(N)일까?</h3>
<p>예를 들어</p>
<pre><code>[10][20][30][40]</code></pre><p>에서 20을 삭제하면</p>
<pre><code>[10][30][40]</code></pre><p>가 되어야 한다.</p>
<p>이때 뒤에 있는 데이터들을 모두 앞으로 한 칸씩 이동해야 하므로 역시 <strong>O(N)</strong> 이다.</p>
<hr>
<h1 id="8-array와-arraylist-비교">8. Array와 ArrayList 비교</h1>
<table>
<thead>
<tr>
<th>Array</th>
<th>ArrayList</th>
</tr>
</thead>
<tbody><tr>
<td>크기 고정</td>
<td>크기 자동 증가</td>
</tr>
<tr>
<td>기본 자료형 저장 가능</td>
<td>객체 타입 저장(기본형은 오토박싱 사용)</td>
</tr>
<tr>
<td>length 사용</td>
<td>size() 사용</td>
</tr>
<tr>
<td>접근 속도 O(1)</td>
<td>접근 속도 O(1)</td>
</tr>
<tr>
<td>삽입·삭제 어려움</td>
<td>메서드로 쉽게 처리 가능</td>
</tr>
</tbody></table>
<hr>
<h1 id="9-코딩테스트에서-arraylist를-사용하는-경우">9. 코딩테스트에서 ArrayList를 사용하는 경우</h1>
<p>ArrayList는 다음과 같은 상황에서 자주 사용된다.</p>
<ul>
<li>입력의 개수를 미리 알 수 없는 경우</li>
<li>그래프의 인접 리스트 구현</li>
<li>결과를 동적으로 저장해야 하는 경우</li>
<li>BFS, DFS에서 인접 노드 저장</li>
<li>데이터를 순차적으로 관리해야 하는 경우</li>
</ul>
<hr>
<h1 id="정리">정리</h1>
<ul>
<li>ArrayList는 크기가 자동으로 조절되는 동적 배열이다.</li>
<li>내부적으로 배열을 사용하며 필요할 때 더 큰 배열을 생성해 데이터를 복사한다.</li>
<li>조회(get)는 O(1)로 매우 빠르다.</li>
<li>중간 삽입(add)과 삭제(remove)는 데이터 이동이 발생하므로 O(N)이다.</li>
<li>Java 실무와 코딩테스트에서 가장 많이 사용하는 컬렉션 중 하나이므로 반드시 익혀두는 것이 좋다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java - 260702]]></title>
            <link>https://velog.io/@seungho7-1/Java-260702</link>
            <guid>https://velog.io/@seungho7-1/Java-260702</guid>
            <pubDate>Thu, 02 Jul 2026 14:17:38 GMT</pubDate>
            <description><![CDATA[<h1 id="java-string--stringbuilder-정리-코딩테스트">Java String &amp; StringBuilder 정리 (코딩테스트)</h1>
<h2 id="1-string">1. String</h2>
<h3 id="특징">특징</h3>
<ul>
<li>문자열을 저장하는 객체</li>
<li>문자열을 수정하면 새로운 객체가 생성됨</li>
</ul>
<pre><code class="language-java">String str = &quot;Hello&quot;;
str += &quot; World&quot;;</code></pre>
<p>기존 문자열이 수정되는 것이 아니라 새로운 String 객체가 만들어진다.</p>
<hr>
<h2 id="자주-사용하는-메서드">자주 사용하는 메서드</h2>
<h3 id="1-length">1. length()</h3>
<p>문자열 길이 반환</p>
<pre><code class="language-java">String str = &quot;Hello&quot;;

System.out.println(str.length()); // 5</code></pre>
<p>시간복잡도 : O(1)</p>
<hr>
<h3 id="2-charatindex">2. charAt(index)</h3>
<p>특정 위치 문자 반환</p>
<pre><code class="language-java">String str = &quot;Hello&quot;;

System.out.println(str.charAt(1)); // e</code></pre>
<p>시간복잡도 : O(1)</p>
<hr>
<h3 id="3-substring">3. substring()</h3>
<p>문자열 자르기</p>
<pre><code class="language-java">String str = &quot;Hello&quot;;

System.out.println(str.substring(1));     // ello
System.out.println(str.substring(1,4));   // ell</code></pre>
<ul>
<li>시작 포함</li>
<li>끝 미포함</li>
</ul>
<hr>
<h3 id="4-indexof">4. indexOf()</h3>
<p>문자의 위치 찾기</p>
<pre><code class="language-java">String str = &quot;banana&quot;;

System.out.println(str.indexOf(&quot;a&quot;)); // 1
System.out.println(str.indexOf(&quot;z&quot;)); // -1</code></pre>
<p>없으면 -1 반환</p>
<hr>
<h3 id="5-contains">5. contains()</h3>
<p>포함 여부 확인</p>
<pre><code class="language-java">String str = &quot;Hello&quot;;

System.out.println(str.contains(&quot;ell&quot;)); // true</code></pre>
<p>반환형 : boolean</p>
<hr>
<h3 id="6-equals">6. equals()</h3>
<p>문자열 비교</p>
<pre><code class="language-java">String a = &quot;abc&quot;;
String b = &quot;abc&quot;;

System.out.println(a.equals(b));</code></pre>
<p>※ == 사용 금지</p>
<hr>
<h3 id="7-split">7. split()</h3>
<p>문자열 분리</p>
<pre><code class="language-java">String str = &quot;A,B,C&quot;;

String[] arr = str.split(&quot;,&quot;);</code></pre>
<p>결과</p>
<pre><code>A
B
C</code></pre><hr>
<h3 id="8-replace">8. replace()</h3>
<p>문자 변경</p>
<pre><code class="language-java">String str = &quot;Hello&quot;;

System.out.println(str.replace(&quot;l&quot;,&quot;x&quot;));</code></pre>
<p>출력</p>
<pre><code>Hexxo</code></pre><hr>
<h3 id="9-touppercase">9. toUpperCase()</h3>
<p>대문자 변환</p>
<pre><code class="language-java">str.toUpperCase();</code></pre>
<hr>
<h3 id="10-tolowercase">10. toLowerCase()</h3>
<p>소문자 변환</p>
<pre><code class="language-java">str.toLowerCase();</code></pre>
<hr>
<h2 id="string에서-자주-나오는-코테-패턴">String에서 자주 나오는 코테 패턴</h2>
<ul>
<li>문자 하나씩 탐색(charAt)</li>
<li>문자열 자르기(substring)</li>
<li>문자열 비교(equals)</li>
<li>문자열 포함 여부(contains)</li>
<li>문자열 분리(split)</li>
<li>특정 문자 찾기(indexOf)</li>
<li>대소문자 변환</li>
</ul>
<hr>
<h1 id="stringbuilder">StringBuilder</h1>
<h2 id="특징-1">특징</h2>
<ul>
<li>문자열 수정이 많을 때 사용</li>
<li>String보다 훨씬 빠름</li>
<li>가변(Mutable)</li>
</ul>
<hr>
<h2 id="생성">생성</h2>
<pre><code class="language-java">StringBuilder sb = new StringBuilder();</code></pre>
<p>또는</p>
<pre><code class="language-java">StringBuilder sb = new StringBuilder(&quot;Hello&quot;);</code></pre>
<hr>
<h2 id="append">append()</h2>
<p>문자열 추가</p>
<pre><code class="language-java">sb.append(&quot;A&quot;);
sb.append(&quot;B&quot;);</code></pre>
<p>결과</p>
<pre><code>AB</code></pre><hr>
<h2 id="insert">insert()</h2>
<p>문자 삽입</p>
<pre><code class="language-java">sb.insert(1,&quot;X&quot;);</code></pre>
<pre><code>ABC

↓

AXBC</code></pre><hr>
<h2 id="delete">delete()</h2>
<p>삭제</p>
<pre><code class="language-java">sb.delete(1,3);</code></pre>
<ul>
<li>시작 포함</li>
<li>끝 미포함</li>
</ul>
<hr>
<h2 id="deletecharat">deleteCharAt()</h2>
<p>한 글자 삭제</p>
<pre><code class="language-java">sb.deleteCharAt(2);</code></pre>
<hr>
<h2 id="replace">replace()</h2>
<p>문자 변경</p>
<pre><code class="language-java">sb.replace(1,3,&quot;AB&quot;);</code></pre>
<hr>
<h2 id="reverse">reverse()</h2>
<p>문자열 뒤집기</p>
<pre><code class="language-java">sb.reverse();</code></pre>
<p>코테에서 매우 자주 사용</p>
<hr>
<h2 id="setcharat">setCharAt()</h2>
<p>특정 문자 변경</p>
<pre><code class="language-java">sb.setCharAt(2,&#39;A&#39;);</code></pre>
<hr>
<h2 id="charat">charAt()</h2>
<p>특정 문자 조회</p>
<pre><code class="language-java">sb.charAt(1);</code></pre>
<hr>
<h2 id="tostring">toString()</h2>
<p>StringBuilder → String 변환</p>
<pre><code class="language-java">String result = sb.toString();</code></pre>
<hr>
<h1 id="string-vs-stringbuilder">String vs StringBuilder</h1>
<table>
<thead>
<tr>
<th>String</th>
<th>StringBuilder</th>
</tr>
</thead>
<tbody><tr>
<td>불변 객체</td>
<td>가변 객체</td>
</tr>
<tr>
<td>수정 시 새 객체 생성</td>
<td>기존 객체 수정</td>
</tr>
<tr>
<td>수정이 많으면 느림</td>
<td>수정이 많아도 빠름</td>
</tr>
<tr>
<td>문자열 저장</td>
<td>문자열 수정</td>
</tr>
</tbody></table>
<hr>
<h1 id="코테에서-언제-사용할까">코테에서 언제 사용할까?</h1>
<h2 id="string">String</h2>
<ul>
<li>문자열 비교</li>
<li>문자열 자르기</li>
<li>문자 탐색</li>
<li>split 사용</li>
</ul>
<hr>
<h2 id="stringbuilder-1">StringBuilder</h2>
<ul>
<li>문자열 이어붙이기</li>
<li>문자열 뒤집기(reverse)</li>
<li>문자열 수정</li>
<li>반복문에서 문자열 생성</li>
</ul>
<hr>
<h1 id="꼭-외워야-하는-메서드">꼭 외워야 하는 메서드</h1>
<h2 id="string-1">String</h2>
<ul>
<li>length()</li>
<li>charAt()</li>
<li>substring()</li>
<li>equals()</li>
<li>contains()</li>
<li>split()</li>
<li>indexOf()</li>
<li>replace()</li>
<li>toUpperCase()</li>
<li>toLowerCase()</li>
</ul>
<hr>
<h2 id="stringbuilder-2">StringBuilder</h2>
<ul>
<li>append()</li>
<li>insert()</li>
<li>delete()</li>
<li>deleteCharAt()</li>
<li>replace()</li>
<li>reverse()</li>
<li>setCharAt()</li>
<li>charAt()</li>
<li>toString()</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ Spring MVC에서 Form 객체를 템플릿(Thymeleaf)으로 전달하는 방법]]></title>
            <link>https://velog.io/@seungho7-1/Spring-MVC%EC%97%90%EC%84%9C-Form-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%ED%85%9C%ED%94%8C%EB%A6%BFThymeleaf%EC%9C%BC%EB%A1%9C-%EC%A0%84%EB%8B%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@seungho7-1/Spring-MVC%EC%97%90%EC%84%9C-Form-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%ED%85%9C%ED%94%8C%EB%A6%BFThymeleaf%EC%9C%BC%EB%A1%9C-%EC%A0%84%EB%8B%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 22 Apr 2025 07:23:29 GMT</pubDate>
            <description><![CDATA[<h2 id="1-자동-바인딩-방식">1. 자동 바인딩 방식</h2>
<pre><code class="language-java">@GetMapping(&quot;/create&quot;)
public String create(QuestionForm questionForm) {
    return &quot;question_form&quot;;
}

// 내부적으로는 아래와 같은 코드와 동일하게 작동함:
model.addAttribute(&quot;questionForm&quot;, new QuestionForm());</code></pre>
<h2 id="2-수동-바인딩-방식-model-이용">2. 수동 바인딩 방식 (Model 이용)</h2>
<pre><code class="language-java">@GetMapping(&quot;/create&quot;)
public String create(Model model) {
    model.addAttribute(&quot;questionForm&quot;, new QuestionForm());
    return &quot;question_form&quot;;
}

</code></pre>
<h2 id="3-명시적-modelattribute-방식">3. 명시적 @ModelAttribute 방식</h2>
<pre><code class="language-java">@GetMapping(&quot;/create&quot;)
public String create(@ModelAttribute(&quot;questionForm&quot;) QuestionForm form) {
    return &quot;question_form&quot;;
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[유효성 검사(@Valid)]]></title>
            <link>https://velog.io/@seungho7-1/%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%ACValid</link>
            <guid>https://velog.io/@seungho7-1/%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%ACValid</guid>
            <pubDate>Wed, 02 Apr 2025 18:22:53 GMT</pubDate>
            <description><![CDATA[<h2 id="도메인-객체-vs-dto-에-대한-유효성-검사">도메인 객체 vs DTO 에 대한 유효성 검사</h2>
<h3 id="-같은-곳에-유효성-검사를-추가하면-코드-중복이-발생한다">= 같은 곳에 유효성 검사를 추가하면 코드 중복이 발생한다.</h3>
<ul>
<li><p>도메인 지식 같은 경우 -&gt; Domain(Entity)에 유효성 검사를 해야한다.</p>
</li>
<li><p>도메인 지식과 무관하게 데이터 그 자체가 유효한가에 대한 검사 -&gt; DTO에서 해야한다.</p>
<pre><code class="language-java">@Getter
@Setter
@Entity
public class Food {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Integer id;// 고유 번호

 @Column(length = 50)
 @Size(max = 50)
 private String foodname; // 식품명

 @Column(length = 30)
 @Size(max = 30)
 private String kind; // 종류

 private List&lt;String&gt; createDate;//날짜

 private String storageArea; // 구역


</code></pre>
</li>
</ul>
<pre><code>private LocalDateTime modifyDate; // 최종 수정 날짜

@ManyToOne// 작성자와의 관계 설정
private SiteUser siteuser;

private String imagePath; //이미지 경로</code></pre><p>}</p>
<p> ```</p>
<ul>
<li><p>@Size를 추가하여 유효성 검사를 추가했다.</p>
<pre><code class="language-java">@Getter
@Setter
@Entity
public class FoodFormDto {
 @NotEmpty(message=&quot;식품 이름은 필수 항목입니다.&quot;)
 @NotNull
 private String foodName;

 @NotEmpty(message=&quot;식품 종류 선택은 필수 항목입니다.&quot;)
 @NotNull
 private String kind;

 @NotEmpty(message=&quot;유통기한은 필수 항목입니다.&quot;)
 @NotNull
 private List&lt;String&gt; createDate;//날짜

 @NotEmpty(message=&quot;영역은 필수 항목입니다.&quot;)
 @NotNull
 private String storageArea;
</code></pre>
</li>
</ul>
<ul>
<li>입력 폼에 대한 @NotEmpty @NotNull을 이용하여 유효성 검사를 추가했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SpringBoot - DTO]]></title>
            <link>https://velog.io/@seungho7-1/SpringBoot-DTO</link>
            <guid>https://velog.io/@seungho7-1/SpringBoot-DTO</guid>
            <pubDate>Tue, 18 Mar 2025 20:43:41 GMT</pubDate>
            <description><![CDATA[<h1 id="dto">DTO</h1>
<h2 id="--data-transfer-object란-의미로-폼에서-전달받은-데이터를-객체로-변환하는것">- Data Transfer Object란 의미로 폼에서 전달받은 데이터를 객체로 변환하는것.</h2>
<ul>
<li>dto로 받은 데이터는 최종적으로 데이터베이스로 저장.</li>
</ul>
<h3 id="1-데이터를-form을-통해-post-요청으로-데이터를-요청">1. 데이터를 form을 통해 Post 요청으로 데이터를 요청.</h3>
<ul>
<li>폼 데이터를 전송 받아 DTO 객체에 담아야한다. -&gt; post 요청</li>
<li>폼 데이터를  DTO에 담기 -&gt; 컨트롤러에 매개 변수로 담는다.</li>
<li>폼과 DTO 필드를 연결하자 -&gt; 타임리프를 통해 mapping</li>
<li>DTO를 데이터베이스에 저장하자 -&gt; DTO를 엔티티로 변환하자.</li>
<li>repository로 엔티티를  db에 저장하자 -&gt; repository.save(Entity);</li>
</ul>
<pre><code class="language-html">// foodcreate_form.html

&lt;form class=&quot;container&quot; action=&quot;/api/food&quot; method=&quot;post&quot;&gt;
 &lt;div class=&quot;mb-3&quot;&gt;
            &lt;label for=&quot;foodName&quot; class=&quot;form-label&quot;&gt;식품명&lt;/label&gt;
            &lt;input type=&quot;text&quot; name=&quot;foodName&quot; id=&quot;foodName&quot; class=&quot;form-control&quot; placeholder=&quot;ex) 버터 치킨 카레&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;mb-3&quot;&gt;
            &lt;label for=&quot;createDate&quot; class=&quot;form-label&quot;&gt;유통 기한&lt;/label&gt;
            &lt;input type=&quot;text&quot; name=&quot;createDate&quot; id=&quot;createDate&quot; class=&quot;form-control&quot; placeholder=&quot;ex) 25/01/01&quot;&gt;
&lt;/div&gt;
&lt;/form&gt;
</code></pre>
<pre><code class="language-java">  DTO

public class FoodFormDto{

private String foodName;
private String createDate;

// 전송 받은 이름과 날짜를 저장하는 생성자 추가
public foodFormDto(String foodName,String createDate){ 
this.foodName = foodName;
this.createDate = createDate;
}
// dto를 entity로 변환
public Food toEntity(){
return new food(null,foodName,createDate);
}</code></pre>
<pre><code class="language-java">@Controller
public class foodcontroller(FoodFormDto foodFormDto){
@Gemapping(&quot;/api/food&quot;)
public String create(FoodFormDto foodFormDto){
Food food = foodFormDto.toEntity();
this.foodrepository.save(food);

}</code></pre>
<pre><code class="language-java">@Entity
public class Food{
@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;// 고유 번호

    @Column(length = 50)
    private String foodname; // 식품명

    private String createDate; // 제조일자
}

public Food(String foodname,String createDate){
this.foodname = foodname;
this.createDate = createDate;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[식품 유통기한 리스트]]></title>
            <link>https://velog.io/@seungho7-1/%EC%8B%9D%ED%92%88-%EC%9C%A0%ED%86%B5%EA%B8%B0%ED%95%9C-%EC%B2%B4%ED%81%AC-%EB%A6%AC%EC%8A%A4%ED%8A%B81</link>
            <guid>https://velog.io/@seungho7-1/%EC%8B%9D%ED%92%88-%EC%9C%A0%ED%86%B5%EA%B8%B0%ED%95%9C-%EC%B2%B4%ED%81%AC-%EB%A6%AC%EC%8A%A4%ED%8A%B81</guid>
            <pubDate>Mon, 27 Jan 2025 22:22:05 GMT</pubDate>
            <description><![CDATA[<h2 id="일하면서-짜증나서-만든-식품-유통기한-체크리스트를-만들기로-했다">일하면서 짜증나서 만든 식품 유통기한 체크리스트를 만들기로 했다.</h2>
<h3 id="1-개요">1. 개요</h3>
<p>본 문서는 Spring Boot 3 기반의 유통기한 기록, 문의사항, 보충 리스트 관리 웹/모바일 반응형 웹사이트 구축에 대한 설계 과정, MVC 패턴 적용, 요구사항 정의한다.</p>
<h3 id="2-시스템-설계">2. 시스템 설계</h3>
<h4 id="21-아키텍처">2.1 아키텍처</h4>
<ul>
<li>프론트엔드: 반응형 디자인 (BootStrap)</li>
<li>백엔드: Spring Boot 3 (RESTful API), Spring Data JPA (데이터베이스 연동), Thymeleaf (서버 사이드 렌더링)</li>
<li>데이터베이스: MySQL 또는 H2 (개발 환경)</li>
<li>웹 스토리지: HTML5 Web Storage (localStorage 또는 sessionStorage)<h4 id="22-mvc-패턴">2.2 MVC 패턴</h4>
</li>
<li>Model: 데이터베이스 엔티티 (Entity) 및 데이터 처리 로직 (Service)</li>
<li>View: Thymeleaf 템플릿 (웹 페이지)</li>
<li>Controller: RESTful API 엔드포인트, 웹 페이지 요청 처리<h4 id="23-주요-기능">2.3 주요 기능</h4>
</li>
<li>유통기한 기록: 제품별 유통기한 등록, 수정, 삭제, 조회</li>
<li>문의사항 관리: 사용자 문의 등록, 답변, 조회</li>
<li>보충 리스트 관리: 유통기한 기반 보충 필요 제품 목록 생성, 보충 수량 기록, 웹 스토리지 연동</li>
</ul>
<h3 id="3-요구사항-정의">3. 요구사항 정의</h3>
<h4 id="31-기능-요구사항">3.1 기능 요구사항</h4>
<h3 id="1-식품-리스트-관리">&lt;1&gt; 식품 리스트 관리</h3>
<ul>
<li>제품 정보 (제품명, 유통기한, 종류) 등록/수정/삭제</li>
<li>제품 정보 검색 기능</li>
<li>페이징 기능</li>
<li>유통기한 임박/만료 제품 목록 조회
&lt; 변경 UI &gt;</li>
<li>카드로 제품들을 정리</li>
</ul>
<h3 id="2-보충-리스트">&lt;2&gt; 보충 리스트</h3>
<ul>
<li>보충 리스트 관리</li>
<li>현장에서 보충 수량 입력으로 웹 스토리지 저장</li>
<li>보충 완료 목록 조회</li>
<li>보충 이력 조회</li>
<li>리스트 검색 기능<h3 id="3-문의-사항">&lt;3&gt; 문의 사항</h3>
</li>
<li>문의사항 관리</li>
<li>사용자 문의 등록 (제품, 문의 내용)</li>
<li>관리자 답변 등록</li>
<li>문의 목록 조회 (사용자, 관리자)</li>
<li>문의 검색 기능 (제품, 내용)</li>
</ul>
<h4 id="32-비기능-요구사항">3.2 비기능 요구사항</h4>
<ul>
<li>반응형 디자인: 다양한 기기 (PC, 태블릿, 모바일) 지원</li>
<li>성능: 빠른 응답 속도, 대용량 데이터 처리</li>
<li>보안: 사용자 인증/인가, 데이터 암호화</li>
<li>사용성: 직관적인 UI/UX, 쉬운 사용법</li>
<li>확장성: 새로운 기능 추가 용이</li>
<li>유지보수성: 코드 가독성, 유지보수 용이</li>
</ul>
<h3 id="4-개발-과정">4. 개발 과정</h3>
<ul>
<li>요구사항 분석 및 설계: 기능 요구사항, 비기능 요구사항 정의, 시스템 아키텍처 설계</li>
<li>데이터베이스 설계: ERD 작성, 테이블 설계</li>
<li>백엔드 개발: Spring Boot 3, Spring Data JPA, RESTful API 개발</li>
<li>프론트엔드 개발: BootStrap -&gt; 반응형 디자인 적용</li>
<li>웹 스토리지 연동: 보충 리스트 데이터 웹 스토리지 저장/관리</li>
<li>테스트: 단위 테스트, 통합 테스트, 시스템 테스트</li>
<li>배포: 서버 배포, 운영 환경 설정</li>
<li>유지보수: 버그 수정, 기능 개선, 성능 최적화</li>
</ul>
<h3 id="5-기술-스택">5. 기술 스택</h3>
<ul>
<li>Spring Boot 3: 백엔드 프레임워크</li>
<li>Data JPA: 데이터베이스 연동</li>
<li>Thymeleaf: 서버 사이드 렌더링</li>
<li>Bootstrap: 반응형 디자인 프레임워크</li>
<li>MySQL/H2: 데이터베이스</li>
<li>HTML5 Web Storage: 웹 스토리지</li>
</ul>
<h3 id="6-문서화">6. 문서화</h3>
<ul>
<li>요구사항 정의서</li>
<li>시스템 설계서</li>
<li>데이터베이스 설계서</li>
<li>API 명세서</li>
<li>테스트 계획서</li>
<li>사용자 매뉴얼</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 컨테이너와 스프링 빈]]></title>
            <link>https://velog.io/@seungho7-1/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88</link>
            <guid>https://velog.io/@seungho7-1/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88</guid>
            <pubDate>Mon, 21 Oct 2024 19:18:39 GMT</pubDate>
            <description><![CDATA[<h2 id="스프링-컨테이너와-스프링-빈">스프링 컨테이너와 스프링 빈</h2>
<h3 id="스프링-컨테이너-생성">스프링 컨테이너 생성</h3>
<pre><code class="language-java">ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);</code></pre>
<ul>
<li>ApplicationContext -&gt; 스프링 컨테이너이자 인터페이스</li>
<li>스프링 컨테이너 xml기반,Annotation을 기반으로 자바 설정 클래스로 만들 수 있다.</li>
</ul>
<h3 id="스프링-컨테이너의-생성-과정">스프링 컨테이너의 생성 과정</h3>
<ol>
<li>스프링 컨테이너 생성 </li>
</ol>
<ul>
<li>스프링 컨테이너에 (key : 빈이름(메소드명),value : 빈 객체(메소드 반환명 impl)</li>
<li>구성 정보 = AppConfig.class</li>
<li>이러한 구성 정보를 지정하고 활용한다.</li>
</ul>
<ol start="2">
<li>스프링 빈 등록</li>
</ol>
<ul>
<li>빈 이름은 메서드 이름 사용.</li>
<li>빈 이름 직접 부여 가능<pre><code class="language-java">@Test
@Bean(name:&#39;sprintHi&#39;)
public ~~</code></pre>
</li>
</ul>
<p>3.스프링 빈 의존관계 설정(DI)</p>
<ul>
<li>메소드의 반환값을 의존하는 메서드로 입력.<pre><code class="language-java">@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
</code></pre>
</li>
</ul>
<p>@Bean
public MemberRepository memberRepository(){
return new MemberMeberRepository();
}</p>
<pre><code>
### 컨테이너에 등록된 모든 빈 조회
-&gt; 스프링 컨테이너에 등록한 빈들이 잘 등록 되었는가?
1. 내부 bean + 등록한 bean 출력
2. 내부 bean만 출력

### 스프링 빈 조회 방법
1. ac.getBean(빈이름,타입)
2. ac.getBean(타입)
3. 예외발생시(NoSuchBeanDefinitionException) 예외를 사용하여 조회

### 스프링 빈 조회 - 동일한 타입이 둘 이상일때
- 같은 타입의 스프링 빈일때 오류 발생 -&gt; 빈이름 저장하여 조회
- ac.getBeansOfType() -&gt; 모든 빈 조회 가능</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 - Sort(합병 정렬)]]></title>
            <link>https://velog.io/@seungho7-1/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Sort%ED%95%A9%EB%B3%91-%EC%A0%95%EB%A0%AC</link>
            <guid>https://velog.io/@seungho7-1/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Sort%ED%95%A9%EB%B3%91-%EC%A0%95%EB%A0%AC</guid>
            <pubDate>Sun, 13 Oct 2024 11:36:04 GMT</pubDate>
            <description><![CDATA[<h2 id="merge-sort">Merge Sort</h2>
<h3 id="정의-분할정복-방식을-사용해-데이터를-분할하고-분할한-집합을-합치는-알고리즘이다">정의: 분할정복 방식을 사용해 데이터를 분할하고 분할한 집합을 합치는 알고리즘이다.</h3>
<ul>
<li>시간 복잡도는 O(nlogn)</li>
<li><blockquote>
<p>배열의 크기를 n이라 할때, 분할 할때 마다 배열의 크기가 n/2씩 감소. 
이때 분할하는데 걸리는 시간은 O(log n)</p>
</blockquote>
</li>
<li><blockquote>
<p>정복(합병)할때 마다 두 배열을 비교하면서 작은 요소부터 차례로 결과 배열에 추가.
이 과정에서 최대 (n)개의 요소를 비교하게 되므로, 두 개의 배열을 합치는 데 걸리는 시간은 O(n)</p>
</blockquote>
</li>
<li>대량의 데이터 정렬에 주로 사용.</li>
</ul>
<pre><code class="language-java">public class sort {
    public static void merge(int A[], int low, int mid, int high) {
        //정렬할게요.
        int B[] = new int[high + 1];
        int h = low; //임시 배열 B의 low.
        int i = low; //배열 A의 low
        int j = mid + 1; // 배열 A의 mid +1
        int k;

        while (i &lt;= mid &amp;&amp; j &lt;= high) {
            if (A[i] &lt; A[j]) {
                B[h] = A[i]; //작은 놈이 임시 배열 low 지수에 저장.
                i++; // 왼쪽 배열 포인터 증가
            } else{
                B[h] = A[j];
                j++;// 오른쪽 배열 포인터 증가
            }
            h++; //임시배열도 저장할 포인터를 1만큼 증가.
        }

        // 비교할게 없다면?
        if (i &gt; mid) { // 오른쪽 배열을 싹다 대입.
            for (k = j; k &lt;= high; k++) {
                B[h] = A[k];
                h++;
            }
        }else { // 왼쪽 배열을 싹다 임시배열 B에 대입.
            for (k = i; k &lt;= mid; k++) {
                B[h] = A[k];
                h++;
            }
        }

        /*while (i &lt;= mid) {
            B[h] = A[i];
            i++;
            h++;
        }
        while (j &lt;= high) {
            B[h] = A[j];
            j++;
            h++;
        }

         */

        for (k = low; k&lt;=high; k++) {
            A[k] = B[k];
        }
    }

    public static void printArray(int A[]) {
        for (int i = 0; i &lt; A.length; i++) {
            System.out.print(A[i] + &quot; &quot;);
        }
    }

    public static void mergeSort(int A[], int low, int high) { //합병할게요
        if (low &lt; high) {
            int mid = (low+high) /2;
            mergeSort(A,low,mid);
            mergeSort(A,mid+1,high);
            merge(A,low,mid,high);
        }
    }

    public static void main(String[] args) {
        int A[] = {91, 82, 13, 85, 68, 70, 98, 24};
        System.out.println(&quot;정렬전&quot;);
        printArray(A);
        System.out.println(&quot;정렬후&quot;);
        mergeSort(A,0,A.length-1);
        printArray(A);
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 - Sort (Quick Sort)]]></title>
            <link>https://velog.io/@seungho7-1/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Sort-Quick-Sort</link>
            <guid>https://velog.io/@seungho7-1/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Sort-Quick-Sort</guid>
            <pubDate>Sat, 12 Oct 2024 16:20:35 GMT</pubDate>
            <description><![CDATA[<h2 id="quick-sort">Quick Sort</h2>
<h3 id="정의--분할정복-알고리즘으로-부분적으로-나누어가면서-정렬하는-방법">정의 : 분할정복 알고리즘으로 부분적으로 나누어가면서 정렬하는 방법.</h3>
<ul>
<li><p>시간 복잡도
최악: O(n^2)
평균: O(n*logn)</p>
</li>
<li><p>java에서 Arrays.sort()로 사용한다.</p>
</li>
</ul>
<h3 id="quick-sort-실행-방식">Quick Sort 실행 방식</h3>
<ol>
<li><p>배열을 두 부분으로 나눈다.
i) 배열 내 기준 요소보다 작으면 앞부분 배열에 위치, 크면 뒷부분 배열에 위치한다.</p>
</li>
<li><p>각 분할된 부분을 재귀적으로 정렬한다.</p>
</li>
</ol>
<pre><code class="language-java">package javastudy;

public class quickSort {
  public static void qs(int a[], int low, int high) {
    if (low &lt; high) {
      int s = partition(a, low, high); // 기준 pivot
      qs(a, low, s - 1); // 왼쪽 배열
      qs(a, s + 1, high); // 오른쪽 배열
    }
  }

  public static int partition(int a[], int low, int high) {
    int pivot = a[low]; // 피벗을 배열의 첫 번째 요소로 설정
    int i = low + 1;
    int j = high;

    while (i &lt;= j) {
      while (i &lt;= high &amp;&amp; a[i] &lt;= pivot) {
        i++;
      }
      while (j &gt;= low &amp;&amp; a[j] &gt; pivot) {
        j--;
      }
      if (i &lt; j) { // i가 j보다 작을 때만 교환
        int tmp = a[i];
        a[i] = a[j];
        a[j] = tmp;
      }
    }
    // 피벗을 올바른 위치에 배치
    a[low] = a[j];
    a[j] = pivot;

    return j; // 피벗의 최종 위치 반환
  }

  public static void printArray(int a[]) {
    for (int num : a) {
      System.out.print(num + &quot; &quot;);
    }
    System.out.println(); // 배열 출력 후 줄바꿈
  }

  public static void main(String[] args) {
    int a[] = {8, 5, 6, 2, 4};
    printArray(a);
    qs(a, 0, a.length - 1); // 배열의 크기를 사용하여 인덱스 설정
    printArray(a);
  }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 - Sort(Insertion Sort)]]></title>
            <link>https://velog.io/@seungho7-1/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-SortInsertion-Sort</link>
            <guid>https://velog.io/@seungho7-1/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-SortInsertion-Sort</guid>
            <pubDate>Sat, 12 Oct 2024 15:59:55 GMT</pubDate>
            <description><![CDATA[<h2 id="insertion-sort">Insertion Sort</h2>
<h3 id="정의--자신의-위치를-찾아-삽입하는-정렬-알고리즘">정의 : 자신의 위치를 찾아 삽입하는 정렬 알고리즘.</h3>
<ul>
<li>정렬된 부분 (A[0]), 정렬 되지 않은 부분 (A[1] ... A[n-1])으로 구분하여 정렬.</li>
<li>시간복잡도 : O(n^2)</li>
</ul>
<pre><code class="language-java">package javastudy;

public class insertSort {
  public static void is(int a[]) {
    int n = a.length;
    for (int i = 1; i &lt; n; i++) {
      int insertionKey = a[i]; // 삽입할 현재 요소
      int j = i - 1;

      while (j &gt;= 0 &amp;&amp; a[j] &gt; insertionKey) {
        a[j+1] = a[j]; // 한칸 뒤로
        j = j-1; // 왼쪽 index로 이동
      }
      a[j+1] = insertionKey; // 현재 요소 삽입
    }
  }

  public static void printArray(int a[]) {
    for (int num : a) {
      System.out.print(num+ &quot; &quot;);
    }
  }

  public static void main(String[] args) {
    int A[] = {8, 5, 6, 2, 4};
    System.out.println(&quot;정렬 전&quot;);
    printArray(A);
    System.out.println(&quot;정렬 후&quot;);
    is(A);
    printArray(A);
  }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 - sort (Selection Sort)]]></title>
            <link>https://velog.io/@seungho7-1/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-sort-Selection-Sort</link>
            <guid>https://velog.io/@seungho7-1/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-sort-Selection-Sort</guid>
            <pubDate>Sat, 12 Oct 2024 15:50:02 GMT</pubDate>
            <description><![CDATA[<h2 id="selection-sort">Selection Sort</h2>
<h3 id="정의--전체-배열에서-가장-작은-요소를-찾고-그-요소를-첫번째-요소와-교환하는-정렬">정의 : 전체 배열에서 가장 작은 요소를 찾고, 그 요소를 첫번째 요소와 교환하는 정렬.</h3>
<ul>
<li>시간 복잡도 : O(n^2)</li>
<li></li>
</ul>
<pre><code class="language-java">package javastudy;

public class selectionSort {
  public static void ss(int a[]) {
    int n = a.length;


    for (int i = 0; i &lt; n - 1; i++) {
      int min = i;
      for (int j = i+1; j &lt; n; j++) {
        if (a[min] &gt; a[j]) {
          min = j;
        }
      }
        if (min != i) {
          int tmp = a[i];
          a[i] = a[min];
          a[min] = tmp;
        }

    }
  }

  public static void printArray(int a[]) {
    for (int num : a) {
      System.out.print(num+&quot; &quot;);
    }
  }

  public static void main(String[] args) {
    int A[] = {8, 5, 6, 2, 4};
    System.out.println(&quot;정렬 전&quot;);
    printArray(A);
    System.out.println(&quot;정렬 후&quot;);
    ss(A);
    printArray(A);

  }

}
</code></pre>
]]></description>
        </item>
    </channel>
</rss>