<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>pockyjx_.log</title>
        <link>https://velog.io/</link>
        <description>백엔드 개발자👩🏻‍💻가 되고 싶다</description>
        <lastBuildDate>Wed, 06 Nov 2024 06:55:26 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>pockyjx_.log</title>
            <url>https://velog.velcdn.com/images/pockyjx_/profile/3553deb5-0dcf-489f-aebb-c790dc3e3a51/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. pockyjx_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/pockyjx_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Spring Batch+MSA] 배치 서버 도입을 통한 성능 최적화]]></title>
            <link>https://velog.io/@pockyjx_/Spring-BatchMSA-%EB%B0%B0%EC%B9%98-%EC%84%9C%EB%B2%84-%EB%8F%84%EC%9E%85%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@pockyjx_/Spring-BatchMSA-%EB%B0%B0%EC%B9%98-%EC%84%9C%EB%B2%84-%EB%8F%84%EC%9E%85%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Wed, 06 Nov 2024 06:55:26 GMT</pubDate>
            <description><![CDATA[<h1 id="⚠️-문제">⚠️ 문제</h1>
<blockquote>
<p><strong>잦은 업데이트</strong>로 인한 <strong>서버 및 데이터베이스 트래픽 증가</strong></p>
</blockquote>
<p>MFC에서 파트너의 포스팅, 파트너의 팔로우 등을 관리하는 SNS 서비스를 구현했는데, 이 중 사용자가 <strong>포스팅 좋아요 등록/취소</strong>나 <strong>파트너 팔로우 추가/취소</strong> 버튼을 계속해서 클릭했을 때 각 작업에 대해 <strong>업데이트</strong> 구문이 발생하게 되는데, 이 부분에서 <strong>MySQL 서버에 계속 접근하는 과정에서 너무 많은 부하를 발생시키지 않을까?</strong> 라는 생각이 들었다.</p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/0afcf857-c2c9-4826-a6d6-325830a36867/image.png" alt=""> 실제로 동시에 한 포스팅 혹은 파트너에 대해 <strong>n명의 사용자</strong>가 <strong>좋아요</strong>나 팔로우를 등록하기만 해도 총 <strong>n건의 업데이트문</strong>이 발생하게 된다. 업데이트문이 실행될 때마다 MySQL 서버에 접근하게 되고, 이러한 작업이 동시에 많이 발생하게 되면 그만큼 부하가 증가하여 처리 속도가 길어지게 된다. </p>
<hr>
<h1 id="💡-해결-방안--배치-서버-도입">💡 해결 방안 : 배치 서버 도입</h1>
<blockquote>
<p>🌟 <strong>배치 서비스</strong> 도입 : <strong>업데이트 내역을 집계</strong>하여 배치로 한 번에 업데이트 처리</p>
</blockquote>
<p>배치 로직 자체는 <a href="https://velog.io/@pockyjx_/Spring-Batch-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0">SSG.COM 프로젝트에서 적용했던 배치 처리</a>와 유사하지만, 이번 프로젝트는 <strong>MSA</strong> 환경으로 구성되어 있기 때문에 처리 로직이 더 복잡해질 수 밖에 없다..</p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/c219d9bf-b2d4-4e43-88c3-099078f4b886/image.png" alt=""> 일단 전반적인 로직은 <strong>SNS 서비스</strong>에서 <strong>업데이트</strong> 작업 발생하면 <strong>Kafka</strong>를 통해 <strong>포스팅/파트너 ID와 업데이트 내역</strong>을 <strong>배치 서비스에 전달</strong>하고, 배치 서비스에서는 이를 <strong>Redis에 집계</strong>해두었다가 스케줄러를 통해 특정 시간마다 업데이트 내역을 <strong>MySQL에 최종적으로 반영</strong> 하도록 구성하였다.</p>
<p>여기서 업데이트 내역을 최대한 빠르게 반영하기 위해 스케줄러 실행 간격을 초 단위로 설정하였다. 이외에도 해당 로직을 구성하기 위해 고민했던 2가지의 문제를 아래에 자세히 설명해보겠다.</p>
<hr>
<h2 id="🤔-고민">🤔 고민</h2>
<blockquote>
<ol>
<li>배치 로직 구현 : <strong>기존 서비스</strong> VS <span style="color:red"><strong>배치 서버 분리</span></strong></li>
</ol>
</blockquote>
<p>이전에는 모놀리식 구조였기 때문에 비즈니스 로직과 배치 로직이 하나의 서비스에 구현되었지만, 이번 프로젝트는 <strong>MSA</strong> 환경이였기 때문에 많은 고민을 했는데,, 결국 <strong>배치 서버를 분리</strong>하기로 한 이유는 아래와 같다,,
<br></p>
<ul>
<li><p>다른 서비스에서도 <strong>배치 로직</strong>이 필요했는데, 이를 각 서비스에서 관리하기 보다는 하나의 서비스에서 <strong>일괄 관리</strong>하는 것이 효율적이라고 판단함</p>
</li>
<li><p>배치 로직이 짧은 간격으로 실행되기 때문에, 배치 로직에서 많은 자원을 사용하게 되면 <strong>다른 비즈니스 로직의 실행에 영향</strong>을 미치게 됨</p>
</li>
<li><p><strong>장애 전파를 최소화</strong>하기 위한 MSA의 장점을 살려, 배치 서비스에서 문제가 발생해도 기존 서비스는 정상적으로 실행될 수 있도록 설계</p>
<br>

</li>
</ul>
<p>이를 구현하기 위해서는 SNS 서비스 업데이트 내역을 배치 서비스로 전달하기 위한 로직이 추가적으로 필요했고, 이는 Kafka를 통해 구현하였다. Kafka를 선택하게 된 이유와 데이터 전달 로직과 관련된 자세한 내용은 <a href="https://velog.io/@pockyjx_/MSA-EDA-Kafka%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%84-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0">이 글</a>에서 자세히 다루고 있다! <span style="color:lightgray">(데이터 전달 관련은 이 부분은 아니긴 함..)</span>
<br></p>
<blockquote>
<ol start="2">
<li>업데이트 내역 집계 : <strong>Map</strong> VS <span style="color:red"><strong>Redis</span></strong></li>
</ol>
</blockquote>
<p>배치 서비스는 전달받은 데이터를을 사용하여 <strong>다음 스케줄러가 실행되기 전까지의 업데이트 내역</strong>을 집계해야 한다. (ex : 30초 사이에 n번 글의 좋아요 개수가 4개 증가 → <code>n, +4</code>)</p>
<p>각 ID 별로 내역을 집계해야 하기 때문에 <strong><code>&lt;ID : 업데이트 내역&gt;</code></strong>, 즉, <strong><code>&lt;Key, Value&gt;</code></strong> 형태로 데이터를 저장할 수 있는 Map과 Redis 중에 고민하다가,, 최종적으로 <strong>Redis</strong>를 선택하게 된 이유는 다음과 같다.
<br></p>
<ul>
<li><p>Map은 메모리 기반이기 때문에 애플리케이션 서버가 꺼지면 저장된 데이터가 휘발되지만, <strong>Redis</strong>는 별도의 데이터베이스 서버 기반으로 동작하므로 <strong>애플리케이션의 서버가 꺼져도 저장된 데이터가 유지</strong>됨</p>
</li>
<li><p><strong>빠른 데이터 접근</strong> 및 대규모 데이터 저장에 적합 </p>
</li>
</ul>
<hr>
<h2 id="💻-구현">💻 구현</h2>
<blockquote>
<p>포스팅과 파트너 처리 로직이 동일하므로, <strong>포스팅 관련 작업에 대한 코드</strong>만 작성 + 배치 처리 로직이 중요하므로 Config, Controller 등의 일부 내용은 생략될 예정!</p>
</blockquote>
<hr>
<h3 id="1-sns-서비스">1. SNS 서비스</h3>
<blockquote>
<p>✅ 포스팅 <strong>좋아요 등록/취소</strong> 요청 시 <strong><code>ID, 업데이트 내역</code></strong>을 <strong>배치 서비스</strong>로 *<em>전송 *</em></p>
</blockquote>
<h4 id="--kafkaproducer-postsummarydto">- <code>KafkaProducer</code>, <code>PostSummaryDTO</code></h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class KafkaProducer {
    private final KafkaTemplate&lt;String, Object&gt; kafkaTemplate;

    public void createBookmark(PostSummaryDto dto) { // 좋아요 등록
        kafkaTemplate.send(&quot;create-bookmark&quot;, dto);
    }

    public void deleteBookmark(PostSummaryDto dto) {
        kafkaTemplate.send(&quot;delete-bookmark&quot;, dto); // 좋아요 취소
    }
}

@Getter
@Builder
public class PostSummaryDto {
    private Long postId;
    private String partnerId;
}</code></pre>
<p><span style="color:gray">지금 와서 드는 생각인데,, producer를 두 개로 분리하지 말고 producer는 하나만 두고 DTO에 업데이트 상태?(등록/취소)를 구분하는 추가해서 분기점 두고 구현하는 게 훨씬 덜 복잡하기 편했을 것 같기도,, </span></p>
<h4 id="--bookmarkserviceimpl">- <code>BookmarkServiceImpl</code></h4>
<ul>
<li><code>createBookmark</code> : 좋아요 내역 저장 후, 메시지 생성</li>
<li><code>deleteBookmark</code> : 좋아요 취소 시, 좋아요 내역 삭제 후 메시지 생성</li>
</ul>
<pre><code class="language-java">@Service
@Transactional
@Slf4j
@RequiredArgsConstructor
public class BookmarkServiceImpl implements BookmarkService {
    private final BookmarkRepository bookmarkRepository;
    private final KafkaProducer producer;

    @Override
    public void createBookmark(Long postId, String userId) { // 좋아요 등록
        if(bookmarkRepository.existsByPostIdAndUserId(postId, userId)) {
            throw new BaseException(BOOKMARK_CONFLICT);
        }

        bookmarkRepository.save(Bookmark.builder()
                .userId(userId)
                .post(postId)
                .build());

        producer.createBookmark(PostSummaryDto.builder().postId(postId).build());
    }

    @Override
    public void deleteBookmark(Long postId, String userId) { // 좋아요 취소
        int cnt = bookmarkRepository.deleteByPostId(postId, userId);
        if(cnt &gt; 0) producer.deleteBookmark(PostSummaryDto.builder().postId(postId).build());
    }
}</code></pre>
<hr>
<h3 id="2-배치-서비스-1--업데이트-내역-집계">2. 배치 서비스 (1) : 업데이트 내역 집계</h3>
<blockquote>
<p>✅ 업데이트 내역을 <strong>Redis</strong>에 집계</p>
</blockquote>
<h4 id="--kafkaconsumer">- <code>KafkaConsumer</code></h4>
<ul>
<li><strong><code>createBookmark</code></strong> : <strong><code>increment(key, 1)</code></strong>을 통해 key번 글의 <strong>좋아요 개수 +1</strong></li>
<li><code>deleteBookmark</code> : <strong><code>decrement(key, 1)</code></strong>을 통해 key번 글의 <strong>좋아요 개수 -1</strong></li>
</ul>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class KafkaConsumer {
    private final PostSummaryRepository postSummaryRepository;
    private final RedisTemplate&lt;String, Object&gt; redisTemplate;
    private static final String POST_PREFIX = &quot;post:like:&quot;;

    @KafkaListener(topics = &quot;create-bookmark&quot;, containerFactory = &quot;postSummaryListener&quot;)
        String key = POST_PREFIX + dto.getPostId();
        redisTemplate.opsForValue().increment(key, 1);
    }

    @KafkaListener(topics = &quot;delete-bookmark&quot;, containerFactory = &quot;postSummaryListener&quot;)
    public void deleteBookmark(PostSummaryDto dto) {
        String key = POST_PREFIX + dto.getPostId();
        redisTemplate.opsForValue().decrement(key, 1);
    }
}</code></pre>
<hr>
<h3 id="3-배치-서비스-2--배치-로직-구현-및-실행">3. 배치 서비스 (2) : 배치 로직 구현 및 실행</h3>
<blockquote>
<p>✅ <strong>Redis의 집계 내역</strong>을 <strong>MySQL</strong>에 최종적으로 <strong>반영</strong> → <strong>30초</strong> 간격으로 실행</p>
</blockquote>
<h4 id="--postjobconfig">- <code>PostJobConfig</code></h4>
<ul>
<li><strong><code>sacnKeys(pattern)</code></strong> : Redis 내에 저장된 다양한 업데이트 내역 중, <strong>구별자</strong>를 통해 <strong>포스팅 관련 데이터만 조회</strong>하기 위한 메서드</li>
</ul>
<pre><code class="language-java">private Set&lt;String&gt; scanKeys(String pattern) {
    Set&lt;String&gt; keys = new HashSet&lt;&gt;();
    ScanOptions options = ScanOptions.scanOptions().match(pattern).count(1000).build();

    try (Cursor&lt;byte[]&gt; cursor = redisTemplate.getConnectionFactory()
            .getConnection()
            .scan(options)) {

        while (cursor.hasNext()) {
            keys.add(new String(cursor.next()));
        }
    } catch (Exception e) {
        throw new RuntimeException(&quot;Failed to scan Redis keys&quot;, e);
    }
    return keys;
}
</code></pre>
<ul>
<li><strong><code>ItemReader</code></strong> : Redis에서 <strong>포스팅 관련 내역</strong>을 조회하여 <strong>포스팅 ID</strong>와 <strong>업데이트 내역</strong>을 <strong><code>PostBookmarkDTO</code></strong> 형태로 저장했다면, <strong>Redis</strong>에는 다음 기간동안의 집계 내역을 새로 저장해야 하므로 <strong>기존의 집계 내역 삭제</strong></li>
</ul>
<pre><code class="language-java">@Bean(name = &quot;postReader&quot;)
@StepScope
public ItemReader&lt;PostBookmarkDto&gt; postSummaryItemReader() {
    Set&lt;String&gt; keys = scanKeys(&quot;post:like:*&quot;);
    List&lt;PostBookmarkDto&gt; bookmarkList = new ArrayList&lt;&gt;();

    for (String key : keys) {
        Long postId = Long.valueOf(key.replace(&quot;post:like:&quot;, &quot;&quot;));
        Integer bookmarkCnt = Integer.valueOf((String)redisTemplate.opsForValue().get(key));

        bookmarkList.add(new PostBookmarkDto(postId, bookmarkCnt));
        redisTemplate.delete(key);
    }
    return new ListItemReader&lt;&gt;(bookmarkList);
}</code></pre>
<ul>
<li><strong><code>ItemProcessor</code></strong> : <strong><code>PostBookMarkDTO</code> → <code>PostSummary</code></strong>  변환 시, <strong>기존 좋아요 개수 + 업데이트 내역</strong></li>
<li><strong><code>ItemWriter</code></strong> : <strong>MySQL</strong>에 업데이트 내역 <strong>최종 반영</strong></li>
</ul>
<pre><code class="language-java">@Bean(name = &quot;postProcessor&quot;)
@StepScope
public ItemProcessor&lt;PostBookmarkDto, PostSummary&gt; postSummaryProcessor() {
    return dto -&gt; {
        PostSummary summary = postSummaryRepository.findByPostId(dto.getPostId())
                .orElseThrow(() -&gt; new BaseException(BaseResponseStatus.POST_NOT_FOUND));

        return PostSummary.builder()
                .id(summary.getId())
                .postId(dto.getPostId())
                .bookmarkCnt(summary.getBookmarkCnt() + dto.getBookmarkCnt())
                .build();
    };
}</code></pre>
<p>전체 코드는 너무 길어서,, <a href="https://github.com/2-PERSONULL/mfc-batch/blob/develop/src/main/java/com/mfc/batch/batch/application/PostJobConfig.java">깃허브</a>에서 확인 가능!</p>
<h4 id="--jobscheduler">- <code>JobScheduler</code></h4>
<pre><code class="language-java">@Slf4j
@Configuration
public class JobScheduler {
    private final JobLauncher jobLauncher;
    private final Job postSummaryJob;

    @Scheduled(fixedRate = 30000) // 30초 간격으로 실행
    public void postJobSchedule() throws Exception {

        JobParameters jobParameters = new JobParametersBuilder()
                .addLocalDateTime(&quot;time&quot;, LocalDateTime.now())
                .toJobParameters();

        JobExecution postJob = jobLauncher.run(postSummaryJob, jobParameters);
    }
}</code></pre>
<hr>
<h2 id="⚙️-실행">⚙️ 실행</h2>
<h4 id="초기-데이터">초기 데이터</h4>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/ea6a3741-bf38-4dfb-b464-ca987807498b/image.png" alt=""></p>
<h4 id="실행-결과">실행 결과</h4>
<blockquote>
<ul>
<li><strong>14번</strong> 포스팅에 사용자 <strong>A, B, C</strong>가 <strong>좋아요 등록</strong> (+3)</li>
</ul>
</blockquote>
<ul>
<li><strong>20번</strong> 포스팅에 사용자 <strong>A, B, C</strong>가 <strong>좋아요 등록</strong> 후, <strong>A</strong>가 <strong>좋아요 취소</strong> (+3-1=+2)</li>
<li><strong>25번</strong> 포스팅에 사용자 <strong>B</strong>가 <strong>좋아요 등록</strong> 후, <strong>취소</strong> (+1-1=0)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/1e8ea5bf-5e33-4feb-b345-d5eb2e9d2973/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/537065b9-7117-4934-b331-8babb8ca4ce4/image.png" alt=""> : <code>kafkaConsumer</code>에서 집계 과정에 로그를 찍어서 확인해본 결과, 좋아요 등록<strong>텍스트</strong> 시에는 <strong><code>집계값+1</code></strong>, <strong>취소</strong> 시에는 <strong><code>집계값-1</code></strong>을 수행하는 것을 확인할 수 있다!</p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/0d6ee851-6ef1-48cc-8570-9988c9079e75/image.png" alt=""> : 배치 실행 후 <code>ItemWriter</code>에서 실행된 쿼리를 보면, <strong>총 2개의 업데이트문</strong>이 실행된 것을 확인할 수 있다. <strong>25번</strong> 포스팅의 경우 등록과 취소가 한 번에 이루어져 <strong>집계값이 0</strong>이기 때문에 <strong>값이 변하지 않았고</strong>, 이로 인해 <strong>JPA의 변경 감지가 적용되지 않아</strong> 업데이트문이 별도로 실행되지 않았다.</p>
<p>업데이트문은 각 포스팅에 대해 실행되기 때문에 <strong>N개의 포스팅</strong>에 대한 업데이트가 발생하면 <strong>N개의 업데이트문</strong>이 실행되기는 하겠지만, 그래도 위 작업들을 <strong>배치 처리 없이 각자 업데이트</strong> 처리 했다면 <strong>3개의 포스팅</strong>에 대해 총 <strong>9개</strong>(14번 3개, 20번 4개, 25번 2개)의 업데이트문이 발생했을 것이다. 쿼리 개수가 줄어든 것은 물론이고, <strong>다수의 업데이트문</strong>이 <strong>하나의 트랜잭션</strong>으로 처리되므로 <strong>DB 서버에 대한 접근이 줄어</strong> 트래픽 수 또한 줄일 수 있을 것이다!</p>
<p>➕</p>
<p>그러나 이를 완벽한 해결책이라고 볼 수는 있을까? 처음에는 집계 처리 과정에서 발생하는 업데이트 처리 최적화에 대해서만 고민해서 위와 같은 해결책을 생각했었는데, 배치 로직을 구현하다 보니 <strong>Redis에 접근</strong>하거나, <strong>변경 감지를 위한 SELECT문 추가 실행</strong>에 대한 트래픽 문제가 신경쓰이기 시작했다,, 변경 감지의 경우 JPA를 사용하지 않으면 해결될 것 같기도 하고?,, 하여튼 이 부분에 대해서는 더 고민해봐야 할 것 같다,,!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Batch] 대용량 데이터 업데이트 처리하기]]></title>
            <link>https://velog.io/@pockyjx_/Spring-Batch-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@pockyjx_/Spring-Batch-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 03 Nov 2024 07:51:02 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-spring-batch">💡 Spring Batch</h1>
<blockquote>
<p><strong>대용량 데이터</strong>를 효율적으로 <strong>일괄(배치) 처리</strong>하기 위한 프레임워크</p>
</blockquote>
<p>: Spring Batch <strong>로깅/추적, 트랜잭션 관리, 작업 재시작/건너뛰기, 작업 처리 통계, 리소스 관리</strong> 등 배치 처리에 필요한 다양한 기능들을 제공</p>
<hr>
<h2 id="📒-용어">📒 용어</h2>
<blockquote>
<p>배치 처리 시 사용되는 기본적인 <strong>객체 및 테이블명</strong> 정리!</p>
</blockquote>
<ul>
<li><p><strong><code>Job</code> *<em>: *</em>한 번에 처리</strong>해야 할 <strong>배치 작업</strong> 단위(객체)</p>
</li>
<li><p><strong><code>JobInstance</code></strong> : <strong>Job</strong>의 논리적 <strong>실행 단위</strong> → 실행될 때마다 새로운 JobInstance 생성</p>
</li>
<li><p><strong><code>JobParameter</code></strong> : Job이 실행될 때 <strong>필요한 파라미터</strong>를 제공하고, 이를 통해 <strong>JobInstance를 구분</strong> → <strong><code>String</code>, <code>Double</code>, <code>Long</code>, <code>Date</code></strong> 타입 지원</p>
</li>
<li><p><strong><code>JobExecution</code></strong> : <strong>JobInstance 실행 시도</strong>에 대한 객체 → 시작/종료시간, <strong>상태</strong> 등에 대한 정보를 담고 있으며, 실패 후 <strong>재시도</strong> 시 <strong>별도의 JobExecution</strong> 생성</p>
</li>
<li><p><strong><code>Step</code></strong> : 처리해야 할 <strong>작업 내용</strong>(단계) → <strong>Job</strong>은 <strong>하나 이상의 Step</strong>으로 이루어지며, 각 Step은 <strong>순차적</strong>으로 처리됨</p>
</li>
<li><p><strong><code>StepExecution</code></strong> : <strong>Step의 실행 시도</strong>에 대한 객체 → JobInstance와 유사하며, read/write/commit/skip 수 등의 정보도 저장</p>
</li>
<li><p><strong><code>ExecutionContext</code></strong> : Step 혹은 Job의 <strong>실행 중</strong>에 발생하는 <strong>데이터를 저장하고 공유</strong> → 작업 실패 시, ExecutionContext에 저장된 데이터를 통해 마지막 실행 상태를 재구성하여 작업 수행</p>
<ul>
<li><code>JobExecutionContext</code> : Commit 시점에 저장</li>
<li><code>StepExecutionContext</code> : 실행 사이에 저장</li>
</ul>
</li>
<li><p><strong><code>JobRepository</code></strong> : <strong>배치 처리</strong>와 관련된 <strong>모든 정보</strong>(Job/StepExecution, JobParameters)를 저장하고 관리</p>
</li>
<li><p><strong><code>JobLauncher</code></strong> : Job과 JobParameters를 사용하여 <strong>Job을 실행</strong>하고, 생명주기를 관리</p>
</li>
<li><p><strong><code>ItemReader</code></strong> : 배치 작업에서 <strong>처리할 아이템(데이터)을 읽어</strong>오기 위한 인터페이스</p>
</li>
<li><p><strong><code>ItemProcessor</code></strong> : ItemReader로부터 <strong>읽어온 데이터를 처리</strong>(필터링, 변환 등)하기 위한 인터페이스</p>
</li>
<li><p><strong><code>ItemWriter</code></strong> : 배치 <strong>처리된 데이터</strong>를 최종적으로 <strong>기록</strong>(DB에 저장, 파일 생성, 메시지 발행 등)하기 위한 인터페이스</p>
</li>
<li><p><strong><code>Tasklet</code></strong> : 간단한 <strong>단일 작업</strong> 시 사용</p>
</li>
<li><p><strong><code>Chunk</code></strong> : <strong>한 번에 처리 될 row수</strong>를 의미하는 <strong>Chunk</strong>라는 단위를 생성하고, <strong>해당 단위로 트랜잭션</strong>을 수행 → 주로 <strong>대용량 데이터</strong> 처리 시 사용되며, Reader, Processor, Writer 단계를 거침</p>
</li>
</ul>
<hr>
<h2 id="💻-구현">💻 구현</h2>
<blockquote>
<p>Spring Boot 3.x + <strong>Spring Batch 5.x</strong> 버전 사용!</p>
</blockquote>
<hr>
<h3 id="⚙️-설정">⚙️ 설정</h3>
<h4 id="--buildgradle">- <code>build.gradle</code></h4>
<pre><code class="language-gradle">implementation &#39;org.springframework.boot:spring-boot-starter-batch&#39; # batch
implementation &#39;org.springframework.boot:spring-boot-starter-quartz&#39; # 스케줄러</code></pre>
<h4 id="--applicationyml">- <code>application.yml</code></h4>
<ul>
<li><p><strong><code>job.enabled</code></strong> : <strong>서비스가 실행</strong>될 때, <strong>배치 작업</strong>을 자동으로 <strong>실행할 것인지</strong>에 대한 설정</p>
<ul>
<li><code>true</code> : 서비스 실행 시 배치 실행 / <code>false</code> : 실행 X</li>
</ul>
</li>
<li><p><strong><code>jdbc.initialize-schema</code></strong> : 배치 관련 <strong>메타 테이블</strong> 생성 여부</p>
<ul>
<li><code>always</code> : 항상 생성 / <code>embedded</code> : 내장 DB인 경우, 생성 / <code>never</code> : 생성하지 않음</li>
</ul>
</li>
</ul>
<pre><code class="language-yml">spring:
  batch:
    job:
      enabled: false
    jdbc:
      initialize-schema: always</code></pre>
<p>만약 <code>jdbc:initialize-schema: always</code> 임에도 테이블이 생성되지 않는다면, <strong><code>schema-db명.sql</code></strong> 파일을 찾아서 <strong>스크립트</strong>를 복사한 후 DB에서 직접 실행시켜주면 된다.</p>
<p>인텔리제이의 경우 CTRL+N 후 files에서 schema로 검색하면 쉽게 찾을 수 있음!<img src="https://velog.velcdn.com/images/pockyjx_/post/5208d2c0-5d29-4739-89e1-d291f24c6d73/image.png" alt=""><img src="https://velog.velcdn.com/images/pockyjx_/post/fa52dce3-960d-4e81-a7ce-a85c9a177b80/image.png" alt=""> <img src="https://velog.velcdn.com/images/pockyjx_/post/1e879b6f-38b4-4a96-abf2-07157f31bcda/image.png" alt=""> 총 9개의 메타테이블이 생성되어야 함!</p>
<hr>
<h3 id="📝-처리-로직">📝 처리 로직</h3>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/7e0c22cc-ef0b-47bd-b1af-dc5fc00e2854/image.png" alt=""></p>
<p><strong>각 상품에 대한 집계 데이터(평균 별점, 리뷰 개수)</strong>를 리뷰가 등록될 때마다 업데이트 하는 것은 비효율적이라고 판단했고, 이를 해결하기 위해 <strong>30분 단위로 데이터를 집계하여 업데이트</strong>하는 방식으로 로직을 구현하기로 함. </p>
<p>이런 방식으로 구현하면 리뷰 데이터를 실시간으로 반영할 수 없다는 문제가 발생하기는 하는데, 실제 쇼핑몰에서도 리뷰 검수 등의 단계를 거쳐 바로 보여주지 않는 곳이 있기 때문에 괜찮다고 판단했고,, 대신 시간을 30분 단위로 정하여 최대한 빠르게 반영될 수 있도록 처리하기로 함!</p>
<hr>
<h3 id="✍🏻-코드">✍🏻 코드</h3>
<h4 id="--jobconfig">- <code>JobConfig</code></h4>
<blockquote>
<p>실행할 <strong>Job</strong>에 대한 정의 : Step, Reader, Processor, Writer 등</p>
</blockquote>
<h4 id="job">Job</h4>
<ul>
<li><p><code>JobRepository</code>와 트랜잭션 매니저를 생성하고, 이를 각 스텝마다 전달</p>
</li>
<li><p><strong><code>JobBuilder</code></strong> : <strong>Job의 이름, Step</strong> 등을 정의 ➡️ 해당 Job은 스텝을 하나만 가지고 있지만, <strong>Step</strong>을 <strong>순차적</strong>으로 실행시키고 싶다면 <strong><code>.next(step)</code></strong>으로 이어나가면 됨!</p>
</li>
</ul>
<pre><code class="language-java">@Bean(name = &quot;itemSummaryJob&quot;)
public Job itemSummaryJob(JobRepository jobRepository, PlatformTransactionManager txm) throws Exception {
    return new JobBuilder(&quot;itemSummaryJob&quot;, jobRepository)
                .start(step(jobRepository, txm))
                .build();
}</code></pre>
<h4 id="step">Step</h4>
<ul>
<li><p><strong><code>@JobScope</code></strong> : Job 실행 시 <strong>bean</strong>을 생성하며, <strong>Job의 생명주기</strong> 관리</p>
</li>
<li><p><strong><code>StepBuilder</code></strong> : <strong>Step의 이름, 작업 단위</strong>(taskle/chunk) 등을 정의</p>
<ul>
<li><p><strong><code>Chunk</code></strong> 단위 사용 : 상품 데이터가 약 1000개 이므로, <strong>100개 단위</strong>로 묶어서 트랜잭션 실행</p>
</li>
<li><p><code>allowStartIfComplete</code> : 이미 완료된 Job을 실행시킬 지에 대한 설정</p>
<pre><code class="language-java">@Bean(name = &quot;itemSummaryStep&quot;)
@JobScope
public Step step(JobRepository jobRepository, PlatformTransactionManager txm) throws Exception {
return new StepBuilder(&quot;itemSummaryStep&quot;, jobRepository)
            .&lt;ReviewSummaryDTO, ItemCalc&gt;chunk(100, txm)
            .reader(reader())
            .processor(processor())
            .writer(writer())
            .allowStartIfComplete(true)
            .build();
}</code></pre>
</li>
</ul>
</li>
</ul>
<h4 id="itemreader">ItemReader</h4>
<ul>
<li><p><strong><code>@StepScope</code></strong> : Step 실행 시 <strong>bean</strong>을 생성하며, <strong>Step의 생명주기</strong> 관리</p>
</li>
<li><p><code>Review</code> 테이블에서 각 <strong>상품 별 집계 데이터</strong>를 조회하여 <strong><code>ReviewSummaryDTO</code> 형태</strong>로 저장하기 위한 Reader 🌟</p>
</li>
<li><p><strong><code>JpaPagingItemReaderBuilder</code></strong> : <strong>JPA</strong>의 <strong>페이징</strong>을 활용하여 데이터를 읽어오기 위한 <strong><code>ItemReader</code> 구현체</strong> → 원래는 Querydsl을 활용하려고 했으나,, 구현체를 직접 구현해야 해서 이미 구현되어 있는 페이징 구현체 사용,,</p>
<ul>
<li><strong><code>pageSize</code></strong> : <strong>Chunk의 단위 사이즈</strong>와 동일하게 지정 (100개)</li>
<li><strong><code>queryString</code></strong> : 데이터를 읽어오기 위한 쿼리</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Bean
@StepScope
public JpaPagingItemReader&lt;ReviewSummaryDTO&gt; reader() throws Exception {

    return new JpaPagingItemReaderBuilder&lt;ReviewSummaryDTO&gt;()
                .pageSize(100)
                .queryString(
                        &quot;select new com.comehere.ssgserver.review.dto.resp.ReviewSummaryDTO(&quot;
                                + &quot;min(i.id), min(ic.id), round(avg(r.star), 1), count(r)) &quot;
                                + &quot;from Review r &quot;
                                + &quot;join Item i on r.itemCode = i.code &quot;
                                + &quot;join ItemCalc ic on i.id = ic.itemId &quot;
                                + &quot;group by r.itemCode&quot;
                )
                .entityManagerFactory(emf)
                .name(&quot;itemSummaryReader&quot;)
                .build();
    }</code></pre>
<h4 id="itemprocessor">ItemProcessor</h4>
<ul>
<li><p>Reader에서 읽어온 <strong><code>ReviewSummaryDTO</code></strong>를 <strong><code>ItemCalc</code> 형태로 변환</strong>하기 위한 Processor 🌟</p>
</li>
<li><p><strong><code>ItemProcessorBuilder</code></strong> : <strong><code>process(가공할 데이터)</code></strong>를 통해 <strong>데이터 처리</strong> 로직 구현</p>
</li>
</ul>
<pre><code class="language-java">@Bean
@StepScope
public ItemProcessor&lt;ReviewSummaryDTO, ItemCalc&gt; processor() {
    return new ItemProcessor&lt;ReviewSummaryDTO, ItemCalc&gt;() {
        @Override
        public ItemCalc process(ReviewSummaryDTO dto) throws Exception {
            return ItemCalc.builder()
                    .id(dto.getCalcId())
                    .reviewCount(dto.getReviewCount())
                    .averageStar(dto.getAverageStar())
                    .build();
        }
    };
}</code></pre>
<h4 id="itemwriter">ItemWriter</h4>
<ul>
<li>Proccesor에서 처리한 <strong><code>ItemCalc</code></strong>를 <strong>데이터베이스의 <code>Item_Calc</code> 테이블</strong>에 최종적으로 저장 ➡️ <strong>변경 내역 업데이트</strong> 🌟</li>
</ul>
<pre><code class="language-java">@Bean
@StepScope
public JpaItemWriter&lt;ItemCalc&gt; writer() {
    return new JpaItemWriterBuilder&lt;ItemCalc&gt;()
            .entityManagerFactory(emf)
            .build();
}</code></pre>
<h4 id="✅-전체-코드">✅ 전체 코드</h4>
<pre><code class="language-java">@Slf4j
@Configuration
@RequiredArgsConstructor
@Transactional
public class BatchJobConfig {
    private final EntityManagerFactory emf;

    @Bean(name = &quot;itemSummaryJob&quot;)
    public Job itemSummaryJob(JobRepository jobRepository, PlatformTransactionManager txm) throws Exception {
        log.info(&quot;job start&quot;);
        return new JobBuilder(&quot;itemSummaryJob&quot;, jobRepository)
                .start(step(jobRepository, txm))
                .build();
    }

    @Bean(name = &quot;itemSummaryStep&quot;)
    @JobScope
    public Step step(JobRepository jobRepository, PlatformTransactionManager txm) throws Exception {
        log.info(&quot;step start&quot;);
        return new StepBuilder(&quot;itemSummaryStep&quot;, jobRepository)
                .&lt;ReviewSummaryDTO, ItemCalc&gt;chunk(100, txm)
                .reader(reader())
                .processor(processor())
                .writer(writer())
                .allowStartIfComplete(true)
                .build();
    }

    @Bean
    @StepScope
    public JpaPagingItemReader&lt;ReviewSummaryDTO&gt; reader() throws Exception {
        log.info(&quot;reader start&quot;);

        return new JpaPagingItemReaderBuilder&lt;ReviewSummaryDTO&gt;()
                .pageSize(100)
                .queryString(
                        &quot;select new com.comehere.ssgserver.review.dto.resp.ReviewSummaryDTO(&quot;
                                + &quot;min(i.id), min(ic.id), round(avg(r.star), 1), count(r)) &quot;
                                + &quot;from Review r &quot;
                                + &quot;join Item i on r.itemCode = i.code &quot;
                                + &quot;join ItemCalc ic on i.id = ic.itemId &quot;
                                + &quot;group by r.itemCode&quot;
                )
                .entityManagerFactory(emf)
                .name(&quot;itemSummaryReader&quot;)
                .build();
    }

    @Bean
    @StepScope
    public ItemProcessor&lt;ReviewSummaryDTO, ItemCalc&gt; processor() {
        log.info(&quot;processor start&quot;);
        return new ItemProcessor&lt;ReviewSummaryDTO, ItemCalc&gt;() {
            @Override
            public ItemCalc process(ReviewSummaryDTO dto) throws Exception {
                return ItemCalc.builder()
                        .id(dto.getCalcId())
                        .reviewCount(dto.getReviewCount())
                        .averageStar(dto.getAverageStar())
                        .build();
            }
        };
    }

    @Bean
    @StepScope
    public JpaItemWriter&lt;ItemCalc&gt; writer() {
        log.info(&quot;writer start&quot;);
        return new JpaItemWriterBuilder&lt;ItemCalc&gt;()
                .entityManagerFactory(emf)
                .build();
    }
}</code></pre>
<hr>
<h3 id="💾-실행">💾 실행</h3>
<blockquote>
<p>현재 상품별 집계 내역은 아래와 같은데, 현재 집계 데이터가 없는 <strong>8번 상품의 리뷰를 작성</strong>하고, <strong>1번 상품의 리뷰를 추가로 작성</strong>한다고 했을 때, <strong>배치 실행 후 집계 내역은 어떻게 변할까?</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/8457545e-52bb-4c52-9e38-0eee0a990070/image.png" alt=""></p>
<h4 id="🌟-배치-작업-수행">🌟 배치 작업 수행</h4>
<blockquote>
<p>실행 시간이 정각이 아닌 이유는 테스트를 위해 <code>job.enabled: true</code>로 설정했기 때문,, </p>
</blockquote>
<h4 id="1-reader">1. Reader</h4>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/1c7c273b-bdfb-4298-ba26-e87a111a4184/image.png" alt=""> : Reader에 작성한 쿼리를 실행하여 집계 내역 조회</p>
<h4 id="2-processor--wirter">2. Processor + Wirter</h4>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/4755b4d2-4db6-4986-9f84-48be48dffae8/image.png" alt=""><img src="https://velog.velcdn.com/images/pockyjx_/post/35bb9602-dee8-4257-a2f9-953d1bd2b90a/image.png" alt=""> : JPA의 변경 감지를 활용하여 업데이트 된 2개의 상품(1번, 8번)에 대해 업데이트 실행</p>
<h4 id="✅-결과">✅ 결과</h4>
<blockquote>
<p><strong>1번과 8번</strong> 각각 <strong>2개의 리뷰</strong>를 추가로 작성하였고, 그에 따라 <strong>리뷰 개수와 평균 별점이 업데이트</strong> 된 것을 확인할 수 있다!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/517b536b-3593-443c-b00f-70fe64cf6bfd/image.png" alt=""></p>
<hr>
<h4 id="🙏🏻-참고">🙏🏻 참고</h4>
<blockquote>
<ul>
<li><a href="https://dkswnkk.tistory.com/707">Spring Batch란? 간단한 개념과 코드 살펴보기</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://khj93.tistory.com/entry/Spring-Batch%EB%9E%80-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">Spring Batch란? 이해하고 사용하기(예제소스 포함)</a></li>
<li><a href="https://velog.io/@wnguswn7/Tasklet-vs-Chunk-%EB%B9%84%EA%B5%90%EC%99%80-%EC%B2%98%EB%A6%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8">Tasklet vs Chunk 비교와 처리 테스트</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] CoolSMS로 문자 인증 구현하기]]></title>
            <link>https://velog.io/@pockyjx_/Spring-Boot-CoolSMS%EB%A1%9C-%EB%AC%B8%EC%9E%90-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@pockyjx_/Spring-Boot-CoolSMS%EB%A1%9C-%EB%AC%B8%EC%9E%90-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 01 Oct 2024 14:29:18 GMT</pubDate>
            <description><![CDATA[<h1 id="✉️-coolsms">✉️ CoolSMS</h1>
<blockquote>
<p><a href="https://coolsms.co.kr/">https://coolsms.co.kr/</a> : <strong>건당 20원</strong>, 최초 가입 시 300P 무료 제공!</p>
</blockquote>
<p>회원가입을 구현하다 보니 자연스럽게 인증 로직을 구현하게 되었는데, 이메일이랑 휴대폰 문자 인증 둘 중에 고민하다가 이메일 계정 무한 생성 &lt;&lt; 같은 부정행위를 막기 위해서 한 사람당 한 번만 가입할 수 있도록 문자 인증을 도입하기로 결정하게 되었다!</p>
<p>이후에 관련 API를 찾아보다 가장 많은 샘플이 존재했고, 또 팀원이 이전 팀에서 써본 경험이 있었던 CoolSMS를 사용하기로 결정. 유료이긴 했으나 무료 포인트를 주니까 테스트용으로 충분할 것 같아서 걍 사용,, <del>하기로 했으나 막판에 모자라서 걍 엔빵해서 결제하긴함,,ㅋㅋ</del></p>
<hr>
<h2 id="🗝️-api-key-발급--spring-연동">🗝️ API Key 발급 + Spring 연동</h2>
<blockquote>
<p><strong>발급</strong> : 회원가입 후 대시보드 ➡️ <strong>개발/연동</strong> ➡️ API Key 발급 ➡️ <strong>새로운 API KEY</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/214ae677-2e9a-4c03-88d7-e67d2f6e4a16/image.png" alt=""></p>
<blockquote>
<p><strong>Spring 연동</strong> : <a href="https://github.com/coolsms/coolsms-java-examples"><strong>공식 깃허브</strong></a> 참고!!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/feefaafb-5f79-4cae-b3ca-0275477c2f20/image.png" alt=""> CoolSMS에서 공식 깃허브를 운영하면서 <strong>언어 + 프레임워크별 의존성 및 샘플 코드</strong>를 제공해준다! 나는 Gradle 기반의 Spring Boot를 사용하므로 <a href="https://github.com/coolsms/coolsms-java-examples">해당 레포</a>를 참고하여 <strong><code>build.gradle</code></strong>에 아래 의존성을 추가해줬다.</p>
<pre><code class="language-gradle">implementation &#39;net.nurigo:sdk:4.3.0&#39;</code></pre>
<p>추가적으로 <code>application.yaml</code>에  위에서 발급했던 API Key와 Secret Key, 그리고 문자 전송 시 사용할 번호 데이터를 미리 입력해둠!</p>
<pre><code class="language-yaml">coolsms:
  api:
    key: &quot;API KEY 입력&quot;
    secret: &quot;SECRET KEY 입력&quot;
    sender: &quot;발신자 번호&quot; # (- 없어야 함)</code></pre>
<hr>
<h2 id="💻-코드-구현">💻 코드 구현</h2>
<p>로직의 큰 틀은 <strong>문자 인증 요청 ➡️ 문자 발송 ➡️ 인증번호 검증</strong>인데, 이 과정에서 <strong>인증번호는 5분동안만 유효하도록!</strong> 구현하는 것이 목표였음.. </p>
<p>문제는 인증번호를 5분동안 어디에 저장하냐?였는데 MySQL은 서버 부하때문에 제외였고,, 선택지는 세션 아니면 레디스였는데 이것저것 찾아보니 <strong>세션의 time-out</strong> 기능을 사용하게 되면 <strong>세션을 사용하는 다른 로직에 영향</strong>을 미칠 수 있다는 점과, <strong>세션 키가 중복</strong>될 위험이 있다는 점이 걸려서 <strong>처리 속도가 빠른 인메모리 캐시 DB</strong>인 <strong>Redis</strong>를 사용하기로 했다! 따라서,</p>
<blockquote>
<p>문자 인증 요청 ➡️ <strong>인증번호 발급 후 Redis에</strong> <strong><code>&lt;휴대폰번호, 인증번호&gt;</code></strong> 저장 (<strong>만료시간 지정</strong>) ➡️ 검증 요청 시 키 값인 번호로 조회 ➡️ <strong>검증 통과 시 데이터 삭제</strong></p>
</blockquote>
<p>이 로직을 최종적인 틀로 잡고 구현 시작,,</p>
<hr>
<h3 id="⚙️-메세지-전송">⚙️ 메세지 전송</h3>
<ul>
<li><strong><code>SmsUtil</code></strong> : CoolSMS에서 제공하는 <code>DefaultMessageService</code>와 <code>SingleMessageSentResponse</code> 객체를 사용하여, 인증번호와 수신자 번호를 전달받으면 <strong>실제 메세지를 생성하여 전송</strong>하는 로직 구현</li>
</ul>
<pre><code class="language-java">@Component
public class SmsUtil {
    @Value(&quot;${coolsms.api.key}&quot;)
    private String apiKey;

    @Value(&quot;${coolsms.api.secret}&quot;)
    private String secretKey;

    @Value(&quot;${coolsms.api.sender}&quot;)
    private String sender;

    DefaultMessageService messageService;

    @PostConstruct
    private void init() {
        this.messageService = NurigoApp.INSTANCE.initialize(apiKey, secretKey, &quot;https://api.coolsms.co.kr&quot;);
    }

    // 인증번호 전송 : 단일 메세지
    public SingleMessageSentResponse sendSms(String receiver, String code) {
        Message message = new Message();
        message.setFrom(sender); // 보내는 사람 : 관리자
        message.setTo(receiver); // 받는 사람 : 유저
        message.setText(&quot;[MFC] 본인 확인 인증번호 : [&quot; + code + &quot;]\n인증번호는 5분간 유효합니다.&quot;); // 전송 내용

        return messageService.sendOne(new SingleMessageSendingRequest(message));
    }
}</code></pre>
<ul>
<li><strong><code>AuthService</code></strong> : 인증을 요청한 휴대폰 번호를 통해 해당 회원이 이미 존재하는지 검증하고, 새로 가입하는 회원인 경우 <strong>6자리의 인증번호 <code>code</code></strong>를 발급하여 <strong><code>SmsUtil</code>의 <code>sendSms()</code></strong>를 호출하여 <strong>메세지를 전송</strong>한 후, <strong>redis에 해당 내역을 저장</strong></li>
</ul>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
public class AuthServiceImpl implements AuthService {
    private final SmsRepository smsRepository;
    private final SmsUtil smsUtil;

    @Override
    public void sendSms(SmsReqDto dto) {
        String to = dto.getPhone();
        if(isDuplicate(to)) {
            throw new BaseException(DUPLICATED_MEMBERS);
        }

        int random = (int) (Math.random() * 900000) + 1000;
        String code = String.valueOf(random);

        smsUtil.sendSms(to, code); // 문자 전송
        smsRepository.createSmsCode(to, code); // 인증번호 redis에 저장
    }

    private boolean isDuplicate(String phone) {
        return memberRepository.findByActivePhone(phone).isPresent();
    }
}</code></pre>
<ul>
<li><p><strong><code>SmsRepository</code></strong> : redis에 <strong><code>&lt;휴대폰번호, 인증번호&gt;</code></strong> 형식으로 인증 내역을 저장하는데, 이때 <strong><code>Duration</code></strong>을 통해 해당 데이터의 <strong>유효기간</strong>을 지정할 수 있음. 인증번호가 5분동안만 유효하므로 <code>LIMIT_TIME</code>을 5분으로 설정해줌.</p>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class SmsRepository {
  private final StringRedisTemplate redisTemplate;

  private final String PREFIX = &quot;sms:&quot;;
  private final int LIMIT_TIME = 5 * 60;

  public void createSmsCode(String phone, String code) {
      redisTemplate.opsForValue()
              .set(PREFIX + phone, code, Duration.ofSeconds(LIMIT_TIME));
  }
}</code></pre>
</li>
</ul>
<hr>
<h3 id="⚙️-인증">⚙️ 인증</h3>
<ul>
<li><strong><code>AuthService</code></strong> : redis에서 휴대폰 번호를 통해 값을 조회해서 <strong>인증번호를 검증</strong>한 후, <strong>통과</strong>했다면 해당 인증번호는 더이상 의미없기 때문에 <strong>redis에서 삭제</strong> 처리.</li>
</ul>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
public class AuthServiceImpl implements AuthService {
    private final SmsRepository smsRepository;
    private final SmsUtil smsUtil;

    @Override
    public void verifyCode(SmsReqDto dto) {
        if(!isVerify(dto)) {
            throw new BaseException(MESSAGE_VALID_FAILED);
        }

        smsRepository.removeSmsCode(dto.getPhone());
    }

    private boolean isVerify(SmsReqDto dto) {
        return smsRepository.hasKey(dto.getPhone()) &amp;&amp;
                smsRepository.getSmsCode(dto.getPhone())
                        .equals(dto.getCode());
    }
}</code></pre>
<ul>
<li><p><strong><code>SmsRepository</code></strong></p>
<ul>
<li><p><strong><code>hasKey(phone)</code></strong> : 해당 <strong>휴대폰 번호를 키로 가지는 값</strong>이 있는지 조회. 만약 인증 요청 후 5분이 지났다면, 해당 데이터는 삭제되어 더이상 존재하지 않음.</p>
</li>
<li><p><strong><code>getSmsCode(phone)</code></strong> : 해당 <strong>휴대폰 번호를 키값</strong>으로 가지는 <strong>인증번호 값 조회</strong>. 사용자가 입력한 인증번호와 일치하는지 검증할 때 사용</p>
</li>
<li><p><strong><code>removeSmsCode(phone)</code></strong> : 해당 휴대폰 번호를 키값으로 가지는 내역 <strong>삭제</strong></p>
</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class SmsRepository {
    private final StringRedisTemplate redisTemplate;

    private final String PREFIX = &quot;sms:&quot;;
    private final int LIMIT_TIME = 5 * 60;

    public boolean hasKey(String phone) {
        return redisTemplate.hasKey(PREFIX + phone);
    }

    public String getSmsCode(String phone) {
        return redisTemplate.opsForValue().get(PREFIX + phone);
    }

    public void removeSmsCode(String phone) {
        redisTemplate.delete(PREFIX + phone);
    }
}</code></pre>
<hr>
<h3 id="✅-테스트">✅ 테스트</h3>
<ol>
<li>휴대폰 번호로 <strong>인증 요청</strong> : 문자가 정상적으로 수신되는 것을 확인!</li>
</ol>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/2379a60a-3b73-4c70-bb4e-c0ebc05b4825/image.png" alt=""> <img src="https://velog.velcdn.com/images/pockyjx_/post/52931a74-ebad-455c-b5e8-f2b06f3939b3/image.png" alt=""></p>
<p>2-1. <strong>잘못된</strong> 인증번호로 검증 : 예외 발생
<img src="https://velog.velcdn.com/images/pockyjx_/post/3f80e03e-1fa2-481c-8faa-f2f6b5f12383/image.png" alt=""></p>
<p>2-2. <strong>올바른</strong> 인증번호로 검증
<img src="https://velog.velcdn.com/images/pockyjx_/post/047ebf9b-f5a1-4ee7-9a3e-1b1ff5885a11/image.png" alt=""></p>
<p>2-3. <strong>올바른</strong> 인증번호로 <strong>재검증</strong> : 첫번째 검증 통과 시 삭제되므로, 해당 내역이 존재하지 않아 예외 발생
<img src="https://velog.velcdn.com/images/pockyjx_/post/c59106ef-efc8-4c3d-9e1d-6ebe015ccfaf/image.png" alt=""></p>
<hr>
<p>사실 문자 인증 처음에 담당하게 됐을 때 좀 쫄았었는데 공식 깃허브랑 몇몇 기술 블로그들이 넘 친절해서 금방 할 수 있었던 것 같다,, 나중에 개인 플젝 작은거나 하나 하면서 이메일 인증도 구현해봐야지,, 시간이 된다면,,</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA] EDA : Kafka를 통해 서비스 간 통신하기]]></title>
            <link>https://velog.io/@pockyjx_/MSA-EDA-Kafka%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%84-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@pockyjx_/MSA-EDA-Kafka%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%84-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 26 Aug 2024 07:15:44 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-eda-event-driven-architecture">💡 EDA (Event Driven Architecture)</h1>
<blockquote>
<p>⭐ <strong>이벤트 기반</strong> 아키텍처 : 분산 시스템 환경에서 서비스들이 <strong>이벤트</strong>를 기반으로 <strong>비동기</strong> 통신하는 구조 </p>
</blockquote>
<p>이벤트를 기반으로 통신한다는 것은 한 서비스에서 <strong>이벤트를 생성</strong>하여 <strong>발행하면(pub)</strong> 해당 이벤트를 필요로 하는 서비스가 <strong>이벤트를 받아서 처리(sub)</strong>하는 방식으로 작동한다. </p>
<p>여기서 <strong>이벤트</strong>란 주목할 만한 사건, 즉, 회원가입이나 주문과 같이 <strong>어떠한 작업이 실행</strong>됐다고 생각하면 편하다. 그렇다면 이벤트가 발행되는 것은 API가 호출되는 것이라고 치고, 이벤트를 <strong>구독</strong>하고, <strong>받아서 처리</strong>한다는 것은 어떤 개념일까? 나는 이걸 <strong>MSA &amp; 분산 DB</strong> 환경에서 <strong>한 서비스의 이벤트가 다른 서비스의 이벤트나 데이터에 영향을 미친다.</strong> 라는 느낌으로 이해하고 작업을 진행했다. </p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/a55e2d67-384a-41ad-9428-f0119af3fcb1/image.png" alt=""> 내가 2차 프로젝트를 진행하면서 EDA를 적용한 부분을 정리해봤는데, 대충 이정도였다. 이걸 참고해서 위 설명에 예시를 들어보자면, <strong>인증</strong>(Auth) 서비스에서 <strong>회원가입 이벤트</strong>가 <strong>발행</strong>되면 <strong>카프카</strong>(아래 설명 예정)에 <strong>관련된 데이터</strong>(메세지)가 저장되고, 이 이벤트를 <strong>구독</strong>한 <strong>회원</strong>(Member) 서비스에서 해당 데이터를 들고 <strong>회원 DB 서버에 프로필 데이터를 저장</strong>하는 방식으로 동작하게 된다. </p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/dc325c5b-f4c7-41e4-b2ef-b97666714d1a/image.png" alt=""> 인증 서버에서 회원가입 요청을 처리할 때 위와 같은 데이터들을 받는데,  <img src="https://velog.velcdn.com/images/pockyjx_/post/62fb48cd-5aa0-462d-a25c-368c8cedb8a2/image.png" alt=""> 각 DB의 테이블을 살펴보면 <strong><code>email, password, name, phone, birth, gender</code></strong>는 <strong>인증</strong> 서버에서, <strong><code>nickname, favoriteStyle</code></strong>은 <strong>회원</strong> 서버에서 관리하는 것을 알 수 있다. 그렇다면, <strong>회원가입</strong> 시 <strong>인증</strong> 서버의 <strong><code>Member</code></strong> 테이블과 <strong>회원</strong> 서버의 <strong><code>User/Partner(role에 따라 분기)</code>, <code>Favorite_Style</code></strong> 테이블에 <strong>INSERT</strong>문을 실행해야 하는데, 인증 서비스에서는 인증 DB 서버만 다루기 때문에, <strong>해당 서비스에서 회원 DB 서버에 프로필 정보를 저장을 처리할 수 없는 문제</strong>가 발생하게 된다.</p>
<p>이러한 경우, <strong>어떻게 다른 서비스로 데이터를 전달</strong>할 수 있을까? 바로 <strong>카프카</strong>와 같은 <strong>메시지 큐</strong>를 활용하면 된다!</p>
<hr>
<h2 id="📢-kafka">📢 Kafka</h2>
<blockquote>
<p><strong>메시지 큐</strong> 방식을 기반으로 하여 분산 시스템 환경에서 <strong>대규모 메시지</strong>를 안정적으로 <strong>전송, 수집, 활용</strong>할 수 있도록 서비스를 중개하는 <strong>분산 메시징 시스템</strong></p>
</blockquote>
<p>메시지 큐를 찾아보면 가장 많이 나오는 툴이 <strong>Kafka</strong>와 <strong>RabbitMQ</strong>인데, 위에서 설명했던 내가 구현한 부분 외에도 다른 팀원분도 EDA를 사용하는 경우가 제법 있어서 <strong>대용량 메시지를 좀 더 안정적으로 처리</strong>할 수 있는 <strong>Kafka를 선택</strong>하게 되었다. </p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/a6225b88-e146-4974-a862-b57869c66be1/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>Broker</strong> : 카프카 애플리션이 설치된 서버</li>
</ul>
</blockquote>
<ul>
<li><strong>Producer</strong> : 메시지(이벤트)를 <strong>발행</strong>하여 브로커에 전달하는 서비스</li>
<li><strong>Consumer</strong> : 브로커의 메시지를 <strong>구독</strong>하여 처리하는 서비스</li>
<li><strong>Topic</strong> : 브로커에서 <strong>메시지</strong>를 구분하기 위해 설정하는 <strong>고유한 이름</strong></li>
<li><strong>Partition</strong> : 브로커 내에서 <strong>병렬</strong> 처리를 가능하게 함</li>
<li><strong>Zookeeper</strong> : 분산 애플리케이션들을 그룹화하여 관리하는 시스템</li>
</ul>
<p>카프카에 대해서 정리하자면 글이 한없이 길어질 것 같아서.. 일단 이 글에서는 이 정도 개념만 가지고 <strong>서비스 통신을 구현하는 코드 위주로 정리</strong>할 예정.. 주키퍼와 카프카 서버를 구축하는 과정이나, 좀 딥한 개념에 대해서는 추후 작성할 인프라 파트에서 다룰 예정!</p>
<hr>
<h3 id="⚙️-producer">⚙️ Producer</h3>
<blockquote>
<p>이벤트 발행 시, <code>&lt;토픽명, 메시지&gt;</code>를 <strong><code>&lt;String, Json&gt;</code></strong> 형태로 전송</p>
</blockquote>
<h4 id="✍🏻-kafkaconfig">✍🏻 <code>KafkaConfig</code></h4>
<pre><code class="language-java">@Configuration
public class KafkaConfig {
    @Value(&quot;${spring.kafka.bootstrap-servers}&quot;) // .yml에 환경변수 선언
    private String BOOTSTRAP_SERVER;

    @Bean
    public KafkaTemplate&lt;String, Object&gt; kafkaTemplate() {
        return new KafkaTemplate&lt;&gt;(producerFactory());
    }

    @Bean
    public ProducerFactory&lt;String, Object&gt; producerFactory() {
        Map&lt;String, Object&gt; configs = new HashMap&lt;&gt;();
        configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVER);
        configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);

        return new DefaultKafkaProducerFactory&lt;&gt;(configs);
    }
}</code></pre>
<h4 id="✍🏻-kafkaproducer">✍🏻 <code>KafkaProducer</code></h4>
<ul>
<li>프로필 정보를 <strong><code>InsertProfieDto</code> 객체</strong> 형태로 전송하면, 설정값에 따라 <strong>DTO를 JSON으로 변환</strong>하여 브로커에 저장</li>
</ul>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class KafkaProducer {
    private final KafkaTemplate&lt;String, Object&gt; kafkaTemplate;

    public void sendProfile(InsertProfileDto dto) {
        kafkaTemplate.send(&quot;profile-insert&quot;, dto);
    }
}</code></pre>
<h4 id="✍🏻-authservice">✍🏻 <code>AuthService</code></h4>
<ul>
<li><code>SignUpReqDto</code>에서 <strong>프로필</strong> 데이터를 <strong><code>InsertProfileDto</code></strong> 객체에 담아 <strong><code>producer</code></strong>를 통해 <strong>브로커에 전송</strong></li>
</ul>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
public class AuthServiceImpl implements AuthService {
    private final MemberRepository memberRepository;
    private final KafkaProducer producer;

    @Override
    public String signUp(SignUpReqDto dto) {
        if(!dto.getRole().equals(&quot;USER&quot;) &amp;&amp; !dto.getRole().equals(&quot;PARTNER&quot;)) {
            throw new BaseException(NO_EXIT_ROLE);
        }

        Member member = createMember(dto);
        producer.sendProfile(InsertProfileDto.toBuild(dto, member.getUuid()));
        return member.getUuid();
    }
}</code></pre>
<hr>
<h3 id="⚙️-consumer">⚙️ Consumer</h3>
<blockquote>
<p>토픽명에 따른 <strong>JSON</strong> 형태의 메시지를 <strong>DTO로 변환하여 처리</strong></p>
</blockquote>
<h4 id="✍🏻-kafkaconfig-1">✍🏻 KafkaConfig</h4>
<ul>
<li><strong><code>InsertProfileDto</code></strong> 타입의 메시지를 받아서 처리하는 <strong><code>ConsumerFactory</code></strong> 생성 <br>
➡️ 처음에는 프로듀서처럼 Object로 받아서 처리하려고 했는데, 계속 예외가 발생해서 각 컨슈머 별로 DTO를 분리해서 설정하니까 정상적으로 처리됨 + 프로듀서도 Object 보다는 나누어서 처리하는게 좋다고 함!</li>
</ul>
<pre><code class="language-java">@Configuration
public class KafkaConfig {
    @Value(&quot;${spring.kafka.bootstrap-servers}&quot;)
    private String BOOTSTRAP_SERVER;

    @Value(&quot;${spring.kafka.consumer.group-id}&quot;)
    private String GROUP_ID;

    @Bean
    public ConsumerFactory&lt;String, InsertProfileDto&gt; insertProfileConsumer() {
        Map&lt;String, Object&gt; configs = new HashMap&lt;&gt;();
        configs.put(BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVER);
        configs.put(GROUP_ID_CONFIG, GROUP_ID);
        configs.put(JsonDeserializer.TRUSTED_PACKAGES, &quot;*&quot;);

        return new DefaultKafkaConsumerFactory&lt;&gt;(
                configs,
                new StringDeserializer(),
                new JsonDeserializer&lt;&gt;(InsertProfileDto.class, false)
        );
    }

    @Bean
    ConcurrentKafkaListenerContainerFactory&lt;String, InsertProfileDto&gt; insertProfileListener() {
        ConcurrentKafkaListenerContainerFactory&lt;String, InsertProfileDto&gt; factory
                = new ConcurrentKafkaListenerContainerFactory&lt;&gt;();
        factory.setConsumerFactory(insertProfileConsumer());
        return factory;
    }</code></pre>
<h4 id="✍🏻-kafkaconsumer">✍🏻 <code>KafkaConsumer</code></h4>
<ul>
<li><p><strong><code>@KafkaListner(topics=&quot;토픽명&quot;, containerFactory=&quot;리스너명&quot;)</code></strong> : 브로커에서 해당 토픽명을 가지는 메시지를 받아와서 처리 </p>
<ul>
<li>DTO내의 <strong><code>role</code></strong> 값에 따라서 유저 혹은 파트너 <strong>프로필 생성 + 선호 스타일</strong> 저장 ➡️ <strong>파트너 프로필</strong> 생성 시, <strong>배치</strong> 서비스에 <strong>집계 데이터</strong>를 생성해야 하므로 <strong>새로운 이벤트 발행</strong></li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class KafkaConsumer {
    private final UserRepository userRepository;
    private final PartnerRepository partnerRepository;
    private final FavoriteStyleRepository favoriteStyleRepository;

    private final KafkaProducer producer;

    @KafkaListener(topics = &quot;profile-insert&quot;, containerFactory = &quot;insertProfileListener&quot;)
    public void insertProfile(InsertProfileDto dto) {
        if(dto.getRole().equals(&quot;USER&quot;)) { // 유저
            userRepository.save(User.builder()
                    .uuid(dto.getUuid())
                    .nickname(dto.getNickname())
                    .build());
        } 

        else if(dto.getRole().equals(&quot;PARTNER&quot;)) { // 파트너
            Partner partner = partnerRepository.save(Partner.builder()
                    .uuid(dto.getUuid())
                    .nickname(dto.getNickname())
                    .build());

            // 새로운 이벤트 발행 : 배치 서비스에서 사용
            producer.createPartner(PartnerSummaryDto.builder()
                    .partnerId(partner.getUuid())
                    .build());
        } 

        else { // 이외의 파라미터 : 예외 발생
            throw new BaseException(NO_EXIT_ROLE);
        }

        dto.getFavoriteStyles() // 선호 스타일 저장 : 파트너 &amp; 유저 공통
                .forEach(i -&gt; favoriteStyleRepository.save(
                        FavoriteStyle.builder()
                                .uuid(dto.getUuid())
                                .styleId(i)
                                .build()));
    }
}    </code></pre>
<hr>
<h3 id="⭐-결과">⭐ 결과</h3>
<h4 id="1-인증-서버">1. 인증 서버</h4>
<ul>
<li><strong>신규 회원</strong> 데이터 생성 → <strong>회원</strong> 서비스에 <strong>UUID</strong>와 <strong>프로필, 선호 스타일</strong> 전달</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/21d0ec4d-48ce-4304-8b19-2c1a8492b694/image.png" alt=""></p>
<h4 id="2-회원-서버">2. 회원 서버</h4>
<ul>
<li>신규 회원의 <strong>UUID</strong>를 가지는 <strong>파트너 프로필, 선호 스타일</strong> 데이터 생성 → <strong>배치</strong> 서비스에 <strong>UUID</strong> 전달</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/ec9bde0b-e354-4a6a-8239-b8294b576bc0/image.png" alt=""></p>
<h4 id="➕-배치-서버">➕ 배치 서버</h4>
<ul>
<li>신규 회원의 <strong>UUID</strong>를 가지는 <strong>파트너 집계 데이터</strong> 생성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/09a96e99-24fc-4a2f-8bd6-05b7c0fc8fec/image.png" alt=""></p>
<hr>
<h2 id="✅-장단점">✅ 장단점</h2>
<blockquote>
<p>그렇다면 분산 시스템에서 데이터를 EDA를 통해 관리하면 어떤 점이 좋고, 어떤 점에서 문제가 발생할까?</p>
</blockquote>
<h3 id="🙆🏻♀️-장점">🙆🏻‍♀️ 장점</h3>
<ul>
<li><p>서비스 간 <strong>느슨한 결합</strong> : 브로커는 메시지 수신 여부와 관계 없이 <strong>독립적으로 메시지를 관리</strong>함. 즉, 수신 측 서비스가 작동하지 않아 메시지를 수신하지 못하더라도 송신 측 서비스는 이미 메시지를 전송하였기 때문에 이에 영향을 받지 않고 독립적으로 작동함.</p>
</li>
<li><p>이벤트 <strong>전달 보장</strong> : 이벤트가 발행되는 순간 <strong>브로커</strong>가 <strong>메시지를 안전하게 저장</strong>하므로, 수신 측 서비스가 작동하지 않더라도 재가동 시 누락된 메시지를 수신할 수 있음.</p>
</li>
<li><p><strong>비동기</strong> 처리를 통한 <strong>성능 최적화</strong> : EDA는 비동기 통신 기반이므로 <strong>여러 요청을 한 번에 처리</strong>할 수 있기 때문에 시스템 전체의 <strong>처리량</strong>을 높일 수 있음.</p>
</li>
<li><p><strong>확장성</strong> 및 <strong>재사용성</strong> 향상 : 하나의 메시지를 여러 서비스에서 구독하여 사용할 수 있으므로, 필요에 따라 기능을 추가하거나 변경할 수 있음.</p>
<br>

</li>
</ul>
<h3 id="🙅🏻♀️-단점">🙅🏻‍♀️ 단점</h3>
<ul>
<li><p>시스템 <strong>복잡도 증가</strong> : 이벤트의** 흐름을 추적하고 관리**하는 처리 로직을 구현하는 부분에서 전체적인 시스템이 복잡해질 수 있음.</p>
</li>
<li><p>이벤트 처리 <strong>순서 보장의 어려움</strong> : 다수의 이벤트가 <strong>비동기</strong>로 처리되기 때문에, 동일한 이벤트에 대해 <strong>순차적 처리를 보장할 수 없음.</strong></p>
</li>
<li><p>데이터 <strong>일관성 유지의 어려움</strong> : DB 서버가 분산되어 있기 때문에, 이벤트 발행 시점에 수신 측 서비스가 작동하지 않는 경우 <strong>데이터가 실시간으로 반영되지 않아</strong> 일관된 데이터 상태를 유지하기 어려울 수 있음.</p>
</li>
</ul>
<hr>
<h4 id="🙏🏻-참고">🙏🏻 참고</h4>
<blockquote>
<ul>
<li><a href="https://techblog.gccompany.co.kr/apache-kafka%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-eda-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-bf263c79efd0">Apache Kafka를 사용하여 EDA 적용하기</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://velog.io/@youngeui_hong/Event-Driven-Architecture">이벤트 기반 아키텍처(Event-Driven Architecture)란?</a></li>
<li><a href="https://velog.io/@cho876/%EC%B9%B4%ED%94%84%EC%B9%B4kafka-vs-RabbitMQ">카프카(kafka) vs RabbitMQ</a></li>
<li><a href="https://f-lab.kr/insight/understanding-event-driven-architecture">이벤트 기반 아키텍처의 이해와 장단점</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Querydsl] 조회 최적화 : DTO 형식으로 조회하기]]></title>
            <link>https://velog.io/@pockyjx_/Spring-%EC%A1%B0%ED%9A%8C-%EC%B5%9C%EC%A0%81%ED%99%94-DTO-%ED%98%95%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@pockyjx_/Spring-%EC%A1%B0%ED%9A%8C-%EC%B5%9C%EC%A0%81%ED%99%94-DTO-%ED%98%95%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 20 Aug 2024 13:41:00 GMT</pubDate>
            <description><![CDATA[<h1 id="⚠️-문제">⚠️ 문제</h1>
<blockquote>
<p><strong>특정 상품과 연관된 리뷰 이미지들</strong>을 조회하는 <strong>쿼리 최적화</strong></p>
</blockquote>
<h4 id="⚙️-review--reviewimage--1--n-연관관계">⚙️ <code>Review : ReviewImage</code> = 1 : N 연관관계</h4>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class Review extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, updatable = false)
    private String itemCode;

    ...
}

@Entity
@NoArgsConstructor
@Getter
public class ReviewImage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch=LAZY)
    @JoinColumn(name = &quot;review_id&quot;, nullable = false, updatable = false)
    private Review review;

    ...
}</code></pre>
<h4 id="⚙️-reviewimagelistdto--최종-반환-형태">⚙️ <code>ReviewImageListDTO</code> : 최종 반환 형태</h4>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class ReviewImageListDTO {
    private Long reviewId;
    private Long imageId;
    private String url;
    private String alt;
}    </code></pre>
<hr>
<h2 id="1-단순-조인">1. 단순 조인</h2>
<blockquote>
<p><strong>단순 조인</strong> 실행 : <strong>N + 1</strong> 문제 발생</p>
</blockquote>
<pre><code class="language-java">@Query(&quot;select ri  from ReviewImage ri left join ri.review r where r.itemCode = :itemCode&quot;)
Slice&lt;ReviewImage&gt; findReviewImages(@Param(&quot;itemCode&quot;) String itemCode, Pageable page);</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/675e7e65-f259-4fc9-a994-a0292b239fcb/image.png" alt="">  결과는 정상적으로 반환되나, 전달된 <code>itemCode</code>에 해당하는 리뷰 이미지들을 조회하는 쿼리 하나와, 해당 이미지와 연관된 리뷰 아이디를 조회해오는 2개의 쿼리, <strong>총 3개의 쿼리</strong>가 실행되었다. </p>
<p>→ <code>ReviewImage</code>의 <code>Review</code> 에 <strong>지연 로딩</strong>이 적용되어 있기 때문에 <strong>이미지와 연관된 리뷰</strong>에 대한 정보는 조회하지 않고, 빈 객체인 <strong>프록시</strong> 객체인 상태로 존재한다. 이 때, 서비스에서 <strong>조회 결과를 DTO 형태로 변환</strong>하는 과정에서 <strong><code>reviewImage.getReview().getId()</code></strong> 를 통해 연관된 리뷰의 아이디 값을 가져오는데, 이 때 <code>review</code>는 프록시이므로 <strong><code>getId()</code></strong>를 통해 값을 가져오려고 할 때가 되어서야 <strong>해당 아이디 값을 가지는 리뷰를 조회</strong>하는 쿼리를 실행한다. </p>
<p>이 때, 같은 리뷰 아이디값을 가진다면 영속성 컨텍스트를 활용하여 한 번의 쿼리로 해결할 수 있지만, <strong>연관된 리뷰의 아이디가 n개</strong>라면 <strong>n개의 추가 쿼리</strong>가 발생하게 되면서 성능을 저하시킬 수 있다.</p>
<hr>
<h2 id="2-fetch-join">2. Fetch Join</h2>
<blockquote>
<p><strong>JPA</strong>에서 제공하는 <strong>페치 조인</strong> 실행 : <strong>필요없는 데이터</strong>까지 조회하게 됨</p>
</blockquote>
<pre><code class="language-java">@Query(&quot;select ri  from ReviewImage ri left join fetch ri.review r where r.itemCode = :itemCode&quot;)
Slice&lt;ReviewImage&gt; findReviewImages(@Param(&quot;itemCode&quot;) String itemCode, Pageable page);</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/e4173f4e-c2bc-4ef8-b269-3fa4c22f1a39/image.png" alt="">  <strong>하나의 쿼리</strong>로 원하는 결과를 얻을 수는 있지만, <strong>페치 조인</strong>은 <strong>연관된 엔티티</strong>들, 즉 리뷰와 리뷰 이미지 테이블 내의 <strong>모든 컬럼</strong>을 한 번에 조회하게 된다. 저기서 실질적으로 <strong>4개의 필드만 사용</strong>하고 나머지 필드들은 사용하지 않는데, 4개의 데이터를 가져오려고 9개의 데이터까지 덤으로 매번 가져오는 건 성능적으로 비효율적이지 않을까? 생각이 들었다.. <del>사실 덩치 큰 쿼리가 마음에 안들었음 ㅎ..</del></p>
<hr>
<h1 id="💡-해결책">💡 해결책</h1>
<blockquote>
<p>엔티티 타입으로 조회해서 DTO로 변환하는 방식이 아닌, <strong>DTO 타입에 맞춰서 조회</strong>하기!</p>
</blockquote>
<hr>
<h2 id="1-jpql">1. JPQL</h2>
<pre><code class="language-java">select new com.comehere.ssgserver.review.dto.resp.ReviewImageListDTO...</code></pre>
<p>: JPQL에서 DTO 타입으로 조회하기 위해서는 <strong>조회 타입</strong>에 조회하고 싶은 형식의 <strong>DTO</strong>를 <strong>생성자</strong> 선언하듯이 선언하면 되는데, DTO가 존재하는 경로 상의 <strong>모든 패키지명을 명시</strong>해야 하므로 쿼리가 지저분해진다.. 따라서 해당 방식은 패스..</p>
<hr>
<h2 id="2-querydsl">2. Querydsl</h2>
<blockquote>
<p><strong>Querydsl</strong>의 <strong><code>Projections</code></strong> 사용 : 생성자, 필드, 어노테이션 등 다양한 방법이 있는데 나는 <strong>생성자 주입 방식</strong> 사용!</p>
</blockquote>
<ul>
<li><strong><code>Projections.constructor</code></strong> : <strong>생성자</strong>(<code>@AllArgsConstructor</code>)를 통해 조회된 값을 DTO 타입에 주입</li>
</ul>
<pre><code class="language-java">List&lt;ReviewImageListDTO&gt; result = query
                .select(Projections.constructor(ReviewImageListDTO.class,
                        reviewImage.review.id.as(&quot;reviewId&quot;),
                        reviewImage.id.as(&quot;imageId&quot;),
                        reviewImage.imageUrl.as(&quot;url&quot;),
                        reviewImage.alt))
                .from(reviewImage)
                .join(reviewImage.review, review)
                .where(review.itemCode.eq(itemCode))
                .limit(page.getPageSize() + 1)
                .offset(page.getOffset())
                .fetch();</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/20091d5c-a879-413c-add9-805b5a8da753/image.png" alt="">  <strong>하나의 쿼리</strong>로 <strong>필요한 데이터</strong>만 조회 완.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA] Spring Cloud Gateway (2) : JWT 검증 필터 (+ 예외 처리)]]></title>
            <link>https://velog.io/@pockyjx_/MSA-Spring-Cloud-Gateway-2-JWT-%EA%B2%80%EC%A6%9D-%ED%95%84%ED%84%B0-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@pockyjx_/MSA-Spring-Cloud-Gateway-2-JWT-%EA%B2%80%EC%A6%9D-%ED%95%84%ED%84%B0-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Mon, 12 Aug 2024 06:46:49 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-spring-cloud-gateway">💡 Spring Cloud Gateway</h1>
<blockquote>
<p>Spring Framework에서 제공하는 오픈 소스 기반의 Gateway 서비스</p>
</blockquote>
<p>기본 설정 및 라우팅에 대해서는 <a href="https://velog.io/@pockyjx_/MSA-Spring-Cloud-Gateway-1-CORS-Routing-%EC%84%A4%EC%A0%95">이 글</a> 참고!</p>
<hr>
<h2 id="✅-jwt-검증-필터">✅ JWT 검증 필터</h2>
<blockquote>
<p><strong>게이트웨이</strong>로 들어오는 요청에 대하여 <strong>JWT 토큰의 유효 여부</strong>를 검증한 후, <strong>검증을 통과한 요청</strong>만 해당 서비스로 전달</p>
</blockquote>
<p>서비스로 들어오는 모든 요청을 <strong>JWT 토큰이 필요한 요청</strong>과 <strong>필요하지 않은 요청</strong>으로 나눌 수 있는데, 이 요청들은 모두 게이트웨이를 거쳐서 서비스로 전달되므로, 각 서비스 별로 토큰 검증 필터를 구현하기 보다는 <strong>게이트웨이</strong>에서 한 번만 구현하여 <strong>검증을 거친 후에 통과한 요청만 서비스로 전달</strong>하는 방식이 더욱 효율적임.</p>
<p>일반적인 Spring Web MVC 프로젝트에서는 tomcat 기반의 Spring Security를 사용하여 토큰 검증 필터를 구현하지만, <strong>Spring Cloud Gateway</strong>는 <strong>Spring Webflux</strong> 프로젝트, 즉, <strong>netty *<em>기반이므로 *</em>Spring Security를 함께 사용할 수 없음.</strong> 따라서, <strong>Spring Cloud gateway</strong>에서 제공하는 <strong><code>AbstractGatewayFilterFactory</code></strong>를 <strong>상속</strong>하여 구현하여야 함.</p>
<hr>
<h3 id="⚙️-authorizationheaderfiler">⚙️ <code>AuthorizationHeaderFiler</code></h3>
<blockquote>
<p>요청에 <strong>토큰이 존재</strong>하는지, 존재한다면 <strong>유효한 토큰</strong>인지 검증 ➡️ <strong>유효하지 않은</strong> 경우 <strong>예외</strong> 처리</p>
</blockquote>
<h4 id="1-secretkey-객체-생성">1. <code>SecretKey</code> 객체 생성</h4>
<pre><code class="language-java">private final SecretKey key;

public AuthorizationHeaderFilter(@Value(&quot;${jwt.secret-key}&quot;) String key) {
    super(Config.class);
    byte[] keyBytes = Decoders.BASE64.decode(key);
    this.key = new SecretKeySpec(keyBytes, SignatureAlgorithm.HS256.getJcaName());
}</code></pre>
<h4 id="2-토큰-존재-여부-검증">2. 토큰 존재 여부 검증</h4>
<blockquote>
<p>⭐ 헤더에 <strong><code>Authorization</code></strong>이 <strong>없다면 토큰이 없다</strong>는 뜻이므로 <strong>예외</strong> 처리!</p>
</blockquote>
<pre><code class="language-java">@Override
public GatewayFilter apply(Config config) {
    return (exchange, chain) -&gt; {
        ServerHttpRequest req = exchange.getRequest();
        HttpHeaders header = req.getHeaders();

        if(!header.containsKey(HttpHeaders.AUTHORIZATION)) {
            throw new BaseException(NOT_FOUND_TOKEN);
        }
    };
}</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/e2af0016-b0ce-443e-a2b8-dea1f12c7f9f/image.png" alt=""></p>
<h4 id="2-토큰-유효성-검증">2. 토큰 유효성 검증</h4>
<blockquote>
<p>⭐ <strong>올바른 형식</strong>이 아니거나, 토큰의 <strong>유효기간이 지나 만료</strong>된 경우 <strong>예외</strong> 처리!</p>
</blockquote>
<ul>
<li><strong>올바르지 않은</strong> 형식 : 토큰이 <strong><code>Bearer</code>로 시작하지 않는</strong> 경우 </li>
</ul>
<pre><code class="language-java">@Override
public GatewayFilter apply(Config config) {
    return (exchange, chain) -&gt; {
        ...

        String authorization = header.get(HttpHeaders.AUTHORIZATION).get(0);
        if(authorization == null || !authorization.startsWith(&quot;Bearer&quot;)) {
            throw new BaseException(INVALID_TOKEN);
        }

        String jwt = authorization.substring(7);
        isValid(jwt);
        return chain.filter(exchange);
    };
}</code></pre>
<h4 id="➕-isvalidjwt--토큰-유효성-검증">➕ <code>isValid(jwt)</code> : 토큰 유효성 검증</h4>
<ul>
<li><p><strong><code>parseClaimsJws(jwt)</code></strong> : 토큰이 <strong>올바른 Secret Key</strong>로 서명되었는지 + <strong>만료</strong>되지 않았는지 검증</p>
<ul>
<li><strong><code>ExpiredJwtException</code></strong> : <strong>토큰이 만료된 경우</strong>에 대한 예외 처리</li>
<li><code>Exception</code> : 그 외의 예외 처리 (유효하지 않은 토큰)</li>
</ul>
</li>
</ul>
<pre><code class="language-java">private void isValid(String jwt) {

    try {
        Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(jwt);
    } catch(ExpiredJwtException e) {
        throw new BaseException(EXPIRED_TOKEN);
    } catch(Exception e) {
        throw new BaseException(INVALID_TOKEN);
    }
}</code></pre>
<h4 id="✍🏻-전체-코드">✍🏻 전체 코드</h4>
<pre><code class="language-java">@Component
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory&lt;AuthorizationHeaderFilter.Config&gt; {
    private final SecretKey key;

    public AuthorizationHeaderFilter(@Value(&quot;${jwt.secret-key}&quot;) String key) {
        super(Config.class);
        byte[] keyBytes = Decoders.BASE64.decode(key);
        this.key = new SecretKeySpec(keyBytes, SignatureAlgorithm.HS256.getJcaName());
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -&gt; {
            ServerHttpRequest req = exchange.getRequest();
            HttpHeaders header = req.getHeaders();

            if(!header.containsKey(HttpHeaders.AUTHORIZATION)) {
                throw new BaseException(NOT_FOUND_TOKEN);
            }

            String authorization = header.get(HttpHeaders.AUTHORIZATION).get(0);
            if(authorization == null || !authorization.startsWith(&quot;Bearer&quot;)) {
                throw new BaseException(INVALID_TOKEN);
            }

            String jwt = authorization.substring(7);
            isValid(jwt);
            return chain.filter(exchange);
        };
    }

    private void isValid(String jwt) {

        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(jwt)
                    .getBody()
                    .getSubject();
        } catch(ExpiredJwtException e) {
            throw new BaseException(EXPIRED_TOKEN);
        } catch(Exception e) {
            throw new BaseException(INVALID_TOKEN);
        }
    }

    public static class Config {
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/2a07d2b0-4a2a-46ae-a5bb-7eebd6e53ecd/image.png" alt=""></p>
<hr>
<h3 id="⚙️-필터-적용">⚙️ 필터 적용</h3>
<blockquote>
<p><strong><code>.yml</code></strong>에서 <strong>토큰이 필요한 요청에 대한 라우팅</strong>에 <strong>토큰 검증 필터 적용</strong></p>
</blockquote>
<p>한 서비스 내에서도 토큰이 필요한 요청과 필요하지 않은 요청이 각각 존재하므로, <strong>토큰이 필요한 요청에 대한 라우팅</strong>과 <strong>필요하지 않은 요청에 대한 라우팅</strong>을 <strong>분리</strong>해서 <strong>토큰이 필요한 요청에 대한 라우팅</strong>에만 <strong>토큰 검증 필터를 적용</strong>해야 함! 
<br></p>
<p>ex) <strong>sns 서비스</strong>에 대한 라우팅</p>
<ul>
<li><strong><code>sns-service-get</code></strong> : <strong>단순 조회</strong> 요청이므로 <strong>토큰 불필요</strong> ➡️ 토큰 검증 <strong>필터 미적용</strong></li>
</ul>
<pre><code class="language-yaml">- id: sns-service-get
  uri: lb://SNS-SERVICE
  predicates:
    - Path=/sns-service/**, /sns-service/bookmark/**, /sns-service/follow/**
    - Method= GET
  filters: # 경로 필터만 적용
    - StripPrefix=1 </code></pre>
<ul>
<li><p><strong><code>sns-service-filter</code></strong> : <strong>포스팅 &amp; 좋아요 &amp; 팔로우 CRUD</strong>와 관련된 요청이므로 <strong>토큰 필요</strong> ➡️ 토큰 검증 <strong>필터 적용</strong></p>
<ul>
<li><strong><code>filters</code></strong> 내에 <strong><code>name: 필터명</code></strong>을 통해 <strong>커스텀 필터</strong> 여러 개 적용 가능!
```yaml</li>
</ul>
</li>
<li><p>id: sns-service-filter # 토큰이 필요한 요청 : 토큰 검증 필터 적용
uri: lb://SNS-SERVICE
predicates:</p>
<pre><code>- Path=/sns-service/posts/**, /sns-service/bookmark/**, /sns-service/follow/**</code></pre><ul>
<li>Method=PUT, POST, DELETE
filters:<ul>
<li>StripPrefix=1</li>
</ul>
</li>
<li>name: AuthorizationHeaderFilter<pre><code></code></pre></li>
</ul>
</li>
</ul>
<hr>
<h2 id="➕-예외-처리">➕ 예외 처리</h2>
<blockquote>
<p>⭐ <strong><code>ErrorWebExceptionHandler</code></strong> :  <strong>예외 전역 처리</strong></p>
</blockquote>
<p>Spring Web MVC 기반 애플리케이션에서 <code>@ExceptionHandler</code>를 사용하여 컨트롤러에서 발생한 예외를 처리하는 것처럼,** Spring Webflux** 기반 애플리케이션에서는 <strong><code>ErrorWebExceptionHandler</code>를 구현</strong>하여 <strong>필터</strong>에서 발생한 <strong>예외를 처리</strong>할 수 있음.</p>
<p>MVC 프로젝트와 동일하게 <strong><code>BaseException</code>, <code>BaseResponse</code>, <code>BaseResponseStatus</code></strong>를 사용하여 커스텀할 예정! 
<br></p>
<h5 id="➕-baseexception--커스텀-예외">➕ <code>BaseException</code> : 커스텀 예외</h5>
<pre><code class="language-java">@Getter
public class BaseException extends RuntimeException{
    private BaseResponseStatus status;

    public BaseException(BaseResponseStatus status) {
        this.status = status;
    }
}</code></pre>
<h5 id="➕-baseresponse--커스텀-응답-형식">➕ <code>BaseResponse</code> : 커스텀 응답 형식</h5>
<pre><code class="language-java">public record BaseResponse&lt;T&gt;(HttpStatusCode httpStatus, Boolean isSuccess, String message, int code, T result) {


    public BaseResponse(BaseResponseStatus status) {
        this(status.getHttpStatusCode(), false, status.getMessage(), status.getCode(), null);
    }

    public BaseResponse(BaseResponseStatus status, T result) {
        this(status.getHttpStatusCode(), false, status.getMessage(), status.getCode(), result);
    }
}</code></pre>
<h5 id="➕-baseresponsestatus--커스텀-응답-상태-코드--메세지">➕ <code>BaseResponseStatus</code> : 커스텀 응답 상태 코드 + 메세지</h5>
<pre><code class="language-java">@Getter
@RequiredArgsConstructor
public enum BaseResponseStatus {

    NOT_FOUND_TOKEN(HttpStatus.UNAUTHORIZED, false, 401, &quot;토큰이 필요한 요청입니다.&quot;),
    INVALID_TOKEN(HttpStatus.UNAUTHORIZED, false, 401, &quot;유효하지 않은 토큰입니다.&quot;),
    EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, false, 401, &quot;만료된 토큰입니다.&quot;),
    INVALID_ACCESS(HttpStatus.FORBIDDEN, false, 403, &quot;잘못된 접근입니다.&quot;),
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, false, 500, &quot;서버 에러&quot;);

    private final HttpStatusCode httpStatusCode;
    private final boolean isSuccess;
    private final int code;
    private final String message;

}</code></pre>
<hr>
<h3 id="⚙️-errorresponse">⚙️ <code>ErrorResponse</code></h3>
<h4 id="1-baseresponse-생성">1. <code>BaseResponse</code> 생성</h4>
<pre><code class="language-java">@Override
public Mono&lt;Void&gt; handle(ServerWebExchange exchange, Throwable ex) {
    ServerHttpResponse response = exchange.getResponse();

    BaseResponse&lt;?&gt; baseResponse;
    if (ex instanceof BaseException e) {
        baseResponse = new BaseResponse&lt;&gt;(e.getStatus());
        response.setStatusCode(baseResponse.httpStatus());
    } else {
        baseResponse = new BaseResponse&lt;&gt;(BaseResponseStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
        response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
    }
}</code></pre>
<h4 id="2-baseresponse-➡️-json-변환">2. <code>BaseResponse</code> ➡️ JSON 변환</h4>
<pre><code class="language-java">private final ObjectMapper objectMapper;

@Override
public Mono&lt;Void&gt; handle(ServerWebExchange exchange, Throwable ex) {
...

response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    try {
        return response
                .writeWith(Mono.just(response.bufferFactory()
                .wrap(objectMapper.writeValueAsBytes(baseResponse))));
    } catch (JsonProcessingException e) {
        return Mono.error(e);
    }
}</code></pre>
<h4 id="✍🏻-전체-코드-1">✍🏻 전체 코드</h4>
<ul>
<li><strong><code>@Order(-1)</code></strong> : 핸들러의 <strong>우선순위</strong> 설정 (<strong>낮은 숫자</strong>일수록 <strong>우선순위가 높음</strong>) ➡️ 커스텀 핸들러에 우선순위를 설정해주지 않으면 기본 핸들러인 <code>DefaultErrorWebExceptionHandler</code>가 실행되므로, <strong>커스텀 핸들러</strong>를 사용하기 위해서는 반드시 <strong>우선순위를 설정</strong>해줘야 함!</li>
</ul>
<pre><code class="language-java">@Order(-1)
@RequiredArgsConstructor
@Component
public class ErrorResponse implements ErrorWebExceptionHandler {
    private final ObjectMapper objectMapper;

    @Override
    public Mono&lt;Void&gt; handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();

        BaseResponse&lt;?&gt; baseResponse;
        if (ex instanceof BaseException e) {
            baseResponse = new BaseResponse&lt;&gt;(e.getStatus());
            response.setStatusCode(baseResponse.httpStatus());
        } else {
            baseResponse = new BaseResponse&lt;&gt;(BaseResponseStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
            response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
        }

        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        try {
            return response
                    .writeWith(Mono.just(response.bufferFactory()
                    .wrap(objectMapper.writeValueAsBytes(baseResponse))));
        } catch (JsonProcessingException e) {
            return Mono.error(e);
        }
    }
}</code></pre>
<br>

<p>➕ 커스텀 예외인 <strong><code>BaseException</code></strong>과 <strong>그 외의 예외</strong>를 <strong>구분</strong>하여 처리 </p>
<ul>
<li><strong><code>BaseException</code></strong> : <strong><code>BaseResponseStatus</code></strong>에 지정한 <strong>상태코드 + 메세지</strong>
<img src="https://velog.velcdn.com/images/pockyjx_/post/a80a0844-b44c-4fe8-8acb-4ec5005fb0f0/image.png" alt=""></li>
</ul>
<ul>
<li><strong>그 외</strong>의 예외 : 상태코드 <strong>500</strong> +  + <strong><code>result</code></strong>에 실제 <strong>원인</strong>을 담은 <strong>메세지</strong> 전달 <img src="https://velog.velcdn.com/images/pockyjx_/post/1af3ac0b-9978-4810-b4e3-3bff7fd53e3c/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] ELB : 대상 그룹 Unhealthy  문제 해결]]></title>
            <link>https://velog.io/@pockyjx_/AWS-ELB-%EB%8C%80%EC%83%81-%EA%B7%B8%EB%A3%B9-Unhealthy-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@pockyjx_/AWS-ELB-%EB%8C%80%EC%83%81-%EA%B7%B8%EB%A3%B9-Unhealthy-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 07 Aug 2024 07:50:03 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-문제">💡 문제</h1>
<blockquote>
<p><a href="https://velog.io/@pockyjx_/AWS-Route-53-%EA%B0%80%EB%B9%84%EC%95%84-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0"><strong>이 글</strong></a>에서 연결한 도메인에 접속해보니 갑자기 <strong>504</strong>가 뜨면서 접속이 안되는 문제가 발생,, 분명 처음에는 잘 됐는데 왜 갑자기 안된걸까?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/3091cbfd-6238-4ca6-b051-20d38703939a/image.png" alt=""> 분명 과제 제출 전에는 잘 됐는데,, 제출 후에 터지니까 너무 당황스러워서 급하게 이것저것 찾아보니 <strong>로드밸런서</strong>에 연결된 <strong>대상 그룹의 상태가 Unhealthy</strong>여서 연결이 끊긴 거였음,,</p>
<p>오른쪽 끝에 <strong>상태 확인 세부 정보</strong>를 보면** <code>Health checks failed with these codes: [404]</code><strong>라고 뜨는데 이게 뭔 말인가 보니 **상태 검사 코드가 404</strong>라서 에러가 뜬다! 라는 뜻이라고 함. 근데 <strong>상태 검사</strong>가 뭔데,,?</p>
<hr>
<h2 id="✅-원인--상태-검사">✅ 원인 : 상태 검사</h2>
<blockquote>
<p><strong>로드밸런서</strong>와 <strong>대상</strong>(인스턴스)이 <strong>정상적으로 연결</strong>되는지 <strong>주기적으로 상태를 검사</strong>하는 것</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/4c84eb68-91cf-4c7b-b324-233e1f4a8add/image.png" alt=""> 대상 그룹에서 <strong>상태 검사</strong> 탭을 보면 위와 같은 설정 값을 볼 수 있다. 내용을 대충 요약해보자면,</p>
<blockquote>
<p>⭐ <strong><code>프로토콜:포트/경로</code></strong> 를 통해 <strong>30초</strong> 간격으로 요청을 보내고, <strong>응답의 상태 코드가 200</strong>이면 <strong>정상</strong>적인 연결</p>
</blockquote>
<ul>
<li>요청을 <strong>연속 5번 성공</strong>하면 <strong>정상 임계값</strong>으로 판단하여 <strong>Healthy</strong></li>
<li>요청을 <strong>연속 2번 실패</strong>하면 <strong>비정상 임계값</strong>으로 판단하여 <strong>Unhealthy</strong></li>
</ul>
<p>인 것 같은데,, 다시 내 에러 코드를 보면 ** <code>Health checks failed with these codes: [404]</code><strong>이므로, <code>/pockyjx.site/</code>로 상태 검사 요청 시 200이 아니라 **404를 반환</strong>해서 에러가 발생한다는 뜻,, </p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/78a0fd83-4a76-4db9-80e2-83918b1ebf1b/image.png" alt=""></p>
<p>여기서 드는 의문 2개</p>
<blockquote>
<ol>
<li>해당 요청에서 왜 <strong>404</strong>를 반환할까?</li>
<li>처음에는 잘 되다가 왜 갑자기 안될까?</li>
</ol>
</blockquote>
<p>이 2개를 한 번에 해결 해버리는 원인을 찾았는데,, <strong><code>app.module.ts</code></strong>에서 <strong><code>Controller: []</code></strong>에 <strong><code>/</code>를 처리하는 컨트롤러</strong>인 *<em><code>AppController</code>를 뺐기 때문,, *</em></p>
<pre><code class="language-ts">// app.module.ts
@Module({
  imports: [
    ...
  ],
  controllers: [], // 여기에 AppController를 넣어야 &#39;/&#39;에 대한 요청을 처리..
  providers: [AppService],
})

export class AppModule {}

// app.controller.ts
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get() // 루트(/)에 대한 요청 처리 : 상태 검사
  getHello(): string {
    return this.appService.getHello();
  }
}</code></pre>
<p>좀 더 자세히 정리해보자면,,</p>
<ul>
<li><p>첫 배포 때는 <strong><code>Controller: [AppController]</code></strong>였기 때문에 <code>/</code>에 대한 요청을 처리하는 <strong><code>AppController</code>가 활성화</strong>되어 있어서 <strong>상태 검사 요청 시 200을 반환</strong>해서 <strong>Healthy</strong> 상태를 유지. 따라서 이때까지는 서버 안터짐.</p>
</li>
<li><p>두 번째 배포에서 swagger에 사용하지도 않는 컨트롤러가 뜨는게 거슬려서 <strong><code>Controller: []</code></strong> 상태로 배포했는데 <del>(⬅️ 여기서 개큰문제 발생..)</del> 이거 때문에 <code>/</code>를 처리할 수 있는 <strong><code>AppController</code>가 비활성화</strong> 되었고, <strong>상태 검사 요청 시 404를 반환</strong>하게 되면서 <strong>Unhealthy</strong> 상태가 된 것,, ⭐</p>
</li>
</ul>
<hr>
<h2 id="⚙️-해결">⚙️ 해결</h2>
<h3 id="1-성공-코드-변경">1. 성공 코드 변경</h3>
<blockquote>
<p><strong><code>/</code></strong>에 요청 시, 상태 코드가 <strong>404</strong>일 때 <strong>성공으로 판단</strong>하도록 변경</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/eca98abb-b63c-4c6a-abc5-8c4aa0c620a1/image.png" alt=""></p>
<h3 id="2-상태-검사-경로-변경">2. 상태 검사 경로 변경</h3>
<blockquote>
<p><strong>상태 검사 요청 경로</strong>를 <strong>서비스가 처리할 수 있는 경로</strong>로 변경</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/0f544754-a105-4c2a-a6a9-a5bc610a41a5/image.png" alt=""></p>
<h3 id="➕-대상-그룹-생성-시-설정">➕ 대상 그룹 생성 시 설정</h3>
<blockquote>
<p>사실 가장 좋은 방법은,, 처음에 대상 그룹 생성 할 때부터 잘 생각한후에 설정값 잘 넣기,, ㅎ</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/abe7e7cc-cfd5-4f6e-93e4-75b1eba379a0/image.png" alt=""> <img src="https://velog.velcdn.com/images/pockyjx_/post/29b57df4-4ce8-47e0-9772-3de8c6c3eaa5/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] Route 53 - 가비아 도메인 연결]]></title>
            <link>https://velog.io/@pockyjx_/AWS-Route-53-%EA%B0%80%EB%B9%84%EC%95%84-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@pockyjx_/AWS-Route-53-%EA%B0%80%EB%B9%84%EC%95%84-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Tue, 06 Aug 2024 13:26:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/pockyjx_/post/97896f49-44e1-47f7-92ce-7d67ed34dbd7/image.png" alt=""></p>
<p><span style="color:gray">2차 때 정신 없어서 도메인 연결 관련해서 정리 못했던 것도 있고,, 마침 과제 하면서 개인 프로젝트? 배포용 도메인 하나 산 기념으로 다시 연결해보면서 정리해보는 글,,</span></p>
<hr>
<h1 id="💡-route-53">💡 Route 53</h1>
<blockquote>
<p>⭐ <strong>Route 53</strong> : 클라우드 기반의 <strong>DNS</strong> 웹 서비스 ➡️ <strong>EC2, ELB</strong>, S3 등의 다양한 인프라와 <strong>연결 가능</strong></p>
</blockquote>
<p>➕ <strong>DNS</strong> (Domain Name System) : <strong>도메인 이름 ➡️ IP 주소</strong> 변환</p>
<hr>
<h2 id="✅-도메인-연결">✅ 도메인 연결</h2>
<blockquote>
<p>도메인 구매하기 : <a href="https://www.gabia.com/">가비아</a></p>
</blockquote>
<hr>
<h3 id="1-route-53--호스팅-영역">1. Route 53 : 호스팅 영역</h3>
<ul>
<li><strong>호스팅 영역</strong> 생성 : <strong>구매한 도메인 주소</strong> 입력
<img src="https://velog.velcdn.com/images/pockyjx_/post/f2566337-4845-4f0e-8d20-244487cd2691/image.png" alt=""> <img src="https://velog.velcdn.com/images/pockyjx_/post/f44ff4b8-e2d6-4fe8-9296-66fd15c40a52/image.png" alt=""><img src="https://velog.velcdn.com/images/pockyjx_/post/153146e3-4032-44e5-83ee-d8413040c4e3/image.png" alt=""> <br></li>
</ul>
<h3 id="2-네임서버-설정">2. 네임서버 설정</h3>
<ul>
<li><p>호스팅 영역 시 <strong>NS, SOA 레코드</strong>가 자동 생성되는데, 이 중 <strong>NS</strong> 레코드가 제공하는 <strong>4개의 트래픽 값</strong>을 <strong>가비아의 네임서버</strong>로 등록해줘야 함. <code>xxx.xxx.xxx.</code> 형태로 제공하는데, 여기서 <strong>마지막 <code>.</code>을 지우고 입력</strong>해야 함.
<img src="https://velog.velcdn.com/images/pockyjx_/post/03d4a1ff-3c05-4559-ba43-48ae87bee782/image.png" alt=""> </p>
</li>
<li><p><strong>가비아</strong>에서 사용할 도메인의 네임서버에 <strong>4개의 트래픽 값</strong>을 <strong>1~4차</strong>에 순서대로 입력!
<img src="https://velog.velcdn.com/images/pockyjx_/post/ec7019d9-f97e-4034-931a-b5f6069ea734/image.png" alt=""></p>
</li>
</ul>
<h3 id="3-acm--인증서-요청">3. ACM : 인증서 요청</h3>
<ul>
<li><p><strong>ACM</strong>(Amazon Certificate Manager)에서 <strong>인증서</strong> 요청 : <strong>도메인</strong> 주소 입력 ➡️ 검증까지 10분정도 걸린듯?
<img src="https://velog.velcdn.com/images/pockyjx_/post/b30cca57-add4-45dd-93ef-13c8660dbb94/image.png" alt=""> <img src="https://velog.velcdn.com/images/pockyjx_/post/b3f580fd-f0b1-42d9-b5fb-097ed1d4aca7/image.png" alt=""> </p>
</li>
<li><p>검증 기다리는 동안 생성된 인증서를 통해 <strong><code>Route 53에서 레코드 생성</code></strong> 해주기. 다시 <strong>Route 53</strong>으로 돌아가서 해당 도메인 호스팅 영역을 확인해보면 <strong>CNAME 레코드</strong>가 생성된 것을 확인할 수 있음! (늦게 캡쳐해서 A 레코드까지 있는데 얘는 밑에서 생성할 예정..)
<img src="https://velog.velcdn.com/images/pockyjx_/post/9a580889-d8e0-4987-b53b-c7dbbd95456c/image.png" alt=""> <img src="https://velog.velcdn.com/images/pockyjx_/post/9f53a667-fcc2-4eea-8541-28a3d33752b3/image.png" alt=""></p>
</li>
<li><p>이것저것 하다보면 어느샌가 인증서가 발급되어 있다,, 다음 단계 ㄱㄱ  <img src="https://velog.velcdn.com/images/pockyjx_/post/729c6661-c4f2-43b7-989b-321a22058b97/image.png" alt=""></p>
</li>
</ul>
<h3 id="4-대상-그룹">4. 대상 그룹</h3>
<blockquote>
<p><strong>EC2</strong> → 로드 밸런싱 → <strong>대상 그룹</strong> </p>
</blockquote>
<ul>
<li><p><strong>대상 유형</strong> : <strong>EC2 인스턴스</strong>에 배포했으므로 인스턴스 선택 <img src="https://velog.velcdn.com/images/pockyjx_/post/6d105d96-b19a-4f98-a0aa-0c7077b90ef2/image.png" alt=""></p>
</li>
<li><p><strong><code>프로토콜 : 포트</code></strong> : nestjs 프로젝트를 3000번 포트로 배포했으므로 <strong><code>HTTP:3000</code></strong>으로 설정 <img src="https://velog.velcdn.com/images/pockyjx_/post/c01b4eeb-c9f3-4ae9-9b29-0505abf4ecd2/image.png" alt=""></p>
</li>
<li><p><strong>대상 등록</strong> : <strong>도메인 연결할 인스턴스</strong> 선택 후 <strong><code>아래에 보류 중인 것으로 포함</code></strong> 꼭 클릭하기,, 안했다가 로드밸런서까지 다 연결했는데 계속 503 떠서 한참 삽질함 ^-^,, <img src="https://velog.velcdn.com/images/pockyjx_/post/dcb8eb66-e2eb-4dcd-a4c9-57d64e239e5a/image.png" alt=""></p>
</li>
</ul>
<h3 id="5-로드밸런서">5. 로드밸런서</h3>
<blockquote>
<p><strong>EC2</strong> → 로드 밸런싱 → <strong>로드밸런서</strong> </p>
</blockquote>
<ul>
<li><p><strong>가용 영역</strong> : 최소 2개 이상 선택해야 함. 각 영역을 체크하면 서브넷 ID가 나오는데, <strong>해당 영역의 서브넷 ID</strong>가 <strong>연결할 인스턴스의 서브넷 ID와 일치</strong>하는 걸 <strong>포함</strong>해줘야 함! 나는 2a가 같길래 그냥 위에서 2개 설정해줌..
<img src="https://velog.velcdn.com/images/pockyjx_/post/c36d9709-6516-486e-9ce9-5c7bc4b6a715/image.png" alt=""> <img src="https://velog.velcdn.com/images/pockyjx_/post/5f7f064f-9378-4c20-b5ca-c62131150646/image.png" alt=""></p>
</li>
<li><p><strong>리스너</strong> : <strong>HTTP</strong>와 <strong>HTTPS</strong>에 연결해줌. HTTP의 경우 프로젝트를 3000번으로 배포했으므로 해당 포트와 연결하고, HTTPS는 기본 포트인 443 + 위에서 만든 인증서까지 연결. 해준 후에 둘 다 위에서 생성한 <strong>대상 그룹과 연결</strong>해주기! <img src="https://velog.velcdn.com/images/pockyjx_/post/987336b2-5b51-4795-9223-4535c10c2415/image.png" alt=""> <img src="https://velog.velcdn.com/images/pockyjx_/post/df968827-f510-4598-ab69-f6d50e781adf/image.png" alt=""></p>
</li>
<li><p>로드밸런서를 생성했으면 <strong>Route 53</strong>으로 돌아가서 해당 호스팅 영역에 <strong><code>레코드 생성</code></strong> 후에 만든 <strong>로드밸런서 연결</strong>해주기! (<strong>별칭</strong> 선택) 그러면 A 레코드까지 생성 완료.  <img src="https://velog.velcdn.com/images/pockyjx_/post/503caad1-c3c1-46fd-b80f-7f6e060cbd0a/image.png" alt=""> </p>
</li>
<li><p>로드밸런서 생성 시 만들어준 <strong>HTTP/HTTPS 리스너</strong>에 새로운 <strong>규칙 생성</strong></p>
<ul>
<li><p><strong>HTTPS</strong> : <strong><code>https://도메인</code></strong> 접속 시 <strong>대상 그룹</strong>으로 바로 연결 <img src="https://velog.velcdn.com/images/pockyjx_/post/94b05a02-45e7-4870-932f-60e0da104215/image.png" alt=""> </p>
</li>
<li><p><strong>HTTP</strong> : <strong><code>http://도메인</code></strong> 접속 시 <strong><code>https://도메인</code>으로 리다이렉션</strong>하도록 설정! <img src="https://velog.velcdn.com/images/pockyjx_/post/250795e3-ee7a-4717-8e77-c00dcbceb6ab/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<hr>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/017a3cab-b509-4776-abe3-fb81ce99d829/image.png" alt=""> 로드밸런서까지 설정한 후에 도메인 접속 시 배포된 프로젝트가 정상적으로 보이는 것을 확인할 수 있음! </p>
<p>현재는 과제로 제출한 nestjs 프로젝트를 연결해놨는데, 앞으로 개인 프로젝트나 개인적으로 뭐 배포할 일 있으면 해당 도메인을 애용할 예정,,</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nest.js] 초기 세팅 : CORS / 환경변수 / TypeORM (Mysql 연동) / Swagger]]></title>
            <link>https://velog.io/@pockyjx_/Nest.js-%EC%B4%88%EA%B8%B0-%EC%84%B8%ED%8C%85-CORS-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-TypeORM-Mysql-%EC%97%B0%EB%8F%99-Swagger</link>
            <guid>https://velog.io/@pockyjx_/Nest.js-%EC%B4%88%EA%B8%B0-%EC%84%B8%ED%8C%85-CORS-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-TypeORM-Mysql-%EC%97%B0%EB%8F%99-Swagger</guid>
            <pubDate>Sun, 04 Aug 2024 05:32:53 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-초기-설정">💡 초기 설정</h1>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/08e91ef1-bc23-42ac-84a5-38002506f49b/image.png" alt=""> <span style="color:lightgray"> API 구현하기 전에 기본적으로 뭘 셋팅해줘야 하나 이것저것 찾아보다가 일단 제일 많이 나온? 부분만 기본적으로 설정해보기..</span></p>
<hr>
<h2 id="✅-cors">✅ CORS</h2>
<blockquote>
<p>*<em><code>main.ts</code> *</em>에서 설정 가능!</p>
</blockquote>
<ul>
<li><strong>default</strong> 설정 : <strong>모든 도메인</strong> 허용<pre><code class="language-ts">const app = await NestFactory.create(AppModule, { cors: true }); // 방법 1
app.enableCors(); // 방법 2</code></pre>
</li>
</ul>
<p>➡️ 위 방법대로 설정 시, <strong>default 옵션</strong>은 다음과 같음</p>
<pre><code class="language-js">{
  &quot;origin&quot;: &quot;*&quot;,
  &quot;methods&quot;: &quot;GET,HEAD,PUT,PATCH,POST,DELETE&quot;,
  &quot;preflightContinue&quot;: false,
  &quot;optionsSuccessStatus&quot;: 204
}</code></pre>
<ul>
<li><strong>커스텀</strong> 옵션 : <strong><code>app.enableCors()</code></strong> 내에 <strong>지정할 옵션</strong> 명시<pre><code class="language-js">app.enalbeCors({
  origin: [&#39;url1&#39;, &#39;url2&#39;, ...], // 허용할 도메인 (단일 도메인일 경우 [] 생략)
  methods: &#39;GET, PUT, ...&#39;, // 허용 메서드
  credentials: true, // 인증 정보 포함 여부
  ...
})</code></pre>
</li>
</ul>
<hr>
<h2 id="✅-환경변수">✅ 환경변수</h2>
<blockquote>
<p><strong>config, cross-env</strong> 모듈을 사용하여 <strong>개발 환경</strong>(개발, 배포)<strong>에 따른 환경 변수 설정</strong>하기</p>
</blockquote>
<ol>
<li><p><strong><code>config</code>, <code>cross-env</code></strong> 모듈 설치</p>
<pre><code class="language-shell">npm i @nestjs/config cross-env</code></pre>
</li>
<li><p><strong><code>package.json</code></strong> 설정 추가 : <strong>실행 환경</strong>에 따른 <strong><code>NODE_ENV</code></strong> 지정 → <code>dev=development, prod=production</code></p>
<pre><code class="language-js">&quot;scripts&quot;: {
 &quot;start:dev&quot;: &quot;npm run prebuild &amp;&amp; cross-env NODE_ENV=development nest start --watch&quot;,
 &quot;start:prod&quot;: &quot;cross-env NODE_ENV=production node dist/main&quot;,
 &quot;prebuild&quot;: &quot;rimraf dist&quot;
},
...</code></pre>
</li>
<li><p><strong><code>app.module.ts</code></strong> : 실행 환경에 따른 <code>env</code> 파일 지정 → <strong><code>start:prod</code></strong> 실행 시 <code>package.json</code> 설정에 의해  <strong><code>NODE_ENV=production</code></strong>이므로 <strong><code>.prod.env</code></strong>를 실행하고, 그 외의 경우(<strong><code>start:dev</code></strong>) <strong><code>.dev.env</code></strong> 실행</p>
</li>
</ol>
<pre><code class="language-ts">import { Module } from &#39;@nestjs/common&#39;;
import { AppController } from &#39;./app.controller&#39;;
import { AppService } from &#39;./app.service&#39;;
import { TypeOrmModule } from &#39;@nestjs/typeorm&#39;;
import { ConfigModule } from &#39;@nestjs/config&#39;;
import * as process from &#39;process&#39;;
import { Member } from &#39;./member/Member.entity&#39;;

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [],
      cache: true,
      envFilePath: process.env.NODE_ENV === &#39;production&#39; ? &#39;.prod.env&#39; : &#39;.dev.env&#39;
    })
  ],
  ...
})</code></pre>
<hr>
<h2 id="✅-typeorm">✅ TypeORM</h2>
<blockquote>
<p><strong><code>TypeORM</code></strong> : <strong>TypeScript</strong>와 <strong>JavaScript</strong>를 사용한 <strong>객체 관계 매핑</strong> 수행 (Java의 <strong>JPA</strong>와 같은 역할!)</p>
</blockquote>
<ol>
<li>npm을 통해 <strong>typeorm, mysql</strong> 패키지 설치<pre><code class="language-shell">$ npm install @nestjs/typeorm typeorm mysql2</code></pre>
</li>
</ol>
<ol start="2">
<li><strong>환경변수</strong> 설정</li>
</ol>
<ul>
<li><p><strong><code>.env</code></strong> : DB 설정 → 보안에 민감한 정보이므로 <code>.gitignore</code> 에 추가</p>
<pre><code class="language-txt">DB_HOST=host 주소
DB_PORT=포트번호
DB_USERNAME=사용자명
DB_PASSWORD=비밀번호
DB_DATABASE=사용할 스키마명</code></pre>
</li>
<li><p><strong><code>app.module.ts</code></strong> : <code>env</code>의 DB 설정을 가져와서 실행</p>
</li>
</ul>
<pre><code class="language-ts">@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: &#39;mysql&#39;,
      host: process.env.DB_HOST,
      port: +process.env.DB_PORT, // + : 문자열 → 숫자 변환
      username: process.env.DB_USERNAME,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_DATABASE,
      entities: [사용할 엔티티 목록],
      synchronize: true // 실행 시 entites 내의 엔티티에 해당하는 테이블 자동 생성
    })
  ],
  ...
})</code></pre>
<h3 id="➕-예제">➕ 예제</h3>
<blockquote>
<p>엔티티를 생성하여 <strong>애플리케이션과 DB가 잘 연결되었는지 확인</strong>해보기!</p>
</blockquote>
<ul>
<li><strong>엔티티</strong> 생성 : <code>src/member/Member.entity.ts</code></li>
</ul>
<pre><code class="language-ts">import { Column, Entity, PrimaryGeneratedColumn } from &#39;typeorm&#39;;

@Entity() // 엔티티 지정 : () 안붙이면 실행 시 에러 발생
export class Member {
  @PrimaryGeneratedColumn() // PK : AI 적용
  id: number;

  @Column({type: &#39;varchar&#39;, nullable: false, length: 20}) // 컬럼 제약조건 지정 가능
  name: string;
}</code></pre>
<ul>
<li><code>app.modules.ts.</code> : <strong><code>entities</code></strong>에 생성한 <strong>엔티티 추가</strong><pre><code class="language-ts">import { Member } from &#39;./member/Member.entity&#39;;
</code></pre>
</li>
</ul>
<p>@Module({
  imports: [
    TypeOrmModule.forRoot({
      entities: [Member],
      ...
    })
  ],
  ...
})</p>
<pre><code>
![](https://velog.velcdn.com/images/pockyjx_/post/58e08f7e-3d98-4aeb-b33a-9fa898d37fba/image.png) : 애플리케이션 실행 시 **`env`에 지정한 스키마에 테이블이 생성**되고, 각 **컬럼마다 제약조건이 잘 설정**된 것을 확인할 수 있다!

---

## ✅ Swagger

1. npm을 통해 **`swagger`** 모듈 설치
```shell
npm i --save @nestjs/swagger swagger-ui-express</code></pre><ol start="2">
<li><strong><code>main.ts</code></strong> : swagger 문서를 사용하기 위한 기본 설정 추가</li>
</ol>
<ul>
<li><code>await app.listen(3000)</code>를 실행하면 애플리케이션이 요청을 수신하기 시작함. 이때, <code>listen()</code>이 호출된 후에 swagger 모듈 설정이 실행되면 해당 모듈이 API 엔드포인트르 찾지 못해 404 에러가 발생하므로, <strong>모든 모듈의 설정 및 생성을 끝낸 후에 <code>listnen()</code>을 가장 마지막에 실행해줘야 함!</strong></li>
</ul>
<pre><code class="language-ts">import { NestFactory } from &#39;@nestjs/core&#39;;
import { AppModule } from &#39;./app.module&#39;;
import { DocumentBuilder, SwaggerModule } from &#39;@nestjs/swagger&#39;;

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { cors: true });

  // 기본 설정
  const config = new DocumentBuilder()
    .setTitle(&#39;API 문서&#39;)
    .setDescription(&#39;API 문서입니다.&#39;)
    .setVersion(&#39;1.0&#39;)
    .build();

  // 문서 생성
  const document = SwaggerModule.createDocument(app, config);

  // localhost:3000/api 접속 시 생성한 swagger 문서 실행
  SwaggerModule.setup(&#39;api&#39;, app, document);

  await app.listen(3000);
}
bootstrap();</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/75e35c7b-f431-415f-b94a-389eb24865c6/image.png" alt=""></p>
<h3 id="➕-컨트롤러에-적용하기">➕ 컨트롤러에 적용하기</h3>
<blockquote>
<ul>
<li><strong><code>@ApiTags()</code></strong> : 컨트롤러에 대한 태그</li>
</ul>
</blockquote>
<ul>
<li><p><strong><code>@ApiOperation()</code></strong> : 메서드에 대한 설명</p>
</li>
<li><p><code>@ApiResponse()</code> : 상태에 따른 응답 반환 형태</p>
</li>
<li><p><code>@ApiBody()</code> : 요청 Body에 대한 정보</p>
</li>
<li><p><code>@ApiParam()</code> : 요청 파라미터에 대한 정보</p>
</li>
<li><p><code>@ApiQuery()</code> : 쿼리에 대한 정보</p>
</li>
<li><p><strong><code>@ApiProperty()</code></strong> : 프로퍼티에 대한 정보 (DTO에서 주로 사용)</p>
</li>
<li><p><code>app.controller.ts</code></p>
</li>
</ul>
<pre><code class="language-ts">import { Controller, Get } from &#39;@nestjs/common&#39;;
import { AppService } from &#39;./app.service&#39;;
import { ApiOperation, ApiResponse, ApiTags } from &#39;@nestjs/swagger&#39;;

@ApiTags(&#39;기본 API&#39;)
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @ApiOperation({ summary: &#39;홈&#39;, description: &#39;홈 접속&#39; })
  @ApiResponse({ status: 200, description: &#39;성공&#39;})
  @ApiResponse({status: 404, description: &#39;실패&#39;})
  getHello(): string {
    return this.appService.getHello();
  }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/cab7a097-b3f7-460b-8231-57cccd2179f3/image.png" alt=""></p>
<hr>
<h4 id="🙏🏻-참고">🙏🏻 참고</h4>
<blockquote>
<ul>
<li><a href="https://velog.io/@sujeong7474/01.-Nest.js-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%B4%88%EA%B8%B0-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0#2-validationpipe">01. Nest.js 프로젝트 초기 세팅하기</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://velog.io/@fcfargo/Nest.js-%EC%B4%88%EA%B8%B0-%EC%84%B8%ED%8C%85#-swagger-%EC%84%A4%EC%A0%95">Nest.js - 초기 세팅</a></li>
<li><a href="https://docs.nestjs.com/security/cors">CORS (Nest.js 공식 문서)</a></li>
<li><a href="https://velog.io/@kwontae1313/NestJS-CORS">NestJS CORS</a></li>
<li><a href="https://velog.io/@wonn23/NestJS-%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0">[NestJS] 환경 변수 관리하기</a></li>
<li><a href="https://chaerim1001.tistory.com/68_">[NestJS] mysql 데이터베이스 연동하기</a></li>
<li><a href="https://develop-const.tistory.com/15">NestJs 프로젝트에서 Swagger 사용하기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nest.js] Node.js & Nest.js란? + 설치 & 프로젝트 생성]]></title>
            <link>https://velog.io/@pockyjx_/Nest.js-Node.js-Nest.js%EB%9E%80-%EC%84%A4%EC%B9%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@pockyjx_/Nest.js-Node.js-Nest.js%EB%9E%80-%EC%84%A4%EC%B9%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Fri, 02 Aug 2024 03:33:58 GMT</pubDate>
            <description><![CDATA[<p><span style="color: lightgray"> <small> 과제 전형에서 nest.js를 사용하게 되어서 node.js와 nest.js에 대해서 기초?만 간단하게 정리해보는 글.. </small> </span></p>
<hr>
<h1 id="💡-nodejs">💡 Node.js</h1>
<blockquote>
<p><strong>Chrome V8 Engine</strong>으로 빌드된 <strong>JavaScript 런타임</strong> 환경 → <strong>다양한 OS</strong> 환경에서 실행 가능</p>
</blockquote>
<ul>
<li><p><strong>싱글 쓰레드 비동기 방식</strong> : <strong>입력</strong>은 <strong>하나의 쓰레드</strong>에서 받지만, 받은 순서대로 처리하지 않고 <strong>먼저 처리된 결과 값을 먼저 반환</strong></p>
<ul>
<li>하나의 쓰레드를 사용하므로 <strong>메모리 &amp; 리소스 사용량이 적음</strong></li>
<li>하나의 쓰레드에서 <strong>에러</strong>가 발생하면 <strong>프로그램 전체로 전파</strong>됨<br>

</li>
</ul>
</li>
</ul>
<h3 id="🙆🏻♀️-장점">🙆🏻‍♀️ 장점</h3>
<ul>
<li>비동기 방식으로 인한 <strong>서버 부하 최소화</strong></li>
<li><strong>npm</strong>을 통한 다양한 모듈 및 패키지 제공</li>
<li>구글이 제작한 <strong>V8</strong> Engine을 사용하므로 <strong>확장성</strong>이 좋음</li>
</ul>
<blockquote>
<p>👍🏻 <strong>로직이 간단</strong>하면서, <strong>대용량</strong>의 요청을 <strong>빠르게 처리</strong>해야 하는 서비스 : 스트리밍, 채팅 등</p>
</blockquote>
<h3 id="🙅🏻♀️-단점">🙅🏻‍♀️ 단점</h3>
<ul>
<li>비동기 방식으로 인해 로직이 복잡해지면 <strong>콜백 함수의 늪</strong>에 빠질 수 있음</li>
<li>싱글 쓰레드이므로, <strong>하나의 작업 처리 시간이 긴 경우에 불리</strong>함 ⭐</li>
<li>코드를 <strong>실행</strong>해야만 <strong>에러</strong>를 알 수 있음 : 싱글 쓰레드로 인해 에러 발생 시 전체 프로세스가 동작하지 않음. 따라서, 테<strong>스트가 매우 중요함!</strong></li>
</ul>
<blockquote>
<p>👎🏻 로직이 복잡하면서 <strong>단일 작업의 처리 시간이 오래 걸리는</strong> 서비스</p>
</blockquote>
<hr>
<h1 id="💡-nestjs">💡 Nest.js</h1>
<blockquote>
<p><strong>Node.js</strong>에 기반을 둔 웹 프레임워크</p>
</blockquote>
<ul>
<li><p>Node.js의 단점 보완</p>
<ul>
<li><strong>TypeScript</strong> 지원</li>
<li><strong>확장성</strong> : <strong>모듈/컴포넌트</strong> 기반으로 프로그램 작성</li>
<li><strong>객체지향</strong> 개념 도입 : <strong>IoC</strong>(제어의 역전), <strong>AOP</strong>(관점 지향 프로그래밍), <strong>DI</strong>(의존성 주입)<br>

</li>
</ul>
</li>
</ul>
<h3 id="🙆🏻♀️-장점-1">🙆🏻‍♀️ 장점</h3>
<ul>
<li>Express, Fastify와 호환 가능</li>
<li><strong>OOP, 함수형 프로그래밍</strong> 일부 요소 사용 가능</li>
<li><strong>규모 가변성</strong> (scalable) : <strong>운영 상황</strong>에 따라 <strong>애플리케이션의 규모</strong>가 <strong>동적</strong>으로 변함</li>
<li>Nest.js에서 제공하는 아키텍처로 인해 <strong>유지보수 용이</strong><br>

</li>
</ul>
<h3 id="🙅🏻♀️-단점-1">🙅🏻‍♀️ 단점</h3>
<ul>
<li>러닝 커브가 있는 편 <del>(Spring 경험이 있으면 괜찮을지도?..)</del></li>
<li>간결함을 추구하는 Node.js에 비해 구조가 복잡한 편</li>
</ul>
<hr>
<h1 id="⚙️-설치">⚙️ 설치</h1>
<blockquote>
<p>✅ <strong>Nest.js</strong>는 <strong>Node.js 기반</strong> : Node.js 먼저 설치 후, <strong>npm</strong>을 통해 <strong>Nest.js 라이브러리 설치</strong></p>
</blockquote>
<h3 id="1-nodejs-설치-lts-권장"><a href="https://nodejs.org/en">1. Node.js 설치</a> (LTS 권장)<img src="https://velog.velcdn.com/images/pockyjx_/post/4748b5b6-0d99-43e6-8c61-038f2960c49c/image.png" alt=""></h3>
<ul>
<li>설치 확인 : <strong>버전</strong>이 출력되면 정상적으로 설치된 거임!</li>
</ul>
<pre><code class="language-shell">node -v</code></pre>
<h3 id="2-nestjs-cli-설치">2. nestjs cli 설치</h3>
<ul>
<li><strong><code>-g</code></strong> : <strong>글로벌 환경</strong>에 설치<pre><code class="language-shell">npm i -g @nestjs.cli # 설치
npm root -g # 글로벌 환경 설치 경로 확인</code></pre>
</li>
</ul>
<h3 id="3-프로젝트-생성">3. 프로젝트 생성</h3>
<pre><code class="language-shell">nest new [프로젝트명]</code></pre>
<ul>
<li>프로젝트 생성 명령어 실행 시, <strong>원하는 패키지 매니저를 선택</strong>해야 함. 나는 <strong>npm</strong> 선택!</li>
</ul>
<pre><code>? Which package manager would you ❤️  to use? (Use arrow keys)
&gt; npm 
  yarn 
  pnpm </code></pre><h5 id="⭐-생성된-프로젝트">⭐ 생성된 프로젝트</h5>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/11458976-d160-4b7c-8611-5f509f77aeaa/image.png" alt=""></p>
<ul>
<li><strong><code>app.controller.ts</code> : 기본 컨트롤러</strong> (라우팅 + 요청 처리)</li>
<li><code>app.controller.spec.ts</code> : 컨트롤러 단위 테스트용 파일</li>
<li><code>app.module.ts</code> : 최상위 모듈 파일</li>
<li><strong><code>app.service.ts</code></strong> : <strong>단일 메서드</strong>를 사용하는 <strong>기본 서비스 로직</strong></li>
<li><strong><code>main.ts</code></strong> : <strong>애플리케이션을 실행</strong>하는 엔트리 파일<br>

</li>
</ul>
<h3 id="4-실행">4. 실행</h3>
<ul>
<li>명령어 실행 후 <strong><a href="http://localhost:3000">http://localhost:3000</a></strong> 접속<pre><code class="language-shell">npm run start # 서버 실행
npm run start:dev # 수정 사항 바로 반영</code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/08da9c3c-723c-48e8-b0d0-716d314ce785/image.png" alt=""></p>
<h5 id="➕-maints">➕ <code>main.ts</code></h5>
<pre><code class="language-ts">import { NestFactory } from &#39;@nestjs/core&#39;;
import { AppModule } from &#39;./app.module&#39;;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000); // 포트번호 지정
}
bootstrap();</code></pre>
<h5 id="➕-appcontrollerts--컨트롤러">➕ <code>app.controller.ts</code> : 컨트롤러</h5>
<pre><code class="language-ts">import { Controller, Get } from &#39;@nestjs/common&#39;;
import { AppService } from &#39;./app.service&#39;;

@Controller() // 컨트롤러 선언
export class AppController {
  constructor(private readonly appService: AppService) {} // 서비스 의존성 주입 : 생성자

  @Get() // Get 요청을 받아서 처리 (루트 경로)
  getHello(): string {
    return this.appService.getHello(); // 서비스 로직 호출
  }
}</code></pre>
<h5 id="➕-appservicets--서비스">➕ <code>app.service.ts</code> : 서비스</h5>
<pre><code class="language-ts">import { Injectable } from &#39;@nestjs/common&#39;;

@Injectable() // 다른 서비스에 주입될 수 있음을 명시 → 컨트롤러에서 주입하여 사용
export class AppService {
  getHello(): string { // 내부 로직 정의
    return &#39;Hello World!&#39;;
  }
}</code></pre>
<hr>
<h4 id="🙏🏻-참고">🙏🏻 참고</h4>
<blockquote>
<ul>
<li><a href="https://seoyeonkk.tistory.com/entry/Nodejs-%EC%9D%98-%ED%8A%B9%EC%A7%95-%EB%B0%8F-%EC%9E%A5%EB%8B%A8%EC%A0%90">[Node.js] 특징 및 장단점</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://velog.io/@hshs0409/NestJS">NestJS 소개</a></li>
<li><a href="https://wikidocs.net/147787">1.1 NestJS 소개</a> </li>
<li><a href="https://develop-const.tistory.com/3">NestJS 시작하기</a></li>
<li><a href="https://sound-story.tistory.com/entry/NestJS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0">[NestJs] 개발하기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA] Spring Cloud Gateway (1) : CORS + Routing 설정]]></title>
            <link>https://velog.io/@pockyjx_/MSA-Spring-Cloud-Gateway-1-CORS-Routing-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@pockyjx_/MSA-Spring-Cloud-Gateway-1-CORS-Routing-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Thu, 01 Aug 2024 14:37:51 GMT</pubDate>
            <description><![CDATA[<h4 id="➕-api-gateway">➕ API Gateway</h4>
<blockquote>
<p>MSA 환경 내의 모든 서비스 <strong>엔드 포인트 단일화</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/a1508727-1302-411f-ab32-eb8aede018be/image.png" alt=""> 최근 진행했던 팀 프로젝트의 서비스아키텍처에서 가져온 부분인데, 간단히 설명하자면 프론트 서버에서 API를 호출하면 해당 서비스에 직접 API를 요청하는 것이 아니라, <strong>게이트웨이 서버</strong>로 요청을 보낸 후에 게이트웨이에서 <strong>라우팅</strong> 작업을 통해 <strong>해당 서비스로 요청을 전달</strong>하는 방식으로 동작하게 된다.</p>
<p>게이트웨이를 사용하지 않으면 프론트에서 각 서버의 url을 알아야 하는데, 게이트웨이 통해 <strong>게이트웨이 url</strong>만을 사용하여 API 요청이 가능해진다!</p>
<hr>
<h1 id="💡-spring-cloud-gateway">💡 Spring Cloud Gateway</h1>
<blockquote>
<p><strong>Spring Framework</strong>에서 제공하는 오픈 소스 기반의 Gateway 서비스</p>
</blockquote>
<ul>
<li><p>Spring Cloud Gateway vs Spring Cloud Zuul (패치 중단)</p>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center">SCG</th>
<th align="center">Zuul</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>기반 웹 서버</strong></td>
<td align="center">Netty</td>
<td align="center">Tomcat</td>
</tr>
<tr>
<td align="center"><strong>동기/비동기</strong></td>
<td align="center">비동기</td>
<td align="center">동기</td>
</tr>
<tr>
<td align="center"><strong>웹 프레임워크</strong></td>
<td align="center">Spring Webflux</td>
<td align="center">Spring Web MVC</td>
</tr>
</tbody></table>
</li>
</ul>
<hr>
<h3 id="✅-역할">✅ 역할</h3>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/b4794cd8-f229-4913-a561-3d30c0eb6333/image.png" alt=""></p>
<ul>
<li><strong><code>Routing</code></strong> : 요청을 전달할 경로(서비스) 설정</li>
<li><strong><code>Load Balancing</code></strong> : 여러 서버에 대한 트래픽을 분배하여 부하 분산 처리</li>
<li><strong><code>Filter</code></strong> : 요청에 대한 특정 작업 수행 → 인증, 에러 처리 등</li>
</ul>
<hr>
<h3 id="1-기본-설정">1. 기본 설정</h3>
<blockquote>
<p>게이트웨이 서비스 생성 + 유레카 서버에 등록</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/3298bc74-72a7-4508-8e28-35f9acb7a482/image.png" alt=""></p>
<ul>
<li><code>application.yml</code><pre><code class="language-yml">server:
port: 8000
</code></pre>
</li>
</ul>
<p>eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: <a href="http://localhost:8761/eureka">http://localhost:8761/eureka</a></p>
<p>spring:
  application:
    name: gateway</p>
<pre><code>
![](https://velog.velcdn.com/images/pockyjx_/post/95a15c51-33f6-40a2-abca-19f33816aad6/image.png)


---

### 2. CORS 설정
&gt; ✅ CORS (**C**ross-**O**rigin **R**esource **S**haring) : **다른 출처**에 있는 리소스에 접근(**요청**) &lt;br&gt;
→ **클라이언트 측에서 서버 측으로 요청**을 보내는 경우 출처가 다르므로 접근을 허용하지 않는 경우를 방지하기 위해, **특정 출처를 가지는 클라이언트의 요청의 접근을 허용**하도록 설정해야 함!

➕ 모놀리식 아키텍처의 경우 Spring Security를 통해 설정 가능함. MSA 환경에서도 각 서비스 별로 설정 가능하지만, 모든 서비스에 Spring Security 의존성을 추가하고 Config를 작성해줘야 함.. 어차피 게이트웨이를 거쳐서 서비스에 갈 것이기 때문에 **게이트웨이에서 CORS에 대한 설정**을 해줌으로써 **모든 서비스에 공통적으로 적용**할 수 있음! 

근데 또 공통적인 부분 말고 각 서비스 별로 추가로 설정해줘야 하는 경우가 있을지도?.. 이건 아직 경험 안해봐서 모르겠음 ㅎ..
&lt;br&gt;

- `application.yml`

  - **`allowedOrigins`** : **`*`**는 **모든 경로**를 허용하지만, **`allow-credentials: true`**인 경우에는 `*`를 사용할 수 없으므로 **허용할 모든 출처를 직접 명시**해야 함. (ex - JWT 토큰 검증)

```yml
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          &quot;[/**]&quot;:
            allowedOrigins: # 허용할 출처(도메인)
              - &quot;http://localhost:3000&quot; # 클라이언트 : 프론트 서버
            allow-credentials: true # 인증 포함 요청 가능 (JWT 토큰)
            allowedHeaders: &quot;*&quot; # 허용할 
            allowedMethods: # 허용할 HTTP 메서드
              - PUT
              - GET
              - POST
              - DELETE
              - OPTIONS # Preflight 요청 시 사용</code></pre><hr>
<h3 id="3-라우팅-설정">3. 라우팅 설정</h3>
<blockquote>
<p><strong>요청 경로</strong>에 따라 <strong>요청을 전달할 서비스</strong> 및 서버 선택 (로드 밸런싱)</p>
</blockquote>
<ul>
<li><p><code>application.yml</code></p>
<ul>
<li><p><strong><code>uri</code></strong> : <strong>요청을 전달</strong>할 주소 → <strong>로드 밸런싱</strong> 적용 시 <strong><code>lb://eureka에 등록된 서비스명</code></strong> 형태로 명시</p>
</li>
<li><p><strong><code>predicates</code></strong> : 해당 조건을 만족하는 요청만 명시된 <code>uri</code>로 전달 (<strong>조건문</strong>)</p>
<ul>
<li><strong><code>Path</code></strong> : 특정 <strong>경로</strong> (ex - <code>/member-service/**</code> 경로의 요청만 전달)</li>
<li><strong><code>Method</code></strong> : 특정 <strong>메서드</strong></li>
</ul>
</li>
<li><p><strong><code>filters</code></strong> : 요청에 특정 작업 수행</p>
<ul>
<li><p><strong><code>StripPrefix=n</code></strong> : <strong>n번째 경로까지 제거</strong>한 후 전달 (ex - <code>/member-service/partners/profile</code> → `/partners/profile)</p>
</li>
<li><p><strong>커스텀 필터</strong>도 등록 가능 (ex - JWT 토큰 검증 필터)</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<pre><code class="language-yml">spring:
    gateway:
      routes:
        - id: member-service # 라우팅 id
          uri: lb://MEMBER-SERVICE # 이동할 주소
          predicates: # 라우팅 조건 : 경로, 메서드, ...
            - Path=/member-service/**
          filters:
            - StripPrefix=1 # 첫번째 경로 제거
            # - rewritePath=/member-service/(?&lt;segment&gt;.*), /$\{segment} : StripPrefix와 같은 역할</code></pre>
<h4 id="ex1-member-service에-직접-요청">ex1) member-service에 직접 요청</h4>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/b18bd32e-9467-426f-89b5-76d16e0426c6/image.png" alt=""></p>
<h4 id="ex2-게이트웨이를-통한-요청">ex2) 게이트웨이를 통한 요청</h4>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/e488b2c8-1147-4fb4-b244-267f9d45800c/image.png" alt=""> : 서비스에 직접 요청했을 때와 결과가 같음. 즉, <strong>게이트웨이</strong>에서 <strong><code>member-service</code>로 요청을 잘 전달했다</strong>는 뜻!</p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/89e6ccdc-28db-4eee-be79-36f088611c93/image.png" alt=""></p>
<p>➕ <strong>게이트웨이 요청</strong> 시 <strong><code>member-service</code>를 붙이지 않으면 404 에러</strong>가 발생하는데, 이는 라우팅 설정 시 <code>/member-service/**</code>에 해당하는 요청만 전달하도록 해놔서 그런거임.. 쉽게 말하면 <strong><code>/partners/profile</code>을 처리하는 라우팅이 없기 때문!</strong></p>
<hr>
<h4 id="🙏🏻-참고">🙏🏻 참고</h4>
<blockquote>
<ul>
<li><a href="https://velog.io/@mrcocoball2/Spring-Cloud-Spring-Cloud-Gateway-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90">[Spring Cloud] Spring Cloud Gateway - 기본 개념</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://velog.io/@black_han26/Spring-Cloud-Gateway-%EA%B5%AC%ED%98%84">API Gateway, MSA의 출입구 ( Spring Cloud Gateway )</a></li>
<li><a href="https://velog.io/@mdy0102/Spring-Cloud-Gateway-CORS-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0">Spring Cloud Gateway CORS 문제 해결하기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA] MSA & Spring Cloud란? : Eureka Server/Client 구축]]></title>
            <link>https://velog.io/@pockyjx_/Spring-Cloud-MSAMicro-Service-Architecture-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@pockyjx_/Spring-Cloud-MSAMicro-Service-Architecture-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 01 Aug 2024 12:25:14 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-msa-micro-service-architecture">💡 MSA (Micro Service Architecture)</h1>
<blockquote>
<p><strong>M</strong>icro (<strong>작은?</strong>) <strong>S</strong>ervice (<strong>서비스</strong>) <strong>A</strong>rchitecture (<strong>구조</strong>) : <strong>독립적</strong>으로 실행 및 배포 가능한 <strong>작은 서비스</strong>들로 이루어진 구조 ➡️ 다수로 <strong>분산</strong>된 프로젝트</p>
</blockquote>
<p><strong>↔️ Monolithic</strong> Architecture (<strong>모놀리식</strong> 아키텍처) : 모든 <strong>서비스</strong>가 <strong>하나</strong>의 프로젝트 내에 존재하는 구조</p>
<hr>
<h2 id="✅-특징">✅ 특징</h2>
<blockquote>
<p>서비스 간 <strong>API</strong>를 통한 상호작용 : 각 서비스의 <strong>엔드 포인트</strong>를 <strong>API</strong> 형태로 외부에 노출 </p>
</blockquote>
<ul>
<li>각 서비스는 <strong>하나의 비즈니스(도메인)</strong>에 관한 기능만 수행 ➡️ 회원 서비스, 상품 서비스, ...</li>
<li><strong>메시징 시스템</strong>을 통해 <strong>서비스 간 통신</strong> 가능 : <strong>Kafka</strong>, Fegin Client, RestTemplate, ...<br>


</li>
</ul>
<h4 id="🙆🏻♀️장점">🙆🏻‍♀️장점</h4>
<ul>
<li><strong>재사용</strong> 및 <strong>유연한 확장</strong> 가능 : 다양한 서비스와 결합 가능</li>
<li><strong>독립적</strong>인 실행 및 배포 : 특정 서비스 배포 시, 다른 서비스는 정상적으로 동작</li>
<li><strong>다양한 기술 스택</strong> 사용 가능 : 서비스 별 필요한 기술 스택 사용 가능</li>
<li>서비스 간 <strong>장애 전파 최소화</strong> : 특정 서비스에 장애가 발생해도, 다른 서비스는 정상적으로 동작<br>

</li>
</ul>
<h4 id="🙅🏻♀️-단점">🙅🏻‍♀️ 단점</h4>
<ul>
<li><strong>설계</strong>의 <strong>복잡성</strong> : 도메인 설정의 모호함  → 잘못된 도메인 설정 시, 더 많은 비용이 들 수 있음</li>
<li><strong>성능 저하</strong> : 서비스 간 API 호출 시 통신 비용 및 지연 발생</li>
<li><strong>트랜잭션 및 데이터 관리</strong>의 <strong>복잡성</strong> : 분산 데이터베이스 사용으로 인해 데이터 관리가 비교적 어려움</li>
</ul>
<hr>
<h1 id="💡-spring-cloud">💡 Spring Cloud</h1>
<blockquote>
<p> <strong>분산 시스템</strong>에 필요한 다양한 기능들을 <strong>추상화</strong>하여 제공하는 라이브러리</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/4060942a-f87c-4d26-8074-3226e20bef58/image.png" alt=""></p>
<ul>
<li><strong>Service Discovery</strong> : MSA 환경 내의 <strong>서비스를 검색, 호출</strong> 및 상태 관리 → <strong>Eureka</strong>, Zookeeper, ...</li>
<li><strong>API Gateway</strong> : MSA 환경 내의 <strong>모든 서비스 엔드 포인트 단일화</strong> + 인증/인가 수행 → Zuul, <strong>Spring Cloud Gateway</strong>, ...</li>
<li><strong>Config Server</strong> : 각 서비스에서 필요한 <strong>설정 정보</strong>를 <strong>중앙</strong>에서 관리 + <strong>변경</strong> 시 재빌드 하지 않고 <strong>런타임 중 적용</strong> → <strong>Spring Cloud Config</strong></li>
</ul>
<hr>
<h3 id="➕-구현-예제">➕ 구현 예제</h3>
<h4 id="1-service-discovery">1. Service Discovery</h4>
<ul>
<li><p><strong>Eureka Server</strong> : 30초에 한 번씩 등록된 서비스 목록 갱신
<img src="https://velog.velcdn.com/images/pockyjx_/post/014a0797-71f7-4bfe-b1cd-60e864416daf/image.png" alt=""></p>
</li>
<li><p><code>Application.java</code> : <strong><code>@EnableEurekaServer</code></strong> 어노테이션 추가</p>
<pre><code class="language-java">@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication { ... }</code></pre>
</li>
<li><p><strong><code>application.yml</code></strong> ⭐</p>
<pre><code class="language-yml">spring:
application:
  name: eureka

output:
  ansi:
    enabled: always
</code></pre>
</li>
</ul>
<p>server:
  port: 8761</p>
<p>eureka:
  client:
    register-with-eureka: false # 유레카 서버 본체이므로, 자신을 클라이언트로 등록하지 않음
    fetch-registry: true # 캐싱 여부</p>
<pre><code>➡️ http://localhost:8761 접속 시 등록된 서비스 목록 확인 가능 (아직 등록된 서비스 없음!)![](https://velog.velcdn.com/images/pockyjx_/post/288c0ca5-89aa-4783-9385-6e5f063903a1/image.png)


#### 2. Discovery Client
- **Eureka Discovery Client** : 서비스 **시작** 시 자신의 정보를 **Eureka Server에 등록** + 30초마다 상태 전달 ![](https://velog.velcdn.com/images/pockyjx_/post/e79f3b78-7e3c-447e-9332-8a1fc614bb4b/image.png)

- `Application.java` : **`@EnableDiscoveryClient`** 어노테이션 추가
```java
@SpringBootApplication
@EnableDiscoveryClient
public class MemberserviceApplication { ... }</code></pre><ul>
<li><strong><code>application.yml</code></strong> ⭐<pre><code class="language-yml">server:
port: 0
</code></pre>
</li>
</ul>
<p>spring:
  application:
    name: member-service # 서비스명</p>
<p>eureka:
  instance: # 인스턴스 생성
    instance-id:
     # 인스턴스 id 랜덤 생성 : 랜덤 포트(0) 사용 시, 여러 개의 서비스가 실행되어도 0번 하나로 인식되는 경우 방지
     ${spring.application.name}:${spring.application.instance_id:${random.value}}
  client:
    register-with-eureka: true # 유레카 서버에 서비스 등록
    fetch-registry: true # 유레카 서버가 갱신될 때마다 정보 가져옴
    service-url:
      defaultZone: <a href="http://localhost:8761/eureka/">http://localhost:8761/eureka/</a> # 유레카 서버 주소</p>
<p>```
➡️ <a href="http://localhost:8761">http://localhost:8761</a> 접속 시 <code>member-service</code>가 등록된 것을 확인할 수 있다!
<img src="https://velog.velcdn.com/images/pockyjx_/post/271b6c29-be47-41ed-8be1-4c4cd553ed47/image.png" alt=""></p>
<p><small> 게이트웨이부터는 복잡하므로 다음 글에 계속..</small></p>
<hr>
<h4 id="🙏🏻-참고">🙏🏻 참고</h4>
<blockquote>
<ul>
<li><a href="https://waspro.tistory.com/429">[MSA 개념 정립하기] MSA의 개념과 장단점</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://hahahoho5915.tistory.com/71">[간단정리] MSA란?(등장배경, 특징, 장단점)</a></li>
<li><a href="https://spring.io/projects/spring-cloud">Spring Cloud 공식 문서</a></li>
<li><a href="https://djlife.tistory.com/10">[Spring Cloud] 스프링 클라우드 란 무엇인가요?</a></li>
<li><a href="https://velog.io/@mrcocoball2/Spring-Cloud-Spring-Cloud-Eureka-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90-%EB%B0%8F-%ED%99%9C%EC%9A%A9-74mogeh0">[Spring Cloud] Spring Cloud Eureka - 기본 개념 및 활용</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] EC2 서버에 Docker 설치하기 : MySQL 설치 + 배포]]></title>
            <link>https://velog.io/@pockyjx_/AWS-EC2%EC%97%90-Jenkins-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-1-Docker-%EC%84%A4%EC%B9%98MySQL%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@pockyjx_/AWS-EC2%EC%97%90-Jenkins-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-1-Docker-%EC%84%A4%EC%B9%98MySQL%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Wed, 01 May 2024 13:40:38 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-목표">💡 목표</h1>
<blockquote>
<p><strong>AWS EC2</strong> 서버에서 <strong>Jenkins</strong>를 사용하여 <strong>CI/CD 자동 배포 서버 구축</strong>하기!</p>
</blockquote>
<p>➕ <a href="https://velog.io/@pockyjx_/AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0">EC2 인스턴스 생성하기</a> + <a href="https://velog.io/@pockyjx_/AWS-%EB%B3%B4%EC%95%88-%EA%B7%B8%EB%A3%B9-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0">보안 그룹 생성 및 설정하기</a> : <code>Ubuntu + t2.micro</code> 서버 생성</p>
<hr>
<h2 id="🐋-docker">🐋 Docker</h2>
<h3 id="1-ec2-서버-접속">1. EC2 서버 접속</h3>
<pre><code class="language-shell">ssh -i [키페어 이름] [리눅스 계정명]@[EC2 퍼블릭 IPv4 DNS]</code></pre>
<ul>
<li>터미널을 사용할 <strong>키페어 파일(<code>.pem</code>)이 존재하는 경로</strong>에서 열어줄 것</li>
<li>리눅스 계정명의 경우, 우분투 서버를 사용한다면 <code>ubuntu</code> </li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/df163646-1014-43a0-b0a1-42b9c1090ea8/image.png" alt=""> : 계정명이 <strong><code>ubuntu@ip[]</code></strong>로 변경되었다면 성공적으로 접속된 것! </p>
<hr>
<h3 id="2-docker-설치">2. Docker 설치</h3>
<ul>
<li><p>리눅스에서 기본으로 제공하는 <strong>apt 패키지 업데이트</strong></p>
<pre><code class="language-shell">sudo apt-get update</code></pre>
</li>
<li><p>Docker <strong>설치</strong></p>
<pre><code class="language-shell">sudo apt-get install docker.io</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/ab911d9a-6add-436e-8c26-695b7194dc2e/image.png" alt=""> : 설치 후 <strong><code>docker --version</code></strong>을 입력했을 때, <strong>버전</strong>이 뜨면 정상적으로 설치된 것!</p>
</li>
<li><p>Docker <strong>실행</strong></p>
<pre><code class="language-shell">sudo service docker start</code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/90dee8f2-fda2-4476-987f-7055a3d40488/image.png" alt=""> : <strong><code>sudo systemctl status docker</code></strong>를 통해 Docker의 실행 상태를 확인했을 때, <strong><code>active</code></strong>이면 정상적으로 실행되고 있다는 뜻!</p>
<hr>
<h3 id="3-docker에-mysql-설치">3. Docker에 MySQL 설치</h3>
<ul>
<li><p><strong>MySQL Docker Image</strong> 다운 : 버전을 명시하지 않은 경우, 가장 최신 버전</p>
<pre><code class="language-shell">sudo docker pull mysql:[version]</code></pre>
</li>
<li><p>다운로드 된 <strong>Docker image 목록</strong> 확인 : <strong><code>mysql</code></strong>이 설치됨</p>
<pre><code class="language-shell">sudo docker images</code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/40ba66b1-9664-4199-99b8-95b2d7e52017/image.png" alt=""> </p>
<ul>
<li><p><strong>MySQL Docker 컨테이너</strong> 생성 및 실행</p>
<pre><code class="language-shell">sudo docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=[password] -d -p 3306:3306 mysql:latest</code></pre>
</li>
<li><p><strong>Docker 컨테이너 리스트</strong> 확인 : <strong><code>mysql-container</code></strong>이 실행 중</p>
<pre><code class="language-shell">sudo docker ps -a</code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/0e5ce3ed-332b-4a9d-b163-2622bf85dced/image.png" alt=""></p>
<ul>
<li><strong>EC2 *<em>내부에서 실행중인 *</em>MySQL Docker 컨테이너에 접속</strong>하기 : <strong>bash</strong>로 mysql 컨테이너에 접근 후 패스워드 입력 (입력해도 안보임) ➡️ mysql에 접속 완료!<pre><code class="language-shell">sudo docker exec -it mysql-container bash
bash $ mysql -uroot -p</code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/ee4c706a-9402-4d82-85f1-072778f00599/image.png" alt=""></p>
<ul>
<li><strong>외부</strong>에서 <strong>MySQL 컨테이너에 접속</strong>하기 (Workbench) : EC2 인스턴스의 <strong>IP</strong>를 사용하여 연결 가능! ➡️ 보안 그룹에서 MySQL의 포트번호인 3306번을 열어줘야 함</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/b5356b96-994a-4d00-a869-e4b14488fb95/image.png" alt=""></p>
<ul>
<li>MySQL Docker 컨테이너 시작/중지/재시작<pre><code class="language-shell"># 실행
docker start mysql-container
</code></pre>
</li>
</ul>
<h1 id="중지">중지</h1>
<p>docker stop mysql-container</p>
<h1 id="재시작">재시작</h1>
<p>docker restart mysql-container</p>
<pre><code>---

### 4. Docker image 빌드

- 프로젝트 최상단 경로에 **`Dockerfile`** 생성 ➡️ `clean` 후 **`bootJar`**을 통해 **jar 파일 빌드**
![](https://velog.velcdn.com/images/pockyjx_/post/ea73a56f-edf2-46b4-95c0-b41894b209e7/image.png)
</code></pre><p>FROM openjdk:17
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]</p>
<pre><code>
- **Dockerfile이 있는 패키지**(프로젝트)에서 터미널을 실행한 후, **Docker image 생성** ➡️ 태그 생략 시 `latest`
```shell
docker build -t [username/image명]:[태그명] .</code></pre><p><img src="https://velog.velcdn.com/images/pockyjx_/post/6c9bf02b-61d9-40a7-ba02-594f1475ed4e/image.png" alt=""></p>
<ul>
<li>image 실행 ➡️ 포트번호 : <code>8888</code></li>
</ul>
<pre><code class="language-shell">docker run -p [외부 포트번호(사용자 지정)]:[컨테이너 내부 포트번호(8080)] [이미지명]:[태그]</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/e17687cc-cfd8-4e95-ab9e-25dced19297d/image.png" alt=""> <img src="https://velog.velcdn.com/images/pockyjx_/post/78793f2b-1f8e-4eb3-9db0-f7125ed1ce8a/image.png" alt=""> : 8888번 포트에서 프로젝트가 정상적으로 실행된다!</p>
<hr>
<h3 id="5-docker-image를-dockerhub에-push">5. Docker image를 DockerHub에 push</h3>
<ul>
<li><p>Docker 로그인 : 이미 로그인 되어있다면 <code>docker login</code> 만으로 가능</p>
<pre><code class="language-shell">docker login -u [username]</code></pre>
</li>
<li><p>image push 하기</p>
</li>
</ul>
<pre><code class="language-shell">docker push [이미지명]</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/5589c8f3-c8c6-471d-9fac-af55b827596e/image.png" alt=""></p>
<hr>
<h3 id="6-배포">6. 배포</h3>
<ul>
<li><p>EC2 서버에 접속 후 docker login 수행</p>
</li>
<li><p>로그인을 수행한 계정의 DockerHub에서 이전에 생성한 image를 컨테이너로 가져오기 (pull)</p>
<pre><code class="language-shell">sudo docker pull [이미지명]</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/745f5a5f-15a3-49b6-bd3c-6804d1609649/image.png" alt=""></p>
</li>
<li><p>image 실행하기 (배포) ➡️ 포트번호 : <code>8888</code></p>
<pre><code class="language-shell">sudo docker run -d -p [외부 포트번호]:[컨테이너 내부 포트번호] [이미지명]:[태그]</code></pre>
</li>
</ul>
<hr>
<h2 id="🚨-트러블-슈팅">🚨 트러블 슈팅</h2>
<blockquote>
<ul>
<li><code>ssh: connect to host [ip주소] port 22: Connection timed out</code> : EC2 서버에 접속되지 않는 문제 발생
<img src="https://velog.velcdn.com/images/pockyjx_/post/4ed41d40-ea1c-41bc-a5b3-465fd0518dcb/image.png" alt=""></li>
</ul>
</blockquote>
<p>➡️ 인스턴스 생성 시 적용한 보안 그룹에 <strong>ssh의 22번 포트를 열어주지 않아서</strong> ssh에 접속할 수 없었음..
<img src="https://velog.velcdn.com/images/pockyjx_/post/b0c1d5be-9d8b-4e82-996b-54a45be9f9f0/image.png" alt=""> 해당 보안 그룹에 22번 포트를 열어주니 정상적으로 연결됨!</p>
<hr>
<h3 id="🙏🏻-참고">🙏🏻 참고</h3>
<blockquote>
<ul>
<li><a href="https://akku-dev.tistory.com/46">AWS EC2에서 Jenkins 자동배포 구축하기 (1)</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://lucas-owner.tistory.com/47">[Docker] Docker로 AWS EC2 - MySQL 설치 및 테스트</a></li>
<li><a href="https://lucas-owner.tistory.com/48">[Docker] Docker - Spring Boot로 EC2 배포하기</a></li>
<li><a href="https://velog.io/@zzarbttoo/docker%EB%8F%84%EC%BB%A4-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%8F%84%EC%BB%A4-%EC%9D%B4%EB%AF%B8%EC%A7%80-dockerhub-%EC%97%90-push-%ED%95%B4%EB%B3%B4%EA%B8%B0feat.springboot">[Docker]도커 이미지 만들기 + 도커 이미지 dockerhub 에 push 해보기(feat.springboot)</a></li>
<li><a href="https://velog.io/@xeonu/SpringBoot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-docker%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0">SpringBoot 프로젝트 docker로 배포하기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] REST API 구현 : HATEOAS, Swagger]]></title>
            <link>https://velog.io/@pockyjx_/Spring-REST-API-%EA%B5%AC%ED%98%84-HATEOAS-Swagger</link>
            <guid>https://velog.io/@pockyjx_/Spring-REST-API-%EA%B5%AC%ED%98%84-HATEOAS-Swagger</guid>
            <pubDate>Mon, 19 Feb 2024 06:59:33 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-rest-api-구현">💡 REST API 구현</h1>
<blockquote>
<p> ✅ <strong>스프링 부트 + JPA</strong>를 사용하여 <strong>회원 관리 API</strong> (등록, 조회, 삭제) 구현하기</p>
</blockquote>
<h4 id="✔️-도메인">✔️ 도메인</h4>
<ul>
<li><p><strong><code>@Table(name = &quot;users&quot;)</code></strong> : H2 시스템 내에 <code>user</code> 테이블이 존재하므로, 생성될 테이블의 이름을 <code>users</code>로 지정</p>
</li>
<li><p><strong><code>@JsonIgnore</code></strong> : 조회 시 <strong>반환하지 않을</strong> 데이터 → 비밀번호와 같은 보안에 민감한 데이터</p>
</li>
</ul>
<pre><code class="language-java">@Entity
@NoArgsConstructor
@Table(name = &quot;users&quot;)
@Getter
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private int age;

    @JsonIgnore
    private String password;

    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }
}</code></pre>
<h4 id="✔️-리포지토리">✔️ 리포지토리</h4>
<ul>
<li><strong>스프링 데이터 JPA</strong>가 제공하는 <strong><code>JpaRepository</code></strong> 사용 : 기본적인 <strong>CRUD</strong> 기능 제공</li>
</ul>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {}</code></pre>
<h4 id="✔️-컨트롤러">✔️ 컨트롤러</h4>
<ul>
<li><p><strong><code>@RestController</code> = <code>@Controller</code> + <code>@ResponseBody</code></strong> : API 구현 시 사용</p>
<ul>
<li><strong><code>@Controller</code></strong> : *<em>뷰 *</em>페이지 반환</li>
<li><strong><code>@ResponseBody</code></strong> : <strong>반환되는 데이터</strong>를 JSON 등의 타입으로 변환하여 <strong>응답 메시지의 바디</strong>에 담아서 반환</li>
</ul>
</li>
<li><p><strong><code>UserDTO</code></strong> : <strong>회원 목록 조회</strong> 시 <strong><code>회원 수 + 회원 목록</code></strong> 형태로 반환하기 위한 DTO </p>
</li>
</ul>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/users&quot;)
@RequiredArgsConstructor
public class UserController {
    private final UserRepository repository;

    @GetMapping
    public UserDTO allUsers() {
        List&lt;User&gt; users = repository.findAll();
        return new UserDTO(users.size(), users);
    }

    @PostMapping
    public ResponseEntity&lt;User&gt; save(@RequestBody User user) {
        User saveUser = repository.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @GetMapping(&quot;/{id}&quot;)
    public User oneUser(@PathVariable Long id) {
        Optional&lt;User&gt; optional = repository.findById(id);
        if(optional.isEmpty()) throw new UserNotFoundException(id+&quot;번 회원을 찾을 수 없습니다.&quot;);

        return optional.get();
    }

    @DeleteMapping(&quot;/{id}&quot;)
    public void deleteUser(@PathVariable Long id) {
        Optional&lt;User&gt; optional = repository.findById(id);
        if(optional.isEmpty()) throw new UserNotFoundException(id+&quot;번 회원을 찾을 수 없습니다.&quot;);

        repository.deleteById(id);
    }

    @Data
    @AllArgsConstructor
    static class UserDTO {
        private int cnt;
        private List&lt;User&gt; users;
    }
}</code></pre>
<p>ex1) <code>GET /users</code> : 회원 목록 조회 <img src="https://velog.velcdn.com/images/pockyjx_/post/d0ceecd1-5a40-48ce-a27b-2ff2e6bac240/image.png" alt=""></p>
<p>ex2) <code>POST /users</code> : 회원 저장 → 상태코드 <code>201 created</code> 반환 <img src="https://velog.velcdn.com/images/pockyjx_/post/0f5bece2-9c7d-4211-9f6d-ffd94fbee484/image.png" alt=""></p>
<p>ex3) <code>GET /users/{id}</code> : 특정 회원 조회 → 존재하지 않는 회원일 경우 <code>404 not found</code> 반환 <img src="https://velog.velcdn.com/images/pockyjx_/post/667b1dce-9084-4f8e-aa54-687c86444f63/image.png" alt=""></p>
<p>ex4) <code>DELETE /user/{id}</code> : 특정 회원 삭제 → 존재하지 않는 회원일 경우 <code>404 not found</code> 반환 <img src="https://velog.velcdn.com/images/pockyjx_/post/990cfb44-b71f-4dfb-9368-ccef5807639f/image.png" alt=""></p>
<hr>
<h2 id="📒-hateoas">📒 HATEOAS</h2>
<blockquote>
<p><strong>H</strong>ypermedia <strong>A</strong>s <strong>T</strong>he <strong>E</strong>ngine <strong>O</strong>f <strong>A</strong>pplication <strong>S</strong>tate : <strong>요청과 관련된 URI</strong>를 <strong>응답</strong>에 담아서 반환</p>
</blockquote>
<p>ex) 회원 등록 시 회원 목록 + 특정 회원 조회 링크 제공, 특정 회원 조회 시 회원 목록 링크 제공 등 ...</p>
<hr>
<h3 id="⭐-rest-구현의-3단계">⭐ REST 구현의 3단계</h3>
<blockquote>
<ol>
<li><strong>리소스</strong>의 도입 : 서버가 가진 모든 <strong>리소스</strong>에 대한 <strong>고유한 URI</strong> 제공</li>
<li><strong>HTTP 동사</strong> : <strong>HTTP Method</strong>를 통해 리소스에 <strong>수행하고자 하는 작업</strong>을 표현</li>
<li><strong>HATEOAS</strong> : 해당 자원에 대해 <strong>호출 가능한 API 정보</strong>를 <strong>상태에 반영</strong>하여 표현</li>
</ol>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/dcb33a8e-0dad-4730-8c9d-7bcf0576b5c3/image.png" alt=""></p>
<p>ex) <strong>회원 삭제</strong> API 설계</p>
<table>
<thead>
<tr>
<th align="center">Lv</th>
<th align="center">Explain</th>
<th align="center">Request</th>
<th align="center">Response</th>
</tr>
</thead>
<tbody><tr>
<td align="center">1</td>
<td align="center"><strong>리소스</strong> 표현</td>
<td align="center"><code>POST /users/1/delete</code></td>
<td align="center">JSON</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center"><strong>메서드</strong>를 통한 <strong>작업</strong> 표현</td>
<td align="center"><code>DELETE /users/1</code></td>
<td align="center">JSON</td>
</tr>
<tr>
<td align="center">3</td>
<td align="center"><strong>관련 API 링크</strong> 제공</td>
<td align="center"><code>DELETE /users/1</code></td>
<td align="center">JSON + LINK (등록, 목록 조회 ... 등)</td>
</tr>
</tbody></table>
<hr>
<h3 id="📝-스프링-부트--hateoas-구현">📝 스프링 부트 + HATEOAS 구현</h3>
<blockquote>
<p>✅ <strong>특정 회원 조회</strong> 시 <strong>회원 목록, 회원 삭제</strong> 링크 전달하기</p>
</blockquote>
<h4 id="✔️-라이브러리-추가-buildgradle">✔️ 라이브러리 추가 (<code>build.gradle</code>)</h4>
<pre><code>implementation &#39;org.springframework.boot:spring-boot-starter-hateoas&#39;</code></pre><h4 id="✔️-컨트롤러-1">✔️ 컨트롤러</h4>
<ul>
<li><p><strong><code>EntityModel</code></strong> : <strong>결과 데이터 + 관련 링크</strong> 반환</p>
<ul>
<li><strong><code>add()</code></strong> : 링크 추가 ➕ <strong><code>withRel()</code></strong> : 해당 <strong>링크</strong>에 대한 <strong>참조</strong> (설명)</li>
</ul>
</li>
<li><p><strong><code>WebMvcLinkBuilder</code></strong> : 링크 <strong>생성 ** → **타겟 컨트롤러</strong> 내의 <strong>타켓 메서드</strong>에 <strong>매핑된 URI</strong></p>
<ul>
<li><p><strong><code>linkTo()</code></strong> : <strong>컨트롤러 클래스</strong>를 가리키는 <strong><code>WebMvcLinkBuilder</code> 객체</strong> 반환</p>
</li>
<li><p><strong><code>methodOn()</code></strong> : 타겟 메서드의 가짜 메서드 콜이 들어있는 <strong>컨트롤러 프록시 클래스</strong>를 생성</p>
<pre><code class="language-java">@GetMapping(&quot;/{id}&quot;)
public EntityModel&lt;User&gt; oneUser_HATEOAS(@PathVariable Long id) {
Optional&lt;User&gt; optional = repository.findById(id);
if(optional.isEmpty()) throw new UserNotFoundException(id+&quot;번 회원을 찾을 수 없습니다.&quot;);
User user = optional.get();

EntityModel&lt;User&gt; entityModel = EntityModel.of(user);
WebMvcLinkBuilder allUser = linkTo(methodOn(this.getClass()).allUsers());
entityModel.add(allUser.withRel(&quot;all-user&quot;));

return entityModel;
}</code></pre>
</li>
</ul>
</li>
</ul>
<p>ex) <code>GET /users/2</code> : 2번 <strong>회원 정보 + 회원 목록 API 링크</strong> 반환</p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/1eb34b23-1351-402f-a7a3-31df4e660ed9/image.png" alt=""></p>
<hr>
<h2 id="📒-swagger">📒 Swagger</h2>
<blockquote>
<p>개발한 <strong>REST API</strong>를 자동으로 <strong>문서화</strong>시키는 프레임워크 → <strong>API 목록 확인 및 테스트</strong> 가능</p>
</blockquote>
<h4 id="✔️-라이브러리-buildgradle">✔️ 라이브러리 (<code>build.gradle</code>)</h4>
<blockquote>
<ul>
<li>Springfox (2020.07 이후로 업데이트 X → doc 사용 권장)</li>
</ul>
</blockquote>
<ul>
<li><strong>Springdoc</strong> ⭐ (스프링 부트 3.x 이후 버전부터 <code>springdoc-openapi-ui</code> 사용 시 404 에러 발생)</li>
</ul>
<pre><code>implementation &#39;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0&#39;</code></pre><p>라이브러리 추가 후 <strong><code>/swagger-ui/index.html</code></strong>에 접속하면 아래와 같은 페이지 확인 가능! (기본값 : 생성된 <strong>모든 API 컨트롤러 (<code>@RestController</code>) + 객체</strong> 출력)<img src="https://velog.velcdn.com/images/pockyjx_/post/b1b36561-6691-40c8-adc5-09ee99201058/image.png" alt=""></p>
<h4 id="✔️-설정--swaggerconfigjava">✔️ 설정 : <code>SwaggerConfig.java</code></h4>
<ul>
<li><p><strong><code>@OpenAPIDefinition(info)</code></strong> : API 명세서의 <strong>기본 정보</strong> (제목, 설명, 버전 등)</p>
</li>
<li><p><strong><code>GroupedOpenApi</code></strong> : <strong>여러 API 컨트롤러</strong>를 <strong>그룹화</strong>하여 표시</p>
<ul>
<li><strong><code>group</code></strong> : 그룹 <strong>이름</strong></li>
<li><strong><code>pathsToMatch</code></strong> : 그룹에 속하는 <strong>경로</strong> 패턴</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Configuration
@OpenAPIDefinition(
        info = @Info(
                title = &quot;회원 관리 API 명세서&quot;,
                description = &quot;회원 관리 API 명세서&quot;,
                version = &quot;v1.0&quot;
        )
)
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi userAPI() {
        return GroupedOpenApi.builder()
                .group(&quot;회원 관리&quot;)
                .pathsToMatch(&quot;/users/**&quot;)
                .build();
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/2259114f-971a-4a70-b804-df62d00e9a51/image.png" alt=""></p>
<h4 id="✔️-도메인-1">✔️ 도메인</h4>
<ul>
<li><strong><code>@Schema</code></strong> : <strong>모델(객체)</strong>에 대한 상세 정보</li>
</ul>
<pre><code class="language-java">@Entity
@NoArgsConstructor
@Table(name = &quot;users&quot;)
@Getter
@Schema(description = &quot;회원 정보를 관리하기 위한 도메인 객체&quot;)
public class User {

    @Id @GeneratedValue
    @Schema(title = &quot;사용자 id&quot;, description = &quot;자동 생성&quot;)
    private Long id;

    @Schema(title = &quot;사용자 이름&quot;, description = &quot;2 ~ 15글자 이내&quot;)
    private String name;

    @Schema(title = &quot;사용자 나이&quot;, description = &quot;5세 이상부터 가입 가능&quot;)
    private int age;

    @JsonIgnore
    @Schema(title = &quot;비밀번호&quot;, description = &quot;8 ~ 20자 이내의 영소문자+숫자+특수기호(!~_=+@#) 조합&quot;)
    private String password;

    ...
}</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/834b0055-b6ab-4b19-993a-1cbeb4cc37da/image.png" alt=""></p>
<h4 id="✔️-컨트롤러-2">✔️ 컨트롤러</h4>
<ul>
<li><strong><code>@Tag</code></strong> : <strong>클래스(컨트롤러)</strong>에 대한 상세 정보</li>
<li><strong><code>@Operation</code></strong> : <strong>특정 경로에 대한 작업(메서드)</strong>에 대한 상세 정보</li>
<li><strong><code>@Parameter</code></strong> : <strong>파라미터</strong>에 대한 상세 정보</li>
<li><strong><code>@ApiResponse</code></strong> : <strong>응답 상태코드</strong>에 대한 상세 정보</li>
</ul>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/users&quot;)
@RequiredArgsConstructor
@Tag(name = &quot;users&quot;, description = &quot;회원 관리 컨트롤러&quot;)
public class UserController {
    private final UserRepository repository;

    @GetMapping
    @Operation(summary = &quot;회원 목록 API&quot;, description = &quot;전체 회원 목록을 조회&quot;)
    public UserDTO allUsers() {
        List&lt;User&gt; users = repository.findAll();
        return new UserDTO(users.size(), users);
    }

    @PostMapping
    @Operation(summary = &quot;회원 등록 API&quot;, description = &quot;새로운 회원을 등록&quot;)
    public ResponseEntity&lt;User&gt; save(@RequestBody User user) {
        User saveUser = repository.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @GetMapping(&quot;/{id}&quot;)
    @Operation(summary = &quot;회원 조회 API&quot;, description = &quot;사용자 ID를 통해 특정 회원 조회&quot;)
    @ApiResponses({
            @ApiResponse(description = &quot;OK&quot;, responseCode = &quot;200&quot;),
            @ApiResponse(description = &quot;NOT FOUND&quot;, responseCode = &quot;404&quot;),
            @ApiResponse(description = &quot;INTERNAL SERVER ERROR&quot;, responseCode = &quot;500&quot;)
    })
    public EntityModel&lt;User&gt; oneUser_HATEOAS(@Parameter(description = &quot;사용자 ID&quot;, required = true, example = &quot;1&quot;)
                                                 @PathVariable Long id) {
        Optional&lt;User&gt; optional = repository.findById(id);
        if(optional.isEmpty()) throw new UserNotFoundException(id+&quot;번 회원을 찾을 수 없습니다.&quot;);
        User user = optional.get();

        EntityModel&lt;User&gt; entityModel = EntityModel.of(user);
        WebMvcLinkBuilder allUser = linkTo(methodOn(this.getClass()).allUsers());
        entityModel.add(allUser.withRel(&quot;all-user&quot;));

        return entityModel;
    }

    @DeleteMapping(&quot;/{id}&quot;)
    @Operation(summary = &quot;회원 삭제 API&quot;, description = &quot;사용자 ID를 통해 특정 회원 삭제&quot;)
    public void deleteUser(@PathVariable Long id) {
        Optional&lt;User&gt; optional = repository.findById(id);
        if(optional.isEmpty()) throw new UserNotFoundException(id+&quot;번 회원을 찾을 수 없습니다.&quot;);

        repository.deleteById(id);
    }

    ...
}</code></pre>
<ul>
<li><strong><code>@Tag</code> + <code>@Operation</code></strong>
<img src="https://velog.velcdn.com/images/pockyjx_/post/9c34a85c-82d3-457a-94ae-96fa17d7d720/image.png" alt=""></li>
</ul>
<ul>
<li><strong><code>@Operation</code> + <code>@Parameter</code></strong> <img src="https://velog.velcdn.com/images/pockyjx_/post/8e6b5c2c-5514-4425-9f03-a025ea390062/image.png" alt=""></li>
</ul>
<ul>
<li><strong><code>@ApiResponses</code></strong> <img src="https://velog.velcdn.com/images/pockyjx_/post/3a408f4c-4b6b-4571-a760-b494bcd92df4/image.png" alt=""></li>
</ul>
<hr>
<blockquote>
<p>🙇🏻‍♀️ 참고 1 : <a href="https://www.inflearn.com/course/spring-boot-restful-web-services/dashboard">[개정판 2023-11-27] Spring Boot 3.x 를 이용한 RESTful Web Services 개발</a>
🙇🏻‍♀️ 참고 2 : <a href="https://m.blog.naver.com/tmondev/220391644590">Hypermedia-driven REST API</a>
🙇🏻‍♀️ 참고 3 : <a href="https://lucas-owner.tistory.com/28">[SpringBoot] Swagger API 문서 자동화 간단 연동, 테스트하기</a>
🙇🏻‍♀️ 참고 4 : <a href="https://colabear754.tistory.com/99">[Spring Boot] Springdoc 라이브러리를 통한 Swagger 적용</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] REST API란?]]></title>
            <link>https://velog.io/@pockyjx_/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-REST-API%EB%9E%80</link>
            <guid>https://velog.io/@pockyjx_/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-REST-API%EB%9E%80</guid>
            <pubDate>Sat, 17 Feb 2024 13:18:46 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-rest">💡 REST</h1>
<blockquote>
<p><strong>RE</strong>presentational <strong>S</strong>tate <strong>T</strong>ransfer : <strong>자원의 표현(이름)</strong>에 의한 <strong>상태(데이터) 전달</strong></p>
</blockquote>
<ul>
<li><p>월드 와이드 웹(www)과 같은 <strong>분산 하이퍼미디어 시스템</strong>을 위한 <strong>소프트웨어 개발 아키텍처</strong>의 한 형식 : <strong>HTTP 프로토콜</strong> 사용 → HTTP <strong>URI</strong>를 통해 <strong>자원을 명시</strong>하고, <strong>HTTP Method</strong>를 통해 해당 <strong>자원에 CRUD 처리</strong></p>
<ul>
<li><p>HTTP <strong>URI</strong> : 자원의 <strong>표현</strong> (이름) → 클라이언트는 <strong>URI</strong>를 통해 서버에 <strong>자원에 대한 처리를 요청</strong>함</p>
</li>
<li><p>HTTP <strong>Method</strong> : 자원에 대한 <strong>처리</strong> (<strong>CRUD</strong> Operation)</p>
<ul>
<li><strong>POST</strong> = <strong>C</strong>REATE : 생성</li>
<li><strong>GET</strong> = <strong>R</strong>EAD : 조회</li>
<li><strong>PUT/PATCH</strong> = <strong>U</strong>PDATE : 완전/일부 수정 </li>
<li><strong>DELETE</strong> = <strong>D</strong>ELETE : 삭제 </li>
</ul>
</li>
</ul>
</li>
<li><p>⭐ 스마트폰이 급성장하게 되면서, <strong>서버</strong>는 웹 브라우저 외에 모바일(안드로이드) 기기와 같은 <strong>다양한 클라이언트와 통신</strong>해야 하는 상황에 직면하게 된다. 이 때 <strong>HTTP 프로토콜을 따르는 플랫폼</strong>이라면 <strong>REST</strong> 방식을 사용하여 <strong>특정 언어나 기술에 종속되지 않고 통신</strong>할 수 있다.</p>
</li>
</ul>
<hr>
<h3 id="📝-특징">📝 특징</h3>
<blockquote>
<ol>
<li><strong>server - client</strong> 구조</li>
</ol>
</blockquote>
<ul>
<li>server : 리소스를 가지고 있는 쪽 → API 제공 및 비즈니스 로직 처리</li>
<li>client : 리소스를 요청하는 쪽 → context 관리</li>
</ul>
<blockquote>
<ol start="2">
<li><strong>Stateless</strong> : 무상태 (<strong>서버</strong>는 <strong>클라이언트의 상태</strong>를 <strong>보존하지 않음</strong>)</li>
</ol>
</blockquote>
<p>: HTTP 프로토콜은 무상태이므로 HTTP 프로토콜 기반인 REST 또한 무상태이다. </p>
<blockquote>
<ol start="3">
<li><strong>Chaceable</strong> : 캐시 처리 가능</li>
</ol>
</blockquote>
<p>: HTTP 프로토콜에서 사용하는 <strong><code>Last-Modified</code>, <code>E-Tag</code></strong> 등을 사용하여 <strong>캐싱</strong>을 구현할 수 있다. 캐시를 사용하면 응답시간이 빨라져 <strong>API의 성능을 향상</strong>시킬 수 있다.</p>
<hr>
<h2 id="📒-rest-api">📒 REST API</h2>
<blockquote>
<p><strong>REST 기반</strong>으로 <strong>서비스 API</strong>를 구현한 것</p>
</blockquote>
<ul>
<li><strong>API</strong> (<strong>A</strong>pplication <strong>P</strong>rogramming <strong>I</strong>nterface) : 운영체제나 <strong>프로그래밍 언어가 제공하는 기능을 제어</strong>할 수 있게 만든 <strong>인터페이스</strong> → 애플리케이션을 연결하여 서로 통신하는 것</li>
</ul>
<p>: <strong>하나의 API</strong>를 웹, 모바일 등 <strong>여러 시스템</strong>에서 사용함으로써 <strong>확장성 및 재사용성이 높아진다.</strong></p>
<hr>
<h3 id="📝-설계-규칙">📝 설계 규칙</h3>
<blockquote>
<ul>
<li><strong>URI</strong> : <strong>리소스</strong> 표현 → <strong>소문자, 복수 명사</strong> 권장</li>
</ul>
</blockquote>
<p>ex) 특정 회원 <strong>조회</strong> : <code>GET /members/{id}</code></p>
<blockquote>
<ul>
<li><strong>HTTP Method</strong> : 리소스에 대한 <strong>행위</strong> 표현 → URI에는 되도록이면 동사를 사용하지 않는다. </li>
</ul>
</blockquote>
<p>ex) 특정 회원 <strong>삭제</strong> : <code>DELETE /members/{id}</code> </p>
<p>➕  REST API가 아닌 <strong>HTML Form</strong>의 경우, <strong>GET/POST</strong>만 지원하므로 필요에 따라 <strong>동사</strong>인 <strong>컨트롤 URI</strong>를 사용할 수도 있음 → <code>POST /members/{id}/delete</code></p>
<blockquote>
<ul>
<li><strong>슬래시 <code>/</code></strong>는 <strong>계층 관계</strong>를 나타냄 : <strong>하나의 리소스</strong>를 전달해야 하므로 <strong>URI</strong>의 <strong>마지막에 <code>/</code>를 사용하지 않는다.</strong></li>
</ul>
</blockquote>
<p>ex) <code>GET /members/</code> ❌ → <code>GET /members</code> ⭕</p>
<blockquote>
<ul>
<li>리소스 간 <strong>연관관계</strong>가 있는 경우, <strong><code>/리소스명/리소스ID/연관된 리소스명</code></strong> 형식으로 표현</li>
</ul>
</blockquote>
<p>ex) 특정 회원이 작성한 모든 게시글 조회 : <code>GET /members/{id}/posts</code></p>
<blockquote>
<ul>
<li><strong>파일 확장자</strong>는 URI에 포함하지 않는다. → <strong>헤더</strong> (Accept) 사용</li>
</ul>
</blockquote>
<p>ex) <code>GET /members/image.jpg</code> ❌ → <code>GET /members</code> + <code>Accept: image/jpg</code> ⭕</p>
<hr>
<h4 id="✔️-rest-api-설계-예시">✔️ REST API 설계 예시</h4>
<br>

<table>
<thead>
<tr>
<th align="center">Example</th>
<th align="center">HTTP Method</th>
<th align="center">REST API</th>
</tr>
</thead>
<tbody><tr>
<td align="center">모든 회원 조회</td>
<td align="center">GET</td>
<td align="center"><code>/members</code></td>
</tr>
<tr>
<td align="center">특정 회원 조회</td>
<td align="center">GET</td>
<td align="center"><code>/members/{id}</code></td>
</tr>
<tr>
<td align="center">회원 저장</td>
<td align="center">POST</td>
<td align="center"><code>/members</code></td>
</tr>
<tr>
<td align="center">특정 회원 수정</td>
<td align="center">PUT</td>
<td align="center"><code>/members/{id}</code></td>
</tr>
<tr>
<td align="center">특정 회원 삭제</td>
<td align="center">DELETE</td>
<td align="center"><code>/members/{id}</code></td>
</tr>
<tr>
<td align="center"><br></td>
<td align="center"></td>
<td align="center"></td>
</tr>
</tbody></table>
<p>: 실제로 사용된 <strong>URI</strong>는 <strong><code>/members</code>, <code>/members/{id}</code></strong> 2개이지만, <strong>HTTP Method</strong>를 통해 각 작업을 구분하여 <strong>다양한 작업을 처리</strong>할 수 있다!</p>
<hr>
<blockquote>
<p> 🙇🏻‍♀️ 참고 : <a href="https://gmlwjd9405.github.io/2018/09/21/rest-and-restful.html">[Network] REST란? REST API란? RESTful이란?</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 빌더 패턴 (Builder Pattern)]]></title>
            <link>https://velog.io/@pockyjx_/Java-%EB%B9%8C%EB%8D%94-%ED%8C%A8%ED%84%B4-Builder-Pattern</link>
            <guid>https://velog.io/@pockyjx_/Java-%EB%B9%8C%EB%8D%94-%ED%8C%A8%ED%84%B4-Builder-Pattern</guid>
            <pubDate>Sun, 11 Feb 2024 15:14:49 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-builder-pattern">💡 Builder Pattern</h1>
<blockquote>
<p>객체의 <strong>생성과 표현을 분리</strong>하여 <strong>다양한 구성의 인스턴스</strong>를 만드는 생성 패턴</p>
</blockquote>
<hr>
<h2 id="📒-점층적-생성자-패턴">📒 점층적 생성자 패턴</h2>
<blockquote>
<p><strong>필수 입력 필드 + 선택 입력 필드</strong>를 하나씩 늘려가며 생성자를 <strong>오버로딩</strong>하는 방식</p>
</blockquote>
<pre><code class="language-java">public class Member {

    // 필수 입력 정보
    private String name;
    private int age;

    // 선택 입력 정보
    private String phoneNumber;
    private String email;
}</code></pre>
<p>클래스를 생성하다 보면 위 경우처럼 값이 무조건 있어야 하는 <strong>필수 입력 필드</strong>와, 값이 있어도, 없어도 되는 <strong>선택 입력 필드</strong>가 존재하게 된다.</p>
<pre><code class="language-java">public class Member {
    ...

    public Member() {}

    // 필수 입력 정보 생성자
    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 필수 입력 정보 + 전화번호 생성자
    public Member(String name, int age, String phoneNumber) {
        this.name = name;
        this.age = age;
        this.phoneNumber = phoneNumber;
    }

    // 필수 입력 정보 + 이메일 + 전화번호 생성자
    public Member(String name, int age, String phoneNumber, String email) {
        this.name = name;
        this.age = age;
        this.phoneNumber = phoneNumber;
        this.email = email;
    }
}</code></pre>
<p>생성자를 통해 값을 입력받는 경우, 필수 정보를 입력받는 생성자, 필수 입력 정보 + 선택 정보 1을 입력받는 생성자, 필수 입력 정보 + 선택 정보 2를 입력받는 생성자, ... 와 같이 <strong>경우에 따른 생성자</strong>를 <strong>별도로 생성</strong>해야 한다. 이는 한 클래스 내의 <strong>필드 수가 증가</strong>할수록 <strong>생성자의 수 또한 증가</strong>하여 <strong>가독성이 떨어지게 된다.</strong></p>
<pre><code class="language-java">Member member1 = new Member(&quot;member1&quot;, 10, &quot;010-1234-5678&quot;, &quot;member1@naver.com&quot;);
Member member2 = new Member(&quot;member2&quot;, 20, &quot;member2@naver.com&quot;, &quot;010-9876-5432&quot;);</code></pre>
<p>또한, <strong>각 생성자의 매개변수 순서</strong>를 기억해야 한다. 타입이 같은 필드가 여러 개인 경우 <strong>순서가 바뀌어도 타입만 일치</strong>하면 컴파일 에러가 발생하지 않고 <strong>정상적으로 실행</strong>되기 때문에 이후 해당 데이터를 사용하는 로직 처리 시 문제가 될 수 있다.</p>
<hr>
<h2 id="📒-자바-빈즈-패턴">📒 자바 빈즈 패턴</h2>
<blockquote>
<p><strong>기본 생성자 + <code>setter()</code></strong>를 통한 값 입력</p>
</blockquote>
<pre><code class="language-java">public class Member {
    // 필수 정보
    private String name;
    private int age;

    // 선택 정보
    private String phoneNumber;
    private String email;

    public Member() {}

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}</code></pre>
<p>점층적 생성자 패턴의 단점을 해결하기 위해 <strong>자바 빈즈 패턴</strong>을 활용할 수 있다. 자바 빈즈 패턴은 생성자를 일일이 만드는 대신 <strong>기본 생성자</strong> 하나만 만들고, <strong>각 필드에 대한 <code>setter()</code> 메서드</strong>를 열어두어 값을 입력받는 방식이다.</p>
<pre><code class="language-java">Member member = new Member();

member.setName(&quot;member&quot;);
member.setAge(10);
member.setEmail(&quot;member@naver.com&quot;);
member.setPhoneNumber(&quot;010-1234-5678&quot;);</code></pre>
<p>자바 빈즈 패턴을 사용하면 필드의 입력 순서를 기억할 필요 없이, <strong><code>setXXX()</code></strong>를 통해 <strong>각 필드에 맞는 값을 입력</strong>할 수 있다. 하지만, 이 방식 또한 <code>setter()</code>를 모두 정의해야 하기 때문에 가독성이 좋은 편은 아니며 (해당 문제는 <strong>Lombok의 <code>@Setter</code></strong>로 해결 가능), <strong>필수 입력 필드에 대한 값 입력</strong>을 필수로 두지 않기 때문에, <strong>누락</strong>할 가능성이 존재한다.</p>
<p>또한, <strong><code>setter()</code></strong>의 가장 큰 문제는 해당 메서드를 통해 <strong>어디서든 객체의 데이터를 변경</strong>할 수 있다는 것이다. 여러 메서드에서 특정 인스턴스를 호출하여 <code>setter()</code>로 값을 변경하다 보면, 이후 <strong>변경에 대한 추적이 힘들어</strong> 유지보수성이 떨어지게 된다. 따라서, <code>setter()</code>를 무작위로 열어두는 것은 위험을 초래할 수 있다.</p>
<hr>
<h2 id="📒-빌더-패턴">📒 빌더 패턴</h2>
<blockquote>
<p>별도의 <strong><code>Builer</code> 클래스</strong>의 메서드 통해 값을 입력받은 후, <strong><code>build()</code></strong>를 통해 <strong>인스턴스 생성</strong></p>
</blockquote>
<hr>
<h3 id="📝-builder-클래스-구현">📝 <code>Builder</code> 클래스 구현</h3>
<blockquote>
<ol>
<li><strong>기준 클래스</strong>와 <strong>같은 필드</strong>를 가지는 <strong>Builder 클래스</strong> 구현</li>
<li><strong>값을 입력</strong>받기 위한 <strong>생성자 + 메서드</strong> 생성 → <strong><code>Builder</code> 객체를 리턴</strong>하여 <strong>체이닝</strong> 기법 적용</li>
<li><strong>최종 인스턴스를 반환</strong>하는 <strong><code>build()</code></strong> 메서드 생성</li>
</ol>
</blockquote>
<pre><code class="language-java">public class Member {
    // 필수 정보
    private String name;
    private int age;

    // 선택 정보
    private String phoneNumber;
    private String email;

    // 빌더 클래스
    static class MemberBuilder {
        private String name;
        private int age;
        private String phoneNumber;
        private String email;

        // 필수 입력 필드 : 생성자
        public MemberBuilder(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public MemberBuilder setPhoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
            return this;
        }

        public MemberBuilder setEmail(String email) {
            this.email = email;
            return this;
        }

        // 최종 인스턴스 생성
        public Member build() {
            Member member = new Member();

            member.name = name;
            member.age = age;
            member.email = email;
            member.phoneNumber = phoneNumber;

            return member;
        }
    }
}</code></pre>
<pre><code class="language-java">Member member = new Member
                        .MemberBuilder(&quot;member&quot;, 10)
                        .setPhoneNumber(&quot;010-1234-5678&quot;)
                        .build();</code></pre>
<p><strong>생성자</strong>를 통해 <strong>필수 입력 필드</strong>인 <strong><code>name</code></strong>과 <strong><code>age</code></strong>의 값을 입력받고, <strong>선택 입력 필드</strong>인 <strong><code>phoneNumber</code></strong>와 <strong><code>email</code></strong>은 <strong>메서드</strong>를 통해 선택적으로 값을 입력할 수 있다. 최종적으로 <strong><code>build()</code></strong> 메서드를 호출해야 <strong>입력값이 적용된 <code>Member</code> 인스턴스</strong>가 정상적으로 반환된다.</p>
<hr>
<h3 id="📝-builder">📝 <code>@Builder</code></h3>
<blockquote>
<p><strong>Lombok</strong>에서 제공하는 <strong>빌더 클래스 생성</strong> 어노테이션 : <strong>클래스</strong> 및 <strong>생성자</strong> 단위에 적용 가능</p>
</blockquote>
<h4 id="✔️-클래스">✔️ 클래스</h4>
<ul>
<li><strong>클래스</strong> 단위에 적용 시, <strong>모든 필드를 포함하는 빌더 클래스</strong> 생성</li>
</ul>
<pre><code class="language-java">@Builder
public class Member {
    // 필수 정보
    private String name;
    private int age;

    // 선택 정보
    private String phoneNumber;
    private String email;
}

Member member = Member.builder()
                .name(&quot;member&quot;)
                .age(10)
                .email(&quot;member@naver.com&quot;)
                .phoneNumber(&quot;010-1234-5678&quot;)
                .build();</code></pre>
<p>➕ <strong>클래스</strong> 단위에 <strong><code>@Builder</code></strong>와 <strong><code>@NoArgsConstructor</code></strong>를 <strong>함께 사용하면 에러</strong> 발생 : <strong><code>@Builder</code></strong>는 <strong>생성자가 없는 경우</strong>에만 <strong>생성자를 자동으로 생성</strong>한다. 따라서, <strong><code>@NoArgsConstructor</code></strong>에 의해 <strong>기본 생성자</strong>가 생성되거나, 클래스 내에 모든 필드를 가지는 생성자가 아닌 <strong>다른 생성자</strong>가 있는 경우 <strong>모든 필드를 가지는 생성자</strong>를 <strong>수동으로 생성</strong>해주어야 한다. (<strong><code>@AllArgsConstructors</code></strong> 추가)</p>
<h4 id="✔️-생성자">✔️ 생성자</h4>
<ul>
<li><strong>생성자</strong> 단위에 적용 시, <strong>해당 생성자의 매개변수에 포함된 필드</strong>만을 사용하여 <strong>빌더 클래스</strong> 생성</li>
</ul>
<pre><code class="language-java">public class Member {
    // 필수 정보
    private String name;
    private int age;

    // 선택 정보
    private String phoneNumber;
    private String email;

    @Builder
    public Member(String name, int age, String phoneNumber) {
        this.name = name;
        this.age = age;
        this.phoneNumber = phoneNumber;
    }
}

Member member = Member.builder()
                .name(&quot;member&quot;)
                .age(10)
                //.email(&quot;member@naver.com&quot;) // 에러 발생 : 생성자에 미포함된 필드
                .phoneNumber(&quot;010-1234-5678&quot;)
                .build();</code></pre>
<hr>
<blockquote>
<p>🙇🏻‍♀️ 참고 1 : <a href="https://esoongan.tistory.com/82">[JAVA] 빌더패턴 (Builder Pattern) , @Builder</a> 
🙇🏻‍♀️ 참고 2 : <a href="https://ejejc.tistory.com/27">Builder 패턴, @NoArgsConstructor, @AllArgsConstructor</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[바킹독의 실전 알고리즘] 9. BFS / A. DFS]]></title>
            <link>https://velog.io/@pockyjx_/%EB%B0%94%ED%82%B9%EB%8F%85%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-9.-BFS-10.-DFS</link>
            <guid>https://velog.io/@pockyjx_/%EB%B0%94%ED%82%B9%EB%8F%85%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-9.-BFS-10.-DFS</guid>
            <pubDate>Wed, 07 Feb 2024 04:58:43 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-bfs">💡 BFS</h1>
<blockquote>
<p><strong>B</strong>reath <strong>F</strong>irst <strong>S</strong>earch : <strong>다차원 배열</strong>을 탐색할 때, <strong>너비를 우선으로 탐색</strong>하는 알고리즘</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/f10e284d-7efe-41f1-80bb-951e9b7b740c/image.png" alt=""></p>
<ul>
<li><p><strong><code>int[][] graph</code></strong> : <strong>그래프</strong>를 <strong>2차원 배열</strong>로 표현</p>
<ul>
<li><p>배열의 <strong>인덱스</strong> : <strong>노드 번호</strong> (0번 노드는 없으므로 아무것도 입력하지 않음)</p>
</li>
<li><p>해당 <strong>인덱스 내의 요소</strong> : <strong>인접한 노드 번호</strong></p>
</li>
</ul>
</li>
<li><p><strong><code>boolean[] isVisit</code></strong> : <strong>노드</strong>의 <strong>방문 여부</strong> 표시</p>
</li>
<li><p><strong><code>bfs(int start)</code></strong> : *<em><code>start</code>번 노드부터 *</em>탐색 수행 </p>
<blockquote>
<ol>
<li>큐에 <strong>시작점</strong>을 넣고 <strong>방문</strong> 표시</li>
<li>큐의 <strong>가장 앞에 위치한 노드</strong>를 꺼내고 <strong>인접한 노드</strong> 탐색
2-1. <strong>인접한 노드</strong>에 <strong>방문 표시가 없다면</strong> <strong>큐</strong>에 넣고 <strong>방문</strong> 표시 후** 2번 반복**
2-2. 인접한 노드가 없거나 이미 방문했다면 2번 반복</li>
<li><strong>큐가 비면</strong> 탐색 <strong>종료</strong></li>
</ol>
</blockquote>
</li>
</ul>
<pre><code class="language-java">import java.util.*;
public class Main {

    // 그래프 : 인덱스 - 인접 노드 형태
    static int[][] graph = {{}, {2,3,5}, {1}, {1,4}, {3}, {1,6}, {7, 8}, {6,8}, {6,7}};

    // 노드 방문 여부
    static boolean[] isVisit = new boolean[graph.length];

    // 탐색 순서 출력
    static StringBuilder sb = new StringBuilder();

    public static void main(String[] args) {

        // 1번 노드부터 탐색 시작
        bfs(1);

        bw.write(sb + &quot;&quot;);
        bw.close();
    }

    static void bfs(int start) {
        Queue&lt;Integer&gt; q = new LinkedList&lt;&gt;();

        q.offer(start); // 1. 시작 노드 삽입
        isVisit[start] = true; // 1. 시작 노드 방문 표시


        while(!q.isEmpty()) { // 3. 큐가 빌 때까지 반복
            int idx = q.poll(); // 2. 탐색할 노드 인덱스
            sb.append(idx + &quot; &quot;);

            for(int i=0;i&lt; graph[idx].length;i++) { // 2. 인접한 노드 탐색
                int tmp = graph[idx][i];

                if(!isVisit[tmp]) { // 2-1. 인접한 노드에 방문하지 않은 경우
                    q.offer(tmp); // 2-1. 인접 노드 큐에 삽입
                    isVisit[tmp] = true; // 2-1. 인접 노드 방문 표시
                }
            }
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/266369f0-7214-4e96-857f-cf7e01f42d91/image.png" alt=""> ✅ <strong>모든 칸</strong>이 <strong>큐</strong>에 한 번씩 들어갔다 나오므로, <strong>N개의 칸</strong>에 대한 <strong>시간 복잡도는 <code>O(N)</code></strong> → 한 칸 당 <code>O(1)</code></p>
<blockquote>
<p>🙇🏻‍♀️ 참고 : <a href="https://codingnojam.tistory.com/41">[Algorithm] BFS(Breadth-first search)를 Java로 구현해보자!</a></p>
</blockquote>
<hr>
<h2 id="📒-연습문제">📒 연습문제</h2>
<h3 id="📝-예제-1">📝 예제 1</h3>
<blockquote>
<p><a href="https://www.acmicpc.net/problem/1926">[1926] 그림</a> : <strong>파란색</strong>으로 칠해진 칸들 중, <strong>상하좌우로 연결된 모든 칸을 탐색</strong>하기 → <strong><code>(0, 0)</code></strong>에서 시작</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/d59410de-b3e6-4304-a44c-d4c7e5cefebe/image.png" alt=""></p>
<ol start="0">
<li><p>시작 지점인 <strong><code>(0, 0)</code></strong>에 대해 <strong>방문 표시</strong>하기</p>
</li>
<li><p><strong><code>(0, 0)</code></strong>의 <strong>상하좌우</strong>를 탐색하여 <strong>파란색</strong>이면서 <strong>방문하지 않은 칸의 좌표</strong>를 <strong>큐</strong>에 삽입한 후 <strong>방문 표시</strong>하기
<img src="https://velog.velcdn.com/images/pockyjx_/post/2ca564c2-d590-42d3-93ab-7c4bbe80c1d0/image.png" alt=""></p>
</li>
<li><p><strong>큐의 가장 앞 원소 좌표</strong>로 이동하며 큐에서 <strong>해당 좌표를 제거</strong>한 후, <strong>1번 과정 반복</strong> <img src="https://velog.velcdn.com/images/pockyjx_/post/1c86f340-17ce-4fc7-9d86-e479f6afe0bf/image.png" alt=""></p>
</li>
<li><p><strong>큐</strong>가 <strong>비면 탐색 종료</strong></p>
<br>

</li>
</ol>
<h4 id="✔️-구현">✔️ 구현</h4>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {

    static int[][] graph;
    static boolean[][] isVisit;
    static int[] dx = {-1, 1, 0, 0};
    static int[] dy = {0, 0, -1, 1};
    static int n, m;


    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        StringTokenizer st = new StringTokenizer(br.readLine());
        n = Integer.parseInt(st.nextToken()); // 세로 (행)
        m = Integer.parseInt(st.nextToken()); // 가로 (열)

        graph = new int[n][m];
        isVisit = new boolean[n][m];

        for(int i=0;i&lt;n;i++) {
            st = new StringTokenizer(br.readLine());
            for(int j=0;j&lt;m;j++) {
                graph[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        int cnt = 0;
        int max = 0;
        for(int i=0;i&lt;n;i++) {
            for(int j=0;j&lt;m;j++) {
                if(graph[i][j] == 1 &amp;&amp; !isVisit[i][j]) {
                    max = Math.max(max, bfs(j, i));
                    cnt++;
                }
            }
        }

        bw.write(cnt + &quot;\n&quot; + max);
        bw.close();
    }

    static int bfs(int x, int y) {
        Queue&lt;int[]&gt; q = new LinkedList&lt;&gt;();
        q.offer(new int[] {x, y});
        isVisit[y][x] = true;

        int area = 1;
        while(!q.isEmpty()) {
            int[] tmp = q.poll();
            int px = tmp[0]; int py = tmp[1];

            for(int i=0;i&lt;4;i++) {
                int nx = px + dx[i];
                int ny = py + dy[i];

                if(nx &lt; 0 || ny &lt; 0 || nx &gt; m-1 || ny &gt; n-1) continue;
                if(graph[ny][nx] == 1 &amp;&amp; !isVisit[ny][nx]) {
                    q.offer(new int[] {nx, ny});
                    isVisit[ny][nx] = true;
                    area++;
                }
            }
        }
        return area;
    }
}</code></pre>
<hr>
<h3 id="📝-예제-2--최단거리-측정">📝 예제 2 : 최단거리 측정</h3>
<blockquote>
<p><a href="https://www.acmicpc.net/problem/2178">[2178] 미로 탐색</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/006c1402-a9ca-445b-8a61-e06f836fb148/image.png" alt=""></p>
<ul>
<li><strong><code>graph[x][y]</code></strong> : <strong>이 칸</strong>으로 오기까지 <strong>거쳐온 칸의 수</strong> = <strong><code>이전 칸의 값 + 1</code></strong></li>
</ul>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {

    static int[][] graph;
    static boolean[][] isVisit;
    static int[] dx = {-1, 1, 0, 0};
    static int[] dy = {0, 0, -1, 1};
    static int n, m;

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        StringTokenizer st = new StringTokenizer(br.readLine());
        n = Integer.parseInt(st.nextToken());
        m = Integer.parseInt(st.nextToken());

        graph = new int[n][m];
        isVisit = new boolean[n][m];

        for(int i=0;i&lt;n;i++) {
            String s = br.readLine();

            for(int j=0;j&lt;m;j++) {
                graph[i][j] = s.charAt(j) - &#39;0&#39;;
            }
        }

        bfs(0, 0);

        bw.write(graph[n-1][m-1] + &quot;&quot;);
        bw.close();
    }

    static void bfs(int x, int y) {
        Queue&lt;int[]&gt; q = new LinkedList&lt;&gt;();
        q.offer(new int[] {x, y});
        isVisit[x][y] = true;

        while(!q.isEmpty()) {
            int[] tmp = q.poll();
            int px = tmp[0];
            int py = tmp[1];

            for(int i=0;i&lt;4;i++) {
                int nx = px + dx[i];
                int ny = py + dy[i];

                if(nx &lt; 0 || ny &lt; 0 || nx &gt; n-1 || ny &gt; m-1) continue;
                if(graph[nx][ny] == 1 &amp;&amp; !isVisit[nx][ny]) {
                    q.offer(new int[] {nx, ny});
                    isVisit[nx][ny] = true;
                    graph[nx][ny] = graph[px][py] + 1;
                }
            }
        }
    }
}</code></pre>
<hr>
<h3 id="📝-예제-3--시작점이-여러-개">📝 예제 3 : 시작점이 여러 개</h3>
<blockquote>
<p><a href="https://www.acmicpc.net/problem/7576">[7576] 토마토</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/06f4d483-5ba0-42bc-bf59-ca97b172d3b8/image.png" alt=""></p>
<ul>
<li><p><strong>탐색 시작점이 여러 개</strong>인 경우 <code>graph</code>를 채울 때 <strong>시작점의 좌표를 큐에 넣고 시작</strong>하면 된다!</p>
</li>
<li><p>시작점의 값이 1이므로, 최종 답은 <code>graph[x][y] - 1</code></p>
</li>
</ul>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {

    static int[][] graph;
    static int[] dx = {-1, 1, 0, 0};
    static int[] dy = {0, 0, -1, 1};
    static int n, m;
    static Queue&lt;int[]&gt; q = new LinkedList&lt;&gt;();

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        StringTokenizer st = new StringTokenizer(br.readLine());
        m = Integer.parseInt(st.nextToken());
        n = Integer.parseInt(st.nextToken());

        graph = new int[n][m];
        for(int i=0;i&lt;n;i++) {
            st = new StringTokenizer(br.readLine());
            for(int j=0;j&lt;m;j++) {
                graph[i][j] = Integer.parseInt(st.nextToken());
                if(graph[i][j] == 1) q.offer(new int[] {i, j});
            }
        }
        bw.write(bfs() + &quot;&quot;);
        bw.close();
    }

    static int bfs() {
        while(!q.isEmpty()) {
            int[] tmp = q.poll();
            int px = tmp[0];
            int py = tmp[1];

            for(int i=0;i&lt;4;i++) {
                int nx = px + dx[i];
                int ny = py + dy[i];

                if(nx &lt; 0 || ny &lt; 0 || nx &gt; n-1 || ny &gt; m-1) continue;
                if(graph[nx][ny] == 0) {
                    graph[nx][ny] = graph[px][py] + 1;
                    q.offer(new int[] {nx, ny});
                }
            }
        }

        int day = 0;
        for(int i=0;i&lt;n;i++) {
            for(int j=0;j&lt;m;j++) {
                if(graph[i][j] == 0) return -1;
                day = Math.max(day, graph[i][j]);
            }
        }
        return day - 1;
    }
}</code></pre>
<hr>
<h3 id="📝-예제-4--시작점이-두-종류">📝 예제 4 : 시작점이 두 종류</h3>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/6093092d-4b3a-4bd0-9752-d26a01421260/image.png" alt=""></p>
<ul>
<li><p>시작점의 종류가 다른 경우, <strong>각 시작점</strong>에 대하여 <strong>BFS</strong> 수행</p>
<ul>
<li><p><strong><code>fire[][]</code></strong> (불) : 좌표가 <strong>범위를 넘지 않으면서 벽(<code>#</code>)이 아니라면</strong> <strong><code>이전 좌표의 시간 + 1</code></strong>  저장</p>
</li>
<li><p><strong><code>move[][]</code></strong> (지훈) : 좌표가 <strong>벽이 아니면서 지훈이가 도달한 시간</strong>이 <strong>불이 퍼지기 전</strong>인 경우 <strong><code>이전 좌표의 시간 + 1</code></strong> 저장 → 좌표가 <strong>범위를 넘으면 탈출</strong>한 것이므로 시간 출력 / 반복문이 종료되면 탈출하지 못한 것이므로 <code>IMPOSSIBLE</code>출력</p>
</li>
</ul>
</li>
</ul>
<pre><code class="language-java">import java.io.*;
import java.util.*;
public class Main {

    public static class Node {
        int x, y;
        public Node(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        StringTokenizer st = new StringTokenizer(br.readLine());
        int r = Integer.parseInt(st.nextToken());
        int c = Integer.parseInt(st.nextToken());

        int[][] move = new int[r][c];
        int[][] fire = new int[r][c];

        Queue&lt;Node&gt; moveQ = new LinkedList&lt;&gt;();
        Queue&lt;Node&gt; fireQ = new LinkedList&lt;&gt;();

        for(int i=0;i&lt;r;i++) {
            String s = br.readLine();
            for(int j=0;j&lt;c;j++) {
                char ch = s.charAt(j);

                if(ch == &#39;J&#39;) {
                    move[i][j] = 1;
                    moveQ.offer(new Node(i, j));
                }
                else if(ch == &#39;F&#39;) {
                    fire[i][j] = 1;
                    fireQ.offer(new Node(i, j));
                }
                else {
                    move[i][j] = ch;
                    fire[i][j] = ch;
                }
            }
        }

        int[] dx = {-1, 1, 0, 0};
        int[] dy = {0, 0, -1, 1};

        Node tmp;
        int px, py, nx, ny;

        // 불 bfs
        while(!fireQ.isEmpty()) {
            tmp = fireQ.poll();
            px = tmp.x; py = tmp.y;

            for(int i=0;i&lt;4;i++) {
                nx = px + dx[i]; ny = py + dy[i];
                if(nx &lt; 0 || ny &lt; 0 || nx &gt; r-1 || ny &gt; c-1) continue;
                if(fire[nx][ny] == &#39;.&#39;) {
                    fire[nx][ny] = fire[px][py] + 1;
                    fireQ.offer(new Node(nx, ny));
                }
            }
        }

        // 지훈 bfs
        while(!moveQ.isEmpty()) {
            tmp = moveQ.poll();
            px = tmp.x; py = tmp.y;

            for(int i=0;i&lt;4;i++) {
                nx = px + dx[i]; ny = py + dy[i];
                if(nx &lt; 0 || ny &lt; 0 || nx &gt; r-1 || ny &gt; c-1) { // 탈출
                    System.out.println(move[px][py]); return;
                }

                if(move[nx][ny] != &#39;#&#39;) {
                    if(move[px][py] + 1 &lt; fire[nx][ny]) {
                        move[nx][ny] = move[px][py] + 1;
                        moveQ.offer(new Node(nx, ny));
                    }
                }
            }
        }

        // 탈출 실패
        System.out.println(&quot;IMPOSSIBLE&quot;);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/31d548ad-6ee4-4465-a0a8-01a85783730d/image.png" alt=""> : 2차원 배열 2개에 큐까지 2개 써서 그런지 메모리 초과 발생,, 인줄 알았으나 다른 사람들의 풀이를 봐도 다 2개씩 쓰고 통과하던데 나는 왜? 싶어서 GPT한테 물어봤는데,, </p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/e32d1cf2-433e-4dfb-b097-7ff79cfe79cc/image.png" alt=""> 상상도 못한 답변 ㄴㅇㄱ.. 시간 비교에만 집중하느라 <strong>해당 좌표에 갔는지 표시를 안했던 것..</strong> 그래서 같은 칸을 중복으로 탐색하다 보니 메모리 초과가 발생한 것.. 그래서 <strong>탐색한 불의 좌표</strong>를 <strong>-1로</strong> 표시하고 이후 <strong>해당 좌표가 -1이면 탐색하지 않도록 처리</strong>하니 성공!</p>
<pre><code class="language-java">// 지훈 bfs
while(!moveQ.isEmpty()) {
    tmp = moveQ.poll();
    px = tmp.x; py = tmp.y;

       for(int i=0;i&lt;4;i++) {
        nx = px + dx[i]; ny = py + dy[i];
        if(nx &lt; 0 || ny &lt; 0 || nx &gt; r-1 || ny &gt; c-1) { // 탈출
            System.out.println(move[px][py]); return;
        }

        if(move[nx][ny] != &#39;#&#39;) {
               // 방문하지 않았거나, 
            if(fire[nx][ny] != -1 &amp;&amp; move[px][py] + 1 &lt; fire[nx][ny]) {
                move[nx][ny] = move[px][py] + 1;
                fire[nx][ny] = -1; // 방문 표시
                  moveQ.offer(new Node(nx, ny));
              }
        }
    }
}</code></pre>
<hr>
<h3 id="📝-예제-5--1차원에서의-bfs">📝 예제 5 : 1차원에서의 BFS</h3>
<blockquote>
<p><a href="https://www.acmicpc.net/problem/1697">[1697] 숨바꼭질</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/0e6013fb-43f0-4c05-9312-4af98df01ca2/image.png" alt=""></p>
<ul>
<li>그래프를 <strong>1차원 배열</strong>로 생성하여, <strong>시작</strong> 위치에서 <strong><code>x+1</code>, <code>x-1</code>, <code>2x</code></strong>로 이동한 후 <strong>각 이동 위치</strong>에서 다시 <strong><code>x+1</code>, <code>x-1</code>, <code>2x</code>로 이동</strong>하는 방식으로 탐색을 수행하다가, <strong>동생이 있는 위치에 도달</strong>하면 <strong>탐색을 종료</strong>한다.</li>
</ul>
<pre><code class="language-java">import java.io.*;
import java.util.*;
public class Main {

    static int[] graph = new int[100001];
    static int s, e;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        s = Integer.parseInt(st.nextToken());
        e = Integer.parseInt(st.nextToken());

        System.out.println(bfs());
    }

    static int bfs() {
        Queue&lt;Integer&gt; q = new LinkedList&lt;&gt;();
        q.offer(s);
        graph[s] = 1;

        while(!q.isEmpty()) {
            int tmp = q.poll();
            if(tmp == e) return graph[tmp]-1;

            if(tmp-1 &gt;= 0 &amp;&amp; graph[tmp-1] == 0) {
                graph[tmp-1] = graph[tmp] + 1;
                q.offer(tmp-1);
            }

            if(tmp+1 &lt; 100001 &amp;&amp; graph[tmp+1] == 0) {
                graph[tmp+1] = graph[tmp] + 1;
                q.offer(tmp+1);
            }

            if(tmp*2 &lt; 100001 &amp;&amp; graph[tmp*2] == 0) {
                graph[tmp*2] = graph[tmp] + 1;
                q.offer(tmp*2);
            }
        }
        return 0;
    }
}</code></pre>
<hr>
<h1 id="💡-dfs">💡 DFS</h1>
<blockquote>
<p><strong>D</strong>epth <strong>F</strong>irst <strong>S</strong>earch : 다차원 배열을 탐색할 때, <strong>깊이를 우선으로 탐색</strong>하는 알고리즘</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/f119821f-09b2-4f18-a78c-4ad322bcefa5/image.jpg" alt=""></p>
<p>✅ <strong>큐</strong>가 <strong>스택으로 변경</strong>된 것 외에는, <strong>BFS</strong>와 <strong>전체적인 흐름이 같다!</strong></p>
<blockquote>
<ol>
<li>스택에 <strong>시작점</strong>을 넣고 <strong>방문</strong> 표시<ol start="3">
<li>스택의 <strong>꼭대기에 위치한 노드</strong>를 꺼내고 <strong>인접한 노드</strong> 탐색
2-1. <strong>인접한 노드</strong>에 <strong>방문 표시가 없다면</strong> <strong>스택</strong>에 넣고 <strong>방문</strong> 표시 후** 2번 반복**
2-2. 인접한 노드가 없거나 이미 방문했다면 2번 반복</li>
<li><strong>스택이 비면</strong> 탐색 <strong>종료</strong></li>
</ol>
</li>
</ol>
</blockquote>
<pre><code class="language-java">import java.io.*;
import java.util.*;
public class Main {

    static int[][] graph = {{}, {2,3,5}, {1}, {1,4}, {3}, {1, 6}, {5, 7, 8}, {6, 8}, {6, 7}};
    static boolean[] isVisit = new boolean[graph.length];
    static StringBuilder sb = new StringBuilder();

    public static void main(String[] args) throws IOException {
        bfs(1);
        System.out.println(sb);
    }

    static void bfs(int v) {
        Stack&lt;Integer&gt; s = new Stack&lt;&gt;();
        s.push(v);
        isVisit[v] = true;

        while(!s.isEmpty()) {
            int idx = s.pop();
            sb.append(idx + &quot; &quot;);

            for(int i=graph[idx].length-1;i&gt;=0;i--) {
                int tmp = graph[idx][i];
                if(!isVisit[tmp]) {
                    s.push(tmp);
                    isVisit[tmp] = true;
                }
            }
        }
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/096bcff2-c1ba-44ed-935a-c3add775ab7a/image.png" alt=""> : 스택은 후입선출이기 때문에, 순서를 맞추기 위해 각 노드에 <strong>인접한 노드</strong>를 <strong>역순</strong>으로 탐색</p>
<hr>
<h2 id="📒-bfs-vs-dfs">📒 BFS vs DFS</h2>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/6d3f27b4-8a94-4a9f-a42a-a2dd5e68bbc5/image.png" alt=""></p>
<ul>
<li><p><strong>BFS</strong> : <strong>시작점</strong>과 인접한 노드 <strong>모두 탐색</strong> → <strong>각 인접 노드</strong>와 인접한 노드 <strong>모두 탐색</strong> → ...</p>
</li>
<li><p><strong>DFS</strong> : <strong>시작점</strong>과 인접한 노드 <strong>하나</strong> 탐색 → <strong>한 노드와 인접한 노드의 탐색이 모두 끝난 후</strong> <strong>시작점</strong>과 인접한 *<em>다른 노드 하나 *</em>탐색 → ...</p>
</li>
</ul>
<blockquote>
<p>⭐ <strong>거리 탐색</strong> 시, <strong>BFS</strong>에서는 <strong>시작점과 인접한 노드의 거리</strong>가 모두 <strong><code>+1</code></strong>이였지만 <strong>DFS</strong>는 <strong>해당 공식이 성립하지 않기 때문에</strong> 거리 탐색 시 사용할 수 없다!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/4c19daff-30e1-474b-b617-54101b856086/image.jpg" alt=""></p>
<hr>
<blockquote>
<p>🙇🏻‍♀️ 출처 </p>
</blockquote>
<ul>
<li><a href="https://www.youtube.com/watch?v=ftOmGdm95XI&amp;list=PLtqbFd2VIQv4O6D6l9HcD732hdrnYb6CY&amp;index=10&amp;t=1369s">[바킹독의 실전 알고리즘] 0x09강 - BFS</a></li>
<li><a href="https://www.youtube.com/watch?v=93jy2yUYfVE&amp;list=PLtqbFd2VIQv4O6D6l9HcD732hdrnYb6CY&amp;index=11">[바킹독의 실전 알고리즘] 0x0A강 - DFS</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] 게시판 :  Pageable 인터페이스를 활용한 페이징]]></title>
            <link>https://velog.io/@pockyjx_/Spring-JPA-%EA%B2%8C%EC%8B%9C%ED%8C%90-Pageable-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%8E%98%EC%9D%B4%EC%A7%95</link>
            <guid>https://velog.io/@pockyjx_/Spring-JPA-%EA%B2%8C%EC%8B%9C%ED%8C%90-Pageable-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%8E%98%EC%9D%B4%EC%A7%95</guid>
            <pubDate>Tue, 06 Feb 2024 04:45:30 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-pageable">💡 <code>Pageable</code></h1>
<blockquote>
<p><strong>Spring Data</strong>가 제공하는 <strong>페이징 + 정렬</strong> 추상화 인터페이스</p>
</blockquote>
<p>: 구현체인 <strong><code>PageReauest</code></strong>를 통해 페이징에 필요한 데이터(<strong>시작 페이지, 최대 결과 개수, 정렬 조건</strong>)를 전달하면 <strong>페이징 결과</strong>를  <strong><code>Page</code></strong>타입으로 반환한다.</p>
<hr>
<h2 id="📒-게시판-페이징">📒 게시판 페이징</h2>
<blockquote>
<p>목표 : <strong>모든 게시글</strong>을 조회한 결과를 <strong>페이징</strong>하여 화면에 나타내기</p>
</blockquote>
<p>✅ 스프링 부트,** 스프링 데이터 JPA**, 타임리프 사용</p>
<hr>
<h3 id="1-entity">1. Entity</h3>
<pre><code class="language-java">@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
    @Id @GeneratedValue
    @Column(name = &quot;board_id&quot;)
    private Long id;
    private String title;
    @Lob
    private String content;
    private LocalDateTime createDate;
    private LocalDateTime updateDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;category_id&quot;)
    private Category category;
}</code></pre>
<h3 id="2-boarddto">2. BoardDTO</h3>
<ul>
<li>화면에 <strong>출력할 정보</strong> : 게시글 번호, 카테고리명(<code>category</code>), 게시글 제목, 작성자 이름(<code>member</code>), 작성일</li>
</ul>
<pre><code class="language-java">@Data
public class BoardDTO {
    private Long id;
    private String categoryName;
    private String title;
    private String userName;

    @DateTimeFormat(pattern = &quot;yyyy-MM-dd HH:mm&quot;)
    private LocalDateTime createDate;

    public BoardDTO(Post post) {
        this.id = post.getId();
        this.categoryName = post.getCategory().getName();
        this.title = post.getTitle();
        this.userName = post.getMember().getUserName();
        this.createDate = post.getCreateDate();
    }
}</code></pre>
<h3 id="2-repository">2. <code>Repository</code></h3>
<ul>
<li><p><strong>Spring Data JPA</strong> 사용 : <code>JpaRepository</code> 상속</p>
</li>
<li><p><strong><code>findBoard(Pageable)</code></strong> : <strong>페치 조인</strong>을 통해 연관관계를 가지는 <strong><code>member</code></strong>와 <strong><code>category</code></strong>를 한 번에 조회 + <strong>파라미터</strong>로 전달된 <strong><code>pageable</code></strong>를 데이터를 통해 <strong>페이징</strong> 수행</p>
</li>
</ul>
<pre><code class="language-java">public interface PostRepository extends JpaRepository&lt;Post, Long&gt; {
    @Query(&quot;select p from Post p join fetch p.category c join fetch p.member m&quot;)
    Page&lt;Post&gt; findBoard(Pageable pageable);
}</code></pre>
<h3 id="3-service">3. <code>Service</code></h3>
<ul>
<li><p><strong><code>findAll(Pagealbe)</code></strong></p>
<ul>
<li>리포지토리의 <strong><code>findBoard(Pageable)</code></strong>에 <strong><code>pageable</code> 전달</strong></li>
<li><strong><code>page.map()</code></strong> : <code>findBoard()</code>가 반환한 <strong>엔티티</strong>를 <strong><code>BoardDTO</code></strong>로 변환하여 컨트롤러에 반환</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
public class PostService {
    private final PostRepository postRepository;

    public Page&lt;BoardDTO&gt; findAll(Pageable pageable) {
        Page&lt;Post&gt; page = postRepository.findBoard(pageable);
        return page.map(board -&gt; new BoardDTO(board));
    }
}</code></pre>
<h3 id="4-controller">4. <code>Controller</code></h3>
<ul>
<li><p><strong>요청</strong>의 <strong>파라미터</strong>로 <strong><code>page</code>, <code>size</code>, <code>sort</code></strong> 등의 정보가 넘어오면 <strong><code>Pageable</code> 타입</strong>으로 바인딩 → 해당 데이터를 사용하여 <strong><code>PageRequest</code> 객체 생성</strong></p>
<ul>
<li><p><strong><code>@PageableDefault</code></strong> : <strong>페이징</strong> 관련 데이터 <strong>기본값</strong> 설정</p>
<ul>
<li><code>size</code> : 최대 결과 개수 (default = 20)</li>
<li><code>page</code> : 조회 페이지 (default = 0)</li>
<li><strong><code>sort</code> + <code>direction</code></strong> : <strong>단일 정렬</strong> 조건</li>
</ul>
</li>
<li><p><strong><code>@SortDefault.SortDefaults({@SortDefault(sort=,direction), ...})</code></strong> : <strong>다중 정렬</strong> 조건</p>
</li>
</ul>
</li>
<li><p><strong>뷰</strong> 페이지에 <strong><code>page</code>, <code>pm</code></strong> (하단부 페이징 관련 데이터) 전달  </p>
</li>
</ul>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/boards&quot;)
@RequiredArgsConstructor
public class BoardController {
    private final PostService postService;

    @GetMapping
    public String postList(@PageableDefault(size = 2, sort = &quot;id&quot;, direction = Sort.Direction.DESC) Pageable pageable,
                               Model model) {

        Page&lt;BoardDTO&gt; page = postService.findAll(pageable);
        PageMaker pm = new PageMaker(page);

        model.addAttribute(&quot;pm&quot;, pm);
        model.addAttribute(&quot;page&quot;, page);

        return &quot;board/postList&quot;;
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/ef2a6fb6-0aaa-44a3-aed3-ec1117e8ac62/image.png" alt=""> : <strong><code>/boards</code></strong> 요청 시 실행되는 쿼리 (게시글 전체 조회 + 카운트 쿼리)</p>
<h3 id="5-pagemaker">5. <code>PageMaker</code></h3>
<ul>
<li><p>컨트롤러에서 전달받은 <strong><code>Page</code></strong>를 사용하여 <strong>화면</strong>의 <strong>하단 페이징 영역</strong> 관련 데이터 (계산식은 이전 프로젝트에서 사용한 공식 사용..)</p>
<ul>
<li><strong><code>page.getNumber()</code></strong> : <strong>현재</strong> 요청한 페이지 → 0부터 시작하므로, 1 더한 값 출력</li>
<li><strong><code>page.getTotalPages()</code></strong> : 페이지의 총 개수  </li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Getter
public class PageMaker {
    private int nowPage; // 현재 페이지
    private int startPage; // 현재 페이지 블럭 내의 첫번째 페이지
    private int endPage; // 현재 페이지 블럭 내의 마지막 페이지
    private boolean prev; // 이전 페이지가 있는가?
    private boolean next; // 다음 페이지가 있는가?
    private int block = 2; // 한 번에 출력할 페이지 개수

    public PageMaker(Page&lt;?&gt; page) {
        nowPage = page.getNumber() + 1;

        endPage = (int)(Math.ceil((double)(nowPage)/block)) * block;
        startPage = endPage - block + 1;
        endPage = Math.min(endPage, page.getTotalPages());

        prev = startPage != 1;
        next = ((long)endPage &lt; page.getTotalPages());
    }
}</code></pre>
<h3 id="6-html--하단부-페이징">6. HTML : 하단부 페이징</h3>
<ul>
<li><p><strong><code>Previous</code></strong> : 해당 페이지 블럭의 <code>prev</code>가 <code>true</code>인 경우에만 출력 </p>
<ul>
<li><strong>첫</strong> 페이지 <strong>(&laquo;)</strong> : <strong><code>/boards?page=0</code></strong></li>
<li><strong>이전 페이지 블럭</strong>의 <strong><code>endPage</code> (&lsaquo;) : <code>/boards?page=startPage-2</code></strong>    </li>
</ul>
</li>
<li><p><strong>페이지 블럭</strong> : <strong><code>startPage ~ endPage</code></strong> 출력 → 출력된 <code>p</code>의 값은 <code>page+1</code>이므로, <strong>클릭</strong> 시 <strong><code>/boards?page=p-1</code></strong>로 이동</p>
</li>
<li><p><strong><code>Next</code></strong> : 해당 페이지 블럭의 <code>next</code>가 <code>true</code>이면서 <code>endPage</code>가 0 이상인 경우에만 출력</p>
<ul>
<li><strong>다음 페이지 블럭</strong>의 <strong><code>startPage</code></strong> <strong>(&rsaquo;)</strong> : <strong><code>/boards?page=endPage</code></strong></li>
<li><strong>마지막</strong> 페이지 <strong><code>endPage</code> (&raquo;) : <code>/boards?page=getTotalPages()-1</code></strong></li>
</ul>
</li>
</ul>
<pre><code class="language-html">&lt;nav aria-label=&quot;Page navigation example&quot;&gt;
    &lt;ul class=&quot;pagination justify-content-center pagination-sm&quot;&gt;

    &lt;!-- 첫 페이지로 이동 --&gt;
    &lt;li class=&quot;page-item&quot; th:if=&quot;${pm.prev}&quot;&gt;
        &lt;a class=&quot;page-link&quot; th:href=&quot;@{/boards(page=0)}&quot; aria-label=&quot;Previous&quot;&gt;
            &lt;span aria-hidden=&quot;true&quot;&gt;&amp;laquo;&lt;/span&gt;
        &lt;/a&gt;
    &lt;/li&gt;
    &lt;!-- 첫 페이지 이동 --&gt;

    &lt;!-- 이전 페이지 블럭으로 이동 --&gt;
    &lt;li class=&quot;page-item&quot; th:if=&quot;${pm.prev}&quot;&gt;
        &lt;a class=&quot;page-link&quot; th:href=&quot;@{/boards(page=${pm.startPage}-2)}&quot; aria-label=&quot;Previous&quot;&gt;
            &lt;span aria-hidden=&quot;true&quot;&gt;&amp;lsaquo;&lt;/span&gt;
        &lt;/a&gt;
    &lt;/li&gt;
    &lt;!-- 이전 페이지 블럭으로 이동 --&gt;

    &lt;!-- 페이지 블럭 --&gt;      
    &lt;li th:class=&quot;page-item&quot; th:each=&quot;p : ${#numbers.sequence(pm.startPage, pm.getEndPage())}&quot;
        th:classappend=&quot;(${pm.nowPage == p})? &#39;active&#39; : &#39;&#39;&quot;&gt;
        &lt;a class=&quot;page-link&quot; th:href=&quot;@{/boards(page=${p}-1)}&quot; 
               th:text=&quot;${p}&quot;&gt;&lt;/a&gt;
    &lt;/li&gt;
    &lt;!-- 페이지 블럭 --&gt; 

    &lt;!-- 다음 페이지 블럭으로 이동 --&gt; 
    &lt;li class=&quot;page-item&quot; th:if=&quot;${pm.next &amp;&amp; pm.endPage &gt; 0}&quot;&gt;
        &lt;a class=&quot;page-link&quot; th:href=&quot;@{/boards(page=${pm.endPage})}&quot; aria-label=&quot;Next&quot;&gt;
            &lt;span aria-hidden=&quot;true&quot;&gt;&amp;rsaquo;&lt;/span&gt;
        &lt;/a&gt;
       &lt;/li&gt;
    &lt;!-- 다음 페이지 블럭으로 이동 --&gt; 

    &lt;!-- 마지막 페이지로 이동 --&gt; 
    &lt;li class=&quot;page-item&quot; th:if=&quot;${pm.next &amp;&amp; pm.endPage &gt; 0}&quot;&gt;
        &lt;a class=&quot;page-link&quot; th:href=&quot;@{/boards(page=${page.getTotalPages()}-1)}&quot; aria-label=&quot;Next&quot;&gt;
            &lt;span aria-hidden=&quot;true&quot;&gt;&amp;raquo;&lt;/span&gt;
        &lt;/a&gt;
    &lt;/li&gt;
    &lt;!-- 마지막 페이지로 이동 --&gt; 

    &lt;/ul&gt;
&lt;/nav&gt;</code></pre>
<hr>
<h3 id="📝-시연">📝 시연</h3>
<p>ex0) *<em>테스트 데이터 *</em> <img src="https://velog.velcdn.com/images/pockyjx_/post/75bd1fa1-6ede-408c-8831-0cb73a8b4d2e/image.png" alt=""></p>
<p>ex1) <code>block = 2</code>, <code>size = 2</code> <img src="https://velog.velcdn.com/images/pockyjx_/post/876632eb-31c4-4c6b-9697-4aeb8bcf7e62/image.gif" alt=""></p>
<p>ex2) <code>block = 5</code>, <code>size = 2</code></p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/5279166b-e12a-4a2c-b4c4-589d1dc5134c/image.png" alt=""></p>
<p>ex3) <code>block = 5</code>, <code>size = 5</code></p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/254ce00d-2a1c-41c6-904b-989d7fbe2e3d/image.png" alt=""></p>
<hr>
<p>➕ 일단은 <code>Page</code> 타입으로 받아서 일반 게시판 형태로 구현해봤는데, 나중에 시간이 되면 <code>Slice</code> 타입으로 받아서 더보기 형식으로 구현해봐야겠다! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[바킹독의 실전 알고리즘] 5. 스택 / 6. 큐 / 7. 덱]]></title>
            <link>https://velog.io/@pockyjx_/%EB%B0%94%ED%82%B9%EB%8F%85%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-5.-%EC%8A%A4%ED%83%9D-6.-%ED%81%90-7</link>
            <guid>https://velog.io/@pockyjx_/%EB%B0%94%ED%82%B9%EB%8F%85%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-5.-%EC%8A%A4%ED%83%9D-6.-%ED%81%90-7</guid>
            <pubDate>Sat, 03 Feb 2024 14:24:48 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-스택-stack">💡 스택 (Stack)</h1>
<blockquote>
<p><strong>한 쪽 끝</strong>에서만 <strong>원소를 넣고 뺄 수 있는</strong> 자료구조 : <strong>후입선출 (LIFO)</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/b9327538-b254-4639-bdbb-dcb6b7299e8a/image.png" alt=""></p>
<ul>
<li>원소의 <strong>추가 &amp; 제거 &amp; 상단의 원소 확인</strong> : <strong><code>O(1)</code></strong> → 원칙적으로 상단의 원소를 제외한 나머지 원소를 확인할 수 없음</li>
</ul>
<hr>
<h2 id="📒-구현">📒 구현</h2>
<blockquote>
<p><strong>배열</strong> 혹은 연결 리스트를 사용하여 구현 가능</p>
</blockquote>
<pre><code>- dat[] : 스택을 구현할 배열
- pos : 상단 원소의 위치</code></pre><ul>
<li><p><strong><code>push()</code></strong> : 스택에 원소 <strong>추가</strong></p>
<pre><code class="language-java">void push(int x) {
  dat[pos++] = x;
}</code></pre>
</li>
<li><p><strong><code>pop()</code></strong> : 스택에서 원소 <strong>제거</strong></p>
<pre><code class="language-java">void pop() {
  pos--;
}</code></pre>
</li>
<li><p><strong><code>top()</code></strong> : 스택의 <strong>상단 원소 확인</strong></p>
<pre><code class="language-java">int top() {
  return dat[pos-1];
}</code></pre>
</li>
</ul>
<hr>
<h2 id="📒-연습문제">📒 연습문제</h2>
<blockquote>
<p><a href="https://www.acmicpc.net/problem/10773">[10733] 제로</a></p>
</blockquote>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        Stack&lt;Integer&gt; stack = new Stack&lt;&gt;();
        int k = Integer.parseInt(br.readLine());

        for(int i=0;i&lt;k;i++) {
            int num = Integer.parseInt(br.readLine());
            if(num == 0) stack.pop();
            else stack.push(num);
        }

        int sum = 0;
        for (Integer i : stack) {
            sum += i;
        }

        bw.write(sum + &quot;&quot;);
        bw.close();
    }
}</code></pre>
<blockquote>
<p><a href="https://www.acmicpc.net/problem/2483">[2493] 탑</a></p>
</blockquote>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        Stack&lt;int[]&gt; stack = new Stack&lt;&gt;();
        int n = Integer.parseInt(br.readLine());

        StringTokenizer st = new StringTokenizer(br.readLine());
        StringBuilder sb = new StringBuilder();
        for(int i=0;i&lt;n;i++) {
            int tmp = Integer.parseInt(st.nextToken());
            while(!stack.isEmpty()) {
                if(stack.peek()[0] &lt; tmp) stack.pop();
                else {
                    sb.append(stack.peek()[1] + &quot; &quot;); break;
                }
            }

            if(stack.isEmpty()) sb.append(&quot;0 &quot;);
            stack.push(new int[] {tmp, i+1});
        }
        bw.write(sb + &quot;&quot;);
        bw.close();
    }
}</code></pre>
<hr>
<h1 id="💡-큐-queue">💡 큐 (Queue)</h1>
<blockquote>
<p><strong>한 쪽 끝</strong>에서 원소를 <strong>넣고</strong>, <strong>반대쪽 끝</strong>에서 원소를 <strong>뺄 수 있는</strong> 자료구조 : <strong>선입선출 (FIFO)</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/4f4aaba2-8c43-4e19-b066-5652725d09f4/image.png" alt=""></p>
<ul>
<li>원소의 <strong>추가 &amp; 제거 &amp; 앞 뒤 원소 확인</strong> : <strong><code>O(1)</code></strong> → 앞/뒤가 아닌 나머지 원소들의 확인 및 변경은 원칙적으로 불가능</li>
</ul>
<hr>
<h2 id="📒-구현-1">📒 구현</h2>
<blockquote>
<p><strong>원형 배열</strong>을 사용한 구현</p>
</blockquote>
<pre><code>- dat[] : 큐를 구현할 배열
- head = 0 : 가장 앞 원소의 인덱스 (제거할 위치)
- tail = 0 : 가장 뒤 원소의 인덱스 + 1 (삽입할 위치)
- mx : 배열 크기</code></pre><p><img src="https://velog.velcdn.com/images/pockyjx_/post/8a4b0fec-130b-431b-b87c-4b3f3e03f9c2/image.png" alt=""> : 큐를 배열로 구현하게 되면 <strong>원소를 하나씩 꺼낼 때마다 <code>head</code>가 증가</strong>하는데, 이 때 <code>head</code>가 한 칸씩 밀리게 되면서 앞 공간에는 더이상 원소를 넣을 수 없게 된다.</p>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/7e8411ab-01f1-4952-a037-90f0e3df8185/image.png" alt=""> : 이를 해결하기 위해, <strong>원형 배열</strong>을 구현하여 <strong><code>head</code></strong>가 배열의 <strong>마지막 원소</strong>까지 도달한 경우 <strong><code>tail</code></strong>을 <strong>비어있던 앞 공간으로 이동시켜 데이터를 삽입</strong>할 수 있다. (입력 개수가 정해진 코딩 테스트의 경우, 벼열의 크기를 입력 개수보다 크게 선언하여 사용하면 선형 배열으로 사용 가능)
<br></p>
<ul>
<li><strong><code>push()</code></strong> : 큐에 원소 <strong>삽입</strong></li>
</ul>
<pre><code class="language-java">void push(int x) {
    if(tail == mx) tail = 0;
    dat[tail++] = x;
}</code></pre>
<ul>
<li><p><strong><code>pop()</code></strong> : 큐의 원소 <strong>제거</strong></p>
<pre><code class="language-java">void pop() {
  if(head == mx) head = 0;
  else head++;
}</code></pre>
</li>
<li><p><strong><code>fornt()</code> / <code>back()</code></strong> : 큐의 가장 <strong>앞/뒤 원소</strong> 반환</p>
</li>
</ul>
<pre><code class="language-java">int front() { return dat[head]; }

int back() { return dat[tail-1]; }</code></pre>
<hr>
<h2 id="📒-연습문제-1">📒 연습문제</h2>
<blockquote>
<p><a href="https://www.acmicpc.net/problem/2164">[2164] 카드2</a></p>
</blockquote>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        Queue&lt;Integer&gt; q = new LinkedList&lt;&gt;();
        int n = Integer.parseInt(br.readLine());
        for(int i=1;i&lt;=n;i++) {
            q.offer(i);
        }

        while(q.size() != 1) {
            q.poll();
            q.offer(q.poll());
        }

        bw.write(q.poll() + &quot;&quot;);
        bw.close();
    }
}</code></pre>
<hr>
<h1 id="💡-덱-deque">💡 덱 (Deque)</h1>
<blockquote>
<p><strong>양쪽 끝</strong>에서 원소를 <strong>넣고 뺄 수 있는</strong> 자료구조</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/e0789c45-aa46-4dda-83e2-af5954d28bef/image.png" alt=""></p>
<ul>
<li>원소의 <strong>추가 &amp; 제거 &amp; 앞 뒤 원소 확인</strong> : <strong><code>O(1)</code></strong> → 가장 앞/뒤가 아닌 나머지 원소들의 확인이 원칙적으로 불가능</li>
</ul>
<hr>
<h2 id="📒-구현-2">📒 구현</h2>
<blockquote>
<p><strong>배열</strong>이나 연결 리스트를 사용하여 구현</p>
</blockquote>
<pre><code>- mx : 배열의 크기
- dat[2*mx+1] : 덱을 구현할 배열
- head = mx / tail = mx </code></pre><p><img src="https://velog.velcdn.com/images/pockyjx_/post/7285200e-7cff-4085-a650-11d561e41469/image.png" alt=""> : 덱에서는 <strong>양쪽</strong>에서 삽입과 제거가 이루어지기 때문에, <strong><code>head</code>와 <code>tail</code>의 시작 지점</strong>을 <strong><code>mx</code></strong>로 두고, <strong>배열의 크기</strong>를 <strong><code>2*mx+1</code></strong>로 두어 <strong>양쪽으로 확장 가능</strong>하게 설계해야 한다.
<br></p>
<ul>
<li><strong><code>push_front/back()</code></strong>  : <strong>앞/뒤</strong>쪽에서 원소 <strong>삽입</strong></li>
</ul>
<pre><code class="language-java">void push_front(int x) { dat[--head] = x; }
vois push_back(int x) { dat[tail++] = x; }</code></pre>
<ul>
<li><strong><code>pop_front/back()</code></strong> : <strong>앞/뒤</strong>쪽에서 원소 <strong>제거</strong></li>
</ul>
<pre><code class="language-java">void pop_front() { head++; }
void pop_back() { tail--; }</code></pre>
<ul>
<li><strong><code>front/back()</code></strong> : <strong>앞/뒤 원소</strong> 반환<pre><code class="language-java">int front() { return dat[head]; }
int back() { return dat[tail-1]; }</code></pre>
</li>
</ul>
<hr>
<h2 id="📒-연습문제-2">📒 연습문제</h2>
<blockquote>
<p><a href="https://www.acmicpc.net/problem/1021">[1021] 회전하는 큐</a>  </p>
</blockquote>
<ul>
<li><strong>중간 인덱스</strong>를 기준으로 특정 원소의 인덱스가 <strong>작으면 2번</strong> 연산, <strong>크면 3번</strong> 연산 수행 → 앞/뒤 원소가 아닌 <strong>중간 원소의 인덱스</strong>에 접근 하므로 <strong>연결 리스트</strong>를 사용하여 구현</li>
</ul>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());

        LinkedList&lt;Integer&gt; deque = new LinkedList&lt;&gt;();
        for(int i=1;i&lt;=n;i++) {
            deque.add(i);
        }

        int[] arr = new int[m];
        st = new StringTokenizer(br.readLine());
        for(int i=0;i&lt;m;i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }

        int cnt = 0;
        for(int i=0;i&lt;m;i++) {
            int target = deque.indexOf(arr[i]);
            int mid = 0;

            if(deque.size() % 2 == 0) mid = deque.size() / 2 - 1;
            else mid = deque.size() / 2;

            if(target &lt;= mid) {
                for(int j=0;j&lt;target;j++) {
                    deque.addLast(deque.pollFirst());
                    cnt++;
                }
            }

            else {
                for(int j=0;j&lt;deque.size()-target;j++) {
                    deque.addFirst(deque.pollLast());
                    cnt++;
                }
            }

            deque.pollFirst();
        }

        bw.write(cnt + &quot;&quot;);
        bw.close();
    }
}</code></pre>
<hr>
<blockquote>
<p>🙇🏻‍♀️ 출처</p>
</blockquote>
<ul>
<li><a href="https://www.youtube.com/watch?v=0DsyCXIN7Wg&amp;list=PLtqbFd2VIQv4O6D6l9HcD732hdrnYb6CY&amp;index=6">[바킹독의 실전 알고리즘] 0x05강 - 스택</a><br></li>
<li><a href="https://www.youtube.com/watch?v=D_fwSy5tRAY&amp;list=PLtqbFd2VIQv4O6D6l9HcD732hdrnYb6CY&amp;index=7">[바킹독의 실전 알고리즘] 0x06강 - 큐</a><br></li>
<li><a href="https://www.youtube.com/watch?v=0mEzJ4S1d8o&amp;list=PLtqbFd2VIQv4O6D6l9HcD732hdrnYb6CY&amp;index=8">[바킹독의 실전 알고리즘] 0x07강 - 덱</a><br></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[바킹독의 실전 알고리즘] 4. 연결 리스트]]></title>
            <link>https://velog.io/@pockyjx_/%EB%B0%94%ED%82%B9%EB%8F%85%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-4.-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@pockyjx_/%EB%B0%94%ED%82%B9%EB%8F%85%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-4.-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Fri, 02 Feb 2024 13:56:01 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-연결-리스트">💡 연결 리스트</h1>
<blockquote>
<p>원소를 저장할 때, <strong>다음 원소가 있는 위치를 포함하여 저장</strong>하는 자료구조</p>
</blockquote>
<ul>
<li><p><strong>k번째 원소</strong>를 <strong>확인/변경</strong> : <strong><code>O(K)</code></strong> 소요 → <strong>첫번째 주소값</strong>을 통해 <strong>차례대로</strong> 두번째, 세번째, .., K번째 원소에 도달</p>
</li>
<li><p><strong>임의의 위치</strong>에 원소를 <strong>추가 &amp; 제거</strong> : <strong><code>O(1)</code></strong> 소요 (<strong>추가하고 싶은 위치의 주소값을 아는 경우</strong>, 모르면 첫번째부터 탐색해야 하므로 K+1) → 뒤의 요소를 모두 옮기는 것이 아닌, <strong>다음 원소의 주소값만 변경</strong>하면 됨</p>
</li>
<li><p>원소들이 메모리에 연속적으로 위치하지 않아 메모리 할당이 쉬움 → 캐시 적중률은 낮음</p>
</li>
<li><p>각 원소가 다른 원소의 주소값을 가지고 있어야 하기 때문에, <strong>N에 비례하는 추가 공간</strong>이 필요하다.</p>
</li>
</ul>
<hr>
<h2 id="📒-종류">📒 종류</h2>
<h3 id="📝-단일-연결-리스트">📝 단일 연결 리스트</h3>
<blockquote>
<p>각 원소가 자신의 <strong>다음 원소의 주소</strong>를 가지고 있는 연결 리스트</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/06ff2bcc-1ad3-4d6a-9ef4-15224bab1c2d/image.png" alt=""></p>
<hr>
<h3 id="📝-이중-연결-리스트">📝 이중 연결 리스트</h3>
<blockquote>
<p>각 원소가 자신의 <strong>이전 원소와 다음 원소의 주소</strong>를 가지고 있는 연결 리스트</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/f328ae36-78a2-44b6-904d-2f20f4f56e4b/image.png" alt=""></p>
<hr>
<h3 id="📝-원형-연결-리스트">📝 원형 연결 리스트</h3>
<blockquote>
<p>끝이 처음과 연결되어 있는 연결 리스트 (단일 &amp; 이중 모두 가능)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/b2a8b586-37f3-4615-baee-7a2ecd825af6/image.png" alt=""></p>
<hr>
<h2 id="📒-배열-vs-연결-리스트">📒 배열 vs 연결 리스트</h2>
<blockquote>
<p><strong>배열</strong>과 <strong>연결 리스트</strong>는 <strong>선형 자료구조</strong>이다!</p>
</blockquote>
<table>
<thead>
<tr>
<th align="center">✅</th>
<th align="center">배열</th>
<th align="center">연결 리스트</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>N번째 원소의 접근</strong></td>
<td align="center"><strong>O(1)</strong></td>
<td align="center">O(N)</td>
</tr>
<tr>
<td align="center"><strong>임의의 위치에 원소 추가 &amp; 제거</strong></td>
<td align="center">O(N)</td>
<td align="center"><strong>O(1)</strong></td>
</tr>
<tr>
<td align="center"><strong>메모리 상의 배치</strong></td>
<td align="center">연속</td>
<td align="center"><strong>분할</strong></td>
</tr>
<tr>
<td align="center"><strong>추가적으로 필요한 공간</strong></td>
<td align="center"><strong>-</strong></td>
<td align="center">O(N)</td>
</tr>
</tbody></table>
<hr>
<h2 id="📒-구현">📒 구현</h2>
<blockquote>
<p><strong>배열</strong>에 <strong>원소를 저장</strong>하고, 각 원소의 <strong>이전 원소와 다음 원소의 주소값</strong>을 <strong>각각의 배열</strong>에 저장 → <strong>0번</strong>에서 <strong>시작</strong>한다고 가정, 이전 혹은 다음 원소가 <strong>없으면 -1</strong></p>
</blockquote>
<ul>
<li><code>dat[]</code> : 원소의 값</li>
<li><code>pre[]</code> : 이전 원소의 주소값</li>
<li><code>nxt[]</code> : 다음 원소의 주소값</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/6c2fb870-42db-492d-9eb8-d714c3ad69f6/image.png" alt=""></p>
<ul>
<li><strong><code>traverse()</code></strong> : <strong>원소값</strong> 출력 → 배열의 순서대로 출력하는 것이 아닌, <strong>연결 리스트 순서대로</strong> 출력</li>
</ul>
<pre><code class="language-java">void traverse() {
    int cur = nxt[0]; // 현재 주소값

    while(cur != -1) { // nxt[] = -1 : 다음 원소가 없다는 뜻
        System.out.println(dat[cur] + &#39; &#39;);
        cur = nxt[cur];
    }
}</code></pre>
<ul>
<li><strong><code>insert()</code></strong> : <strong>임의의 위치</strong>에 원소 <strong>추가</strong></li>
</ul>
<pre><code class="language-java">void insert(int addr, int num) {
    dat[unused] = num; // 1. 새로운 원소 생성
    pre[unused] = addr; // 2. 새 원소의 pre : 삽입 위치의 주소값
    nxt[unused] = nxt[addr]; // 3. 새 원소의 nxt : 삽입 위치의 nxt

    // 4. 삽입 위치 다음 원소의 pre : 새 원소의 주소값 (삽입 위치에서 다음 원소가 있는 경우)
    if(nxt[addr] != -1) pre[nxt[addr]] = unused; 
    nxt[addr] = unused; // 4. 삽입 위치의 nxt : 새 원소의 주소값 

    unused++; // 5. 다음에 값을 추가할 주소
}</code></pre>
<ul>
<li><strong><code>erase()</code></strong> : <strong>임의의 위치</strong>의 원소 <strong>삭제</strong></li>
</ul>
<pre><code class="language-java">void erase(int addr) {

    nxt[pre[addr]] = nxt[addr]; // 1. 이전 위치의 nxt : 삭제할 위치의 nxt    
    if(nxt[addr] != -1) pre[nxt[addr]] = pre[addr]; // 2. 다음 위치의 pre : 삭제할 위치의 pre
}</code></pre>
<hr>
<h2 id="📒-연습문제">📒 연습문제</h2>
<blockquote>
<p><a href="https://www.acmicpc.net/problem/1406">[1406] 에디터</a></p>
</blockquote>
<pre><code class="language-java">import java.io.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;

public class Main {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        LinkedList&lt;Character&gt; list = new LinkedList&lt;&gt;();
        String s = br.readLine();
        for(int i=0;i&lt;s.length();i++) {
            list.add(s.charAt(i));
        }

        int n = Integer.parseInt(br.readLine());
        int cursor = list.size();
        for(int i=0;i&lt;n;i++) {
            String order = br.readLine();
            switch (order.charAt(0)) {
                case &#39;L&#39; : if(cursor &gt; 0) cursor--; break;
                case &#39;D&#39; : if(cursor &lt; list.size()) cursor++; break;
                case &#39;B&#39; : if(cursor &gt; 0) { list.remove(cursor - 1); cursor--; } break;
                case &#39;P&#39; : list.add(cursor, order.charAt(2)); cursor++;
            }
        }

        StringBuilder sb = new StringBuilder();
        for(char ch : list) {
            bw.write(ch);
        }
        bw.close();
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/19f4624f-6715-4492-8661-acc3bdf5d4b5/image.png" alt=""> : 위 코드를 제출하니 시간 초과가 떴다.. 아무래도 <strong>삽입/삭제</strong> 시 해당 인덱스를 바로 찾아가는 것이 아닌 <strong>첫 시작점부터 차례대로 찾아가야 하므로 O(N)의 시간 복잡도</strong>를 가지게 되어 발생한 문제인 것 같다. 그렇다면 이를 어떻게** O(1)**로 풀 수 있을까?</p>
<h4 id="✔️-listiterator">✔️ <code>ListIterator</code></h4>
<blockquote>
<p><strong>Iterator를 상속</strong>한 인터페이스 : <strong>양방향</strong> 탐색 가능</p>
</blockquote>
<p>✅ <code>ListIterator</code>를 사용해서 해당 인덱스를 처음부터 순서대로 찾아가는 것이 아닌, 실제 <strong>커서</strong>가** 특정 위치에서 한 칸씩 움직이는 형태<strong>로 동작하므로, **O(1)</strong>의 시간 복잡도를 가지게 된다!
<br></p>
<ul>
<li><p><strong><code>list.listIterator()</code></strong> : 가장 앞에서부터 <strong>탐색 시작</strong> → 문제의 조건은 <strong>제일 끝에서 시작</strong>하므로 <strong><code>list.size()</code></strong>를 <strong>시작 위치</strong>로 지정</p>
</li>
<li><p><strong><code>hasPrevious()</code></strong> : <strong>이전 원소</strong>가 있는지 확인 (역방향) → <strong><code>previous()</code></strong> : 이전 원소 리턴</p>
</li>
<li><p><strong><code>hasNext()</code></strong> : <strong>다음 원소</strong>가 있는지 확인 (정방향) → <code>next()</code> : 다음 원소 리턴</p>
</li>
<li><p><strong><code>remove()</code></strong> : <strong><code>next()</code></strong> 또는 <strong><code>previous()</code></strong>가 <strong>가리키는 원소 제거</strong></p>
</li>
<li><p><strong><code>add()</code></strong> : <strong>현재 탐색 위치</strong>에 <strong>원소 추가</strong></p>
</li>
</ul>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        LinkedList&lt;Character&gt; list = new LinkedList&lt;&gt;();
        String s = br.readLine();
        for(int i=0;i&lt;s.length();i++) {
            list.add(s.charAt(i));
        }

        int n = Integer.parseInt(br.readLine());
        ListIterator&lt;Character&gt; cursor = list.listIterator(list.size());
        for(int i=0;i&lt;n;i++) {
            String order = br.readLine();
            switch (order.charAt(0)) {
                case &#39;L&#39; : if(cursor.hasPrevious()) cursor.previous(); break;
                case &#39;D&#39; : if(cursor.hasNext()) cursor.next(); break;
                case &#39;B&#39; : if(cursor.hasPrevious()) { cursor.previous(); cursor.remove(); } break;
                case &#39;P&#39; : cursor.add(order.charAt(2));
            }
        }

        StringBuilder sb = new StringBuilder();
        for(char ch : list) {
            bw.write(ch);
        }
        bw.close();
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/pockyjx_/post/38b97a1c-5904-410f-aa4b-fea73ddfe697/image.png" alt=""></p>
<blockquote>
<p>🙇🏻‍♀️ 참고 : <a href="https://velog.io/@yuiopre98/%EB%B0%B1%EC%A4%80-1406%EB%B2%88-%EC%97%90%EB%94%94%ED%84%B0ListIterator">백준 1406번) 에디터(ListIterator)</a></p>
</blockquote>
<p>➕ 스택을 사용한 풀이도 있었는데 그건 스택 강의 듣고 다시 보기..</p>
<hr>
<blockquote>
<p>🙇🏻‍♀️ 출처 : <a href="https://www.youtube.com/watch?v=C6MX5u7r72E&amp;list=PLtqbFd2VIQv4O6D6l9HcD732hdrnYb6CY&amp;index=5">[바킹독의 실전 알고리즘] 0x04강 - 연결 리스트</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>