<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jinjoo-lab.log</title>
        <link>https://velog.io/</link>
        <description>Young , Wild , Free</description>
        <lastBuildDate>Sat, 07 Dec 2024 12:02:44 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jinjoo-lab.log</title>
            <url>https://velog.velcdn.com/images/jinjoo-lab/profile/328e706f-bc16-4a12-b176-7cc9e3d0403e/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jinjoo-lab.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jinjoo-lab" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[이메일 전송 서버 구현기 - 1(Dead Letter Queue)]]></title>
            <link>https://velog.io/@jinjoo-lab/%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%A0%84%EC%86%A1-%EC%84%9C%EB%B2%84-%EA%B5%AC%ED%98%84%EA%B8%B0-1Dead-Letter-Queue</link>
            <guid>https://velog.io/@jinjoo-lab/%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%A0%84%EC%86%A1-%EC%84%9C%EB%B2%84-%EA%B5%AC%ED%98%84%EA%B8%B0-1Dead-Letter-Queue</guid>
            <pubDate>Sat, 07 Dec 2024 12:02:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>FrameCheckMate에서 담당한 부분은 이메일 전송 서버 구축이다. 
이번 회고는 이메일 전송 서버를 구축하기까지의 사고 과정을 담고 있다.</p>
</blockquote>
<h2 id="email-전송-과정">Email 전송 과정</h2>
<blockquote>
<p>Email 전송을 수행하는 상황은 다음과 같다.</p>
</blockquote>
<p><img src="https://github.com/user-attachments/assets/5aa01566-76ac-4502-8266-64e6c470b21e" alt="image"></p>
<blockquote>
<p>Card Server와 Member Server에서 이벤트가 발생한 경우 이메일 전송 Request를 Notification Server로 전송하고 Notification Server에서는 사용자에게 Email을 전송한다.</p>
</blockquote>
<h3 id="email-전송-발생-event">Email 전송 발생 Event</h3>
<ul>
<li>Card Server(Card는 각 팀원의 작업 단위이다.)에서 이메일을 전송하는 경우<ul>
<li>사용자에게 작업이 할당된 경우</li>
<li>작업을 완료하고 Confirm 요청한 경우</li>
<li>Confirm이 관리자에 의해 승인된 경우</li>
<li>Confirm이 관리자에 의해 반려된 경우</li>
</ul>
</li>
<li>Member Server에서 이메일을 전송하는 경우<ul>
<li>회원 가입이 완료된 경우</li>
<li>(일반 회원) Project에 초대된 경우</li>
<li>(관리자) Project를 생성한 경우</li>
</ul>
</li>
</ul>
<h2 id="첫-번째-고찰점--서버-간-전송">첫 번째 고찰점 : 서버 간 전송</h2>
<p><img src="https://github.com/user-attachments/assets/ae0d984d-23ff-41af-be77-7bb26b93e5ee" alt="image"></p>
<ul>
<li>서버 간 전송이 필요한 상황에서 처음엔 WebClient(RestClient) 등의 방법을 생각했다.</li>
</ul>
<p>하지만 다음과 같은 의문이 생겼다.</p>
<blockquote>
<p>Notification Server로 요청이 집중된다면 ?, 또 Notification Server에서 요청 처리에 실패한다면 ?</p>
</blockquote>
<ul>
<li>그렇다. 위 구조의 경우 서버 간 결합도가 크고 장애 발생에 대한 대처를 하기 어렵다.</li>
<li>서버 간 전송에서 결합도를 낮추고 메시지 유실에 대처하기 위해 RabbitMQ를 도입했다.</li>
</ul>
<h2 id="rabbitmq-적용">RabbitMQ 적용</h2>
<p><img src="https://github.com/user-attachments/assets/40cfbdd9-9395-40d2-8733-7473feef0152" alt="image"></p>
<ul>
<li>변경된 전송 로직<ol>
<li>Card, Member 서버에서는 Email 전송 요청을 RabbitMQ Queue로 보낸다.</li>
<li>Notification Server는 Consumer로서 큐에서 메시지를 꺼내 Email을 전송한다.</li>
<li>2번 과정에 있어 이메일 전송이 실패한 경우 최대 2번 재전송한다.</li>
</ol>
</li>
</ul>
<h3 id="notification-rabbitmq-listener">Notification RabbitMQ Listener</h3>
<p><img src="https://github.com/user-attachments/assets/dc023315-7103-4f63-822b-8d26e5c89d19" alt="image"></p>
<h3 id="notification-rabbitmq-retry">Notification RabbitMQ Retry</h3>
<blockquote>
<p>RabbitListener에 대한 재시도 전략을 적용해줬다.</p>
</blockquote>
<p><img src="https://github.com/user-attachments/assets/55699d31-9301-495c-9dbd-18947e2919d5" alt="image"></p>
<h2 id="두-번째-고찰점--재시도-실패-시">두 번째 고찰점 : 재시도 실패 시</h2>
<p>RabbitMQ의 Listener 재시도를 도입해 Consumer(Notification Server)가 실패 시 다시 데이터를 처리하도록 적용했다.</p>
<p>하지만 위의 방식은 완벽하지 않고 문제가 있다는 것을 알았다.</p>
<blockquote>
<p>만약 재시도 시에도 실패한다면 어떻게 해야할까?</p>
</blockquote>
<ul>
<li>단순히 재시도 전략을 구축하더라도 실패하여 처리되지 못하는 경우가 발생한다고 생각한다.</li>
<li>메시지 재시도가 실패하는 경우는 무엇일까?</li>
</ul>
<h3 id="재시도가-실패하는-경우">재시도가 실패하는 경우</h3>
<ol>
<li><p><strong>메시지의 형식이 불완전</strong>하거나 <strong>전송되면 안되는 경우</strong></p>
<ul>
<li><p>로직 상 Member Server와 Card Server는 DB로부터 조회된 사용자의 Email을 바탕으로 Notification Server에 요청을 보낸다.</p>
</li>
<li><p>또한 메일 내용은 Notification Server 자체에서 관리되는 “문자열”을 전송한다.</p>
<blockquote>
<p>결론적으로 메시지 형식에 문제가 생기는 경우는 없을거라 판단</p>
</blockquote>
</li>
</ul>
</li>
<li><p><strong>메일 시스템에 장애</strong>가 발생하는 경우</p>
<ul>
<li><p>이메일 시스템에 장애가 발생한 경우 메일 요청은 계속 재시도하더라도 실패하게 될 것이다.</p>
</li>
<li><p>나는 Consumer(Notification Server)와 RabbitMQ 사이의 장애 대응만 신경 쓰면 된다 생각했지만 결국 메일 전송 자체에 문제가 생기면 올바르게 작동하지 않을 것이다.</p>
<blockquote>
<p>메일 시스템에 장애가 발생하는 경우의 대응을 구축해야 한다 !</p>
</blockquote>
</li>
</ul>
</li>
</ol>
<h2 id="dlqdead-letter-queue">DLQ(Dead Letter Queue)</h2>
<p>참고 : <a href="https://aws.amazon.com/ko/what-is/dead-letter-queue/">https://aws.amazon.com/ko/what-is/dead-letter-queue/</a></p>
<ul>
<li>DLQ(Dead Letter Queue)는 소프트웨어 시스템에서 오류로 인해 처리할 수 없는 메시지를 임시로 저장하는 특수한 유형의 메시지 대기열</li>
<li>Dead Letter를 적용하고 Dead Letter Queue에 쌓이는 메시지는 Slack을 통해 확인하도록 구축했다.</li>
</ul>
<h2 id="dead-letter-queue-적용">Dead Letter Queue 적용</h2>
<p><img src="https://github.com/user-attachments/assets/cec1515a-7258-4627-8fbd-f4422bfee14e" alt="image"></p>
<h3 id="dead-letter-queue-생성-코드">Dead Letter Queue 생성 코드</h3>
<p><img src="https://github.com/user-attachments/assets/c2f18ea6-735b-49ff-b802-7ad27bcae32d" alt="image"></p>
<h3 id="member-queue에-dlq-적용">Member Queue에 DLQ 적용</h3>
<p><img src="https://github.com/user-attachments/assets/3eafdab8-c4e7-4079-8079-09e7f4178786" alt="image"></p>
<h3 id="message-queue--dlq-logic">Message Queue &amp; DLQ Logic</h3>
<p><img src="https://github.com/user-attachments/assets/96bccd8d-339c-40e0-880b-a819fdbc2251" alt="image"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Record Class 적용기 근데 TMI를 곁들인]]></title>
            <link>https://velog.io/@jinjoo-lab/Record-Class-%EC%A0%81%EC%9A%A9%EA%B8%B0-%EA%B7%BC%EB%8D%B0-TMI%EB%A5%BC-%EA%B3%81%EB%93%A4%EC%9D%B8</link>
            <guid>https://velog.io/@jinjoo-lab/Record-Class-%EC%A0%81%EC%9A%A9%EA%B8%B0-%EA%B7%BC%EB%8D%B0-TMI%EB%A5%BC-%EA%B3%81%EB%93%A4%EC%9D%B8</guid>
            <pubDate>Wed, 04 Dec 2024 09:04:15 GMT</pubDate>
            <description><![CDATA[<h3 id="서론">서론</h3>
<p>프로젝트에 대해서 면접관은 정말 다양한 질문을 한다. Spring Project를 했다 하면 질문하는 것 중의 하나는 다음과 같다.</p>
<blockquote>
<p>Java는 몇 버전을 사용하셨고 이유가 뭔가요 ?</p>
</blockquote>
<ul>
<li>나는 보통 Spring과의 호환성을 위해 버전 업을 했다고 얘기한다.</li>
</ul>
<p>정확하게 원하는 답변이 뭔지는 잘 모르겠지만 틀린 말은 아니라고 생각한다. 실제로 많이 사용하는 Spring Boot 3.0부터는 Java 17이 필수다.</p>
<p>이러한 이류로 Java 17을 사용한다면 17의 특징을 살려 프로그래밍을 해보는 것도 좋다 생각한다.</p>
<h3 id="java-17">Java 17</h3>
<blockquote>
<p>Java 17에는 다양한 변화가 적용되었다. 대표적인 키워드로는 다음과 같다.</p>
</blockquote>
<ul>
<li>Record class</li>
<li>Sealed class</li>
<li>Stream.toList() 사용 가능</li>
<li>ZGC의 등장</li>
</ul>
<p>다양한 변경점이 있지만 오늘 내가 다루고 실제로 적용한 주제는 Record Class이다.</p>
<h2 id="record-class">Record Class</h2>
<blockquote>
<p>Java 14에 등장한 <strong>불변(immutable)객체</strong>를 쉽게 생성할 수 있도록 하는 유형의 클래스</p>
</blockquote>
<p>자세한 설명 참고 : <a href="https://www.baeldung.com/java-record-keyword">https://www.baeldung.com/java-record-keyword</a></p>
<h3 id="불변-객체-immutable-object">불변 객체 (Immutable Object)</h3>
<blockquote>
<p>객체 생성 이후 내부 상태가 변하지 않는 객체 → Read Only 메서드만을 제공 (대표적으로 String 클래스 객체들)</p>
</blockquote>
<ul>
<li>불변 객체의 가장 큰 장점은 동기화를 고려하지 않아도 된다는 것이다.</li>
</ul>
<blockquote>
<p>Java 17 이전에도 불변 객체라는 개념은 존재했고 당연히 정의 가능했다. 그렇다면 Java 진영은 왜 17에 Record라는 별도의 클래스까지 도입했을까?</p>
</blockquote>
<p>Baeldung에는 다음과 같이 기술되어 있다.</p>
<ol>
<li>There’s a lot of boilerplate code</li>
<li>We obscure the purpose of our class: to represent a person with a name and address</li>
</ol>
<ul>
<li>요약하자면 ‘단순히 필드만 정의한 불변 객체에 쓸데 없는 코드가 너무 많다’ 이런 거다.</li>
</ul>
<h3 id="그냥-만들어보자--before-record">그냥 만들어보자 : Before Record</h3>
<pre><code class="language-java">public class Person {

    private final String name;
    private final String address;

    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, address);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof Person)) {
            return false;
        } else {
            Person other = (Person) obj;
            return Objects.equals(name, other.name)
              &amp;&amp; Objects.equals(address, other.address);
        }
    }

    // standard getters
}</code></pre>
<ul>
<li>private final로 캡슐화된 필드, getter, 전체 생성자, (equals &amp; hashcode)형제들은 Java에서 Immutable Data Class를 기술하는데 사용되는 코드이다.</li>
<li>모든 클래스를 기술할 때 위의 내용을 다 포함해야 한다. 가독성도 떨어지고 코드의 길이가 너무 길다.</li>
</ul>
<h3 id="우리에게는-lombok이-있다-">우리에게는 Lombok이 있다 !</h3>
<ul>
<li>맞는 말이다. 하지만 Lombok에는 한 가지 문제점이 있다. 바로 Lombok 라이브러리가 필요하다는 것이다. (Lombok 안쓰는 프로젝트가 어디 있어? 라고 묻는다면 사실 나도 잘 모르겠다)</li>
<li>그렇다면 사용하는 관점을 바라볼 필요가 있다. Lombok은 불변 객체를 생성 즉 정의하기 위해 사용하는 것이 아닌 복잡한 데이터 모델 정의 시 사용한다.</li>
</ul>
<h3 id="record-적용-시--after-record">Record 적용 시 : After Record</h3>
<pre><code class="language-java">public record Person (String name, String address) {}</code></pre>
<ul>
<li>정말 단순하다. Record Class를 선언하고 필드를 추가한 것만으로 다음과 같은 내용이 추가된다.<ul>
<li>equals, hashCode, toString 메서드</li>
<li>private 및 final 필드</li>
<li>public 생성자</li>
</ul>
</li>
<li>어떻게 가능한거지 ?<ul>
<li>컴파일러 !</li>
</ul>
</li>
</ul>
<blockquote>
<p>컴파일러는 <strong>레코드의 목적</strong>(단순히 데이터를 담는 불변 객체)을 이해하고, 이에 필요한 메서드와 필드를 자동으로 생성</p>
</blockquote>
<h2 id="record와-dto">Record와 DTO</h2>
<blockquote>
<p>위에서는 Java 17에서 사용되는 Record Class에 대해 알아보았다. 그렇다면 어디에 쓸까? 다양한 활용법이 있지만 내가 적용한 것은 DTO이다.</p>
</blockquote>
<h3 id="dtodata-transfer-object">DTO(Data Transfer Object)</h3>
<blockquote>
<p>계층간 데이터 교환을 위해 사용되는 객체로서 불변성을 보장한다.</p>
</blockquote>
<ul>
<li>자세한 설명은 주제를 벗어나는 거 같으니 생략하고 ‘불변성’에 집중해보겠다.<ul>
<li>DTO는 Record Class가 적용되기 굉장히 적합하다.</li>
</ul>
</li>
</ul>
<h3 id="기존-dto">기존 DTO</h3>
<pre><code class="language-java">@Getter
@NoArgsConstructor
public class AssignCardWorkRequest {

    private UUID workerId;

    private String workerEmail;

    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private LocalDateTime startDate;

    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private LocalDateTime endDate;

    private String description;
}</code></pre>
<h3 id="record-class-변경">Record Class 변경</h3>
<pre><code class="language-java">import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDateTime;
import java.util.UUID;

public record AssignCardWorkRequest(
    UUID workerId,
    String workerEmail,
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy-MM-dd&#39;T&#39;HH:mm:ss&quot;)
    LocalDateTime startDate,
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy-MM-dd&#39;T&#39;HH:mm:ss&quot;)
    LocalDateTime endDate,
    String description
) {}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Mapper 사용기]]></title>
            <link>https://velog.io/@jinjoo-lab/Mapper-%EC%82%AC%EC%9A%A9%EA%B8%B0-FrameCheckMate-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@jinjoo-lab/Mapper-%EC%82%AC%EC%9A%A9%EA%B8%B0-FrameCheckMate-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 29 Nov 2024 01:07:33 GMT</pubDate>
            <description><![CDATA[<h2 id="mapper-사용기">Mapper 사용기</h2>
<blockquote>
<p>프로젝트를 수행하는데 있어 DTO와 Entity간의 변환을 Mapper를 적용했다.
확실히 코드의 길이는 줄어들었고 중복 부분도 많이 처리했다. 
그렇다면 Mapper란 무엇이며 실제 적용 방법과 확장에 대해 기술해보겠다.</p>
</blockquote>
<h3 id="문제-상황">문제 상황</h3>
<blockquote>
<p>‘FrameCheckMate’에 있어 하나의 비즈니스 로직을 예시로 얘기를 풀어보고자 한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jinjoo-lab/post/890f204a-6d20-4f1f-944d-1cc9bdbfcf3d/image.png" alt=""></p>
<ul>
<li>위 로직은 사용자의 email로 메일을 전송하고 메일 정보를 DB에 저장하는 로직이다.</li>
<li>로직<ol>
<li>customMailSender를 통해 Mail 전송</li>
<li>Mail(Notification) Enitity를 JPA를 통해 Repository에 저장</li>
<li>NotificationSaveResponse를 클라이언트에게 반환</li>
</ol>
</li>
</ul>
<blockquote>
<p>위 로직에 있어 코드에서 가장 많은 부분을 차지하는 것은 DTO와 Entity간의 변환과정이다. 
예시로 하나의 로직만을 가져왔지만 실제 서비스에 있어 변환 과정은 중복이 많은 부분이다. 
변환 과정의 중복을 줄여 보는 것이 좀더 객체 지향에 가까운 코드를 작성하는 것이라 생각한다.</p>
</blockquote>
<h2 id="service-로직에서-구현한다면-">Service 로직에서 구현한다면 ?</h2>
<ul>
<li>실제로 프로젝트를 진행하면서 팀원 중 한명은 Service에서 변환 과정을 메서드로 구현하는 것에 대한 의견을 제시했다.</li>
<li>확실히 중복 코드는 줄어들 것이지만 과연 올바른 방법일까? (얘기를 나눠보고자 한다)</li>
</ul>
<h3 id="역할과-책임">역할과 책임</h3>
<blockquote>
<p>객체 지향 프로그래밍에 관한 도서를 읽다 보면 많이 나오는 말이 역할과 책임이다. ‘오브젝트’라는 근본 도서에서 정말 깊이 있게 다루는 내용이다. 해당 내용은 도서에서 너무 잘 나와 있어 별도로 첨부하지는 않겠다.</p>
</blockquote>
<h3 id="단일-책임-원칙-single-responsibility-principle">단일 책임 원칙 (Single Responsibility Principle)</h3>
<ul>
<li>‘하나의 클래스는 하나의 책임을 가져야 한다.’ 라는 객체 지향 설계에 있어 중요한 원칙이다. Service 로직 내부에서 변환을 구현하는 것은 단일 책임 원칙을 위배한다.</li>
<li>서비스 로직은 실제 비즈니스 로직을 담당한다. 변환 과정은 실제 비즈니스 로직과는 별개의 관심사이다.</li>
</ul>
<h2 id="mapper">Mapper</h2>
<blockquote>
<p>mapper는 서로 다른 데이터 구조나 객체 간의 변환과 관련된 개념
매퍼는 한 타입의 객체를 다른 타입의 객체로 변환하는 역할을 하는 함수나 클래스를 의미한다. 이 과정을 일반적으로 mapping이라고 부른다.</p>
</blockquote>
<p>참고 : <a href="https://chatgpt.com/c/67490b51-6700-8002-90d4-b2be17474101">https://chatgpt.com/c/67490b51-6700-8002-90d4-b2be17474101</a></p>
<h3 id="mapper-적용">Mapper 적용</h3>
<ul>
<li>NotificationMapper
<img src="https://velog.velcdn.com/images/jinjoo-lab/post/304bafa4-13ba-4864-b395-203e637feefc/image.png" alt=""></li>
</ul>
<ul>
<li>NotificationMapper를 적용한 Service
<img src="https://velog.velcdn.com/images/jinjoo-lab/post/6e0f7c63-e455-4e8e-afab-f0dd73da4835/image.png" alt=""></li>
</ul>
<h2 id="다른-방법은-없을까">다른 방법은 없을까?</h2>
<blockquote>
<p>별도의 클래스를 정의하여 사용하는 것을 좋아하지만 해당 방법만 존재하는 것은 아니다. 
Mapping 과정을 자동으로 제공해주는 MapStruct에 대해 간단히 말하고 글을 마무리하겠다.</p>
</blockquote>
<h3 id="mapstruct">MapStruct</h3>
<blockquote>
<p>Java Bean 유형 간의 매핑 구현 단순(자동)화하는 코드 생성기</p>
</blockquote>
<p>참고 : <a href="https://www.baeldung.com/mapstruct">https://www.baeldung.com/mapstruct</a></p>
<h3 id="특징">특징</h3>
<ol>
<li>컴파일 시점에 코드 생성 </li>
<li>반복적 구현을 줄여준다.</li>
<li>Annotation processor를 이용하여 객체 간 매핑 자동화</li>
<li>MapStruct는 Lombok의 Getter,Setter,Builder를 이용한다.<ol>
<li>꺼내오는 객체 → Getter 필요</li>
<li>저장하는 객체 → Builder OR 모든 필드를 포함하는 생성자</li>
</ol>
</li>
</ol>
<h3 id="mapstruct-동작-우선순위">MapStruct 동작 우선순위</h3>
<ol>
<li>Builder가 있으면 Builder를 사용합니다.</li>
<li>Builder가 없으면 Setter를 사용하여 객체를 생성하고 값을 설정합니다.</li>
<li>필드에 직접 접근하도록 설정할 수도 있습니다(기본적으로 비활성화).</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[실시간 통신 분석 프로젝트 (테스트 지표 분석)]]></title>
            <link>https://velog.io/@jinjoo-lab/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EB%B6%84%EC%84%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A7%80%ED%91%9C-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@jinjoo-lab/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EB%B6%84%EC%84%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A7%80%ED%91%9C-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Thu, 20 Jun 2024 08:38:07 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<blockquote>
<p>각 실시간 통신을 스프링 환경에서 구현하는 방법과 테스트 로직을 앞서 구현하였다. 이제는 각 통신 방법을 <strong>시나리오에 따라 테스트</strong>하고 <strong>결과를 분석</strong>해보겠다.</p>
</blockquote>
<h2 id="시나리오">시나리오</h2>
<blockquote>
<p>테스트 시나리오를 구성할 때 몇가지 상황을 고려하였다.</p>
</blockquote>
<ul>
<li>서비스가 시작되고 <strong>사용자들이 진입</strong>하는 상황 : <strong>정상 진입 확인</strong></li>
<li><strong>사용자 최고치(최대 처리량)에서 서비스 요청 상황</strong> : <strong>최대 부하 확인</strong></li>
<li>사용자들의 요청이 완료되고 <strong>사용자 수가 줄어드면서</strong>의 상황 : <strong>회복성 확인</strong></li>
</ul>
<blockquote>
<p>총 테스트 시간을 1분으로 설정했고 위 3가지 상황을 고려하여 3가지 구간으로 나눠서 시나리오를 구성했다.</p>
</blockquote>
<h3 id="테스트-시나리오">테스트 시나리오</h3>
<ol>
<li>서비스가 시작되고 나서 사용자 수가 유입되는 시간을 20초로 설정하였다.</li>
<li>20초 후에는 많은 사용자가 유입되서 통신을 요청하도록 설정하였다.<ul>
<li><strong>1000 , 5000 , 10000명</strong>의 최대 사용자를 기준으로 테스트하였다.</li>
</ul>
</li>
<li>회복성을 보기 위해 나머지 10초 동안에는 유입되는 사용자를 0으로 설정하여 앞서 설정한 사용자들이 점진적으로 통신을 종료하게끔 작성했다.</li>
</ol>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/d2ac1dd5-baf8-4c84-868b-be4c1754d49e" alt="image"></p>
<h2 id="테스트-툴">테스트 툴</h2>
<blockquote>
<p>부하 테스트를 확인하기 위해 고려했던 테스트 툴은 <strong>Jmeter</strong>, <strong>k6</strong> 총 3가지였다.</p>
</blockquote>
<h3 id="jmeter">Jmeter</h3>
<blockquote>
<p>테스트를 위해 처음 선택했던 툴은 Jmeter였다. 단순한 GUI에서 테스트를 하는 것이 편리해보였고 러닝 커브가 적다고 생각했다. 실제로 Jmeter를 통해 <strong>단일 상황에 대한 테스트</strong>는 정상 작동을 확인했다.</p>
</blockquote>
<p><strong>Polling에 대한 점진적 사용자 증가 테스트</strong></p>
<ul>
<li>50명이 3초동안 점진적으로 추가되어 1번 요청</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/c874a505-1063-46b9-aa0f-467cf3514f47" alt="image"></p>
<ul>
<li>100명이 3초동안 점진적으로 추가되어 1번 요청</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/5e7cc70a-429d-46eb-bb1e-f3945b1bf872" alt="image"></p>
<ul>
<li>500명이 3초동안 점진적으로 추가되어 1번 요청</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/8bf425b0-7070-480c-a5a1-bb0d572b3b7c" alt="image"></p>
<h3 id="한계--jmeter">한계 : Jmeter</h3>
<blockquote>
<p>하지만 가장 큰 문제에 직면하였는데 그것은 WebSocket이였다.</p>
</blockquote>
<ul>
<li>JMeter는 기본적으로 WebSocket 프로토콜을 직접적으로 지원하지 않는다. 외부 플러그인을 추가하여 테스트해야 하는데 해당 플러그인이 공식 플러그인이 아니라 신뢰성이 부족했다.</li>
<li>또한 WebSocket 커넥션이 연결되고 통신되는 실시간 과정에 대한 분석이 아쉽다.</li>
</ul>
<h3 id="k6">k6</h3>
<blockquote>
<p>두번째로 고려하고 테스트 툴로 선택한건 k6였다. k6를 선택한 이유는 다음과 같다.</p>
</blockquote>
<ul>
<li><p>간단한 스크립트 작성을 통한 테스트 시나리오 수행 가능</p>
</li>
<li><p>WebSocket을 기본적으로 지원하여 추가 플러그인 없이 연결 설정과 관리 가능 → STOMP는 WebSocket연결을 통해 테스트 가능</p>
<ul>
<li>k6에서 기본적으로 WebSocket 관련 테스트 기능을 지원하는 것을 알 수 있다 !</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/df8a661a-df2b-4f30-a4ce-21fb51a80168" alt="image"></p>
</li>
<li><p>Jmeter와 비교하여 더 다양한 지표 확인 가능</p>
</li>
</ul>
<h3 id="prometheus">Prometheus</h3>
<blockquote>
<p>추가적으로 프로메테우스를 추가하여 Spring Server를 모니터링하였다. CPU 사용량과 메모리를 통신이 수행되는 동안 분석하기 위한 것이었다.</p>
</blockquote>
<h2 id="테스트-스크립트--k6">테스트 스크립트 : k6</h2>
<h3 id="polling">Polling</h3>
<ul>
<li>Polling에 대한 스크립트는 간단하다. polling url로 그룹 id를 추가하여 통신한다. 주기는 3초로 설정했다.</li>
</ul>
<pre><code class="language-jsx">function shortPollingTest() {
    http.get(`http://localhost:8080/cur/${randomInt}`);
}

export default function () {
    shortPollingTest();
    sleep(3);
}</code></pre>
<h3 id="long-polling">Long Polling</h3>
<ul>
<li>initializeLongPollingConnection()<ul>
<li>Connection을 생성한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-jsx">// Connection 초기화 함수
function initializeLongPollingConnection() {
    if (longPollingConnection) {
        return longPollingConnection;
    }

    const connection = http.get(`${baseUrl}/location/long/${randomInt}`, {
        headers: {Accept: &#39;text/event-stream&#39;},
        tags: {type: &#39;longPolling&#39;}
    });
    check(connection, {&#39;Long Polling Connection Status is 200&#39;: (r) =&gt; r.status === 200});
    return connection;
}</code></pre>
<ul>
<li>longPollingTest()<ul>
<li>자신의 위치 정보를 전송하는 event 발생</li>
<li>그룹원의 위치 정보를 받아오는 통신</li>
</ul>
</li>
</ul>
<pre><code class="language-jsx">// 커넥션을 초기화(정보 가져 오기) + 자신의 위치를 알리고(event 발생) + 그룹원의 위치 정보 받아오기
export function longPollingTest() {
    initializeLongPollingConnection();

    const notifyResponse = http.post(`${baseUrl}/location/long/${randomInt}/notify`);
    check(notifyResponse, {&#39;Long Polling Notify Status is 200&#39;: (r) =&gt; r.status === 200});

    const url = `${baseUrl}/location/long/${randomInt}`;
    const response = http.get(url);
    check(response, {&#39;Long Polling Post Status is 200&#39;: (r) =&gt; r.status === 200});
}</code></pre>
<h3 id="websocket">WebSocket</h3>
<ul>
<li>initializeWebSocketConnection()<ul>
<li>WebSocket connection을 생성</li>
<li>연결 , 통신 , 종료 정보 삽입</li>
</ul>
</li>
</ul>
<pre><code class="language-jsx">function initializeWebSocketConnection() {
    if (websocketConnection) {
        return websocketConnection;
    }

    const connection = ws.connect(wsUrl, function (socket) {
        socket.on(&#39;open&#39;, function () {
            socket.send(&#39;Hello from k6 WebSocket!&#39;);
        });

        socket.on(&#39;message&#39;, function (message) {
        });

        socket.on(&#39;close&#39;, function () {
        });
    });

    websocketConnection = connection;
    return connection;
}</code></pre>
<ul>
<li>websocketTest()<ul>
<li>웹 소켓 커넥션을 초기화하고 연결 후 데이터 전송</li>
</ul>
</li>
</ul>
<pre><code class="language-jsx">export function websocketTest() {
    initializeWebSocketConnection();
    ws.connect(wsUrl, function (socket) {
        socket.on(&#39;open&#39;, function () {
            socket.send(&#39;Hello from k6!&#39;);
        });
    });
}</code></pre>
<h3 id="stomp">STOMP</h3>
<ul>
<li>initializeStompClient()<ul>
<li>WenSocket을 이용하여 STOMP 커넥션 생성</li>
<li>연결 시 STOMP 연결 정보와 Subscribe 설정</li>
</ul>
</li>
</ul>
<pre><code class="language-jsx">function initializeStompClient(callback) {
    if (stompClient) {
        callback(stompClient); // 이미 stompClient가 존재하면 콜백 호출
        return;
    }

    ws.connect(stompUrl, {}, function (socket) {
        socket.on(&#39;open&#39;, function () {
            console.log(&#39;WebSocket connection opened&#39;);

            // STOMP CONNECT 메시지를 전송합니다.
            socket.send(&#39;CONNECT\naccept-version:1.0,1.1,2.0\n\n\x00\n&#39;);

            // STOMP SUBSCRIBE 메시지를 전송합니다.
            const subscribeMessage = `SUBSCRIBE\nid:${uuid()}\ndestination:/sub/location/${randomInt}\n\n\x00\n`;
            socket.send(subscribeMessage);

            // WebSocket 연결이 열렸을 때의 콜백 함수 호출
            callback(socket);
        });

        socket.on(&#39;message&#39;, function (message) {});

        socket.on(&#39;close&#39;, function () {});

        stompClient = socket; // WebSocket 객체를 stompClient에 할당
    });
}</code></pre>
<ul>
<li>stompTest()<ul>
<li>STOMP connection 초기화</li>
<li>메시지를 설정하여 전송</li>
</ul>
</li>
</ul>
<pre><code class="language-jsx">export function stompTest() {
    initializeStompClient(function (socket) {
    const stompMessage = `SEND\ndestination:/pub/share/${randomInt}\n\n${JSON.stringify({ content: &#39;Hi from k6 STOMP!&#39; })}\x00\n`;

     // WebSocket 연결이 열리면 STOMP 메시지를 전송합니다.
     socket.send(stompMessage);
    });
}</code></pre>
<h3 id="전체-스크립트">전체 스크립트</h3>
<pre><code class="language-jsx">import {check, sleep} from &#39;k6&#39;;
import http from &#39;k6/http&#39;;
import ws from &#39;k6/ws&#39;;

let longPollingConnection = null;
let sseConnection = null;
let websocketConnection = null;
let stompClient = null;

export let options = {
    stages: [
        {duration: &#39;20s&#39;, target: 300},
        {duration: &#39;30s&#39;, target: 10000},
        {duration: &#39;10s&#39;, target: 0},
    ],
};

const baseUrl = &#39;http://localhost:8080&#39;;
const wsUrl = &#39;ws://localhost:8080/ws&#39;;
const stompUrl = &#39;ws://localhost:8080/location&#39;;
const randomInt = getRandomInt();

function getRandomInt() {
    return Math.floor(Math.random() * 10) + 1;
}

function shortPollingTest() {
    http.get(`http://localhost:8080/cur/${randomInt}`);
}

function initializeLongPollingConnection() {
    if (longPollingConnection) {
        return longPollingConnection;
    }

    const connection = http.get(`${baseUrl}/location/long/${randomInt}`, {
        headers: {Accept: &#39;text/event-stream&#39;},
        tags: {type: &#39;longPolling&#39;}
    });
    check(connection, {&#39;Long Polling Connection Status is 200&#39;: (r) =&gt; r.status === 200});
    return connection;
}

function initializeSSEConnection() {
    if (sseConnection) {
        return sseConnection;
    }
    const connection = http.get(`${baseUrl}/location/sse/connect`, {
        headers: {Accept: &#39;text/event-stream&#39;},
        tags: {type: &#39;sse&#39;}
    });
    check(connection, {&#39;SSE Connection Status is 200&#39;: (r) =&gt; r.status === 200});
    return connection;
}

function initializeWebSocketConnection() {
    if (websocketConnection) {
        return websocketConnection;
    }

    const connection = ws.connect(wsUrl, function (socket) {
        socket.on(&#39;open&#39;, function () {
            socket.send(&#39;Hello from k6 WebSocket!&#39;);
        });

        socket.on(&#39;message&#39;, function (message) {
        });

        socket.on(&#39;close&#39;, function () {
        });
    });

    websocketConnection = connection;
    return connection;
}

function initializeStompClient(callback) {
    if (stompClient) {
        callback(stompClient); // 이미 stompClient가 존재하면 콜백 호출
        return;
    }

    ws.connect(stompUrl, {}, function (socket) {
        socket.on(&#39;open&#39;, function () {
            console.log(&#39;WebSocket connection opened&#39;);

            // STOMP CONNECT 메시지를 전송합니다.
            socket.send(&#39;CONNECT\naccept-version:1.0,1.1,2.0\n\n\x00\n&#39;);

            // STOMP SUBSCRIBE 메시지를 전송합니다.
            const subscribeMessage = `SUBSCRIBE\nid:${uuid()}\ndestination:/sub/location/${randomInt}\n\n\x00\n`;
            socket.send(subscribeMessage);

            // WebSocket 연결이 열렸을 때의 콜백 함수 호출
            callback(socket);
        });

        socket.on(&#39;message&#39;, function (message) {});

        socket.on(&#39;close&#39;, function () {});

        stompClient = socket; // WebSocket 객체를 stompClient에 할당
    });
}

function uuid() {
    const pattern = &#39;xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx&#39;;
    return pattern.replace(/[xy]/g, function(c) {
        const r = Math.random() * 16 | 0;
        const v = c === &#39;x&#39; ? r : (r &amp; 0x3 | 0x8);
        return v.toString(16);
    });
}

export default function () {
    shortPollingTest();
    longPollingTest();
    sseTest();
    websocketTest();
    stompTest();

    sleep(3);
}

export function longPollingTest() {
    initializeLongPollingConnection();

    const notifyResponse = http.post(`${baseUrl}/location/long/${randomInt}/notify`);
    check(notifyResponse, {&#39;Long Polling Notify Status is 200&#39;: (r) =&gt; r.status === 200});

    const url = `${baseUrl}/location/long/${randomInt}`;
    const response = http.get(url);
    check(response, {&#39;Long Polling Post Status is 200&#39;: (r) =&gt; r.status === 200});
}

export function sseTest() {
    initializeSSEConnection();
    const postResponse = http.post(`${baseUrl}/location/sse/share`);
    check(postResponse, {&#39;SSE Share Status is 200&#39;: (r) =&gt; r.status === 200});
}

export function websocketTest() {
    initializeWebSocketConnection();
    ws.connect(wsUrl, function (socket) {
        socket.on(&#39;open&#39;, function () {
            socket.send(&#39;Hello from k6!&#39;);
        });
    });
}

export function stompTest() {
    initializeStompClient(function (socket) {
        const stompMessage = `SEND\ndestination:/pub/share/${randomInt}\n\n${JSON.stringify({ content: &#39;Hi from k6 STOMP!&#39; })}\x00\n`;

        // WebSocket 연결이 열리면 STOMP 메시지를 전송합니다.
        socket.send(stompMessage);
    });
}
</code></pre>
<h1 id="테스트-결과">테스트 결과</h1>
<blockquote>
<p>1,2,3 테스트 시나리오에 있어 2(사용자가 최대로 진입하고 통신하는 시점)에서 사용자 수를 1000, 5000, 10000명으로 설정하고 3가지 경우를 각 통신 방법으로 테스트하였다.</p>
</blockquote>
<h2 id="short-polling">Short Polling</h2>
<hr>
<h3 id="1000-명">1000 명</h3>
<h3 id="k6-1">k6</h3>
<pre><code>     data_received..............: 3.0 MB  48 kB/s
     data_sent..................: 822 kB  13 kB/s
     http_req_blocked...........: avg=64.42µs min=0s    med=5µs    max=7ms     p(90)=257.3µs p(95)=550µs   
     http_req_connecting........: avg=46.11µs min=0s    med=0s     max=6.85ms  p(90)=207.3µs p(95)=428.14µs
     http_req_duration..........: avg=2.42ms  min=441µs med=1.51ms max=24.75ms p(90)=5.09ms  p(95)=7.15ms  
     http_req_failed............: 100.00% ✓ 9658       ✗ 0     
     http_req_receiving.........: avg=67.93µs min=6µs   med=44µs   max=13.41ms p(90)=106µs   p(95)=148µs   
     http_req_sending...........: avg=29.44µs min=2µs   med=20µs   max=2.11ms  p(90)=54µs    p(95)=76µs    
     http_req_tls_handshaking...: avg=0s      min=0s    med=0s     max=0s      p(90)=0s      p(95)=0s      
     http_req_waiting...........: avg=2.32ms  min=415µs med=1.43ms max=24.5ms  p(90)=4.94ms  p(95)=6.92ms  
     http_reqs..................: 9658    154.226693/s
     iteration_duration.........: avg=3s      min=3s    med=3s     max=3.02s   p(90)=3s      p(95)=3s      
     iterations.................: 9658    154.226693/s
     vus........................: 16      min=15       max=998 
     vus_max....................: 1000    min=1000     max=1000
</code></pre><p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/a9280034-5342-441c-a340-9c9d18a42920" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/315f5358-0761-4c17-aa3d-a56ae642ffa9" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/19619799-8b92-470d-8f6a-532c4897a733" alt="image"></p>
<h3 id="prometheus-1">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/0aa35715-a74a-465e-bbaf-149822f448d4" alt="image"></p>
<h3 id="5000-명">5000 명</h3>
<h3 id="k6-2">k6</h3>
<pre><code>     data_received..............: 12 MB   187 kB/s
     data_sent..................: 3.3 MB  52 kB/s
     http_req_blocked...........: avg=63.12µs  min=0s    med=3µs    max=48.35ms  p(90)=236µs   p(95)=366µs  
     http_req_connecting........: avg=46.79µs  min=0s    med=0s     max=47.81ms  p(90)=191µs   p(95)=284µs  
     http_req_duration..........: avg=8.01ms   min=418µs med=1.07ms max=299.64ms p(90)=14.78ms p(95)=42.13ms
     http_req_failed............: 100.00% ✓ 38255      ✗ 0     
     http_req_receiving.........: avg=110.55µs min=4µs   med=23µs   max=118.48ms p(90)=82µs    p(95)=164µs  
     http_req_sending...........: avg=27.46µs  min=1µs   med=11µs   max=74.37ms  p(90)=43µs    p(95)=64µs   
     http_req_tls_handshaking...: avg=0s       min=0s    med=0s     max=0s       p(90)=0s      p(95)=0s     
     http_req_waiting...........: avg=7.87ms   min=394µs med=1.01ms max=299.58ms p(90)=14.51ms p(95)=41.5ms 
     http_reqs..................: 38255   607.681958/s
     iteration_duration.........: avg=3s       min=3s    med=3s     max=3.29s    p(90)=3.01s   p(95)=3.04s  
     iterations.................: 38255   607.681958/s
     vus........................: 2       min=2        max=4988
     vus_max....................: 5000    min=5000     max=5000
</code></pre><p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/e624788c-36ed-4cca-b1c2-6da36d4909fd" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/ad20caac-a6fc-4f87-a26f-cca29c149f05" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/0708483b-d566-4d73-a9fe-8af489dffd88" alt="image"></p>
<h3 id="prometheus-2">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/ec6444ad-aeb6-42f0-aff2-14282e65b291" alt="image"></p>
<h3 id="10000-명">10000 명</h3>
<h3 id="k6-3">k6</h3>
<pre><code>     data_received..............: 19 MB   237 kB/s
     data_sent..................: 5.5 MB  67 kB/s
     http_req_blocked...........: avg=64.85µs  min=0s    med=4µs     max=162.66ms p(90)=185µs p(95)=288µs
     http_req_connecting........: avg=36.13µs  min=0s    med=0s      max=16.3ms   p(90)=142µs p(95)=214µs
     http_req_duration..........: avg=422.63ms min=392µs med=43.5ms  max=3.6s     p(90)=1.47s p(95)=1.94s
     http_req_failed............: 100.00% ✓ 63081      ✗ 0      
     http_req_receiving.........: avg=504.35µs min=4µs   med=28µs    max=574.14ms p(90)=182µs p(95)=652µs
     http_req_sending...........: avg=133.4µs  min=1µs   med=14µs    max=183.77ms p(90)=64µs  p(95)=134µs
     http_req_tls_handshaking...: avg=0s       min=0s    med=0s      max=0s       p(90)=0s    p(95)=0s   
     http_req_waiting...........: avg=421.99ms min=370µs med=42.99ms max=3.6s     p(90)=1.46s p(95)=1.94s
     http_reqs..................: 63081   770.071737/s
     iteration_duration.........: avg=3.42s    min=3s    med=3.04s   max=6.6s     p(90)=4.47s p(95)=4.94s
     iterations.................: 63081   770.071737/s
     vus........................: 252     min=9        max=9999 
     vus_max....................: 10000   min=10000    max=10000
</code></pre><p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/ac9681a9-ec80-4476-8df9-6621b40f0c57" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/36981c4e-f32d-4329-8c0c-2fdf342b85cb" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/b2db2dc6-e130-4419-834b-cfd26e72533d" alt="image"></p>
<h3 id="prometheus-3">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/bb23ca84-bbd5-472f-b7ad-0adcdfb9505e" alt="image"></p>
<h2 id="long-polling-1">Long Polling</h2>
<hr>
<h3 id="1000-명-1">1000 명</h3>
<h3 id="k6-4">k6</h3>
<pre><code>     ✗ Long Polling Connection Status is 200
      ↳  66% — ✓ 1465 / ✗ 739
     ✓ Long Polling Post Status is 200
     ✓ Long Polling Notify Status is 200

     checks.........................: 88.82% ✓ 5873      ✗ 739   
     data_received..................: 814 kB 9.7 kB/s
     data_sent......................: 748 kB 8.9 kB/s
     http_req_blocked...............: avg=100.35µs min=0s    med=2µs    max=13.23ms  p(90)=471.8µs p(95)=727.89µs
     http_req_connecting............: avg=77.43µs  min=0s    med=0s     max=8.14ms   p(90)=374µs   p(95)=566µs   
     http_req_duration..............: avg=4.91s    min=380µs med=5.21s  max=11.14s   p(90)=10.96s  p(95)=10.98s  
       { expected_response:true }...: avg=4.76s    min=380µs med=2.02s  max=11.14s   p(90)=10.97s  p(95)=10.98s  
     http_req_failed................: 11.17% ✓ 739       ✗ 5873  
     http_req_receiving.............: avg=250.88µs min=3µs   med=17µs   max=168.69ms p(90)=102.9µs p(95)=333.34µs
     http_req_sending...............: avg=26.77µs  min=1µs   med=7µs    max=4.82ms   p(90)=75µs    p(95)=109µs   
     http_req_tls_handshaking.......: avg=0s       min=0s    med=0s     max=0s       p(90)=0s      p(95)=0s      
     http_req_waiting...............: avg=4.91s    min=358µs med=5.2s   max=11.14s   p(90)=10.96s  p(95)=10.98s  
     http_reqs......................: 6612   78.426891/s
     iteration_duration.............: avg=17.74s   min=3.01s med=21.84s max=25.04s   p(90)=22.96s  p(95)=24.26s  
     iterations.....................: 2204   26.142297/s
     vus............................: 6      min=6       max=1000
     vus_max........................: 1000   min=1000    max=1000
</code></pre><p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/3afd6009-7348-45d4-8de8-58319a1871b4" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/3a610adb-e699-422c-8ef2-9da1efe66c39" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/dd7c5f94-115f-416c-a00d-be8efe994440" alt="image"></p>
<h3 id="prometheus-4">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/9ea01ba5-6451-46b7-804c-a5e45eb79801" alt="image"></p>
<h3 id="5000-명-1">5000 명</h3>
<h3 id="k6-5">k6</h3>
<pre><code>     ✗ Long Polling Connection Status is 200
      ↳  67% — ✓ 6512 / ✗ 3164
     ✗ Long Polling Post Status is 200
      ↳  99% — ✓ 9675 / ✗ 1
     ✓ Long Polling Notify Status is 200

     checks.........................: 89.09% ✓ 25863      ✗ 3165  
     data_received..................: 3.6 MB 43 kB/s
     data_sent......................: 3.3 MB 40 kB/s
     http_req_blocked...............: avg=100.22µs min=0s    med=2µs      max=119.83ms p(90)=361µs  p(95)=510µs 
     http_req_connecting............: avg=76.03µs  min=0s    med=0s       max=119.76ms p(90)=279µs  p(95)=397µs 
     http_req_duration..............: avg=4.73s    min=373µs med=3.56s    max=11.44s   p(90)=10.75s p(95)=10.9s 
       { expected_response:true }...: avg=4.64s    min=373µs med=345.14ms max=11.44s   p(90)=10.78s p(95)=10.93s
     http_req_failed................: 10.90% ✓ 3165       ✗ 25863 
     http_req_receiving.............: avg=546.6µs  min=3µs   med=19µs     max=301.16ms p(90)=94µs   p(95)=377µs 
     http_req_sending...............: avg=44.11µs  min=1µs   med=7µs      max=18.33ms  p(90)=63µs   p(95)=93µs  
     http_req_tls_handshaking.......: avg=0s       min=0s    med=0s       max=0s       p(90)=0s     p(95)=0s    
     http_req_waiting...............: avg=4.73s    min=362µs med=3.56s    max=11.44s   p(90)=10.75s p(95)=10.9s 
     http_reqs......................: 29028  349.110016/s
     iteration_duration.............: avg=17.19s   min=3s    med=21.79s   max=25.16s   p(90)=22.51s p(95)=24.32s
     iterations.....................: 9676   116.370005/s
     vus............................: 336    min=12       max=5000
     vus_max........................: 5000   min=5000     max=5000
</code></pre><p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/00e4638c-2705-4f25-ac34-2a08e98e6dac" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/f76aa589-0036-4e23-8ec6-e27eab786005" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/e24e3939-9fc9-4f63-8803-94d188485ebe" alt="image"></p>
<h3 id="prometheus-5">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/8dcd624f-1514-4933-a78c-1a5b4a5e1584" alt="image"></p>
<h3 id="10000-명-1">10000 명</h3>
<h3 id="k6-6">k6</h3>
<pre><code>  checks.........................: 66.96% ✓ 38419      ✗ 18949  
     data_received..................: 6.9 MB 78 kB/s
     data_sent......................: 6.7 MB 75 kB/s
     http_req_blocked...............: avg=75.59µs min=0s    med=2µs    max=99.86ms p(90)=257µs  p(95)=366µs   
     http_req_connecting............: avg=56.52µs min=0s    med=0s     max=97.54ms p(90)=203µs  p(95)=283µs   
     http_req_duration..............: avg=3.32s   min=402µs med=3.08s  max=13.36s  p(90)=5.67s  p(95)=8.06s   
       { expected_response:true }...: avg=2.54s   min=402µs med=2.4s   max=12.65s  p(90)=4.76s  p(95)=5.37s   
     http_req_failed................: 33.03% ✓ 18949      ✗ 38419  
     http_req_receiving.............: avg=1.15ms  min=3µs   med=39µs   max=2.05s   p(90)=217µs  p(95)=643.64µs
     http_req_sending...............: avg=53.32µs min=1µs   med=11µs   max=64.86ms p(90)=52µs   p(95)=89µs    
     http_req_tls_handshaking.......: avg=0s      min=0s    med=0s     max=0s      p(90)=0s     p(95)=0s      
     http_req_waiting...............: avg=3.32s   min=367µs med=3.08s  max=13.36s  p(90)=5.67s  p(95)=8.06s   
     http_reqs......................: 57368  643.211303/s
     iteration_duration.............: avg=12.96s  min=3.01s med=13.08s max=25.48s  p(90)=17.07s p(95)=19.92s  
     iterations.....................: 19122  214.396293/s
     vus............................: 1      min=1        max=10000
     vus_max........................: 10000  min=10000    max=10000
</code></pre><p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/e2051e20-6bde-4977-ae47-7d14e7577ce4" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/be92363d-b108-4e85-b892-e8e6e8baa020" alt="image"></p>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/3f163137-10c8-408a-be99-e9344a1851ff" alt="image"></p>
<h3 id="prometheus-6">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/0d61bdab-491a-426d-9f9c-8123eada4052" alt="image"></p>
<h2 id="websocket-1">WebSocket</h2>
<hr>
<h3 id="1000-명-2">1000 명</h3>
<h3 id="k6-7">k6</h3>
<pre><code>     data_received......: 2.5 MB 28 kB/s
     data_sent..........: 233 kB 2.6 kB/s
     vus................: 5      min=5        max=1000
     vus_max............: 1000   min=1000     max=1000
     ws_connecting......: avg=4.38ms min=904.5µs med=3.54ms max=111.04ms p(90)=7.52ms p(95)=8.59ms
     ws_msgs_received...: 50083  556.458673/s
     ws_msgs_sent.......: 1000   11.11073/s
     ws_sessions........: 1000   11.11073/s</code></pre><h3 id="prometheus-7">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/4a9e0d44-0ab5-49f8-a523-d792a321dd3b" alt="image"></p>
<h3 id="5000-명-2">5000 명</h3>
<h3 id="k6-8">k6</h3>
<pre><code>     data_received.........: 66 MB   734 kB/s
     data_sent.............: 1.3 MB  15 kB/s
     iteration_duration....: avg=16.21s min=3.05s    med=17.16s  max=31.34s p(90)=26.62s p(95)=27.43s
     iterations............: 196     2.177476/s
     vus...................: 126     min=12         max=5000
     vus_max...............: 5000    min=5000       max=5000
     ws_connecting.........: avg=1.36s  min=595.91µs med=17.64ms max=10.14s p(90)=5.59s  p(95)=5.89s 
     ws_msgs_received......: 1377197 15300.071189/s
     ws_msgs_sent..........: 5792    64.346649/s
     ws_session_duration...: avg=6.36s  min=2.14ms   med=5.56s   max=21.74s p(90)=14.01s p(95)=15.95s
     ws_sessions...........: 5792    64.346649/s
</code></pre><h3 id="prometheus-8">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/18f18721-3dbf-4f39-911d-9feb4e3c061d" alt="image"></p>
<h3 id="10000-명-2">10000 명</h3>
<h3 id="k6-9">k6</h3>
<pre><code>    data_received.........: 65 MB   717 kB/s
     data_sent.............: 2.6 MB  28 kB/s
     iteration_duration....: avg=18.16s min=3.03s    med=14.36s max=58.08s p(90)=38.85s p(95)=44.15s
     iterations............: 136     1.509431/s
     vus...................: 3007    min=9          max=10000
     vus_max...............: 10000   min=10000      max=10000
     ws_connecting.........: avg=11.86s min=590.66µs med=3.78s  max=43.12s p(90)=25.55s p(95)=25.77s
     ws_msgs_received......: 1336749 14836.253751/s
     ws_msgs_sent..........: 8705    96.614689/s
     ws_session_duration...: avg=18.31s min=1.71ms   med=14.66s max=54.13s p(90)=44.02s p(95)=48.3s 
     ws_sessions...........: 8771    97.347207/s
</code></pre><h3 id="prometheus-9">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/8c45b70d-32a9-48d3-905c-7c63eb520dbf" alt="image"></p>
<h2 id="stomp-1">STOMP</h2>
<hr>
<h3 id="1000-명-3">1000 명</h3>
<h3 id="k6-10">k6</h3>
<pre><code>     data_received......: 13 MB  139 kB/s
     data_sent..........: 410 kB 4.6 kB/s
     vus................: 3      min=3        max=1000
     vus_max............: 1000   min=1000     max=1000
     ws_connecting......: avg=4.89ms min=1.46ms med=4.5ms max=48.9ms p(90)=6.18ms p(95)=7.41ms
     ws_msgs_received...: 52490  583.193678/s
     ws_msgs_sent.......: 3000   33.331702/s
     ws_sessions........: 1000   11.110567/s</code></pre><h3 id="prometheus-10">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/872c0a68-29ae-46d7-8482-586165420154" alt="image"></p>
<h3 id="5000-명-3">5000 명</h3>
<h3 id="k6-11">k6</h3>
<pre><code>     data_received......: 303 MB  3.4 MB/s
     data_sent..........: 2.1 MB  23 kB/s
     vus................: 63      min=14         max=5000
     vus_max............: 5000    min=5000       max=5000
     ws_connecting......: avg=3.52ms min=488.83µs med=952.54µs max=94.02ms p(90)=7.42ms p(95)=14.68ms
     ws_msgs_received...: 1260821 14006.731584/s
     ws_msgs_sent.......: 15000   166.638225/s
     ws_sessions........: 5000    55.546075/s</code></pre><h3 id="prometheus-11">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/c8f4f1f8-fca7-4d83-b5e1-e9bfde5a968e" alt="image"></p>
<h3 id="10000-명-3">10000 명</h3>
<h3 id="k6-12">k6</h3>
<pre><code>     data_received.........: 823 MB  9.1 MB/s
     data_sent.............: 3.7 MB  42 kB/s
     iteration_duration....: avg=32.37s min=12.01s   med=33s      max=33.02s p(90)=33s p(95)=33s
     iterations............: 878     9.754204/s
     vus...................: 329     min=12         max=10000
     vus_max...............: 10000   min=10000      max=10000
     ws_connecting.........: avg=5.54s  min=434.79µs med=354.49ms max=43.78s p(90)=30s p(95)=30s
     ws_msgs_received......: 3409118 37873.839154/s
     ws_msgs_sent..........: 24696   274.361
     ws_session_duration...: avg=29.65s min=9.01s    med=30s      max=30.05s p(90)=30s p(95)=30s
     ws_sessions...........: 9844    109.362619/s</code></pre><h3 id="prometheus-12">Prometheus</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/86fdaf4c-2a61-428c-af25-42519e30373b" alt="image"></p>
<h1 id="분석">분석</h1>
<blockquote>
<p>최고 접속자의 수를 1000, 5000, 10000명으로 설정하고 각 실시간 통신에 대한 시나리오 테스트를 수행했다. 테스트 결과를 3가지 측면에서 분석하였다.</p>
</blockquote>
<h2 id="polling-vs-long-polling">Polling VS Long Polling</h2>
<hr>
<blockquote>
<p>우선 Polling과 Long Polling을 10000명의 사용자 지표에 따른 지표를 바탕으로 비교해보았다.</p>
</blockquote>
<h3 id="iteration_duration--interations">iteration_duration / interations</h3>
<blockquote>
<p>iteration_duration: 한 번의 반복(iteration)에 걸리는 시간을 측정한 것
interations : 테스트 시간 동안 반복된 횟수 (전체 횟수 , 초당 횟수)</p>
</blockquote>
<pre><code>Polling : 
    iteration_duration.........: p(90)=4.47s p(95)=4.94s
  iterations.................: 63081   770.071737/s

Long Polling : 
    iteration_duration.............: p(90)=17.07s p(95)=19.92s  
  iterations.....................: 19122  214.396293/s</code></pre><blockquote>
<p>위 지표를 보면 Polling이 <strong>총 수행 횟수가 더 많고 한번의 주기 시간은 짧은 것</strong>을 알 수 있다.</p>
</blockquote>
<ul>
<li>서버로부터 데이터가 오지 않더라도 API를 응답하는 Polling의 특성 상 호출 횟수가 더 많은 것이다.</li>
<li>반면 데이터가 오기 까지 연결을 지속하는 Long Polling은 평균 수행 시간이 더 긴 것을 알 수 있는데 Long Polling 자체에 설정한 TimeOut 값에 따라 결과는 달라질 것이다.</li>
</ul>
<h3 id="heap-used">Heap Used</h3>
<ul>
<li>Polling</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/54bab203-23f9-4a3c-8728-25e24d1fbd71" alt="image"></p>
<ul>
<li>Long Polling</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/10f48633-bfbe-41a2-be97-23b7f001c6e5" alt="image"></p>
<blockquote>
<p>다음은 Spring 자체의 Heap 사용량에 대한 비교이다. 지표를 분석해보면 Long Polling이 Polling 방식에 비해 Heap 사용량이 더 많은 것을 알 수 있다.</p>
</blockquote>
<ul>
<li>이에 대한 이유는 Long Polling 방식을 구현하는데 사용한 DeferredResult 때문이다.</li>
<li>Polling 방식의 경우에는 결과값을 반환하고 더 이상 서버에서 유지하지 않는다. 하지만 Long Polling의 방식에는 DeferredResult 객체에 setResult() 메서드를 통한 값이 삽입되기 전까지 데이터가 유지되어야 하기 때문에 Heap 사용량이 더 많을 수밖에 없다.</li>
</ul>
<h2 id="polling-vs-websocket">Polling VS WebSocket</h2>
<hr>
<blockquote>
<p>두번째로 Polling 방식과 WebSocket 방식의 비교이다. 두 방식에 대해 k6에서 표로 제공하는 지표는 차이가 있다. 통신 방식에 차이가 있기 때문에 서로 다른 지표에 대한 비교는 어려웠다. 그래서 prometheus의 지표를 중심으로 비교했다.</p>
</blockquote>
<ul>
<li>두 방식에 대해 보통적으로 말하는 비교라면 WebSocket이 connection의 횟수가 적어 <strong>자원 사용량이 적을 것</strong>이라는 것이다 !!!!!!</li>
<li><strong>자원 사용량</strong>을 중심으로 비교해보겠다.</li>
</ul>
<h3 id="system-cpu-usage--process-cpu-usage">System CPU Usage / Process CPU Usage</h3>
<blockquote>
<p>System : 운영 체제 전체에서 모든 프로세스가 차지하는 CPU 사용량
Process : 특정 프로세스가 차지하는 CPU 사용량</p>
</blockquote>
<ul>
<li>Polling</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/bd0fd9a6-0d5b-4730-9b65-a87c270243df" alt="image"></p>
<ul>
<li>WebSocket</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/6a79d26a-05cd-4155-952f-33beb1353557" alt="image"></p>
<blockquote>
<p>WebSocket이 평균적으로 Polling 방식과 비교하여 CPU 사용량이 적은 것을 알 수 있다.</p>
</blockquote>
<ul>
<li>매번 connection을 생성하고 종료하는 과정이 삭제되어 전체적인 CPU 사용량에도 영향을 준 것을 알 수 있다.</li>
</ul>
<h3 id="heap-use">Heap Use</h3>
<ul>
<li>Polling</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/9c8c6358-823a-4280-8d05-7ac16751b76c" alt="image"></p>
<ul>
<li>WebSocket</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/d1fba1a9-73b1-4b7b-9d35-00bad6ddfcf2" alt="image"></p>
<blockquote>
<p>하지만 Spring 자체의 Heap 사용량은 WebSocket이 더 높게 측정되는 것을 알 수 있다.</p>
</blockquote>
<ul>
<li>이는 WebSocketSession 객체 때문이다. WebSocket Session을 계속 유지하며 지속적으로 Heap에 존재하게 되므로 커넥션을 유지하지 않는 ShortPolling 보다 WebSocket이 Heap 사용량이 더 많다.</li>
</ul>
<h2 id="websocket-vs-stomp">WebSocket VS STOMP</h2>
<hr>
<blockquote>
<p>마지막으로 WebSocket과 STOMP에 대한 비교이다. k6상에서 두 방식에 대해 동일한 지표를 제공한다. 각 지표로 확인할 수 있는 정보는 다음과 같다.</p>
</blockquote>
<ul>
<li><strong>반복 작업</strong>: <code>iteration_duration</code>과 <code>iterations</code>는 테스트 반복 주기와 성능</li>
<li><strong>사용자 부하</strong>: <code>vus</code>와 <code>vus_max</code>는 가상의 사용자 부하 (테스트 상에서는 동일)</li>
<li><strong>WebSocket 성능</strong>: <code>ws_connecting</code>과 <code>ws_session_duration</code>은 WebSocket 연결과 세션의 성능</li>
<li><strong>메시지 처리</strong>: <code>ws_msgs_received</code>와 <code>ws_msgs_sent</code>는 WebSocket을 통해 처리된 메시지 수</li>
</ul>
<h3 id="ws_msgs_sent-ws_msgs_received-iterations">ws_msgs_sent, ws_msgs_received, iterations</h3>
<ul>
<li><p>WebSocket</p>
<pre><code>  ws_msgs_received......: 1336749 14836.253751/s
  ws_msgs_sent..........: 8705    96.614689/s
  iterations............: 136     1.509431/s</code></pre></li>
<li><p>STOMP</p>
<pre><code>  ws_msgs_received......: 3409118 37873.839154/s
  ws_msgs_sent..........: 24696   274.361
  iterations............: 878     9.754204/s</code></pre></li>
</ul>
<blockquote>
<p>STOMP가 WebSocket과 비교해서 3가지 지표에서 더 높다는 것을 알 수 있다.</p>
</blockquote>
<ul>
<li>이는 동일한 <strong>테스트 시간동안 더 많은 메시지를 처리</strong>한 것이고 STOMP가 WebSocket과 비교하여 처리량이 더 높다는 것을 알 수 있다.</li>
<li>이에 대해 메시지 브로커를 통한 처리가 큰 이유라고 생각한다. 그룹 별로 나눠지 세션에 메시지를 분배하고 통신하는 과정을 WebSocket을 사용할 때는 사용자가 직접 구현해줬고 이에 대한 성능이 최적화되어 있는 메시지 브로커보다는 효율이 떨어진다 판단했다.</li>
</ul>
<h3 id="ws_sessions-ws_connecting">ws_sessions, ws_connecting</h3>
<pre><code>WebSocket :
    ws_connecting.........: avg=11.86s min=590.66µs med=3.78s  max=43.12s
    ws_sessions...........: 8771    97.347207/s
Stomp : 
    ws_connecting.........: avg=5.54s  min=434.79µs med=354.49ms max=43.78s
    ws_sessions...........: 9844    109.362619/s</code></pre><blockquote>
</blockquote>
<p>평균적으로 STOMP가 WebSocket에 비해 <strong>연결 설정에 더 적은 시간을 소비</strong>하는 것을 알 수 있다.
가장 주의 깊게 본 지표는 ws_sessions이다. 해당 지표는 테스트가 수행되는 동안 성공적으로 열린 websocket session 수이다.</p>
<blockquote>
</blockquote>
<ul>
<li>해당 지표에서 10000의 사용자를 대상으로 STOMP가 더 많은 websocket session을 포함한 것을 알 수 있다. 즉 <strong>부하에 더 강하다는 것</strong>이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[실시간 통신 분석 프로젝트 (각 방법의 구현 코드)]]></title>
            <link>https://velog.io/@jinjoo-lab/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EB%B6%84%EC%84%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%81-%EB%B0%A9%EB%B2%95%EC%9D%98-%EA%B5%AC%ED%98%84-%EC%BD%94%EB%93%9C</link>
            <guid>https://velog.io/@jinjoo-lab/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EB%B6%84%EC%84%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%81-%EB%B0%A9%EB%B2%95%EC%9D%98-%EA%B5%AC%ED%98%84-%EC%BD%94%EB%93%9C</guid>
            <pubDate>Mon, 17 Jun 2024 13:08:42 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<blockquote>
<p>앞서 실시간 통신 방법에 대한 이론을 전체적으로 다뤄봤다. 이번에는 각 실시간 통신을 <strong>Spring 환경에서 구현하는 방법</strong>에 대해 설명하고 <strong>비교 분석 테스트를 위한 코드</strong>를 설명하겠다.</p>
</blockquote>
<h3 id="테스트-상황">테스트 상황</h3>
<blockquote>
<p><strong>‘그룹별 실시간 위치 공유’</strong> 로직을 위한 테스트 코드를 작성할 것이다. 사용자가 각 좌표를 공유하는 상황이고 순수하게 실시간 통신 기술의 비교를 위해 데이터 베이스를 추가하지는 않았다.</p>
</blockquote>
<h2 id="common-logic">Common Logic</h2>
<hr>
<h3 id="locationdto">LocationDto</h3>
<blockquote>
<p>각 사용자의 좌표에 대한 dto다. 해당 데이터를 공유되는 사용자들에게 전송할 것이다.</p>
</blockquote>
<pre><code class="language-java">@Getter
@NoArgsConstructor
@AllArgsConstructor
public class LocationDto {
    private double x;
    private double y;
}</code></pre>
<h3 id="make-location">Make Location</h3>
<blockquote>
<p>클라이언트의 실시간 위치 공유를 위해서는 부가적인 추가가 필요해보였다. 단순히 비교만을 위해 서버에서 <strong>Random 함수를 통해 임의의 좌표값을 생성</strong>하도록 했다.</p>
</blockquote>
<pre><code class="language-java">/* Math.random() 함수를 통해 임의의 (x,y)좌표를 생성 */
private LocationDto makeRandomLocation() {
     double randomX = Math.random() * 100;
     double randomY = Math.random() * 100;

     return new LocationDto(randomX, randomY);
}</code></pre>
<h1 id="구현-방법-code">구현 방법 (code)</h1>
<hr>
<h2 id="polling">Polling</h2>
<hr>
<ul>
<li>Polling을 Spring에서 구현하는 방법은 생각보다 간단하다.<ul>
<li>Polling이란 ‘특정 주기마다 반복적 호출’이 핵심이다.</li>
<li>‘반복’의 기능을 Spring 자체에서 구현하는 것보다는 클라이언트 로직에서 추가하는 것이 올바르다 판단해서 Spring에서는 단순한 API로만 남겨놓았다.</li>
</ul>
</li>
</ul>
<h3 id="controller">Controller</h3>
<pre><code class="language-java">@GetMapping(&quot;/cur&quot;)
public ResponseEntity&lt;LocationDto&gt; shareCurLocation() {
     return ResponseEntity.ok(locationService.shareCurLocation());
}</code></pre>
<h3 id="service">Service</h3>
<ul>
<li>그룹별 위치 정보를 관리하기 위해 Map&lt;Long, List<LocationDto>&gt; 를 선언하였다.</li>
<li>로직에 대해서는 사용자는 자신이 속한 groupId를 인자로 전달한다. 해당 그룹이 존재하지 않으면 생성하고 그룹에 랜덤한 위치 정보를 추가한다.</li>
</ul>
<pre><code class="language-java">private final Map&lt;Long, List&lt;LocationDto&gt;&gt; locations = new ConcurrentHashMap&lt;&gt;();

public LocationDto shareCurLocation(Long groupId) {
       List&lt;LocationDto&gt; groupLocations = locations.computeIfAbsent(groupId, k -&gt; new CopyOnWriteArrayList&lt;&gt;());

     // 새로운 위치 정보 생성 및 추가
     LocationDto newLocation = makeRandomLocation();
     groupLocations.add(newLocation);

     return newLocation;
}</code></pre>
<h2 id="long-polling">Long Polling</h2>
<hr>
<ul>
<li>Spring에서 Long Polling 기능을 구현하는 방법 중 대표적인 것은 <strong>DeferredResult</strong>를 이용하는 것이다.</li>
</ul>
<pre><code class="language-java">private final Map&lt;Long, BlockingQueue&lt;DeferredResult&lt;LocationDto&gt;&gt;&gt; groupRequests =
            new ConcurrentHashMap&lt;&gt;();</code></pre>
<ol>
<li>사용자가 자신의 그룹 id에 해당하는 Queue에 DefferedResult 객체를 넣어 놓는다.</li>
<li>다른 그룹원이 해당 객체를 큐에서 빼 setResult() 메서드를 통해 정보를 갱신한다.</li>
<li>그럴 경우 해당 데이터가 큐를 집어넣은 사용자에게 반환된다.</li>
</ol>
<h3 id="controller-1">Controller</h3>
<p><strong>poll()</strong> : long polling API</p>
<p><strong>notifyGroup()</strong> : long polling에 있어서 event 발생 API</p>
<pre><code class="language-java">@GetMapping(&quot;/long/{groupId}&quot;)
public DeferredResult&lt;LocationDto&gt; poll(@PathVariable final Long groupId) {
    return locationService.longPoll(groupId);
}

@PostMapping(&quot;/long/{groupId}/notify&quot;)
public void notifyGroup(@PathVariable final Long groupId) {
    locationService.notifyGroup(groupId);
}</code></pre>
<h3 id="service-1">Service</h3>
<pre><code class="language-java">public DeferredResult&lt;LocationDto&gt; longPoll(final Long groupId) {
         // TIMEOUT에 대한 deferredResult를 생성한다. Timeout이 발생하면 error를 반환하도록 한다.
     final DeferredResult&lt;LocationDto&gt; deferredResult = new DeferredResult&lt;&gt;(TIMEOUT);
         deferredResult.onTimeout(() -&gt; deferredResult.setErrorResult(&quot;Request timeout&quot;));

         // 그룹 id에 대한 큐가 존재하지 않으면 안에 Queue를 생성하고 객체를 저장한다.
     groupRequests
             .computeIfAbsent(groupId, k -&gt; new LinkedBlockingQueue&lt;&gt;())
             .add(deferredResult);

     return deferredResult;
}

public void notifyGroup(final Long groupId) {
         // 자신의 그룹 id에 대한 Queue를 반환한다.
     final BlockingQueue&lt;DeferredResult&lt;LocationDto&gt;&gt; queue = groupRequests.get(groupId);

     // 큐에서 DefferedResult 객체를 빼고 해당 객체에 random을 통해 생성한 객체를 setResult()를 통해 저장
     Optional.ofNullable(queue)
              .ifPresent(
                     q -&gt; {
                         while (!q.isEmpty()) {
                             final DeferredResult&lt;LocationDto&gt; connection = q.poll();
                             if (connection != null) {
                                 connection.setResult(makeRandomLocation());
                             }
                         }
     });
}</code></pre>
<h2 id="부록--sse">부록 : SSE</h2>
<hr>
<h2 id="websocket">WebSocket</h2>
<hr>
<ul>
<li>Spring에서 WebSocket을 사용하기 위해서는 몇가지 작업이 필요하다.<ol>
<li><strong>WebSocketHandler를 구현</strong><ul>
<li>afterConnectionEstablished : websocket 연결이 생성되었을 때 수행 로직</li>
<li>afterConnectionClosed : websocket 연결이 종료되었을 때 수행 로직</li>
<li>handleTextMessage : 메시지 통신 과정의 로직</li>
</ul>
</li>
<li><strong>WebSocketConfigurer interface를 구현한 WebSocketConfig 등록</strong></li>
</ol>
</li>
</ul>
<h3 id="customwebsockethandler">CustomWebSocketHandler</h3>
<ul>
<li>groupId에 대한 WebSocketSession을 관리하기 위해 Map을 사용<ul>
<li>사용자가 websocket에 연결되었을 때 자신의 group에 websocketsession을 등록한다.</li>
<li>통신 : 자신의 그룹에 random 좌표 정보를 전달한다.</li>
<li>연결을 종료할 경우 자신의 session 정보를 group에서 삭제한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Component
@Slf4j
public class CustomWebSocketHandler extends TextWebSocketHandler {
    private final ObjectMapper objectMapper;
    private final LocationService locationService;

        // group id에 따른 세션 set
    private final Map&lt;Long, Set&lt;WebSocketSession&gt;&gt; groupSessions =  new ConcurrentHashMap&lt;&gt;();

    public CustomWebSocketHandler(LocationService locationService, ObjectMapper objectMapper) {
        this.locationService = locationService;
        this.objectMapper = objectMapper;
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message)
            throws Exception {
        Long groupId = extractGroupIdFromSession(session);
        LocationDto location = locationService.makeRandomLocation();

        Set&lt;WebSocketSession&gt; set = groupSessions.getOrDefault(groupId,new CopyOnWriteArraySet&lt;&gt;());
        set.forEach(
                s -&gt; {
                    if(!s.isOpen()) {
                        set.remove(s);
                        return;
                    }

                    try {
                        log.info(&quot;WEBSOCKET : &quot;+location.getX()+&quot; &quot;+location.getY());
                        s.sendMessage(new TextMessage(objectMapper.writeValueAsString(location)));
                    }catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
        );

    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        Long groupId = (long)(Math.random() * 10);
        Set set = groupSessions.getOrDefault(groupId,new CopyOnWriteArraySet&lt;&gt;());
        set.add(session);
        groupSessions.put(groupId,set);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status)
            throws Exception {
        Long groupId = extractGroupIdFromSession(session);
        if (groupId != null) {
            Set&lt;WebSocketSession&gt; sessions = groupSessions.get(groupId);
            if (sessions != null) {
                sessions.remove(session);
                if (sessions.isEmpty()) {
                    groupSessions.remove(groupId);
                }
            }
        }
    }

        // random group id 생성
    private Long extractGroupIdFromSession(WebSocketSession session) {
        Long groupId = (long)(Math.random() * 10);
        return groupId;
    }
}
</code></pre>
<h3 id="websocketconfig">WebSocketConfig</h3>
<pre><code class="language-java">@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    private final CustomWebSocketHandler customWebSocketHandler;

    public WebSocketConfig(CustomWebSocketHandler customWebSocketHandler) {
        this.customWebSocketHandler = customWebSocketHandler;
    }
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(customWebSocketHandler,&quot;/ws&quot;).setAllowedOrigins(&quot;*&quot;);
    }
}</code></pre>
<h2 id="stomp">STOMP</h2>
<hr>
<blockquote>
<p>STOMP는 기본적으로 내장된 Message Brocker를 제공한다. RabbitMQ를 추가하여 테스트할 수도 있지만 순수한 성능 분석을 위해 내장 message brocker를 사용했다.</p>
</blockquote>
<ul>
<li>WebSocketMessageBrokerConfigurer를 구현한 StompConfig 등록</li>
</ul>
<pre><code class="language-java">@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(final StompEndpointRegistry registry) {
        registry.addEndpoint(&quot;/location&quot;).setAllowedOrigins(&quot;*&quot;);
    }

    @Override
    public void configureMessageBroker(final MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes(&quot;/pub&quot;); // 그룹에 메시지 전달 경로
        registry.enableSimpleBroker(&quot;/sub&quot;); // 그룹 구독 경로
    }
}</code></pre>
<ul>
<li>Stomp controller<ul>
<li>groupid를 입력받아 해당 group에 위치 정보를 전달한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Controller
@Slf4j
public class StompController {

    private final SimpMessagingTemplate template;
    private final LocationService locationService;

    public StompController(
            final SimpMessagingTemplate template, final LocationService locationService) {
        this.template = template;
        this.locationService = locationService;
    }

    @MessageMapping(&quot;/share/{id}&quot;)
    public void shareCurLocationByStomp(@DestinationVariable final Long id) {
        log.info(&quot;STOMP&quot;);
        template.convertAndSend(
                String.format(&quot;/sub/location/%d&quot;, id), locationService.makeRandomLocation());
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[실시간 통신 분석 프로젝트 (이론)]]></title>
            <link>https://velog.io/@jinjoo-lab/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EB%B6%84%EC%84%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9D%B4%EB%A1%A0</link>
            <guid>https://velog.io/@jinjoo-lab/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EB%B6%84%EC%84%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9D%B4%EB%A1%A0</guid>
            <pubDate>Mon, 17 Jun 2024 10:22:50 GMT</pubDate>
            <description><![CDATA[<h1 id="실시간-위치-공유-이론">실시간 위치 공유 (이론)</h1>
<h3 id="서론">서론</h3>
<blockquote>
<p><strong>‘그룹별 실시간 위치 공유’</strong>라는 기능을 구현하기 위해 프로젝트에서 사용했던 기술은 STOMP였다. 고백하자면 STOMP를 선택하고 사용한 이유에 대해 명확한 근거가 부족했다. 당시 <strong>‘단순히 많이 사용하고 Spring에서의 사용이 좋다’</strong>였다. 프로젝트가 끝나고 고찰을 하는 과정에서 실시간 통신에 대해 깊이 있게 탐구해보기로 했다.</p>
</blockquote>
<h2 id="실시간-통신을-위해서">실시간 통신을 위해서……</h2>
<blockquote>
<p>우선 일반적인 <strong>요청 - 응답 모델</strong>과 <strong>실시간 통신</strong>의 차이점을 짚고 넘어가자</p>
</blockquote>
<h3 id="요청---응답-모델">요청 - 응답 모델</h3>
<blockquote>
<p>전통적인 <strong>클라이언트 - 서버 모델</strong>에서 동작하며 <strong>HTTP 프로토콜</strong>을 기반으로 동작한다.</p>
</blockquote>
<ul>
<li>클라이언트는 서버에 요청(Request)하고 서버는 이에 대한 응답(Response)를 제공한다.</li>
<li>특징<ul>
<li>비연결성</li>
<li>단방향성</li>
</ul>
</li>
</ul>
<p><strong>비연결성 (Stateless)</strong></p>
<blockquote>
<p>각 요청은 독립적이며 , 이전 요청에 대한 상태를 유지하지 않는다.
즉 (Request - Response)가 종료되면 클라이언트와 서버간의 연결은 유지되지 않는다.</p>
</blockquote>
<p><strong>단방향 통신</strong></p>
<blockquote>
<p>클라이언트 - 서버간의 역할이 확실하며 서버는 단순히 클라이언트의 요청에 대한 응답의 매개체 역할을 수행한다.</p>
</blockquote>
<h3 id="요청---응답-모델이-실시간-통신에-부적합한-이유">요청 - 응답 모델이 실시간 통신에 부적합한 이유</h3>
<blockquote>
<p>앞서 언급한 비연결성과 단방향 통신이 실시간 통신에 부적합한 이유이다. 실시간 통신이 수행되는 동안 양 측은 서로의 상태를 지속적으로 유지해야 한다. 즉 <strong>연결이 지속적</strong>이어야 한다. 또한 서로의 데이터를 수신하기 위해 <strong>양방향성 통신</strong>이 제공되어야 한다.</p>
</blockquote>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/530cbdce-537c-4802-89cf-22979353e974" alt="image"></p>
<h1 id="실시간-통신의-방법">실시간 통신의 방법</h1>
<blockquote>
<p>실시간 통신을 구현하기 위한 방법은 다양하다. 대표적인 방법인 Polling, Long Polling, SSE와WebSocket에 대해 알아보자</p>
</blockquote>
<h2 id="polling">Polling</h2>
<hr>
<blockquote>
<p><strong>일정한 주기</strong>를 가지고 <strong>클라이언트가 서버와 응답</strong>을 주고 받는 방식</p>
</blockquote>
<ul>
<li>예를 들어 3초의 주기를 가지고 있다면 3초마다 새로운 데이터를 계속해서 얻어오는 방식이다.</li>
</ul>
<h3 id="polling에-대한-중요-특징">Polling에 대한 중요 특징</h3>
<ul>
<li>Polling의 정의를 곱씹어본다면 중요한 것을 알 수 있다. 바로 ‘주기’라는 것이다.</li>
<li>‘일정 주기 마다 데이터를 주고 받는다’는 것은 다른 말로 <strong>연결이 유지된다는게 아니라는 것</strong>이다 !!!!!!</li>
<li>즉 요청마다 연결을 다시 생성하고 그 과정이 반복된다는 것이다 → 오버헤드의 위험성</li>
</ul>
<h3 id="polling의-주기">Polling의 주기</h3>
<ul>
<li>주기가 짧다면 서버에 더 자주 요청을 보내며, <strong>실시간에 가까운 데이터 갱신</strong>을 목표</li>
<li>주기가 길다면 서버에 덜 자주 요청을 보내며, <strong>서버 부하를 줄이면서도</strong> 데이터를 정기적으로 갱신</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/c4d2fe13-cc88-4f3b-80d8-c105e99c801d" alt="image"></p>
<blockquote>
<p>즉 실시간 통신을 위해 Polling 방법을 사용한다는 것은 <strong>주기를 짧게 설정해서 지속적인 통신이 이루어지는 것처럼 보이게 한다는 것</strong>이다. 하지만 이 과정에서 지속적으로 연결을 만들고 종료하는 과정이 반복되므로 선호되는 방법은 아니다.</p>
</blockquote>
<h2 id="long-polling">Long Polling</h2>
<hr>
<blockquote>
<p>Polling 방식의 단점을 보완하기 위한 방법으로서 <strong>서버의 연결 시간을 좀 더 길게 유지</strong>하여 데이터를 주고받는 방식이다.</p>
</blockquote>
<ul>
<li>즉 클라이언트가 서버에 데이터를 요청했을 때 바로 반환하는 것이 아닌 서버와의 접속 시간을 길게 설정하여 <strong>서버에서 이벤트가 발생하면</strong> 데이터를 반환받도록 하는 방식이다.</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/842cd486-644c-474c-9e48-b0d6f17b931e" alt="image"></p>
<h3 id="polling-vs-long-polling">Polling VS Long Polling</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/a3847b4b-f2a0-4aec-af28-5166027761c5" alt="image"></p>
<h3 id="long-polling--완벽-대체가-가능할까">Long Polling : 완벽 대체가 가능할까?</h3>
<blockquote>
<p>Long Polling을 통해 서버에서 이벤트가 발생할때까지 기다렸다가 통신을 통해 기존 Polling의 무자비한 통신을 줄일 수 있다. 하지만 완벽한 대체라고 보기엔 몇 가지를 고려해봐야 한다.</p>
</blockquote>
<ul>
<li>이벤트 발생의 주기<ul>
<li>이벤트 발생의 주기가 짧다면 지속적으로 서버 클라이언트 사이의 연결과 통신이 발생하게 된다. 즉 이벤트가 자주 발생한다면 <strong>polling 방식과 큰 차이가 없다는 것</strong>이다. (오히려 성능이 더 떨어질 수 있다)</li>
</ul>
</li>
<li>다수의 사용자에게 동시에 이벤트 발생 시 ?<ul>
<li>모든 클라이언트들이 동시에 요청을 보내 서버에 부담 발생</li>
</ul>
</li>
</ul>
<h2 id="부록--sse--server-sents-events-">부록 : SSE ( Server-Sents-Events )</h2>
<hr>
<blockquote>
<p>실시간 통신을 구현하는 여러가지 방법 중 SSE라는 방법이 있다. SSE를 한마디로 요약하자면 <strong>서버에서만 지속적으로 클라이언트에게 데이터를 전송</strong>하겠다는 것이다.</p>
</blockquote>
<ul>
<li>앞서 다룬 Polling과 Long Polling의 단점으로는 <strong>클라이언트가 요청할때마다 연결이 생성되고 종료</strong>된다는 것이다.<ul>
<li>Connection을 요청마다 만들고 종료하는 것은 지속해서 통신이 발생할 경우 큰 오버헤드로 이어질 수 있다.</li>
</ul>
</li>
<li>실시간 통신을 위해 클라이언트에서 <strong>한번의 요청으로 연결을 생성하고 지속적으로 통신</strong>할 수 있다면 이러한 문제를 해결할 수 있다. (궁극적인 실시간 통신을 가능하게 한다)</li>
</ul>
<blockquote>
<p>SSE의 경우 한번의 요청으로 Connection을 생성하고 Server에서 event가 발생할 때마다 <strong>클라이언트에게 데이터를 전송</strong>한다.</p>
</blockquote>
<p><strong>단방향 통신</strong></p>
<blockquote>
<p>SSE의 가장 큰 특징이다. <strong>SSE는 서버 측에서의 단방향 통신</strong>만을 지원한다.</p>
</blockquote>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/ebd2809e-eb23-43f7-b3bf-bd3d48103098" alt="image"></p>
<h3 id="text_event_stream">TEXT_EVENT_STREAM</h3>
<blockquote>
<p>SSE를 사용할 경우 Media Type은 TEXT_EVENT_STREAM이다.</p>
<ul>
<li>위의 값을 헤더를 통해 지정해야 한다. 위 값이 의미하는 것은 <strong>한번의 요청을 받고 연결을 끊지 않고 지속</strong>한다는 것이다.</li>
</ul>
</blockquote>
<h3 id="왜-부록일까">왜 부록일까?</h3>
<blockquote>
<p>사실 SSE는 실시간 통신의 방법 중 하나로 설명하였지만 구현하고자 하는 ‘실시간 위치 공유’와는 맞지 않는 방법이다. 그 이유는 가장 큰 특징인 <strong>‘서버에서 단방향 통신’</strong>이다.</p>
</blockquote>
<ul>
<li>실시간 위치 공유를 위해서는 클라이언트가 자신의 위치를 서버 측에 지속적으로 전송해야 한다. 서버는 클라이언트로부터 전송받은 위치를 다른 사용자에게 전송해야 한다.</li>
<li>즉 위의 로직을 SSE로 구현하기 위해서는 아래 2가지 동작이 계속해서 같이 수행되어야 한다는 것이다.<ul>
<li>SSE 커넥션에서 서버가 데이터를 전송하는 API</li>
<li>클라이언트가 자신의 위치를 서버에 전송하는 API</li>
</ul>
</li>
</ul>
<h2 id="websocket">WebSocket</h2>
<hr>
<blockquote>
<p>앞서 살펴본 Polling은 단순히 주기를 통해 실시간으로 데이터를 주고 받는 것처럼 보이게 하는 것이었다. 그렇다면 지속적으로 연결을 유지하여 통신하는 방법은 뭐가 있을까? 바로 <strong>WebSocket</strong>이다.</p>
</blockquote>
<ul>
<li>클라이언트와 서버가 <strong>전이중(Full-Duplex) 채널</strong>을 통해 통신</li>
</ul>
<p><strong>동작 과정</strong></p>
<blockquote>
<p>WebSocket을 통한 통신 과정은 크게 연결 - 통신 - 연결 종료 3단계로 구분할 수 있다.</p>
</blockquote>
<h3 id="연결">연결</h3>
<blockquote>
<p>WebSocket 연결 단계에 있어 가장 중요한 점은 <strong>HTTP 위에서 동작</strong>한다는 것이다. ( 정확히는 HTTP 기반의 <strong>2-hand shake 과정</strong>을 거친다. )</p>
</blockquote>
<ol>
<li><p><strong>클라이언트 Request</strong></p>
<ul>
<li><p>클라이언트는 HTTP 기반의 요청 메시지를 전송한다. 메시지를 요약하자면 <strong>‘앞으로의 통신을 WebSocket 프로토콜 위에서 동작하도록 하자’</strong> 라는 것이다.</p>
<pre><code>GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13</code></pre></li>
<li><p>Upgrade : websocket : 클라이언트가 WebSocket 연결을 요청</p>
</li>
<li><p>Sec-WebSocket-Key : WebSocket 프로토콜을 위한 키로서 랜덤한 값이 사용된다.</p>
</li>
</ul>
</li>
<li><p><strong>서버 Response</strong></p>
<ul>
<li><p>서버는 클라이언트의 요청에 대해 HTTP 응답을 반환</p>
<pre><code>HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=</code></pre></li>
</ul>
<p>Sec-WebSocket-Accept : 클라이언트가 보낸 Key 값을 공개 키로 암호화한 값이다. 해당 메시지를 클라이언트에서 받았을 때 정상적인 값이라면 연결이 제대로 성공했다는 것이다.</p>
</li>
</ol>
<h3 id="통신">통신</h3>
<blockquote>
<p>서버와 클라이언트 간의 WebSocket 연결이 성공했다면 이제 전이중 통신이 가능하다.</p>
</blockquote>
<ul>
<li>서버와 클라이언트 간에 전송되는 메시지는 <strong>일반적인 HTTP 메시지 형식보다 가벼운 구조</strong>이다.</li>
<li>텍스트 형식이 아닌 데이터 프레임 형식이다.</li>
<li>웹 소켓의 메시지는 헤더 + 데이터 로 구분된다.<ul>
<li>웹 소켓의 데이터는 <strong>텍스트 or 바이너리 데이터</strong>가 가능하다.</li>
</ul>
</li>
</ul>
<h3 id="장점">장점</h3>
<ul>
<li>매번 connection을 생성하지 않고 한번의 connection 생성으로 통신이 가능 → 오버헤드가 적다.</li>
<li>클라이언트와 서버 간의 양방향 통신 가능</li>
</ul>
<h3 id="websocket--완벽한-방법일까">WebSocket : 완벽한 방법일까?</h3>
<blockquote>
<p>목표했던 ‘그룹원들 간의 위치 정보를 실시간으로 공유’하는데 있어 WebSocket은 적절한 방법이다. 연결이 지속되고 양방향 통신이 가능하기 때문이다. 하지만 WebSocket에도 아쉬운 부분은 있다.</p>
</blockquote>
<h3 id="데이터-형식의-한계">데이터 형식의 한계</h3>
<ul>
<li>WebSocket은 기본적으로 텍스트 or 바이너리 데이터 형식만을 지원한다. 그렇기 때문에 주고 받는 데이터가 간단한 ‘채팅’이라면 효과적인 기능일 수 있다. 하지만 <strong>데이터 형식이 조금만 복잡해진다면 데이터를 parsing하는 과정이 매 통신마다 수행</strong>되어야 한다.</li>
</ul>
<h3 id="그룹별-처리">그룹별 처리</h3>
<ul>
<li>위치가 공유됨에 있어 동일한 그룹 내에서만 데이터가 이동되어야 한다. 즉 그룹 별로 다른 처리가 이루어져야 하는데 <strong>WebSocket은 그룹 관리에 있어 복잡</strong>하다.<ul>
<li>그룹 정보를 별도로 관리해야 한다.</li>
<li>그룹 별 데이터 전송 + 수신 로직을 별도로 구현해야 한다.</li>
</ul>
</li>
</ul>
<h2 id="stomp">STOMP</h2>
<hr>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/8fccf286-3759-486b-94b9-6b13046ea852" alt="image"></p>
<h3 id="simple-text-oriented-messaging-protocol">Simple Text Oriented Messaging Protocol</h3>
<blockquote>
<p>직역하자면 <strong>‘간단한 메시지 전송을 위한 프로토콜‘</strong></p>
</blockquote>
<ul>
<li>STOMP는 일반적으로 <strong>서브 프로토콜</strong>로서 <strong>WebSocket 위에서 동작</strong>한다.</li>
<li>STOMP의 특징<ul>
<li>메시지 브로커의 사용</li>
<li>pub - sub 방식</li>
</ul>
</li>
</ul>
<h3 id="메시지-브로커">메시지 브로커</h3>
<blockquote>
<p>비동기적 메시지 전송을 위한 중간 미들웨어</p>
</blockquote>
<ul>
<li>메시지 브로커를 사용하는 것은 일반적인 통신 방식과 차이점이 있다.<ul>
<li>WebSocket을 사용할 경우 Server와 클라이언트 사이에서 서로 <strong>직접 메시지를 전송</strong>한다.</li>
<li>하지만 중간단에 메시지 브로커를 배치할 경우 <strong>메시지 브로커가 각 사용자에게 메시지를 전송</strong>한다. <strong>(느슨한 결합성)</strong></li>
</ul>
</li>
</ul>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/b3de602d-e699-4e89-a76b-34eb38f3188d" alt="image"></p>
<h3 id="pub--sub">Pub / Sub</h3>
<blockquote>
<p>발행 구독 패턴으로서 비동기 메시징 패러다임이다.</p>
</blockquote>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/81f7c34a-89a9-462a-ac60-b02f2e1d1441" alt="image"></p>
<ul>
<li><strong>발행자</strong>는 <strong>특정 채널로 메시지를 전달</strong>하고 <strong>해당 채널의 구독자만 메시지를 수신</strong>한다.<ul>
<li>발행자는 특정 수신자를 대상으로 전송하는 것이 아닌 특정 토픽(채널)을 대상으로 전송한다.</li>
<li>즉 발행자와 수신자는 서로에 대한 정보 없이도 통신이 가능하다.</li>
</ul>
</li>
<li><strong>1 : N 관계</strong>에 있어 <strong>다중 소비자를 대상</strong>으로 메시지 전달이 가능하다.<ul>
<li>일반적으로 모든 구독자가 메시지를 수신할 때까지 메시지를 보관해야 한다.</li>
</ul>
</li>
</ul>
<h3 id="통신-과정">통신 과정</h3>
<p><img src="https://github.com/jinjoo-lab/Real-Time-Communication-Test/assets/84346055/d2d91e5f-9c65-45cc-ad09-94f219bd517e" alt="image"></p>
<ol>
<li>subscriber는 특정 토픽에 대해 구독 요청을 보낸다. ( Subscribing )</li>
<li>publisher는 특정 토픽에 메시지를 발행한다. ( Publishing )<ul>
<li>이 때 보낸 메시지는 Message Broker에 저장된다.</li>
<li>클라이언트는 자신이 구독한 토픽에 메시지를 전송받는다.</li>
</ul>
</li>
</ol>
<h3 id="stomp의-장점">STOMP의 장점</h3>
<ol>
<li>Message Brocker 사용으로 구독 - 발행 패턴 사용을 통해 <strong>메시지를 브로드캐스팅 가능</strong></li>
<li>Spring Framework 위에서 적용이 쉽다<ul>
<li>Spring에서 단순한 WebSocket을 사용할 때에는 WebSocketHandler를 직접 구현하고 적용해야 한다.</li>
<li>하지만 STOMP를 사용할 경우 @Controller를 통해 쉽게 사용할 수 있다.</li>
</ul>
</li>
</ol>
<h3 id="one-more--message-brocker">One More : Message Brocker</h3>
<blockquote>
<p>STOMP는 기본적으로 내장되어 있는 SimpleBrocker를 지원한다. 하지만 보통 STOMP를 사용할 때 외부 메시지 브로커를 추가해 사용한다.</p>
</blockquote>
<ul>
<li>데이터가 많아진다면, 내장되어있는 SimpleBroker는 철저하게 Spring Boot가 실행되는 곳의 <strong>메모리</strong>를 잡아먹는다.</li>
<li>이를 해결하기 위해 보통 외부 메시지 큐(RabbitMQ)를 많이 사용한다.</li>
</ul>
<h1 id="결론--stomp-선택의-이유">결론 : STOMP 선택의 이유</h1>
<hr>
<p>‘그룹별 실시간 위치 공유’ 기능을 선택하는데 있어 중요하게 고려해야 하는 것은 2가지이다.</p>
<ol>
<li>그룹의 멤버들간의 <strong>지속적</strong> <strong>실시간 통신</strong></li>
<li>그룹간 통신 관리</li>
</ol>
<ul>
<li>1을 고려했을 때 매번 통신을 위해 연결을 생성하고 종료하는 Polling과 Long Polling은 오버헤드가 심하다.</li>
<li>그렇다면 WebSocket과 STOMP가 1을 만족하는 기술인데 이중 pub - sub 구조를 통해 그룹에 대한 구독을 통해 그룹간 통신을 효과적으로 관리할 수 있는 STOMP가 효율적인 방법일 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[RabbitMQ 비동기 처리와 데드레터 처리]]></title>
            <link>https://velog.io/@jinjoo-lab/RabbitMQ-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC%EC%99%80-%EB%8D%B0%EB%93%9C%EB%A0%88%ED%84%B0-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@jinjoo-lab/RabbitMQ-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC%EC%99%80-%EB%8D%B0%EB%93%9C%EB%A0%88%ED%84%B0-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Sun, 21 Apr 2024 15:34:10 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황--접근">문제 상황 &amp; 접근</h2>
<hr>
<ol>
<li><strong>FCM 오류 처리</strong></li>
</ol>
<blockquote>
<p>그룹 초대 , 친구 요청 서비스를 수행하는데 있어 <strong>‘알림 전송 서비스’</strong>를 구현할 때 FCM을 사용했다. </p>
</blockquote>
<ol>
<li>FCM 자체의 성능 개선을 위해 RabbitMQ을 사용함. </li>
<li>FCM 요청에 있어 오류가 발생할 경우 처리를 위해 데드레터 처리<blockquote>
</blockquote>
</li>
</ol>
<h2 id="rabbitmq를-통한-비동기-전송">RabbitMQ를 통한 비동기 전송</h2>
<blockquote>
<p><strong>기존 Spring 서버에서 FCM 까지의 메시지 전송</strong>은 <strong>동기방식</strong>이기 때문에 짧은 시간에 많은 요청이 수행되는 경우 성능상  개선 필요, <strong>메시지 큐를 사용하여 비동기 방식</strong>으로 처리</p>
</blockquote>
<p><strong>Message Type</strong> 정의</p>
<ul>
<li>이길저길 내에서 FCM을 통한 알림 전송은 크게 4가지 유형이 있다.</li>
</ul>
<pre><code class="language-java">public enum NotificationType {
    FRIEND_REQUEST, // 친구 요청
    GROUP_REQUEST, // 그룹 참여 요청
    DESTINATION_CHANGE, // 목적지 변경
    PLAN_REQUEST // 만남 참여 요청
}</code></pre>
<p><strong>NotificationRequest 정의</strong></p>
<pre><code class="language-java">    public NotificationRequest(
            final String deviceToken, final String title, final String body, final String id) {
        this.deviceToken = deviceToken;
        this.title = title;
        this.body = body;
        this.id = id;
    }</code></pre>
<p><strong>FcmProducer : RabbitMQ에 알림 메시지 저장</strong></p>
<blockquote>
<p>알림관련 정보 즉, rabbitmq로 발행할 메시지 정보들을 db에 저장한 이후 rabbitmq로 알림 발송을 위한 메시지를 발행한다.</p>
</blockquote>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class FcmProducer {

    private final RabbitTemplate rabbitTemplate;
    private final NotificationRepository notificationRepository;
    private final NotificationMapper notificationMapper;

        // 알림 전송 (DB 저장 후 큐로 전송)
    public void sendNotification(final NotificationRequest request, final NotificationType type) {
        final UUID id =
                notificationRepository
                        .save(
                                new Notification(
                                        request.getTitle(),
                                        request.getBody(),
                                        request.getId(),
                                        request.getDeviceToken(),
                                        type))
                        .getId();

        request.setNotificationId(id.toString());

        sendToQueue(request);
    }

        // 알림 메시지를 큐로 전송
    private void sendToQueue(final NotificationRequest request) {
        rabbitTemplate.convertAndSend(
                RabbitMQConstant.NOTIFICATION_EXCHANGE.getName(),
                RabbitMQConstant.NOTIFICATION_ROUTING_KEY.getName(),
                request);
    }
}</code></pre>
<p><strong>FcmCosumer : 큐에 저장된 메시지를 FCM 전송</strong></p>
<ol>
<li>FCM을 통해 알림을 전송</li>
<li>DB에 저장된 알림 Entity 조회</li>
<li>엔티티 존재 여부에 따른 처리<ul>
<li>존재하는 ID의 경우 : complete() 메서드를 통해 해당 메시지의 전송 완료 처리</li>
<li>존재하지 않는 경우 : nack을 통해 재시도하지 않는다.</li>
</ul>
</li>
</ol>
<pre><code class="language-java">    @Transactional
    @RabbitListener(queues = &quot;notification.queue&quot;)
    public void sendNotification(
            final NotificationRequest request,
            final Channel channel,
            @Header(AmqpHeaders.DELIVERY_TAG) final long tag)
            throws FirebaseMessagingException, IOException {
        firebaseMessaging.send(request.toMessage());

        final Optional&lt;Notification&gt; notification =
                notificationRepository.findById(UUID.fromString(request.getNotificationId()));

        if (notification.isPresent()) {
            notification.get().complete();
            return;
        }
        channel.basicNack(tag, false, false);
    }</code></pre>
<p><strong>(적용) 만남 초대 서비스에서의 사용</strong></p>
<pre><code class="language-java">    @Transactional
    public PlanResponse invitePlan(PlanMemberRequest request) {
        Member member = authService.getMemberByJwt();
        Plan plan = getPlanEntity(request.getPlanId());
        plan.addMember(member);

                // 만남 초대 알림 전송
        sendRequestNotification(member.getDeviceTokenValue(), plan.getName(), plan.getId());

        return planMapper.toPlanResponse(plan);
    }

    private void sendRequestNotification(
            final String deviceToken, final String planName, final UUID id) {
        fcmProducer.sendNotification(
                new NotificationRequest(
                        deviceToken,
                        NotificationTitle.PLAN_REQUEST_TITLE.getName(),
                        NotificationBody.PLAN_REQUEST_BODY.toNotificationBody(planName),
                        id.toString()),
                NotificationType.PLAN_REQUEST);
    }</code></pre>
<h2 id="데드레터-전략-수립">데드레터 전략 수립</h2>
<h3 id="데드-레터dead-letter">데드 레터(Dead Letter)</h3>
<blockquote>
<p>소프트웨어 시스템에서 오류로 인해 처리할 수 없는 메시지</p>
</blockquote>
<h3 id="dead-letter-queue">Dead Letter Queue</h3>
<blockquote>
<p>데드 레터 메시지를 임시로 저장하는 특수 유형의 메시지 큐</p>
</blockquote>
<ul>
<li>기본적인 Message Queue는 FIFO 방식이다. 다음 메시지를 처리하기 위해서는 이전의 메시지들이 어떤 형태로든 처리되어야 한다.</li>
<li>앞의 메시지에 장애가 발생할 경우 DLQ를 통해 유연한 처리가 가능하도록 해야 한다.</li>
</ul>
<h3 id="데드레터-파악--재시도-전략">데드레터 파악 &amp; 재시도 전략</h3>
<ul>
<li>여러 단계에 걸친 RabbitMQ를 통한 로직은 없었으며, 로깅을 수행한 이후 개발자의 <strong>Slack으로 알림을 전송</strong>한다.</li>
<li>DB에 데드레터 처리 여부 및 요청 데이터를 갖는 테이블을 추가하여 상태 필드로 처리 여부 확인 및 RabbitMQ 장애를 파악한다.<ul>
<li>RabbitMQ로 보내기 직전 <strong>DB에 데이터를 저장</strong>한다.</li>
<li>RabbitMQ로 처리 완료되면 <strong>해당 row의 상태를 업데이트</strong>한다.</li>
</ul>
</li>
<li>3초 간격으로 최대 2번 재시도한다.<ul>
<li>Firebase 관련 로직이기에 치명적으로 오랜 기간 장애가 발생할 가능성은 적다고 판단해 일시적 오류를 고려하여 재시도 하도록 설정했다.</li>
</ul>
</li>
</ul>
<h3 id="데드레터-발생-시-slack-알림-처리"><strong>데드레터 발생 시 Slack 알림 처리</strong></h3>
<ol>
<li>알림을 FCM으로 전송하는 RabbitMQ에서 오류 발생 시 DLQ로 메시지 전송</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jinjoo-lab/post/4fc06544-a7c0-4a19-9838-5510fe24d4b9/image.png" alt=""></p>
<ol>
<li>데드레터 발생 시 로그를 남기고 슬랙으로 예외 상황 정보를 전송</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jinjoo-lab/post/ec140f16-1196-40cd-ba38-2124f38983fd/image.png" alt=""></p>
<p><strong>데드레터 발생 시 슬랙 알림</strong></p>
<p><img src="https://velog.velcdn.com/images/jinjoo-lab/post/33e8ecbe-e2aa-4565-8250-d06064fa3569/image.png" alt=""></p>
<h2 id="개선해볼-사항">개선해볼 사항</h2>
<h3 id="메시지-유실-상황-고려">메시지 유실 상황 고려</h3>
<blockquote>
<p>현재는 알림 Entity의 상태를 실패로 미리 저장해둔 다음 성공시 상태를 성공으로 업데이트
DB를 확인했을 때 일정 시간(대략 30초) 성공으로 업데이트되지 않으면 실패했다고 인지</p>
</blockquote>
<h3 id="관리자-권한-api">관리자 권한 API</h3>
<blockquote>
<p>메시지가 유실된 경우 관리자 권한의 API를 생성하여 유실된 알림 재전송 구현</p>
</blockquote>
<ul>
<li>모든 실패 상태의 알림을 발송하기에는 현재진행 중인 알림이 있을 수 있기에 id를 직접 받아 처리</li>
<li>직접 관리자가 확인하여 수동으로 처리해야하기에 이 부분에 대한 개선 방안이 있을지 고민 중</li>
</ul>
<p><strong>관리자 권한 재전송 API</strong></p>
<p><img src="https://velog.velcdn.com/images/jinjoo-lab/post/3acb13b6-6f6d-4c72-9bc2-95ab0bb14374/image.png" alt=""></p>
<p><strong>알림 발송 로직</strong></p>
<p><img src="https://velog.velcdn.com/images/jinjoo-lab/post/3cd4ee96-503f-45c3-a56a-5a68a25a174d/image.png" alt=""></p>
<p><strong>이러한 수동적인 방법 말고 메시지 유실 자체를 방지하는 방법은?</strong></p>
<ul>
<li>서칭해본 결과 saga 혹은 outbox 패턴 등이 있다고 하지만 러닝커브가 심할 것으로 예상되며 미숙한 숙련도에서 사용하기에 적합하지 않다고 판단했다.</li>
<li>이후 역량을 더욱 키운 후에 시스템의 안정성을 위해 고려해보고 싶다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Resilience4j & 슬랙 모니터링]]></title>
            <link>https://velog.io/@jinjoo-lab/Resilience4j-%EC%8A%AC%EB%9E%99-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81</link>
            <guid>https://velog.io/@jinjoo-lab/Resilience4j-%EC%8A%AC%EB%9E%99-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81</guid>
            <pubDate>Sat, 20 Apr 2024 12:38:50 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황">문제상황</h2>
<hr>
<blockquote>
<p>사용자 경로 제공에 있어 Kakao, Naver, Tmap Open API를 사용하였고 Open API에 장애가 발생한 경우 처리에 있어 유연한 처리가 필요함을 느낌</p>
</blockquote>
<h3 id="접근">접근</h3>
<hr>
<ul>
<li>Resilience4j를 통해 Open API에 장애가 발생한 경우 close - open - half open 단계에 걸쳐 유연한 처리</li>
</ul>
<h3 id="설명">설명</h3>
<hr>
<ul>
<li>Close<ul>
<li>Open API에 장애가 발생하지 않은 경우 → 기존과 동일하게 로직 수행</li>
</ul>
</li>
<li>Open<ul>
<li>Open API에 장애가 발생 ( Open API의 실패율이 30%에 도달한 경우)</li>
<li>이후 10초간 Open API 요청을 수행하지 않고 Default Failure 처리</li>
</ul>
</li>
<li>Half Open<ul>
<li>Open API 처리가 정상화된 경우 → Close 상태 변경</li>
<li>정상화되지 않은 경우 → Open 상태 변경</li>
</ul>
</li>
</ul>
<h3 id="모니터링">모니터링</h3>
<ul>
<li>서킷브레이커의 상태 확인이 필요해 모니터링 시스템을 도입했다.</li>
<li>장애 상황을 빠르게 파악하고자 AlertManager로 슬랙 알림 발송하도록 구성했다.</li>
<li>Spring Actuator 중 Prometheus와 Resilience 4J 를 사용하여 모니터링을 수행했다.</li>
</ul>
<h3 id="grafana-대시보드로-확인한-메트릭과-사례">Grafana 대시보드로 확인한 메트릭과 사례</h3>
<ul>
<li>[부하테스트시] Request Latency 확인
개선 작업시 API Latency를 확인하며 성능테스트를 수행하는 경우가 많았다.</li>
<li>[운영시] Request Status code, 서킷브레이커 Status별 개수 확인
상태코드를 중점적으로 확인하며 에러 상태코드 발생 부분 확인
시큐리티의 에러를 확인하여 ControllerAdvice로 에러 핸들링하도록 수정</li>
</ul>
<h2 id="분석">분석</h2>
<hr>
<h2 id="알림-시스템">알림 시스템</h2>
<ul>
<li>ex ) Slack 알림으로 Docker Container가 종료되어 알림이 발송되었다.
<img src="https://velog.velcdn.com/images/jinjoo-lab/post/ad414857-1b82-4f06-97d1-4782390ee71a/image.png" alt=""></li>
</ul>
<h2 id="resilience4j-모니터링">Resilience4j 모니터링</h2>
<ul>
<li>서킷브레이커 1개가 OpenAPI 장애로 Open 돼있고 3개가 Close 돼있는 것을 볼 수 있다.</li>
</ul>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/b6ab3b74-f99a-4b14-8b51-74311484b91d" alt="image"></p>
<h2 id="springboot-서버-모니터링-jvm-관련-지표">SpringBoot 서버 모니터링 (JVM 관련 지표)</h2>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/eddf8eca-c91f-428d-aed1-e4af9a7d30cf" alt="image"></p>
<h2 id="springboot-서버-모니터링-요청-로그-관련-지표">SpringBoot 서버 모니터링 (요청, 로그 관련 지표)</h2>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/8f225ef4-bf0a-4986-a1c9-fc79e32768b8" alt="image"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Open API 결과에 Redis Cache 적용기]]></title>
            <link>https://velog.io/@jinjoo-lab/Open-API-%EA%B2%B0%EA%B3%BC%EC%97%90-Redis-Cache-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@jinjoo-lab/Open-API-%EA%B2%B0%EA%B3%BC%EC%97%90-Redis-Cache-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Sat, 20 Apr 2024 12:32:53 GMT</pubDate>
            <description><![CDATA[<h1 id="redis-cache-성능-비교">Redis Cache 성능 비교</h1>
<h2 id="문제상황">문제상황</h2>
<hr>
<blockquote>
<p>사용자에게 경로를 제공하기 위해 Kakao, Naver,  Tmap Open API를 사용한다. 위 API 제공에 있어 호출까지의 <strong>시간이 소요</strong>되고 <strong>비용 낭비</strong>가 발생</p>
</blockquote>
<ul>
<li>이미 반환된 경로를 사용자가 재사용하는 경우가 많기 때문에 경로를 임시 메모리에 저장하는 로직 필요</li>
<li>‘경로’라는 데이터 특성 상 한번 참조된 값에 대해 변경이 자주 일어나지 않음</li>
</ul>
<h3 id="접근">접근</h3>
<ul>
<li><strong>Redis Cache를 이용</strong>하여 Open API의 반환값을 일부 저장하여 사용자에게 제공</li>
</ul>
<h3 id="적용">적용</h3>
<ul>
<li>@CacheEvict와 @Cacheable Annotation을 사용하여 API에 레디스 캐시 적용</li>
</ul>
<h3 id="시퀀스-다이어그램">시퀀스 다이어그램</h3>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/6cb63e15-767d-4a21-89cc-91724323f0b0" alt="image"></p>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/3375f917-e88e-480a-a3f1-bbcbd401557d" alt="image"></p>
<h3 id="시나리오">시나리오</h3>
<blockquote>
<p>Redis Cache를 적용하였을 때 성능 분석을 위해 다음과 같은 시나리오를 세우고 테스트 진행</p>
</blockquote>
<ol>
<li>회원가입 수행</li>
<li>보행자 경로 API를 요청하여 수행</li>
</ol>
<h2 id="결론">결론</h2>
<hr>
<h2 id="평균-latency-412배-향상">평균 Latency 4.12배 향상</h2>
<ul>
<li>추가로, 매번 OpenAPI를 호출하지 않기 때문에 네트워크 비용을 아끼는 효과도 있음</li>
</ul>
<h2 id="동기-처리-평균-latency-9188ms">동기 처리 [평균 Latency 91.88ms]</h2>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/cf1f5fe9-4aa9-48e2-96e5-0e89a53cbbc7" alt="image"></p>
<h2 id="비동기-처리-평균-latency-2226ms">비동기 처리 [평균 Latency 22.26ms]</h2>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/e3d1fb30-0f25-4c79-a4b2-1e1a9a9b45d4" alt="image"></p>
<h3 id="분석">분석</h3>
<hr>
<ol>
<li>Redis Cache를 사용할 경우 사용하지 않은 경우에 비해 4.12배의 속도 성능 향상</li>
<li>Open API를 중복 요청하지 않아 네트워크 비용 절감</li>
<li>CacheEvict를 통해 사용자의 요청 경로가 변경된 경우 기존 Cache값을 삭제하고 변경된 경로를 저장하고 제공하여 서비스상 유연성 향상</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[전략패턴을 이용한 단위 테스트 적용기]]></title>
            <link>https://velog.io/@jinjoo-lab/%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@jinjoo-lab/%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Sat, 20 Apr 2024 12:28:48 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황">문제상황</h2>
<hr>
<blockquote>
<p>‘TWTW’의 테스트는 크게 Controller, Service, Repository Layer에서 진행되었다. 우리의 목표는 단위 테스트 적용이었다. 하지만 문제점을 발견하였다.</p>
</blockquote>
<h3 id="첫-번째-문제점">첫 번째 문제점</h3>
<ul>
<li>Service 테스트 코드 내에서 <strong>Repository를 통한 실제 DB 접근</strong>이 이루어져 완벽한 단위 테스트를 수행할 수 없었다.</li>
</ul>
<h3 id="두-번째-문제점">두 번째 문제점</h3>
<ul>
<li>첫 번째 문제점을 해결하기 위해 Service 테스트 내에서 <strong>Mocking 처리</strong>를 하였다. 하지만 Repository와의 상호작용마다 Mocking 처리를 함으로서 Mock의 사용량이 많이 증가했다.</li>
</ul>
<h2 id="접근-방식">접근 방식</h2>
<hr>
<ul>
<li><strong>Repository Test</strong><ul>
<li>DB를 통한 접근이 수행되는가에 초점을 맞추어 테스트 코드 작성</li>
</ul>
</li>
<li><strong>Service Test</strong><ul>
<li>Stub을 활용하여 DB 접근을 하지 않고 서비스 로직에만 초점을 맞추어 테스트 코드 작성</li>
</ul>
</li>
<li><strong>Controller Test</strong><ul>
<li>mock을 활용하여 Service 로직을 타지 않고 테스트 수행</li>
<li>테스트를 수행하면서 자동으로 rest docs 생성</li>
</ul>
</li>
</ul>
<h3 id="테스트용-repository-분리">테스트용 Repository 분리</h3>
<hr>
<p><strong>전략 패턴을 사용한 전체 구조</strong></p>
<h3 id="전략-패턴이란-">전략 패턴이란 ?</h3>
<blockquote>
<p>객체들이 할 수 있는 행위 각각에 대해 <strong>전략 클래스를 생성</strong>하고, 유사한 <strong>행위들을 캡슐화 하는 인터페이스</strong>를 정의하여,</p>
<p>객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 <strong>전략을 바꿔주기만 함</strong>으로써 행위를 유연하게 확장하는 방법</p>
</blockquote>
<h3 id="repository에-적용">Repository에 적용</h3>
<ul>
<li>행위들을 정의한 <strong>Repository 인터페이스</strong>를 정의<ul>
<li><strong>실제 서비스 로직</strong>에서는 JpaMemberRepository 즉 <strong>운영 레포</strong>를 사용</li>
<li><strong>테스트 로직</strong>에서는 StubMemberRepository 즉 <strong>테스트 레포</strong>를 사용</li>
</ul>
</li>
</ul>
<p><img src="https://github.com/HongDam-org/TWTW/assets/89020004/2b4a97bc-1627-4bf8-94c9-235563630de2" alt="img"></p>
<p><strong>Repository의 추상화</strong></p>
<pre><code class="language-java">@Repository
public interface MemberRepository {
    List&lt;Member&gt; findAllByNickname(final String nickname);

    List&lt;Member&gt; findAllByNicknameContainingIgnoreCase(final String nickname);

    Optional&lt;Member&gt; findByOAuthIdAndAuthType(final String oAuthId, final AuthType authType);

    boolean existsByNickname(final String nickname);

    Member save(final Member member);

    Optional&lt;Member&gt; findById(final UUID id);

    List&lt;Member&gt; findAllByIds(final List&lt;UUID&gt; friendMemberIds);

    void deleteById(final UUID memberId);
}
</code></pre>
<p><strong>실제 서비스용 JpaRepository</strong></p>
<pre><code class="language-java">@Repository
public interface JpaMemberRepository extends JpaRepository&lt;Member, UUID&gt;, MemberRepository {

    @Query(
            value =
                    &quot;SELECT * FROM member m WHERE MATCH (m.nickname) AGAINST(:nickname IN BOOLEAN&quot;
                        + &quot; MODE)&quot;,
            nativeQuery = true)
    List&lt;Member&gt; findAllByNickname(@Param(&quot;nickname&quot;) String nickname);

    @Query(
            &quot;SELECT m FROM Member m WHERE m.oauthInfo.clientId = :oAuthId AND&quot;
                    + &quot; m.oauthInfo.authType = :authType&quot;)
    Optional&lt;Member&gt; findByOAuthIdAndAuthType(
            @Param(&quot;oAuthId&quot;) String oAuthId, @Param(&quot;authType&quot;) AuthType authType);

    @Query(&quot;SELECT m FROM Member m WHERE m.id in :friendMemberIds&quot;)
    List&lt;Member&gt; findAllByIds(@Param(&quot;friendMemberIds&quot;) final List&lt;UUID&gt; friendMemberIds);
}
</code></pre>
<p><strong>테스트용 StubRepository</strong></p>
<pre><code class="language-java">public class StubMemberRepository implements MemberRepository {

    private final Map&lt;UUID, Member&gt; map = new HashMap&lt;&gt;();

    @Override
    public List&lt;Member&gt; findAllByNickname(final String nickname) {
        return map.values().stream()
                .filter(
                        member -&gt;
                                member.getNickname().toUpperCase().contains(nickname.toUpperCase()))
                .toList();
    }

    @Override
    public List&lt;Member&gt; findAllByNicknameContainingIgnoreCase(final String nickname) {
        return map.values().stream()
                .filter(
                        member -&gt;
                                member.getNickname().toUpperCase().contains(nickname.toUpperCase()))
                .toList();
    }

    @Override
    public Optional&lt;Member&gt; findByOAuthIdAndAuthType(
            final String oAuthId, final AuthType authType) {
        return map.values().stream()
                .filter(
                        member -&gt; {
                            final OAuth2Info oauthInfo = member.getOauthInfo();
                            return oauthInfo.getClientId().equals(oAuthId)
                                    &amp;&amp; oauthInfo.getAuthType().equals(authType);
                        })
                .findFirst();
    }

    @Override
    public boolean existsByNickname(final String nickname) {
        return map.values().stream().anyMatch(member -&gt; member.getNickname().equals(nickname));
    }

    @Override
    public Member save(final Member member) {
        map.put(member.getId(), member);
        return member;
    }

    @Override
    public void deleteById(final UUID memberId) {
        map.remove(memberId);
    }
}
</code></pre>
<blockquote>
<p>각 기능별 StubRepository를 만든 후 StubConfig를 통해 테스트 시 빈으로 주입되도록 설정</p>
</blockquote>
<pre><code class="language-java">@TestConfiguration
public class StubConfig {

    private final Map&lt;UUID, Friend&gt; map = new HashMap&lt;&gt;();

    @Bean
    @Primary
    public FriendQueryRepository stubFriendQueryRepository() {
        return new StubFriendQueryRepository(map);
    }

    @Bean
    @Primary
    public FriendCommandRepository stubFriendCommandRepository() {
        return new StubFriendCommandRepository(map);
    }

    @Bean
    @Primary
    public MemberRepository memberRepository() {
        return new StubMemberRepository();
    }

    @Bean
    @Primary
    public PlanRepository planRepository() {
        return new StubPlanRepository();
    }
}
</code></pre>
<h3 id="repository-test">Repository Test</h3>
<hr>
<blockquote>
<p>Repository 테스트 시 실제 DB와의 상호작용을 테스트하도록 코드 작성</p>
</blockquote>
<pre><code class="language-java">@DisplayName(&quot;MemberRepository의&quot;)
class MemberRepositoryTest extends RepositoryTest {

    @Autowired private MemberRepository memberRepository;

    @Test
    @DisplayName(&quot;PK를 통한 저장/조회가 성공하는가?&quot;)
    void saveAndFindId() {
        // given
        final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity());

        // when
        final UUID expected = member.getId();
        final Member result = memberRepository.findById(expected).orElseThrow();

        // then
        assertThat(result.getId()).isEqualTo(member.getId());
    }

    @Test
    @DisplayName(&quot;soft delete가 수행되는가?&quot;)
    void softDelete() {
        // given
        final Member member = MemberEntityFixture.FIRST_MEMBER.toEntity();
        final UUID memberId = memberRepository.save(member).getId();

        // when
        memberRepository.deleteById(memberId);

        // then
        assertThat(memberRepository.findById(memberId)).isEmpty();
    }
}</code></pre>
<h3 id="service-test">Service Test</h3>
<hr>
<blockquote>
<p>서비스 로직 테스트를 위해 StubRepository를 이용하여 테스트 작성</p>
</blockquote>
<ul>
<li>MemberServiceTest의 경우 주입받는 memberRepository는 StubRepository</li>
</ul>
<pre><code class="language-java">@DisplayName(&quot;MemberService의&quot;)
class MemberServiceTest extends LoginTest {
    @Autowired private MemberService memberService;
    @Autowired private MemberRepository memberRepository;

    @Test
    @DisplayName(&quot;닉네임 중복 체크가 제대로 동작하는가&quot;)
    void checkNickname() {
        // given
        final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity());
        // when
        DuplicateNicknameResponse response = memberService.duplicateNickname(member.getNickname());
        // then
        assertTrue(response.getIsPresent());
    }

    @Test
    @DisplayName(&quot;UUID를 통해 Member 조회가 되는가&quot;)
    void getMemberById() {
        // given
        final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity());

        // when
        Member response = memberService.getMemberById(member.getId());

        // then
        assertThat(response.getId()).isEqualTo(member.getId());
    }

    @Test
    @DisplayName(&quot;Member가 MemberResponse로 변환이 되는가&quot;)
    void getResponseByMember() {
        // given
        final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity());

        // when
        MemberResponse memberResponse = memberService.getResponseByMember(member);

        // then
        assertThat(memberResponse.getMemberId()).isEqualTo(member.getId());
    }

    @Test
    @DisplayName(&quot;Nickname을 통한 Member 검색이 수행되는가&quot;)
    void searchMemberByNickname() {
        // given
        final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity());

        // when
        final List&lt;MemberResponse&gt; responses =
                memberService.getMemberByNickname(member.getNickname().substring(0, 1));

        // then
        assertThat(responses).isNotEmpty();
    }
}
</code></pre>
<h3 id="controller-test">Controller Test</h3>
<hr>
<blockquote>
<p>컨트롤러 Layer에서의 Request &amp; Response 테스트를 위해 Service를 mock으로 만들어 테스트 작성</p>
</blockquote>
<pre><code class="language-java">    @Test
    @DisplayName(&quot;닉네임이 중복되었는가&quot;)
    void duplicate() throws Exception {
        final DuplicateNicknameResponse expected = new DuplicateNicknameResponse(false);
        given(memberService.duplicateNickname(any())).willReturn(expected);

        final ResultActions perform =
                mockMvc.perform(
                        get(&quot;/member/duplicate/{name}&quot;, &quot;JinJooOne&quot;)
                                .contentType(MediaType.APPLICATION_JSON));

        // then
        perform.andExpect(status().isOk()).andExpect(jsonPath(&quot;$.isPresent&quot;).exists());
        // docs

        perform.andDo(print())
                .andDo(
                        document(
                                &quot;get duplicate nickname&quot;,
                                getDocumentRequest(),
                                getDocumentResponse()));
    }
</code></pre>
<h2 id="why-test-double">Why Test Double</h2>
<h3 id="검증-대상">검증 대상</h3>
<blockquote>
<p>다시 문제 상황으로 돌아가서 프로젝트에서 Test Double을 사용해야 하는 곳은 Repository이다. 근본적으로 Repository란 무엇인지 생각해봤을 때 DAO의 역할을 한다. 데이터 베이스와 상호작용하며 데이터를 가지고 온다.</p>
</blockquote>
<ul>
<li>StubRepository를 적용하는 곳은 Service 계층이다. 즉 데이터 베이스로부터 데이터를 읽고 가져오는 과정을 검증하는 것이 아닌 데이터를 가져와 <strong>서비스의 로직을 검증</strong>하는 것이다.</li>
<li>Service 테스트 로직에 집중하기 위해 Test Double을 사용하는 것이 좋을 것 같아 선택하였다.</li>
</ul>
<h2 id="why-stub">Why Stub</h2>
<blockquote>
<p>Stub과 Mock은 사실 크게 다른 차이점을 보이지 않는다. 둘다 Test Double로서 ‘실제 호출될 테스트에 대한 미리 예상된 결과를 제공한다’라는 의미를 지닌다.</p>
</blockquote>
<h3 id="상태-검증-vs-행위-검증">상태 검증 VS 행위 검증</h3>
<ul>
<li>상태 검증 : 메서드가 수행 시 연관된 <strong>객체의 상태에 초점</strong>을 두고 검증</li>
<li>행위 검증 : 메서드가 수행시 참조하는 객체의 <strong>메서드 즉 동작이 제대로 수행</strong>되는지에 대해 초점</li>
</ul>
<h3 id="stub">Stub</h3>
<blockquote>
<p>테스트 로직에 있어 Stub을 선택하게 된 이유로는 크게 2가지가 있다.</p>
</blockquote>
<ol>
<li><strong>동적 테스트 실행</strong></li>
</ol>
<blockquote>
<p>Mock을 사용할 경우 설정한 값만 계속해서 반환을 하게 된다. 하지만 Stub을 이용할 경우 Repository를 HashMap으로 관리하기 때문에 테스트 중 저장한 값에 따라 다른 반환을 유도할 수 있기 때문에 동적 테스트가 가능하다.</p>
</blockquote>
<ol start="2">
<li><strong>무분별한 Mocking 처리 방지</strong></li>
</ol>
<h3 id="분석">분석</h3>
<hr>
<ul>
<li><strong>Stub을 사용하여 유연한 처리</strong><ul>
<li>Repository Layer가 <strong>JPA에 종속적이지 않고</strong> 테스트에 용이한 <strong>유연한 구조</strong> 가져감</li>
<li>Controller 테스트의 경우 하나의 메서드만 mocking하면 되었지만, Service 테스트에서는 많은 의존성 때문에 모두 mock으로 처리하기에 부담, 같은 메서드도 매번 mock 처리하기에도 어려움</li>
</ul>
</li>
<li><strong>TestContainer 도입</strong><ul>
<li>MySQL에서 제공하는 기능을 기존에 사용하던 테스트용 H2 DB에서 지원하지 않음(FULL TEXT INDEX)</li>
<li>Redis, RabbitMQ와 같은 외부 시스템과 연동되는 부분을 원활히 테스트</li>
</ul>
</li>
</ul>
<h3 id="테스트-커버리지">테스트 커버리지</h3>
<hr>
<ul>
<li><p>Jacoco 도입으로 테스트시와 PR시 커버리지 확인 가능</p>
<p>  <img src="https://github.com/HongDam-org/TWTW/assets/89020004/3d90c15c-9020-4df8-82d0-3669c1d58250" alt="img"></p>
</li>
<li><p>Jacoco 커버리지
<img src="https://github.com/HongDam-org/TWTW/assets/89020004/c670bcd9-c719-428a-932e-bc017148c148" alt="img"></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[FULL TEXT INDEX 적용기]]></title>
            <link>https://velog.io/@jinjoo-lab/FULL-TEXT-INDEX-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@jinjoo-lab/FULL-TEXT-INDEX-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Sat, 20 Apr 2024 11:37:58 GMT</pubDate>
            <description><![CDATA[<h1 id="full-text-index-적용기">Full Text Index 적용기</h1>
<h2 id="문제-상황">문제 상황</h2>
<hr>
<blockquote>
<p>‘이길저길’ 서비스 내에서 친구 검색 시 Member의 <strong>닉네임(문자열)으로 검색을 수행하는 API</strong>가 있고 기존 Like 검색은 데이터의 개수가 많아질수록 <strong>성능에서 부족함</strong>이 있었다.</p>
</blockquote>
<h2 id="like-연산시">Like 연산시</h2>
<hr>
<p>Member 데이터를 500만 row 저장하고 특정 닉네임을 검색하는 쿼리를 Like 연산을 이용하여 수행하였다.</p>
<h3 id="준비-작업-프로시저">준비 작업 (프로시저)</h3>
<ol>
<li>sql로 프로시저 작성 (그대로 실행하면 500만개의 member를 insert한다)<ul>
<li>기존 자바 코드로 500만개를 넣으려 시도하니 heap 영역의 공간이 없어 error 발생 !</li>
</ul>
</li>
</ol>
<pre><code class="language-sql">DELIMITER $$
DROP PROCEDURE IF EXISTS twtw.insertLoop$$

CREATE PROCEDURE twtw.insertLoop()
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i &lt;= 5000000 DO
        INSERT INTO twtw.MEMBER(id, created_at, updated_at, auth_type, nickname, profile_image, role, client_id)
                VALUES (UNHEX(REPLACE(uuid(), &#39;-&#39;, &#39;&#39;)), now(), now(), &#39;APPLE&#39;, concat(&#39;n&#39;, i), concat(&#39;profile_image_&#39;, i), &#39;ROLE_USER&#39;, concat(&#39;client_id&#39;, i));
        SET i = i + 1;
    END WHILE;
END$$
DELIMITER $$

CALL twtw.insertLoop;
$$</code></pre>
<ul>
<li>500만 row가 insert 됐다.</li>
</ul>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/a4cc5c7b-39db-4aa3-89cd-91c97faee2cc" alt="image"></p>
<h3 id="like-연산-결과">Like 연산 결과</h3>
<pre><code class="language-sql">SELECT * FROM member WHERE nickname like &#39;%123456%&#39;;</code></pre>
<h3 id="like-연산-쿼리-결과-263s">like 연산 쿼리 결과 [2.63s]</h3>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/57bf2918-b2fe-4614-bd08-8499168b1d9a" alt="image"></p>
<h1 id="full-text-index-적용">Full Text Index 적용</h1>
<hr>
<blockquote>
<p>Like 쿼리의 성능이 좋지 않아 효과적인 문자열 탐색에 있어 <strong>Full Text Index를 적용</strong>하기로 했다.</p>
</blockquote>
<ul>
<li>FULL TEXT INDEX 인덱스 적용</li>
</ul>
<pre><code class="language-sql">CREATE FULLTEXT INDEX idx_member_nickname ON member (nickname) with parser ngram;</code></pre>
<ul>
<li>FULL TEXT INDEX 쿼리</li>
</ul>
<pre><code class="language-sql">SELECT * FROM member WHERE MATCH(nickname) AGAINST(&#39;123456&#39; IN BOOLEAN MODE);</code></pre>
<h2 id="첫번째-시도">첫번째 시도</h2>
<h3 id="match-쿼리-결과-error">match 쿼리 결과 [error]</h3>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/a6f0f6e8-f723-481d-abd9-7874d2cdc0ad" alt="image"></p>
<ul>
<li><strong>ngram</strong></li>
</ul>
<blockquote>
<p>문자열을 모두 [디폴트 최소 길이: 2] 만큼 다 분할해 저장했기 때문에 500만 데이터를 쿼리하는데 드는 비용이 커져 <strong>쿼리 캐시 공간이 부족</strong>했다.</p>
</blockquote>
<ul>
<li><strong>캐시 사이즈 2배 늘려봤지만</strong> 에러는 해결이 되지 않음</li>
</ul>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/0e62fe27-b3ae-4450-95fb-e347efa8a713" alt="image"></p>
<blockquote>
<p>캐시 사이즈를 늘린다면 메모리 부담이 발생하므로 <strong>최적의 방법이라 판단하지 않았다 !</strong></p>
</blockquote>
<ul>
<li>다른 해결 방법인 [인덱스 분할], [ElasticSearch 도입], [하드웨어 리소스 확장]은 오버 엔지니어링이라 판단하고 다른 해결방법 고민해보았다 !</li>
</ul>
<h3 id="분석">분석</h3>
<ul>
<li>테스트시 최대 길이 20의 문자열을 더미데이터로 삽입한 부분이 문제라 판단</li>
</ul>
<blockquote>
<p>현재 서비스 요구사항에서 닉네임의 길이 제한이 없다는 것에 의문을 두고 팀원들과 회의를 거쳐 닉네임 길이의 최대치를 정하기로 결정</p>
</blockquote>
<ul>
<li>최대치 8로 선정 ( 선정 과정에 있어 다른 서비스 모델을 참고 )</li>
</ul>
<h2 id="문자열-길이-최대-8인-경우">문자열 길이 최대 8인 경우</h2>
<h3 id="like-연산-결과-237s">like 연산 결과 [2.37s]</h3>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/e300d6d2-ab41-41b9-ba2a-2292b757d6c3" alt="image"></p>
<h3 id="match-against-boolean-mode는-다음과-같은-결과-2922s">match against boolean mode는 다음과 같은 결과 [29.22s]</h3>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/5045258d-cc73-43e0-84f3-accf6bc7e014" alt="image"></p>
<ul>
<li>쿼리가 상당히 느리다.</li>
</ul>
<blockquote>
<p>쿼리 Profiling으로 실행 계획 확인</p>
</blockquote>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/43da9c5b-8b90-4a95-b056-00a943890123" alt="image"></p>
<ul>
<li>FULLTEXT initialization 이라는 전체 텍스트 인덱스를 초기화하고, 매번 검색을 위해 데이터를 메모리로 로딩하는 과정 에서 약 29s 가 걸림</li>
<li>Optimize table member 커맨드를 사용하여 FULLTEXT initialization의 성능을 높이려는 시도</li>
</ul>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/ef5cbc78-e8b6-4ac5-99af-24e330ce2126" alt="image"></p>
<ul>
<li>하지만 이후에도 성능에 큰 변화가 없었다.</li>
</ul>
<h2 id="쿼리를-분석수정-해보자">쿼리를 분석/수정 해보자</h2>
<h3 id="기존-쿼리의-문제점-분석">기존 쿼리의 문제점 분석</h3>
<blockquote>
<p>검색을 하는 문자열의 길이가 기존 N-gram의 길이 2보다 크기 때문에 더 세세한 검색 과정을 거치기 때문이다.</p>
</blockquote>
<ul>
<li><strong>2씩 나눠진 토큰을 포함</strong>하면서 동시에 <strong>해당 문자열의 순서까지 일치</strong>하는 데이터를 찾아야 한다.</li>
<li>N-gram의 길이로 나뉘어진 문자열은 즉 자체로 인덱스를 의미하고 해당 인덱스로만의 탐색이 어렵다는 것이다.</li>
</ul>
<h3 id="쿼리-수정">쿼리 수정</h3>
<blockquote>
<p>ngram parser의 최소 단위인 2만큼 문자열을 나눈 후 이 나눈 문자열이 모두 포함된 부분을 검색하면 더 빠른 검색이 가능할거라 예상</p>
</blockquote>
<pre><code class="language-sql">SHOW GLOBAL VARIABLES LIKE &quot;ngram_token_size&quot;; ( 2인걸 확인 )</code></pre>
<pre><code class="language-sql">SELECT * FROM member WHERE MATCH(nickname) AGAINST(&#39;+12 +23 +34 +45 +56&#39; IN BOOLEAN MODE);</code></pre>
<h3 id="길이-2씩-나누어-검색-053s">길이 2씩 나누어 검색 [0.53s]</h3>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/7e9b0951-ad90-41de-8ec3-0f9000b91c79" alt="image"></p>
<ul>
<li>몇개의 결과가 더 나왔으며 이전과 비교해 누락된 row는 없었다.<ul>
<li>더 나온 이유는 순서를 보장하지 않고 12, 23, 34, 45, 56 을 포함하는 문자열을 검색하기 때문</li>
</ul>
</li>
<li>LIKE 연산 결과보다 4.96배 개선</li>
</ul>
<h3 id="혹시-몰라-길이-3씩-나누어-검색-1m-419s">혹시 몰라 길이 3씩 나누어 검색 [1m 4.19s]</h3>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/af50d00f-c1ae-40b4-ade6-1b8ebd9f5dff" alt="image"></p>
<ul>
<li>이건 예상대로 훨씬 성능이 좋지 않다.</li>
<li>길이 3만큼 나누어 검색하면 기존의 문자열 하나만 조건으로 넣었을 때보다 더 느림</li>
<li>길이가 2가 아닌 문자열인데 심지어 여러개를 검사했기 때문이라 파악</li>
</ul>
<h3 id="full-text-index-사용-시">FULL TEXT INDEX 사용 시</h3>
<hr>
<ul>
<li>N-gram parser를 이용할 경우 우리가 지정한 길이만큼의 토큰으로 검사를 진행해야 좋은 성능을 발휘할 수 있다.</li>
<li>길이가 더 긴 문자열로 탐색을 진행할 경우 해당 문자열의 토큰들의 배치(순서)까지 고려하여 탐색을 진행하기 때문에 시간이 더 오래 걸린다.</li>
</ul>
<h2 id="결과-263s-→-053s-로-496배-496-개선">결과: [2.63s → 0.53s 로 4.96배 (496%) 개선]</h2>
<h3 id="before">before</h3>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/78841f74-4dd0-48f6-89e8-7cb6ddb96812" alt="image"></p>
<h3 id="after">after</h3>
<p><img src="https://github.com/HongDam-org/TWTW/assets/84346055/be4722b3-7eab-45ae-8160-4138a6e862d8" alt="image"></p>
<ul>
<li>추후 데이터가 훨씬 많이 쌓이고 서비스가 확장되면 Elastic Search와 같은 다른 기술의 도입도 가능하지만,
현재의 주어진 인프라 내에서 서비스 요구사항 수정으로 오류를 해결하고 쿼리를 수정하여 성능도 향상할 수 있었음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[상속]]></title>
            <link>https://velog.io/@jinjoo-lab/%EC%83%81%EC%86%8D</link>
            <guid>https://velog.io/@jinjoo-lab/%EC%83%81%EC%86%8D</guid>
            <pubDate>Wed, 10 Jan 2024 12:02:23 GMT</pubDate>
            <description><![CDATA[<h2 id="객체지향-프로그래밍의-특징">객체지향 프로그래밍의 특징</h2>
<ul>
<li>Abstraction (추상화)</li>
<li>Polymorphism (다형성)</li>
<li><strong>Inheritance (상속)</strong></li>
<li>Encapsulation (캡슐화)</li>
</ul>
<blockquote>
<p>우리는 <strong>자동차라는 클래스</strong>를 생성했다. 근데 <strong>‘소나타’ 라는 새로운 클래스를 작성</strong>해야 한다 가정하자. 소나타는 자동차가 가지고 있는 모든 정보를 가지고 있다. 우리는 과연 전부 새로운 코드로 작성한 ‘소나타’ 클래스를 만들어야 할까?</p>
</blockquote>
<h2 id="상속">상속</h2>
<ul>
<li>객체지향 프로그래밍의 가장 큰 특징 중 하나인 상속은 <strong>기존의 클래스를 바탕으로 새로운 클래스를 작성</strong>하는 것<ul>
<li>상속을 통해 부모 클래스는 자식 클래스에게 멤버(변수 , 메서드) + 타입을 물려준다.</li>
</ul>
</li>
<li>상속을 통해 코드의 공통 관리가 가능하여 <strong>추가와 변경이 용이</strong>하다.</li>
<li>재사용성이 향상되고 중복된 코드가 제거되기 때문에 <strong>유지보수에 용이</strong>하다.</li>
<li><strong>확장성</strong> 증가</li>
</ul>
<h3 id="슈퍼--서브">슈퍼 &amp; 서브</h3>
<ul>
<li>조상, 슈퍼 클래스 : 상속을 해주는 대상 클래스</li>
<li>자식, 서브 클래스 : 상속을 받는 대상 클래스</li>
</ul>
<pre><code class="language-java">class SubClass extends SuperClass{} // 상속은 extends 연산자를 사용한다.</code></pre>
<h3 id="상속의-특징">상속의 특징</h3>
<ul>
<li><strong>상속의 방향성 (부모 → 자식)</strong></li>
</ul>
<blockquote>
<p>조상 클래스가 변경된다면 자식 클래스에도 자동적으로 영향 , But <strong>자식 클래스의 변경은 조상 클래스에게 영향이 없다.</strong></p>
</blockquote>
<ul>
<li><strong>멤버와 타입의 상속</strong><ul>
<li>상속은 멤버(변수와 메서드)와 타입을 물려준다. (생성자와 초기화 블럭은 상속의 대상이 아닌다.)</li>
</ul>
</li>
<li><strong>단일 상속</strong><ul>
<li>C++은 하나의 클래스가 여러 조상 클래스로부터 상속 가능한 다중 상속을 지원한다.</li>
<li>하지만 자바에서는 오직 하나의 조상 클래스로부터만 상속이 가능한 <strong>단일 상속을 지원</strong>한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">class LgTv{
    int price;
}

class ApplceTv{
    int price;
}

class MyTv extends LgTv,AppleTv{
 // ???? price 변수를 누구로부터 상속받아야 하지?
}</code></pre>
<blockquote>
<p>위 예시에서 MyTv라는 클래스는 LgTv, AppleTv 2개의 조상 클래스를 가지고 있다. 과연 이런 상황에서 MyTv의 price 변수는 어떤 클래스로부터 상속받아야 할까?</p>
</blockquote>
<ul>
<li>위와 같은 상황이 다중 상속의 문제점이다. 자바는 이러한 고민거리를 해결하는 방법으로 다중 상속을 그냥 포기했다….<ul>
<li>뒤에서 배우겠지만 인터페이스의 존재로 어느정도 다중 상속의 장점을 채우고 있다.</li>
</ul>
</li>
</ul>
<h3 id="object-class---클래스의-시조">Object Class - 클래스의 시조</h3>
<blockquote>
<p>여담으로 우리 모두 시조가 있다…(몇대손…) 클래스도 부모와 자식 관계가 있다면 시조도 존재하지 않겠는가? → Object Class (모든 클래스의 시조)</p>
</blockquote>
<pre><code class="language-java">class MyClass{} -&gt; class MyClass extends Object{}</code></pre>
<ul>
<li>아무런 상속 관계를 지정하지 않을 경우 컴파일러는 자동적으로 컴파일 시 <strong>‘extends Object’</strong>를 추가하여 컴파일한다.<ul>
<li>대표적으로 equals() , toString() 메소드가 바로 Object 클래스로부터 상속받는 것이다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>부모와 자식 관계는 지정하지만 <strong>클래스간 형제 관계라는 용어는 없다.</strong> 그 이유가 바로 이 Object 클래스 때문이다. 결국 모든 클래스는 형제 클래스이기 때문이다….</p>
</blockquote>
<h2 id="오버라이딩-overriding">오버라이딩 (Overriding)</h2>
<pre><code class="language-java">class 자동차{
        void print(){
            System.out.println(&quot;자동차&quot;);
        }
}

class 람보르기니 extends 자동차{
        // 나의 조상 클래스가 &#39;자동차&#39;이지만 명색이 람보르기니인데 자동차로 불리는 건..... 
        //나만의 print() 메서드를 정의하고 싶어
}</code></pre>
<ul>
<li>조상 클래스로부터 상속 받은 메서드의 내용을 변경하는 것</li>
</ul>
<h3 id="오버라이딩-조건">오버라이딩 조건</h3>
<blockquote>
<p>메서드의 선언부가 일치해야 한다.</p>
<ul>
<li><strong>메서드의 이름이 동일</strong>해야 한다.</li>
<li><strong>매개변수</strong>가 같아야 한다.</li>
<li><strong>반환 타입</strong>이 같아야 한다. (공변 반환 타입)</li>
</ul>
</blockquote>
<ol>
<li><strong>접근 제어자</strong>는 조상 클래스보다 <strong>좁은 범위로 변경</strong>할 수 없다.<ul>
<li>public &gt; protected &gt; (default) &gt; private<ul>
<li>ex) 부모 클래스의 메서드의 접근 제어자가 public일 때 자식 클래스의 접근 제어자를 protected로 지정할 수 없다.</li>
</ul>
</li>
</ul>
</li>
<li>조상 클래스의 메서드보다 <strong>많은 수(넓은 범위)의 예외를 선언할 수 없다.</strong></li>
</ol>
<h3 id="오버라이딩과-오버로딩">오버라이딩과 오버로딩</h3>
<ul>
<li>명백히 다른 개념이라는 것을 알아야 한다.<ul>
<li>오버로딩 : 기존에 없는 메서드를 새로 정의하는 개념</li>
<li>오버라이딩 : 기존의 메서드의 내용을 변경하는 것</li>
</ul>
</li>
</ul>
<h3 id="공변-반환-타입">공변 반환 타입</h3>
<ul>
<li>JDK 15에 추가된 개념<ul>
<li>반환 타입을 자식 클래스의 타입으로 지정할 수 있다.</li>
<li>명시적 형변환을 외부에서 처리할 필요가 없다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">class Parent{
    Parent createOne(){ return Parent();}
}

class Child extends Parent{
    @Override
    Child createOne(){ return Child();} // 공변 반환 타입 사용하여 오버라이딩
}</code></pre>
<h2 id="super--super">super , super()</h2>
<blockquote>
<p>위에서 생성자는 상속 대상이 아니라고 했다.</p>
</blockquote>
<h3 id="super">super</h3>
<ul>
<li>자손 클래스에서 <strong>부모 클래스로부터 상속받은 멤버를 참조하는데 사용되는  참조 변수</strong><ul>
<li>속성과 메소드를 super.XX 형태로 호출 가능</li>
<li>자손 클래스와 조상 클래스의 멤버가 중복 정의되어 서로 구별해야 하는 경우 사용 가능</li>
</ul>
</li>
<li><strong>super 역시 static 메서드에서는 사용 불가능</strong></li>
</ul>
<h3 id="super-1">super()</h3>
<ul>
<li>조상클래스의 생성자를 호출</li>
</ul>
<blockquote>
<p>상속이라는 것은 자손의 멤버와 조상의 멤버를 모두 합쳐 하나의 인스턴스가 생성된다. 이때 조상 클래스의 생성자가 호출되어야 하지만 기본적으로 생성자는 상속 대상이 아니다.</p>
</blockquote>
<ul>
<li>자식 클래스의 생성자에서 조상 클래스의 생성자가 먼저 호출되어 조상의 멤버들이 먼저 초기화되어야 한다.<ul>
<li>자식 클래스 생성자에서 super()를 호출하지 않으면 컴파일러가 자동적으로 생성자의 첫줄에 삽입한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">class Parent{
    int x;

    Parent(int x){
        this.x = x;
    }
}
class Child extends Parent{

    int y;

    Child(int y){
        this.y = y;
    }
}</code></pre>
<ul>
<li>기본적으로 상속을 받기 위해서는 <strong>부모 클래스에 기본 생성자가 생성</strong>되어 있어야 한다.<ul>
<li>아니면 super()를 사용자가 추가해야 한다.</li>
<li>컴파일러가 자동으로 자식 클래스에 super()를 호출하기 때문이다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MapStruct 사용기]]></title>
            <link>https://velog.io/@jinjoo-lab/MapStruct-%EC%82%AC%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@jinjoo-lab/MapStruct-%EC%82%AC%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Tue, 28 Nov 2023 13:20:23 GMT</pubDate>
            <description><![CDATA[<h2 id="mapstruct-에서-2개-이상의-인자-사용">MapStruct 에서 2개 이상의 인자 사용</h2>
<h3 id="mapstruct">MapStruct</h3>
<blockquote>
<p>Java Bean 유형 간의 매핑 구현 단순화하는 코드 생성기</p>
</blockquote>
<h3 id="특징">특징</h3>
<ol>
<li>컴파일 시점에 코드 생성 </li>
<li>반복적 구현을 줄여준다.</li>
<li>Annotation processor를 이용하여 객체 간 매핑 자동화</li>
<li>MapStruct는 Lombok의 Getter,Setter,Builder를 이용한다.<ol>
<li>꺼내오는 객체 → Getter 필요</li>
<li>저장하는 객체 → Builder OR 모든 필드를 포함하는 생성자</li>
</ol>
</li>
</ol>
<h3 id="상황">상황</h3>
<blockquote>
<p>MapStruct를 통해 <strong>DTO ↔ Entity</strong> 변환 과정을 수행하고 있었다. Group 을 생성하는 로직에 있어 개선점을 필요로 하였다.</p>
</blockquote>
<h3 id="group">Group</h3>
<pre><code class="language-java">@Entity
@Getter
@Where(clause = &quot;deleted_at is null&quot;)
@EntityListeners(AuditListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Group implements Auditable {
    @Id
    @GeneratedValue(generator = &quot;uuid2&quot;)
    @Column(name = &quot;id&quot;, columnDefinition = &quot;BINARY(16)&quot;)
    private UUID id;

    private String name;
    private String groupImage;

    private UUID leaderId;
}</code></pre>
<ul>
<li>다음은 Group Entity의 일부이다. 핵심은 그룹 장의 정보를 알 수 있는 <strong>leaderId</strong>가 포함되어 있는 것이다.</li>
<li>leaderId를 기존에는 MakeGroupRequest에서 인자로 받고 있었다.</li>
</ul>
<h3 id="makegrouprequest">MakeGroupRequest</h3>
<pre><code class="language-java">@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MakeGroupRequest {
    private String name;
    private String groupImage;
    private UUID leaderId;
}</code></pre>
<ul>
<li>하지만 내부 회의를 하면서 그룹을 만드려고 접근한 사용자가 leader일 것이고 추가적으로 Id를 별도로 입력 받는 것은 TOO MUCH라고 판단하였다.</li>
</ul>
<blockquote>
<p>결국 해당 인자를 제거하고 로직을 짜야 하는데 MapStruct 상의 변화가 필요하다 !!!!</p>
</blockquote>
<h3 id="오류">오류</h3>
<pre><code class="language-java">@Mapping(target = &quot;name&quot;, source = &quot;name&quot;)
@Mapping(target = &quot;groupImage&quot;, source = &quot;groupImage&quot;)
**@Mapping(target = &quot;leaderId&quot;, source = &quot;leaderId&quot;)**
@Mapping(target = &quot;baseTime&quot;, ignore = true)
@Mapping(target = &quot;groupMembers&quot;, ignore = true)
@Mapping(target = &quot;groupPlans&quot;, ignore = true)
Group toGroupEntity(MakeGroupRequest groupDto, UUID leaderId);</code></pre>
<ul>
<li>leaderId를 읽어올 수 없다는 오류가 계속해서 발생하였다.</li>
</ul>
<h3 id="원리">원리</h3>
<blockquote>
<p>2개 이상의 인자를 MapStruct를 통해 변환하기 위해서는 해당 인자(source)가 어느 인자 객체에서 왔는지 명시해줘야 한다. MakeGroupRequest에서 name과 groupImage를 받아오기 때문에 source명을 “groupDto.XXX” 이렇게 표기해줘야 한다.</p>
</blockquote>
<h3 id="수정">수정</h3>
<pre><code class="language-java">@Mapping(target = &quot;baseTime&quot;, ignore = true)
@Mapping(target = &quot;groupMembers&quot;, ignore = true)
@Mapping(target = &quot;groupPlans&quot;, ignore = true)
@Mapping(target = &quot;name&quot;,source = &quot;groupDto.name&quot;)
@Mapping(target = &quot;groupImage&quot;, source = &quot;groupDto.groupImage&quot;)
@Mapping(target = &quot;leaderId&quot;, source = &quot;leaderId&quot;)
Group toGroupEntity(MakeGroupRequest groupDto, UUID leaderId);</code></pre>
<ul>
<li>인자가 2개 이상일 경우 source의 표시를 객체부터 시작해야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DI-2]]></title>
            <link>https://velog.io/@jinjoo-lab/DI-2</link>
            <guid>https://velog.io/@jinjoo-lab/DI-2</guid>
            <pubDate>Wed, 22 Nov 2023 12:52:25 GMT</pubDate>
            <description><![CDATA[<h3 id="stereotype---annotation">StereoType - Annotation</h3>
<ul>
<li>아키텍처 관점에서 특정 계층에 사용하는 어노테이션<ul>
<li>명명을 통해 직관적으로 구조를 파악할 수 있다.</li>
</ul>
</li>
<li>스프링은 자동으로 StereoType 어노테이션이 붙은 클래스들을 빈으로 등록한다.<ul>
<li>@ComponentScan의 감지 대상</li>
</ul>
</li>
<li>AOP 적용시 관점 적용을 위해 역할에 맞게 사용해야 한다.</li>
</ul>
<h3 id="종류">종류</h3>
<ul>
<li>@Repository<ul>
<li>검사 되지 않은 예외(DAO 메소드에서 발생)를 Spring DataAccessException으로 변환 할 수 있게 해준다.</li>
</ul>
</li>
<li>@Controller</li>
<li>@Service</li>
<li>@Component</li>
</ul>
<blockquote>
<p>@Repository, @Service 및 @Controller는 보다 구체적인 사용 사례(각각 지속성, 서비스 및 프레젠테이션 계층에서)를 위한 @Component의 특수화입니다</p>
</blockquote>
<h3 id="component--bean-vs-configuration--bean">@Component + @Bean VS @Configuration + @Bean</h3>
<ul>
<li>@Compoent + @Bean 에서는 메소드 및 필드의 호출을 위해 CGLIB 프록싱을 사용하지 않는다.</li>
<li>CGLIB 프록싱<ul>
<li>@Configuration 클래스의 @Bean 메소드 내에서 메소드 또는 필드를 호출하여 협업 객체에 대한 빈 메타데이터 참조를 생성하는 수단</li>
<li>스프링의 빈이 싱글톤으로 보장될수 있는 이유<ul>
<li><strong>CGLIB</strong>라는 라이브러리가 <code>@Configuration</code> 을 적용한 클래스를 상속받은 임의의 다른 클래스를 만들어 그 클래스를 스프링 빈으로 등록</li>
</ul>
</li>
<li>Bean 메소드에 대한 직접 호출 시에 빈이 싱글톤으로 유지되는가 ? , 새로운 빈을 생성하는가에 대한 차이이다.</li>
</ul>
</li>
</ul>
<h3 id="inject">@Inject</h3>
<ul>
<li>Spring이 아닌 Java에서 제공하는 의존성 주입 어노테이션</li>
<li>생성자 , 수정자 , 필드 주입에 사용할 수 있다. ( @Autowired와 동일 )</li>
</ul>
<h3 id="named--managedbean">@Named , @ManagedBean</h3>
<ul>
<li>@ComponentScan의 탐지 대상이며 @Component의 Java에서 제공하는 어노테이션이다.</li>
<li>Custom Component 어노테이션을 만들 때에는 사용하지 못한다. 만들고 싶다면 StereoType 어노테이션 사용</li>
</ul>
<h3 id="spring-java-configuration">Spring Java Configuration</h3>
<ul>
<li>@Configuration + @Bean<ul>
<li>@Bean은 메소드가 Spring IoC 컨테이너에 의해 관리될 새 객체를 인스턴스화하는데 사용</li>
<li>@Configuration<ul>
<li>클래스에 사용 빈 정의의 경로임을 나타내는 목적 , 동일한 클래스내에 여러 빈 간의 종속성을 정의</li>
</ul>
</li>
</ul>
</li>
</ul>
<pre><code class="language-kotlin">@Configuration
class AppConfig {
    @Bean
    fun myService(): MyServiceImpl {
        return MyServiceImpl()
    }}</code></pre>
<ul>
<li>@Configuration 이 붙은 클래스 자체가 빈으로 등록되고 내부의 @Bean 메소드도 빈으로 등록된다.</li>
</ul>
<blockquote>
<p>모든 @Configuration 클래스는 시작 시 CGLIB로 서브클래싱됩니다. 하위 클래스에서 하위 메소드는 상위 메소드를 호출하고 새 인스턴스를 작성하기 전에 먼저 캐시된(범위 지정) 빈이 있는지 컨테이너를 확인합니다.</p>
</blockquote>
<h3 id="configuration-를-사용하지-않는-경우의-bean">@Configuration 를 사용하지 않는 경우의 @Bean</h3>
<ul>
<li>lite mode로 동작</li>
<li>bean간의 종속성을 정의할 수 없다. → @Component 불가</li>
<li>No CGLIB 프록싱</li>
</ul>
<h3 id="annotationconfigapplicationcontext">AnnotationConfigApplicationContext</h3>
<ul>
<li>자바 설정 정보를 바탕으로 빈 객체의 설정 정보를 관리하고 사용자로 하여금 가져올 수 있다.</li>
</ul>
<h3 id="scope-proxy-mode">Scope Proxy Mode</h3>
<ul>
<li>@Scope(value = &quot;prototype&quot;, proxyMode = ScopedProxyMode.TARGET_CLASS)</li>
</ul>
<p><code>ScopedProxyMode.TARGET_CLASS</code>, <code>ScopedProxyMode.INTERFACES</code> , <code>ScopedProxyMode.NO</code></p>
<h3 id="import">@Import</h3>
<ul>
<li>설정 파일간의 계층 생성 시 사용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DI - 1]]></title>
            <link>https://velog.io/@jinjoo-lab/DI-1</link>
            <guid>https://velog.io/@jinjoo-lab/DI-1</guid>
            <pubDate>Wed, 22 Nov 2023 12:51:18 GMT</pubDate>
            <description><![CDATA[<h1 id="di-1">DI-1</h1>
<ul>
<li>IOC → 프로그램의 제어 흐름이 뒤바끼는 것<ul>
<li>스프링 관점에서 바라보자면 스프링 컨테이너가 빈 오브젝트에 대한 생성 및 생명 주기를 관리하는 것이다.</li>
<li>이러한 IOC는 조금 더 포괄적인 개념으로 바라볼 수 있다. DI는 스프링에서 IOC 기능의 대표적인 동작 방식으로 바라 볼 수 있다.</li>
<li>IOC 자체를 Principle 관점에서 바라보고 이를 구현한 많은 Pattern이 존재한다.</li>
</ul>
</li>
</ul>
<h3 id="di--종속성-주입--의존성-주입-">DI ( 종속성 주입 , 의존성 주입 )</h3>
<blockquote>
<p>종속성 주입(DI)은 개체가 생성자 인수, 팩터리 메서드에 대한 인수 또는 개체 인스턴스가 생성된 후 <strong>개체 인스턴스에 설정된 속성을 통해서만 종속성(즉, 함께 작동하는 다른 개체)을 정의</strong>하는 프로세스입니다. (디자인 패턴)</p>
</blockquote>
<blockquote>
<p>Dependency Injection (DI) is a design pattern that <strong>allows you to remove the hard-coded dependencies between objects in your application, and instead, provide them with their dependencies through a central location</strong>.</p>
</blockquote>
<ul>
<li>팩토리 메서드<ul>
<li>구체적으로 사용할 오브젝트를 결정해주는 메서드 (추상적인 개념)</li>
</ul>
</li>
<li>종속성<ul>
<li>의존 관계 (Dependency Relationship) 관점에서 바라볼 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="의존-관계">의존 관계</h3>
<ul>
<li>의존 관계에는 항상 방향성이 존재하며 <strong>방향성</strong>을 기준으로 오브젝트가 변경되었을 때 의존 관계를 맺고 있는 다른 오브젝트에도 영향이 생긴다는 것이다.</li>
<li>A가 B에 의존하고 있다 ( <strong>A → B</strong> )<ul>
<li>B에 변화가 생긴다면 변화에 대한 영향이 A에도 있다.</li>
<li>의존 관계에 대한 예시 → A에서 B의 메소드를 호출해서 사용하는 경우</li>
</ul>
</li>
</ul>
<blockquote>
<p>구체적인 <strong>클래스에 대한 의존관계</strong>를 가지고 있는 경우 의존 관계의 정도가 강하다. 즉 결합도가 강한데 이는 소프트웨어 공학 관점에서 올바르지 않다. 그렇기 때문에 의존 관계에 대한 결합도를 낮추는 것이 중요하다.</p>
</blockquote>
<ul>
<li>의존 관계의 결합도를 낮추는 방법으로는 구체적인 클래스와의 의존 관계를 형성하는 것이 아닌 인터페이스와 의존 관계를 형성하도록 하는 것이다.</li>
</ul>
<blockquote>
<p>The object does not look up its dependencies and does not know the location or class of the dependencies.</p>
</blockquote>
<h3 id="런타임-의존-관계">런타임 의존 관계</h3>
<ul>
<li>모델 , 설계 과정에서 형성된 것이 아닌 런타임 시점에 오브젝트 사이에 형성되는 의존 관계</li>
</ul>
<pre><code class="language-kotlin">@Service
class MemoService(
    private val memoRepository: MemoRepository, // Interface
    private val matchRepository: MatchRepository // Interface
)</code></pre>
<ul>
<li><p>런타임 시점에 형성되는 의존 관계는 무엇인가? (조건 2가지 ?)</p>
<p>프로그램이 실행되기 전까지 구체적으로 의존 관계가 형성되는 오브젝트를 알 수 없다는 의미인데 기본적으로 런타임 의존관계는 인터페이스를 주입 받는 경우를 말한다.</p>
<p>또한 생성자나 팩토리 메서드를 통해 주입을 받아야 한다.</p>
</li>
</ul>
<h3 id="di의-조건">DI의 조건</h3>
<ol>
<li>코드나 설계에는 런타임 의존 관계가 드러나지 않는다. 즉 인터페이스에 대한 의존관계가 형성되어야 한다.</li>
<li>컨테이너나 팩토리 등 제 3의 존재가 의존 관계를 결정해준다. ( IOC 관점 )</li>
<li>외부에서 사용할 오브젝트에 대한 레퍼런스를 제공(주입)</li>
</ol>
<h3 id="di의-정의">DI의 정의</h3>
<ul>
<li><p>컨테이너나 팩토리등 제 3의 존재가 <strong>오브젝트 사이의 런타임 의존 관계를 결정하고 오브젝트를 주입</strong>해주는 것, 종속성을 결정하는 것</p>
</li>
<li><p>스프링에서 DI</p>
<ul>
<li>스프링에서 빈 오브젝트에 종속성을 주입해주기 위해서는 주입되는 오브젝트도 빈이여야 한다.</li>
</ul>
</li>
<li><p><strong>이유</strong></p>
<p>스프링 프레임워크에서 빈간의 주입을 해주는 것은 컨테이너이다. 컨테이너에서 관리되는 오브젝트여야지만 주입이 가능하다.</p>
</li>
</ul>
<h3 id="스프링-컨테이너">스프링 컨테이너</h3>
<ul>
<li>IOC 컨테이너이면서 DI 컨테이너<ul>
<li>즉 빈 오브젝트의 생명 주기를 관리하면서 빈 사이의 종속성을 주입한다.</li>
</ul>
</li>
</ul>
<h3 id="생성자-기반-di">생성자 기반 DI</h3>
<pre><code class="language-kotlin">class SimpleMovieLister(private val movieFinder: MovieFinder) {}</code></pre>
<ul>
<li>객체의 생성과 종속성 주입이 동시에 일어난다.<ul>
<li>생성자 호출 시점에 1회 호출되는 것이 보장됨<ul>
<li>주입 받는 빈이 null 일 수 있는 가능성이 배제된다.</li>
<li>객체의 불변성 확보</li>
</ul>
</li>
<li>순환 참조 방지 가능<ul>
<li>순환 참조가 발생할 경우 컴파일 에러의 형태로 알 수 있기 때문에 수정하기 편리하다.</li>
</ul>
</li>
<li>Test 하기 편리하다.<ul>
<li>필드 주입 방식의 경우 Spring 프레임워크 위에서만 테스트가 가능하다. 순수 java 코드로 테스트하기 위해서는 생성자 주입 방식이 필요하다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="수정자setter-기반-di">수정자(Setter) 기반 DI</h3>
<pre><code class="language-kotlin">class SimpleMovieLister {
    lateinit var movieFinder: MovieFinder
}</code></pre>
<ul>
<li>실행 시점<ul>
<li>빈에서 No-args 생성자를 호출 한 후 컨테이너에서 빈의 수정자(setter)메소드를 호출한다.</li>
</ul>
</li>
<li>수정자 주입은 기본적으로 선택적 종속성에 해당(변화 가능성)<ul>
<li>기본값을 설정해줘야만 불필요한 null 체크를 피할 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="bean-주입-과정">Bean 주입 과정</h3>
<ol>
<li>ApplicationContext 가 생성되고 초기화</li>
<li>빈이 생성되고 의존성이 주입된다.<ul>
<li>생성자 주입의 경우에는 동시에 발생</li>
<li>수정자 주입 , 필드 주입은 빈 생성 → 의존성 주입</li>
</ul>
</li>
</ol>
<blockquote>
<p>Singleton 범위에 사전 인스턴스화가 설정된 빈은 컨테이너가 생성되면서 빈이 생성되고 주입 , 그렇지 않으면 요청될 때 빈이 생성</p>
</blockquote>
<h3 id="순환-참조">순환 참조</h3>
<blockquote>
<p><strong>순환참조 문제란 A 클래스가 B 클래스의 Bean 을 주입받고, B 클래스가 A 클래스의 Bean 을 주입받는 상황처럼 서로 순환되어 참조할 경우 발생하는 문제를 의미</strong></p>
</blockquote>
<ul>
<li>생성자 주입의 방식을 사용할 경우 어떠한 Bean도 생성하지 못하게 되어 무한 반복</li>
<li>필드 주입이나 수정자 주입의 경우 해당 오브젝트에 대한 메소드가 호출되어야 순환 호출의 문제가 발생</li>
<li>애초에 <strong>설계 시점에 순환 참조가 일어나지 않도록 하는 것이 중요</strong>하다.</li>
</ul>
<h3 id="lazy---initalized-bean">Lazy - initalized Bean</h3>
<ul>
<li>컨테이너가 생성되고 빈을 생성하는 것이 아닌 빈에 대한 요청이 들어왔을 때 빈을 생성하는 방식</li>
<li>스프링에서 권장하는 방식은 아니다.</li>
<li>@Lazy 어노테이션 사용 → 순간적인 순환 참조를 방지할 수 있지만 말 그대로 회피 느낌이다.</li>
</ul>
<h3 id="autowiring">Autowiring</h3>
<ul>
<li>오토와이어링은 스프링이 빈의 요구사항과 매칭되는 애플리케이션 컨텍스트상에서 다른 빈을 찾아 빈 간의 의존성을 자동으로 만족하게 하도록 하는 수단</li>
<li>스프링에서 Autowirig을 제공하는 방법<ul>
<li>생성자 주입</li>
<li>수정자 주입</li>
<li>@Autowired 기반의 필드 주입</li>
</ul>
</li>
</ul>
<h3 id="singleton-빈에-prototype-빈을-주입할-때">Singleton 빈에 Prototype 빈을 주입할 때</h3>
<ul>
<li><p>컨테이너는 싱글톤 빈 A를 한 번만 생성하므로 속성을 설정할 수 있는 기회는 한 번뿐입니다. 컨테이너는 필요할 때마다 Bean B의 새 인스턴스를 Bean A에 제공할 수 없습니다.</p>
</li>
<li><p>해결 방법</p>
<p>해결책은 제어의 역전을 포기하는 것입니다. ApplicationContextAware 인터페이스를 구현하고 컨테이너에 대한 getBean(&quot;B&quot;) 호출을 수행하여 Bean A가 필요할 때마다 (일반적으로 새로운) Bean B 인스턴스를 요청함으로써 Bean A가 컨테이너를 인식하도록 할 수 있습니다.</p>
</li>
<li><p>다른 방법으로는 @Lookup 어노테이션을 활용하는 것</p>
</li>
</ul>
<h3 id="annotation-기반-설정">Annotation 기반 설정</h3>
<ul>
<li>구성 요소간의 연결을 위해 바이트 코드 메타데이터에 의존하는 어노테이션 기반 설정</li>
</ul>
<blockquote>
<p>Annotation injection is performed before XML injection.</p>
</blockquote>
<ul>
<li>Annotation 기반 주입이 먼저 일어나고 XML 주입이 일어난다 !</li>
</ul>
<h3 id="autowired">@Autowired</h3>
<ul>
<li><p>Spring에서 의존성 주입을 위해 지원하는 어노테이션</p>
</li>
<li><p>BeanPostProcessor의 구현체인 AutowiredAnnotationBeanPostProcessor가 빈의 초기화 라이프 사이클 이전, 즉 빈이 생성되기 전에 @Autowired가 붙어있으면 해당하는 빈을 찾아서 주입해주는 작업을 하는 것</p>
</li>
<li><p>찾는 순서</p>
<p>타입 -&gt; 이름 -&gt; @Qualifier -&gt; 실패</p>
</li>
</ul>
<p>생성자</p>
<pre><code class="language-kotlin">class MovieRecommender @Autowired constructor(
    private val customerPreferenceDao: CustomerPreferenceDao)</code></pre>
<ul>
<li>정의된 생성자가 1개인 경우 @Autowired 생략 가능</li>
</ul>
<p>수정자</p>
<pre><code class="language-kotlin">class SimpleMovieLister {
        @set:Autowired
    lateinit var movieFinder: MovieFinder
}</code></pre>
<ul>
<li>컴포넌트나 빈의 로드 순서를 정렬할 때 <code>@Order</code> 또는 <code>@Priority</code> 사용 가능<ul>
<li>같은 타입의 빈이 여러 개인 경우</li>
</ul>
</li>
<li>Autowired(required = false)<ul>
<li>해당 속성이 Autowiring 될 수 없는 경우 무시된다.</li>
</ul>
</li>
<li>@Autowired 어노테이션이 붙은 생성자가 여러 개인 경우<ul>
<li>Bean으로 만족할 수 있는 가장 많은 종속성을 가지고 있는 생성자가 선택된다.</li>
</ul>
</li>
</ul>
<aside>
📖 @Autowired , @Inject , @Value , @Resource 주석은 스프링의 BeanPostProcessor 구현에 의해 처리된다. 따라서 해당 주석을 사용자 정의 BeanPostProcessor 타입에 사용할 수 없다.

</aside>

<h3 id="qualifier">@Qualifier</h3>
<ul>
<li>빈의 이름을 사용하여 특정 빈을 주입하도록 할 수 있다.</li>
</ul>
<pre><code class="language-kotlin">class MovieRecommender {
    @Autowired
    @Qualifier(&quot;main&quot;)
    private lateinit var movieCatalog: MovieCatalog
}</code></pre>
<ul>
<li>MovieCatalog의 구현체 중에서 main이라는 이름의 Bean을 주입하도록 한다.</li>
</ul>
<h3 id="resource">@Resource</h3>
<ul>
<li><p>Spring은 <code>필드</code> 또는 빈 주입 <code>수정자 메소드</code>에 @Resource 어노테이션을 사용하여 주입을 지원한다. 즉 생성자에는 적용할 수 없다.</p>
</li>
<li><p>Java에서 지원하는 어노테이션</p>
</li>
<li><p>찾는 순서</p>
<p>이름 -&gt; 타입 -&gt; @Qualifier -&gt; 실패</p>
</li>
</ul>
<pre><code class="language-kotlin">class SimpleMovieLister {
        @Resource(name=&quot;myMovieFinder&quot;) 
    private lateinit var movieFinder:MovieFinder
}</code></pre>
<h3 id="value">@Value</h3>
<ul>
<li>외부 프로퍼티 파일의 속성을 주입하는데 사용한다.</li>
</ul>
<pre><code class="language-kotlin">@Component
class MovieRecommender(@Value(&quot;\${catalog.name}&quot;) private val catalog: String)</code></pre>
<ul>
<li>클래스 단에 @PropertySource를 사용하여 외부 프로퍼티가 정의된 파일을 찾아줄 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Decorator-Pattern]]></title>
            <link>https://velog.io/@jinjoo-lab/Decorator-Pattern</link>
            <guid>https://velog.io/@jinjoo-lab/Decorator-Pattern</guid>
            <pubDate>Mon, 20 Nov 2023 13:11:58 GMT</pubDate>
            <description><![CDATA[<h2 id="구현상황">구현상황</h2>
<ul>
<li>스타벅스의 주문 시스템<ul>
<li>수많은 추가 옵션과 파생 음료를 깔끔하게 포용할 수 있는 시스템을 설계하라 !</li>
<li>가격 산출</li>
<li>추가된 옵션에 따른 설명 변경</li>
</ul>
</li>
</ul>
<p><strong>기존 시스템</strong></p>
<p><img src="https://user-images.githubusercontent.com/84346055/284273739-a2ddf291-0d43-4915-be3d-b968e59a36a5.png" alt="Untitled"></p>
<h3 id="일단박죠">일단박죠~!</h3>
<p><img src="https://user-images.githubusercontent.com/84346055/284273767-aca35135-0e95-4478-98e8-bafe28eebf58.png" alt="Untitled"></p>
<h3 id="문제상황">문제상황</h3>
<ul>
<li>첨가물의 종류가 많아진다면 → 새로운 메소드를 추가해야 한다.</li>
<li>첨가물 가격이 바뀔 때마다 기존 코드를 수정해야 한다.</li>
<li>상속받는 음료 중에 우유는 필요하지 않은 음료가 있다고 가정하자 (아이스티)<ul>
<li>근데 여전히 우유에 대한 정보를 상속받는다.</li>
</ul>
</li>
</ul>
<p>→ 위와 같은 설계는 <strong>OCP(개방 - 폐쇠의 원칙)</strong>을 위반한다.</p>
<h3 id="ocp">OCP</h3>
<blockquote>
<p>클래스의 확장에는 열려 있고 변경에는 닫혀 있어야 한다.</p>
</blockquote>
<ul>
<li>기존 코드에 대한 변경을 최소화하면서 기능을 추가할 수 있도록 설계해야한다.</li>
<li>확장<ul>
<li>코드의 유연성 증가 (<strong>코드의 수정이 아닌 추가</strong>를 통해 확장하는 것이 정석)</li>
</ul>
</li>
<li>변경<ul>
<li>객체의 직접적 수정을 제한</li>
</ul>
</li>
</ul>
<h2 id="상속에-대한-고찰">상속에 대한 고찰</h2>
<ul>
<li>객체 지향 프로그래밍에서 상속은 굉장히 강력한 도구이다. 하지만 상속에 대해 생각해 볼 필요가 있다.</li>
</ul>
<ol>
<li>상속은 <strong>정적</strong>이다.<ul>
<li>런타임 시 기존 객체의 행동을 변경할 수 없다.</li>
<li>그저 상위 클래스 객체를 통해 자식 클래스 객체를 바꿀 수  만 있다.</li>
</ul>
</li>
<li><strong>자식 클래스는 하나의 부모 클래스만 가질 수 있다.</strong></li>
</ol>
<p><strong>결론</strong></p>
<blockquote>
<p>즉 우리는 객체의 행동을 동적으로 변경하고 싶다면 <strong>상속</strong>은 좋은 방법이 아닐 수 있다.</p>
</blockquote>
<h3 id="객체의-집합-관계">객체의 집합 관계</h3>
<blockquote>
<p>집합 관계란 한 객체가 다른 객체에 대한 참조를 가짐으로서 <strong>일부 작업을 위임</strong>하는 것</p>
</blockquote>
<ul>
<li>데코레이터 패턴은 이러한 객체의 집합 관계를 응용한 디자인 패턴이다.</li>
</ul>
<p><img src="https://user-images.githubusercontent.com/84346055/284273772-ee3cc7e8-7eaa-46ec-8834-08956a387d9e.png" alt="Untitled"></p>
<h1 id="데코레이터-패턴">데코레이터 패턴</h1>
<blockquote>
<p><strong>객체의 추가 요소를 동적으로 더할 수 있어</strong> 유연한 기능 확장이 가능한 디자인 패턴</p>
</blockquote>
<ul>
<li>데코레이터는 자신이 장식하는 객체(감싸는 객체)에게 행동을 위임하거나 추가 작업 수행 가능</li>
<li>데코레이터의 슈퍼 클래스는 자신이 장식하는 슈퍼클래스와 동일<ul>
<li>이를 통해 객체의 집합 관계를 구현하는 것이다 !</li>
</ul>
</li>
<li>하나의 객체를 여러 개의 데코레이터로 감쌀 수 있다.</li>
</ul>
<h3 id="구조">구조</h3>
<p><img src="https://user-images.githubusercontent.com/84346055/284273774-3db27215-cd65-446c-ba02-9585e92b930a.png" alt="Untitled"></p>
<ol>
<li><strong>컴포턴트</strong><ul>
<li>최상위 인터페이스 : 래퍼(데코레이터)들과 래핑되는 클래스의 공통 인터페이스</li>
</ul>
</li>
<li><strong>구상 컴포넌트</strong><ul>
<li>감싸지는 객체 : 기본 행동을 정의</li>
</ul>
</li>
<li><strong>기초 데코레이터</strong><ul>
<li>래핑되는 객체를 참조하기 위한 필드가 있다.<ul>
<li>필드의 유형은 <strong>구상 컴포넌트 + 구상 데코레이터 모두 포용</strong>하기 위해 컴포넌트 인터페이스로 선언</li>
</ul>
</li>
</ul>
</li>
</ol>
<p><img src="https://user-images.githubusercontent.com/84346055/284273778-fd1f3386-6403-4f9f-b4d6-a5fed6710b5d.png" alt="Untitled"></p>
<ol>
<li><strong>구상 데코레이터</strong><ul>
<li>컴포턴트들에 동적으로 추가할 수 있는 행동을 정의</li>
<li>오버라이드 기반 (기초 데코레이터 메소드)</li>
</ul>
</li>
</ol>
<h3 id="적용-시기">적용 시기</h3>
<ul>
<li>객체의 행동이 런타임 시 변경 + 추가가 많은 경우</li>
<li>객체 간의 결합을 통해 새로운 기능이 생성되는 경우</li>
</ul>
<h3 id="장점">장점</h3>
<ul>
<li>유연한 기능 확장 가능<ul>
<li>런타임 시 동적으로 기능 확장 가능</li>
</ul>
</li>
<li>클라이언트의 코드 수정 없이 기능 확장 가능 → <strong>OCP 원칙 준수</strong></li>
<li>구현체가 아닌 인터페이스를 바라본다 → <strong>DIP 원칙 준수</strong></li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>데코레이터 제거가 어렵다.<ul>
<li>데코레이터 조합 생성 코드에 대한 파악이 어렵다.</li>
</ul>
</li>
<li>순서가 의존하는 데코레이터 방식 (A → B → C 순서만 가능해요 !)는 구현하기 어렵다.</li>
</ul>
<hr>
<h2 id="적용">적용</h2>
<h3 id="uml">UML</h3>
<p><img src="https://user-images.githubusercontent.com/84346055/284273783-715a47ae-d4fa-4a28-b9dd-57644f6ba120.png" alt="Untitled"></p>
<h3 id="test">TEST</h3>
<pre><code>public class Main {
    public static void main(String[] args) {
        Beverage beverage = new HouseBlend();
        System.out.println(beverage.getDescription());
        System.out.println(beverage.getClass());

        beverage = new Milk(beverage);
        System.out.println(beverage.getDescription());
        System.out.println(beverage.cost());

        beverage = new Soy(beverage);
        System.out.println(beverage.getDescription());
        System.out.println(beverage.cost());
    }
}</code></pre><p><strong>Result</strong></p>
<pre><code>House Blend :
3.0
House Blend : + MILK 
4.0
House Blend : + MILK  + 두유 
6.0</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Observer-Pattern]]></title>
            <link>https://velog.io/@jinjoo-lab/Observer-Pattern</link>
            <guid>https://velog.io/@jinjoo-lab/Observer-Pattern</guid>
            <pubDate>Wed, 15 Nov 2023 15:17:36 GMT</pubDate>
            <description><![CDATA[<h1 id="옵저버-패턴">옵저버 패턴</h1>
<h3 id="구현-상황">구현 상황</h3>
<p><img src="https://user-images.githubusercontent.com/84346055/283157893-6faec209-585b-45d2-8202-d1fdaff2c0e5.png" alt="Untitled"></p>
<ul>
<li>WeatherData<ul>
<li>온도  , 습도 , 기압</li>
<li>각 정보를 최신화하여 디스플레이에 전달 !</li>
</ul>
</li>
<li>Display 종류<ul>
<li>현재 조건</li>
<li>기상 통계</li>
<li>기상 예보</li>
</ul>
</li>
</ul>
<h3 id="일단-박죠-">일단 박죠 ~!</h3>
<p><strong>WeatherData</strong></p>
<pre><code class="language-java">public class WeatherData {
    private float temperature;
    private float humidity;
    private float pressure;

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }

    public void measurementsChanged(){
        // 최신 정보로 온도 , 습도 , 압력 Update !

        // 첫번째 Display :  정보 전달 !
        // 두번째 Display :  정보 전달 !
        // 세번째 Display :  정보 전달 !
    }
}</code></pre>
<h3 id="문제-상황">문제 상황</h3>
<ol>
<li>Display의 종류가 많아진다면? (확장성의 문제)<ul>
<li>늘어날때마다 measurementsChange() 메소드에 디스플레이관련 코드를 추가해야 한다.</li>
<li>단순히 <strong>정보를 측정하여 정보를 전달하는 과정</strong>을 반복해야 한다.<ul>
<li>어떤 종류의 디스플레이건 똑같은 작업인데…..</li>
</ul>
</li>
</ul>
</li>
</ol>
<h2 id="옵저버-패턴-1">옵저버 패턴</h2>
<blockquote>
<p><strong>주체</strong>와 <strong>관찰자</strong> 두개의 개념이 등장한다.</p>
</blockquote>
<ul>
<li>주체 : 데이터를 직접 다루고 상태를 관리하는 객체</li>
<li>관찰자(옵저버) : 주체로부터 데이터를 전달받는 객체</li>
</ul>
<blockquote>
<p>옵저버 패턴이란 관찰자들이 주체에 등록되어 주체의 상태 변화 시 주체가 자신에게 등록된 관찰자들에게 정보를 전달하는 디자인 패턴</p>
</blockquote>
<h3 id="uml">UML</h3>
<p><img src="https://user-images.githubusercontent.com/84346055/283157921-6181785c-5b3c-4e90-8e3c-823c42422018.png" alt="Untitled"></p>
<ul>
<li>Subject (주체)<ul>
<li>attach(o) : 관찰자를 등록</li>
<li>detach(o) : 해당 관찰자를 제거</li>
<li>notify() : 자신에게 등록된 관찰자들에게 상태 전달</li>
</ul>
</li>
<li>Observer (관찰자)<ul>
<li>update() : 주체로부터 정보를 전달받음</li>
</ul>
</li>
</ul>
<h3 id="특징">특징</h3>
<ol>
<li>One-To-Many 의존성이 정의된다.<ul>
<li>관찰자들은 단 하나의 주체에 의존하게 된다.</li>
</ul>
</li>
</ol>
<h3 id="장점">장점</h3>
<ul>
<li>실시간으로 객체의 상태를 다른 객체들에게 전파할 수 있다.</li>
<li><strong>느슨한 결합성</strong><ul>
<li>객체 사이의 상호 의존성 최소화 → 유연성 확대</li>
</ul>
</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>알림(notify)의 순서를 제어할 수 없다.<ul>
<li>결국 인터페이스에 의한 함수 호출이 이루어지기 때문 !</li>
</ul>
</li>
<li>옵저버 객체를 등록하고 해지하지 않는다면 메모리 낭비</li>
</ul>
<h2 id="observer-패턴으로-→-문제-해결">Observer 패턴으로 → 문제 해결!</h2>
<h3 id="구현-범위-정리">구현 범위 정리</h3>
<pre><code>Observer
    Interface
        1. Observer
        2. Subject
        3. DisplayElement
    Class
        1. WeatherData
        2. CurrentConditionsDisplay : 현재 측정값
        3. StatisticsDisplay : 평균 값 (X)
        4. ForecastDisplay : 기상 예보 (X)</code></pre><h3 id="interface">Interface</h3>
<p><strong>Observer</strong></p>
<pre><code class="language-java">public interface Observer {
    public void update(float temperature,float humidity,float pressure);
}</code></pre>
<p><strong>Subject</strong></p>
<pre><code class="language-java">public interface Subject {
    public void register(Observer observer);
    public void remove(Observer observer);
    public void notify(Observer observer);
}</code></pre>
<p><strong>DisplayElement</strong></p>
<pre><code class="language-java">public interface DisplayElement {
    public void display();
}</code></pre>
<h3 id="class">Class</h3>
<p><strong>WeatherData</strong></p>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject{
    private List&lt;Observer&gt; observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData(){
        this.observers = new ArrayList&lt;Observer&gt;();
    }

    @Override
    public void register(Observer observer) {
        this.observers.add(observer);
    }

    @Override
    public void remove(Observer observer) {
        this.observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for(Observer observer : observers){
            observer.update(temperature,humidity,pressure);
        }
    }

    public void measurementsChanged(){
        notifyObservers();
    }

    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }

}</code></pre>
<p><strong>CurrentConditionsDisplay</strong></p>
<ul>
<li>Observer 등록의 과정은 어떻게 수행해야 할까?<ul>
<li>Observer 객체가 생성될 때 등록되도록 한다!</li>
<li>즉 Subject의 구현 객체를 내부 속성으로 가지도록한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">package Observer;

public class CurrentConditionsDisplay implements Observer , DisplayElement{
    private float temperature;
    private float humidity;
    private float pressure;

    private WeatherData weatherData;

    public CurrentConditionsDisplay(WeatherData weatherData){
        this.weatherData = weatherData;
        weatherData.register(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }

    @Override
    public void display() {
        System.out.println(&quot;T : &quot;+temperature+&quot; H : &quot;+humidity+&quot; P : &quot;+pressure);
    }
}</code></pre>
<h3 id="추가">추가</h3>
<ul>
<li><p>Push : 주체 → 관찰자</p>
</li>
<li><p>Pull : 관찰자 ← 주체</p>
</li>
<li><p>장난?</p>
<p>엄연히 다른 것이다. Push 방식은 주체가 관찰자에게 정보를 보내는 것이고 Pull 방식은 관찰자가 주체에게서 정보를 받아오는 것이다.</p>
<ul>
<li>누가 <strong>능동적 주체</strong>인가에 대한 영역인 것이다.</li>
</ul>
</li>
</ul>
<pre><code>@Override
public void update() {
    this.temperature = this.weatherData.getTemperature();
    this.humidity = this.weatherData.getHumidity();
    this.pressure = this.weatherData.getPressure();
    display();
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[TWTW - ObjectMapper (2)]]></title>
            <link>https://velog.io/@jinjoo-lab/TWTW-ObjectMapper-2</link>
            <guid>https://velog.io/@jinjoo-lab/TWTW-ObjectMapper-2</guid>
            <pubDate>Thu, 14 Sep 2023 14:19:41 GMT</pubDate>
            <description><![CDATA[<h2 id="composite-패턴을-통한-objectmapper-통합-관리">Composite 패턴을 통한 ObjectMapper 통합 관리</h2>
<h2 id="문제의-발생">문제의 발생</h2>
<ul>
<li>ObjectMapper의 NamingStrategies를 여러개 쓰이게 되는 상황이 발생했다.<ul>
<li>여러 종류의 외부 API를 반영하는 서비스였기에 외부 API 마다의 json 네이밍 컨벤션이 달랐다.</li>
<li>특히, 하나의 NamingStrategies가 늘어날 때 마다 ObjectMapper에 대한 빈을 하나 더 등록하면서 주입 받을 때도 빈이 여러개라서 신경을 써야 하는 등의 불편함이 발생했다.</li>
</ul>
</li>
</ul>
<h3 id="이-문제를-해결하는-방법을-찾던-중-디자인-패턴인-composite-패턴을-통해-해결하기로-했다"><strong>이 문제를 해결하는 방법을 찾던 중, 디자인 패턴인 Composite 패턴을 통해 해결하기로 했다.</strong></h3>
<h2 id="composite-패턴이란">Composite 패턴이란?</h2>
<ul>
<li>Component 라 불리는 최상위 클래스와 Leaf, Composite 라 불리는 하위 클래스가 협력하는 패턴이다.</li>
<li>Leaf는 Composite에서 사용될 클래스로 Composite에 여러 다른 구현체가 등록되어 사용된다.</li>
<li>클라이언트에서는 다양한 구현체를 한 번에 사용 가능하다.</li>
</ul>
<h3 id="뭔-소리지-다이어그램을-살펴보자">뭔 소리지? 다이어그램을 살펴보자</h3>
<p><img src="https://user-images.githubusercontent.com/84346055/268005635-77c29e87-85e6-4be2-bb40-1d0e092fedae.png" alt="Untitled"></p>
<h2 id="구조는-알겠으니-이제-코드를-통해-알아보자">구조는 알겠으니 이제 코드를 통해 알아보자</h2>
<ul>
<li>아래 코드에서 클래스의 각 역할은 다음과 같다.<ul>
<li>PropertyNamingStrategy == Component</li>
<li>CompositePropertyNamingStrategy == Composite</li>
<li>ComponentPropertyNamingStrategy의 생성자로 받는 PropertyNamingStrategy == Leaf</li>
<li>objectMapper 메서드 == 클라이언트 (Component를 사용하는 주체)</li>
</ul>
</li>
</ul>
<h3 id="compositepropertynamingstrategy">CompositePropertyNamingStrategy</h3>
<pre><code class="language-java">public class CompositePropertyNamingStrategy extends PropertyNamingStrategy {
    private final PropertyNamingStrategy[] strategies;

    public CompositePropertyNamingStrategy(final PropertyNamingStrategy... strategies) {
        this.strategies = strategies;
    }

    @Override
    public String nameForField(final MapperConfig&lt;?&gt; config, final AnnotatedField field, final String defaultName) {
        String name = defaultName;
        for (final PropertyNamingStrategy strategy : strategies) {
            name = strategy.nameForField(config, field, name);
        }
        return name;
    }

    @Override
    public String nameForGetterMethod(final MapperConfig&lt;?&gt; config, final AnnotatedMethod method, final String defaultName) {
        String name = defaultName;
        for (final PropertyNamingStrategy strategy : strategies) {
            name = strategy.nameForGetterMethod(config, method, name);
        }
        return name;
    }

    @Override
    public String nameForSetterMethod(final MapperConfig&lt;?&gt; config, final AnnotatedMethod method, final String defaultName) {
        String name = defaultName;
        for (final PropertyNamingStrategy strategy : strategies) {
            name = strategy.nameForSetterMethod(config, method, name);
        }
        return name;
    }
}</code></pre>
<h3 id="objectmapper-메서드">objectMapper 메서드</h3>
<pre><code class="language-java">@Bean
public ObjectMapper objectMapper() {
    return new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
            .setPropertyNamingStrategy(
                    new CompositePropertyNamingStrategy(                 // 여기서 사용된다.
                            PropertyNamingStrategies.SNAKE_CASE,         // 첫 번째 Leaf
                            PropertyNamingStrategies.LOWER_CAMEL_CASE)); // 두 번째 Leaf
}</code></pre>
<h2 id="결론">결론</h2>
<ul>
<li>위와 같은 코드를 통해 ObjectMapper를 하나만 사용할 수 있게 되었다.</li>
<li>또한, 여러개의 PropertyNamingStrategy를 한 번에 사용할 수 있게 되었다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TWTW - ObjectMapper]]></title>
            <link>https://velog.io/@jinjoo-lab/TWTW-ObjectMapper</link>
            <guid>https://velog.io/@jinjoo-lab/TWTW-ObjectMapper</guid>
            <pubDate>Thu, 14 Sep 2023 14:14:36 GMT</pubDate>
            <description><![CDATA[<h2 id="2개-이상의-objectmapper-사용">2개 이상의 ObjectMapper 사용</h2>
<h3 id="문제-설명">문제 설명</h3>
<p><img src="https://user-images.githubusercontent.com/84346055/268003580-e0226f9e-d89b-4333-b90b-9a0b2876fc15.png" alt="Untitled"></p>
<ul>
<li><p>구체적인 지명에 대한 정보는 <strong>Kakao Maps API</strong>를 사용하고 자동차 경로에 대한 정보는 <strong>Naver Maps API</strong>를 사용해야 하는 상황이다.</p>
<ul>
<li>각각의 WebClientConfig에서는 서로 다른 설정의 ObjectMapper를 주입받아 사용한다.<ul>
<li>Kakao Maps API는 SNAKE_CASE</li>
<li>Naver Maps API는 CAMEL_CASE</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>ObjectMapper란</strong></p>
<p>  JSON 형식에 대하여 응답을 직렬화하고 요청을 역직렬화 할 때 사용하는 기술</p>
</li>
</ul>
<blockquote>
<p>ObjectMapper 타입의 Bean을 2개 등록하기 때문에 주입을 받는 대상(WebClient)에서 해당 Bean에 대한 정보를 알아야 한다.</p>
</blockquote>
<h3 id="2개-이상의-동일한-타입의-빈---qualifier">2개 이상의 동일한 타입의 빈 - @Qualifier</h3>
<ul>
<li><strong>@Qualifier</strong><ul>
<li>@Qualifier 어노테이션을 사용하여 별도의 명칭을 정해주면 2개 이상의 동일한 타입의 빈에 대하여 구분하여 주입을 할 수 있다.</li>
</ul>
</li>
</ul>
<p><a href="https://www.baeldung.com/spring-qualifier-annotation">Spring @Qualifier Annotation</a></p>
<h3 id="문제가-발생한-코드">문제가 발생한 코드</h3>
<ol>
<li>KakaoWebClientConfig</li>
</ol>
<pre><code class="language-kotlin">@Configuration
public class KakaoWebClientConfig {
    private static final String HEADER_PREFIX = &quot;KakaoAK &quot;;
    private final ObjectMapper objectMapper;

    public KakaoWebClientConfig(@Qualifier(&quot;kakaoObjectMapper&quot;) ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Bean(name = &quot;KakaoWebClient&quot;)
    public WebClient webClient(
            @Value(&quot;${kakao-map.url}&quot;) final String url,
            @Value(&quot;${kakao-map.key}&quot;) final String authHeader) {
        final ExchangeStrategies exchangeStrategies =
                ExchangeStrategies.builder()
                        .codecs(
                                configurer -&gt; {
                                    configurer.defaultCodecs().maxInMemorySize(-1);
                                    configurer
                                            .defaultCodecs()
                                            .jackson2JsonDecoder(
                                                    new Jackson2JsonDecoder(objectMapper));
                                })
                        .build();

        return WebClient.builder()
                .exchangeStrategies(exchangeStrategies)
                .baseUrl(url)
                .defaultHeader(HttpHeaders.AUTHORIZATION, HEADER_PREFIX + authHeader)
                .build();
    }
}</code></pre>
<ol>
<li>NaverWebClientConfig</li>
</ol>
<pre><code class="language-kotlin">@Configuration
public class NaverWebClientConfig {
    private static final String HEADER_CLIENT_ID = &quot;X-NCP-APIGW-API-KEY-ID&quot;;
    private static final String HEADER_CLIENT_SECRET = &quot;X-NCP-APIGW-API-KEY&quot;;
    private final ObjectMapper objectMapper;

    @Autowired
    public NaverWebClientConfig(@Qualifier(&quot;naverObjectMapper&quot;) ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Bean(name = &quot;NaverWebClient&quot;)
    public WebClient webClient(
            @Value(&quot;${naver-map.url}&quot;) final String url,
            @Value(&quot;${naver-map.id}&quot;) final String clientId,
            @Value(&quot;${naver-map.secret}&quot;) final String secretKey) {

        final ExchangeStrategies exchangeStrategies =
                ExchangeStrategies.builder()
                        .codecs(
                                configurer -&gt; {
                                    configurer.defaultCodecs().maxInMemorySize(-1);
                                    configurer
                                            .defaultCodecs()
                                            .jackson2JsonDecoder(
                                                    new Jackson2JsonDecoder(objectMapper));
                                })
                        .build();

        return WebClient.builder()
                .exchangeStrategies(exchangeStrategies)
                .baseUrl(url)
                .defaultHeader(HEADER_CLIENT_ID, clientId)
                .defaultHeader(HEADER_CLIENT_SECRET, secretKey)
                .build();
    }
}</code></pre>
<ol>
<li>KakaoObjectMapperConfig</li>
</ol>
<pre><code class="language-kotlin">@Configuration
public class KakaoObjectMapperConfig {

    @Qualifier(&quot;kakaoObjectMapper&quot;)
    @Bean
    public ObjectMapper kakaoObjectMapper() {
        return new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
                .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
    }
}</code></pre>
<ol>
<li>NaverObjectMapperConfig</li>
</ol>
<pre><code class="language-kotlin">@Configuration
public class NaverObjectMapperConfig {

    @Qualifier(&quot;naverObjectMapper&quot;)
    @Bean
    public ObjectMapper naverObjectMapper() {
        return new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
                .setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE);
    }
}</code></pre>
<h3 id="error-code">Error Code</h3>
<pre><code>Parameter 0 of method mappingJackson2HttpMessageConverter in org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration required a single bean, but 2 were found:
    - kakaoObjectMapper: defined by method &#39;kakaoObjectMapper&#39; in class path resource [com/twtw/backend/config/mapper/KakaoObjectMapperConfig.class]
    - naverObjectMapper: defined by method &#39;naverObjectMapper&#39; in class path resource [com/twtw/backend/config/mapper/NaverObjectMapperConfig.class]</code></pre><ul>
<li>위 에러 코드는 <strong>2개 이상의 동일한 Bean 타입에 대해 무엇을 주입해서 사용해야 할지 모를 때</strong> 나오는 것이다 …..</li>
</ul>
<blockquote>
<p>분명히 각 WebClientConfig와 ObjectMapperConfig 파일에 <strong>@Qualifier</strong> 어노테이션을 사용하여 주입 대상을 명시해주었다.. 그렇다면 위 에러 코드는 왜 발생한 것일까????</p>
</blockquote>
<ul>
<li><strong>에러코드를 유심히 보자 오류가 발생한 대상이 무엇인가?</strong><ul>
<li>mappingJackson2HttpMessageConverter</li>
</ul>
</li>
</ul>
<h3 id="mappingjackson2httpmessageconverter">MappingJackson2HttpMessageConverter</h3>
<pre><code class="language-java">public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {

    private static final List&lt;MediaType&gt; problemDetailMediaTypes =
            Collections.singletonList(MediaType.APPLICATION_PROBLEM_JSON);

    @Nullable
    private String jsonPrefix;

    /**
     * Construct a new {@link MappingJackson2HttpMessageConverter} with a custom {@link ObjectMapper}.
     * You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
     * @see Jackson2ObjectMapperBuilder#json()
     */
    public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, MediaType.APPLICATION_JSON, new MediaType(&quot;application&quot;, &quot;*+json&quot;));
    }
}</code></pre>
<ul>
<li>Jackson ObjectMapper를 사용할 경우 위와 같은 클래스를 사용한다.</li>
<li>생성자를 유심히 살펴보면 <strong>ObjectMapper 타입의 파라미터를 주입받는 것</strong>을 알 수 있다.</li>
</ul>
<h3 id="why">Why?</h3>
<ul>
<li>@Qualifier 어노테이션을 사용할 경우 주입을 받아 사용하는 대상에서도 똑같이 어노테이션을 명시한다면 해당 빈을 우선적으로 파악하여 주입이 정상적으로 수행될 것이다.</li>
<li>하지만 @Qualifier 어노테이션이 명시되어 있지 않은 대상이라면???<ul>
<li>2가지 이상의 주입 빈에 대한 <strong>우선순위를 결정하지 못한다.</strong></li>
</ul>
</li>
</ul>
<h3 id="해결-방법">해결 방법</h3>
<ul>
<li>@Primary<ul>
<li>2개 이상의 동일한 타입의 빈에 대한 구분을 해주는 방법에는 여러가지가 있다.<ul>
<li>Field Name</li>
<li>@Qualifier</li>
<li><strong>@Primary</strong></li>
</ul>
</li>
<li>@Primary 어노테이션을 사용한다면 해당 빈을 우선적으로 주입받아 사용한다.<ul>
<li>@Qualifier보다는 우선순위가 뒤에 있지만 위의 문제 경우에는 @Qualifier가 적용될 수 없기 때문에 @Primary의 사용이 더 올바른 방법이다.</li>
</ul>
</li>
</ul>
</li>
<li>2개의 ObjectMapper에 대하여 1개의 ObjectMapper에 @Primary 어노테이션을 사용한다.</li>
</ul>
<pre><code class="language-java">@Configuration
public class KakaoObjectMapperConfig {
    @Primary
    @Qualifier(&quot;kakaoObjectMapper&quot;)
    @Bean
    public ObjectMapper kakaoObjectMapper() {
        return new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
                .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
    }
}</code></pre>
]]></description>
        </item>
    </channel>
</rss>