<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>쭌의 개발기록👩🏻‍💻✨</title>
        <link>https://velog.io/</link>
        <description>서버 개발자를 꿈꾸며 성장하는 쭌입니다 😽</description>
        <lastBuildDate>Tue, 30 Jan 2024 10:11:17 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>쭌의 개발기록👩🏻‍💻✨</title>
            <url>https://velog.velcdn.com/images/dev_tmb/profile/dbfcbe5f-a876-418f-94d4-2685fff3737d/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 쭌의 개발기록👩🏻‍💻✨. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_tmb" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[푸시알림 구현 실습 📲🔔]]></title>
            <link>https://velog.io/@dev_tmb/%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84-%EC%8B%A4%EC%8A%B5</link>
            <guid>https://velog.io/@dev_tmb/%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84-%EC%8B%A4%EC%8A%B5</guid>
            <pubDate>Tue, 30 Jan 2024 10:11:17 GMT</pubDate>
            <description><![CDATA[<aside>
  <b>👾 실습 흐름은 크게 아래와 같이 정리할 수 있습니다.</b>

<p>1️⃣ 파이어베이스 서버와 스프링부트 서버 <code>연결</code></p>
<p>2️⃣ 파이어베이스에 보낼 <code>메시지</code> 형식을 DTO로 만들어서 커스텀하고 관리하자</p>
<p>3️⃣ 메시지 만들고, 파이어베이스 서버로 <code>전송</code>! → 이걸 호출하는 시점이 푸시알림을 보내고 싶은 서비스 로직 상에 있으면 됨</p>
</aside>


<h2 id="기본-세팅">기본 세팅</h2>
<p>[전제] User 테이블 안에 있는 1명의 유저를 대상으로 푸시 알림 테스트를 진행한다. </p>
<ol>
<li>스프링부트 프로젝트에 미리 ‘fcm_token’ 필드를 가지고 있는 User 테이블을 생성해두었습니다! 저는 회원가입 시 전달받은 FCM 토큰을 유저 DB에 한번에 저장해두고 유저 아이디와 각각 매핑되는 방식으로 구현해보겠습니다
 <img src="https://velog.velcdn.com/images/dev_tmb/post/d4541880-4102-41d1-b115-ede0f5752e59/image.png" alt=""></li>
</ol>
<ol start="2">
<li><p>Firebase Console &gt; ‘프로젝트 만들기’</p>
<p> <img src="https://velog.velcdn.com/images/dev_tmb/post/7b0c0aca-b7c9-4451-b1df-1b5ff1232b02/image.png" alt=""></p>
</li>
</ol>
<pre><code>그냥 ‘계속’ 을 쭉쭉 누르시고 생성하세요~

그럼 이렇게 내가 지정한 프로젝트명의 콘솔로 들어오게 됩니다

![](https://velog.velcdn.com/images/dev_tmb/post/2dc80fb0-20a3-45c9-9265-fda54f07d2fc/image.png)


***✅ 여기서 ‘엡에 Firebase를 추가하여 시작하기’ 부분이 클라분들(안드,아요)이 등록해주셔야 하는 부분입니다!!***</code></pre><ol start="3">
<li><p>왼쪽 상단에 톱니바퀴를 누르고, [프로젝트 설정]을 클릭 </p>
<p> <img src="https://velog.velcdn.com/images/dev_tmb/post/8ca06c0c-b5c1-46ae-abba-27b5e9e5ae80/image.png" alt=""></p>
</li>
</ol>
<ol start="4">
<li><p>[서비스 계정] 탭 &gt; [새 비공개 키 생성] 버튼 클릭</p>
<p> <img src="https://velog.velcdn.com/images/dev_tmb/post/48c7e245-05a7-4269-98a7-c4a52221ac44/image.png" alt=""></p>
</li>
</ol>
<p> 비공개 키(*.json)를 다운 받은 후, 프로젝트의 <code>src/main/resources</code> 디렉토리 안에 파일을 넣어줍시다!</p>
<p>  <img src="https://velog.velcdn.com/images/dev_tmb/post/f7c85f5a-6fb6-455b-b181-bf649b016bba/image.png" alt=""></p>
<p>  🚨 <code>.gitignore</code>에 미리 파일명을 추가해두고, <strong>절대절대절대 Github에 올리지 않도록 주의합니다</strong>. (저는 잘못 올렸다가 커밋 기록 삭제해달라고 깃헙에 문의드린 경험이 있습니다 ,,허허)</p>
<p> *파일명이 엄청 길텐데 얘는 그냥 임의로 바꿔주셔도 됩니다!</p>
<h2 id="본격적인-실습-gogogo-🏃🏻♀️">본격적인 실습 GOGOGO 🏃🏻‍♀️</h2>
<ol>
<li><p><code>build.gradle</code>에 FCM 설정에 필요한 의존성 라이브러리 추가</p>
<pre><code class="language-groovy"> // FCM
 implementation &#39;com.google.firebase:firebase-admin:9.1.1&#39;
 implementation &#39;com.squareup.okhttp3:okhttp:4.10.0&#39;  // Firebase 서버로 푸시 메시지 전송 시 필요</code></pre>
</li>
<li><p><code>application.yml</code> 설정 추가</p>
<pre><code class="language-yaml"> fcm:
   key:
     path: firebase-adminsdk.json
     scope: https://www.googleapis.com/auth/cloud-platform</code></pre>
<ul>
<li><p><code>path</code> : 위에서 추가한 비공개 키 파일의 경로와 파일명을 명시</p>
<p>  *지금은 같은 <code>resources/</code> 하위에 있으므로 그냥 파일명만 적어주자!</p>
</li>
<li><p><code>scope</code> : 권한의 범위를 설정</p>
<p>  → Google Cloud 데이터 확인, 수정, 구성, 삭제 및 Google 계정 이메일 주소 확인 에 대한 권한을 부여하도록 명시</p>
<p>  *참고 - <a href="https://developers.google.com/identity/protocols/oauth2/scopes?hl=ko#fcm">https://developers.google.com/identity/protocols/oauth2/scopes?hl=ko#fcm</a></p>
</li>
</ul>
</li>
<li><p><code>FCMConfig</code> 에서 기본 설정에 대한 내용 추가</p>
<pre><code class="language-java"> // common/config/FCMConfig.java
 @Slf4j
 @Configuration
 public class FCMConfig {

     @Value(&quot;${fcm.key.path}&quot;)
     private String SERViCE_ACCOUNT_JSON;

     @PostConstruct
     public void init() {
         try {
             ClassPathResource resource = new ClassPathResource(SERViCE_ACCOUNT_JSON);
             InputStream serviceAccount = resource.getInputStream();

             FirebaseOptions options = FirebaseOptions.builder()
                     .setCredentials(GoogleCredentials.fromStream(serviceAccount))
                     .build();

             FirebaseApp.initializeApp(options);
                         log.info(&quot;파이어베이스 서버와의 연결에 성공했습니다.&quot;);
         } catch (IOException e) {
             log.error(&quot;파이어베이스 서버와의 연결에 실패했습니다.&quot;);
         }
     }

 }</code></pre>
<ul>
<li><p><code>@@Value</code> 어노테이션으로 <code>application.yml</code> 에서 지정해준 파일명을 SERVICE_ACCOUNT_JSON 변수에 넣어준다.</p>
</li>
<li><p>파이어베이스 서버와의 연결을 위해서 키의 내용이 필요한데, 파일경로+이름을 통해 해당 파일로 접근해  <code>InputStream</code> 클래스의 <code>fromStream()</code> 메서드를 이용해 파일의 내용을 읽는다.</p>
</li>
<li><p>키를 통해 FirebaseApp의 설정정보로 연결을 시도하고 <strong><em>IOException</em></strong>이 발생하지 않았다면 연결 성공이다!</p>
<p> <img src="https://velog.velcdn.com/images/dev_tmb/post/798b2714-61c2-4f5d-aec2-5cf2d042a88f/image.png" alt=""></p>
</li>
</ul>
</li>
</ol>
<pre><code>- Android, iOS 플랫폼 별 설정을 각각 지정해주려면?

    → 플랫폼마다 별도의 설정이 필요한 경우에 사용한다.

    ```groovy
    // Android
    public AndroidConfig TokenAndroidConfig(FCMPushRequestDto request) {
        return AndroidConfig.builder()
                .setCollapseKey(request.getCollapseKey())
                .setNotification(AndroidNotification.builder()
                        .setTitle(request.getTitle())
                        .setBody(request.getBody())
                        .build())
                .build();
    }

    // Apple
    public ApnsConfig TokenApnsConfig(FCMPushRequestDto request) {
        return ApnsConfig.builder()
                .setAps(Aps.builder()
                        .setAlert(
                                ApsAlert.builder()
                                        .setTitle(request.getTitle())
                                        .setBody(request.getBody())
                                        .setLaunchImage(request.getImgUrl())
                                        .build()
                        )
                        .setCategory(request.getCollapseKey())
                        .setSound(&quot;default&quot;)
                        .build())
                .build();
    }
    ```</code></pre><ol start="4">
<li><p>공식 문서와 동일한 메시지 형식에 따라 <code>FCMMessage</code> 클래스 생성</p>
<ul>
<li><p>구성 : 공식 문서의 전송 요청 형태와 완벽히 일치</p>
<p>  <a href="https://firebase.google.com/docs/cloud-messaging/send-message?hl=ko">앱 서버 전송 요청 작성  |  Firebase 클라우드 메시징</a></p>
</li>
</ul>
</li>
</ol>
<pre><code>```java
@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class FCMMessage {

    **private boolean validateOnly;
    private Message message;**

    @Builder
    @AllArgsConstructor
    @Getter
    public static class Message {
        **private Notification notification;**   // 모든 모바일 OS에 통합으로 사용할 수 있는 Notification
        private String token;   // 특정 디바이스(클라이언트)에 알림을 보내기 위한 토큰
        private String topic;   // 주제 구독 시 사용
    }

    @Builder
    @AllArgsConstructor
    @Getter
    **public static class Notification {
        private String title;
        private String body;
        private String image;
    }**

}
```

- 공식 문서

    ```java
     * - Request
     * {
     *   **&quot;validate_only&quot;: boolean,
     *   &quot;message&quot;: {
     *     object (Message)
     *   }**
     * }
     *
     *
     * - Message
     * {
     *   **&quot;name&quot;: string,
     *   &quot;data&quot;: {
     *     string: string,
     *     ...
     *   },
     *   &quot;notification&quot;: {   ✅모든 플랫폼에서 사용할 기본 알림 템플릿
     *     object (Notification)
     *   },**
     *   &quot;android&quot;: {    FCM 연결 서버를 통해 전송된 메시지에 대한 Android 전용 옵션 
     *     object (AndroidConfig)
     *   },
     *   &quot;webpush&quot;: {   Web 푸시 알림을 위한 webpush 프로토콘 옵션
     *     object (WebpushConfig)
     *   },
     *   &quot;apns&quot;: {      Apple 푸시 알림 서비스 특정 옵션  
     *     object (ApnsConfig)
     *   },
     *   &quot;fcm_options&quot;: {  모든 플랫폼에서 사용할 FCM SDK 기능 옵션용 템플릿
     *     object (FcmOptions)
     *   },
     *
     *   // Union field target can be only one of the following:
     *   &quot;token&quot;: string,    메시지를 보낼 등록 토큰 (특정 클라이언트 대상)
     *   &quot;topic&quot;: string,    Topic 발행의 경우, 사용
     *   &quot;condition&quot;: string
     *   // End of list of possible types for union field target.
     * }
    ```

    → 우리는 Android, iOS 공통으로 사용할 것이므로 ***Notification*** 부분만 참고하면 된다!</code></pre><ol start="5">
<li><p>커스텀 알림 DTO 생성</p>
<p> <img src="https://velog.velcdn.com/images/dev_tmb/post/9d918e8c-793c-4f8d-bdcb-7a61d27306ec/image.png" alt=""></p>
<p> → title, body로 구성</p>
<p> <img src="https://velog.velcdn.com/images/dev_tmb/post/7f9027d2-971b-4006-8c81-b651a7e38a58/image.png" alt=""></p>
<p> → title, body, image로 구성</p>
<p> 앱에서 수신하는 푸시알림의 형태를 살펴보면 다음과 같이 제목, 바디의 형태로 구성되어 있다는 공통점을 발견할 수 있는데요, (앱의 아이콘과 시간 정보는 저희 서버에서 처리해주는 것이 아닙니다!)
 이를 커스텀하기 위한 DTO를 만들어서 알림을 보내는 서비스 로직에서 공통적으로 적용시켜줍시다</p>
<ul>
<li><p><strong>title</strong></p>
</li>
<li><p><strong>body</strong></p>
</li>
<li><p>image</p>
</li>
<li><p><strong>targetToken</strong></p>
<p>→ 이 DTO 필드의 값들은 위 Notification 클래스의 <strong>title</strong>, <strong>body</strong>, Message 클래스의 <strong>token</strong>에 매핑됩니다.</p>
<pre><code class="language-java">@Slf4j
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FCMPushRequestDto {

  private String targetToken;

  @JsonInclude(JsonInclude.Include.NON_NULL)
  private String title;

  @JsonInclude(JsonInclude.Include.NON_NULL)
  private String body;
}</code></pre>
</li>
<li><p><code>@JsonInclude(JsonInclude.Include.NON_NULL)</code></p>
<p>  JSON 문자열로 요청, 응답값이 보내질 때 <strong><em>null</em></strong> 인 경우에 해당 필드는 아예 제외하고 반환하게 하는 어노테이션입니다. </p>
<p>  *이 어노테이션이 없다면 값이 null인 경우에 아래와 같이 표시되지만, 이를 아예 제외시켜버리기 위해서 해당 어노테이션을 붙여주었습니다!</p>
<pre><code class="language-json">  {
      &quot;targetToken&quot; : &quot;fcm-token-unique-value&quot;,
      &quot;title&quot;: null,
      &quot;body&quot;: &quot;제목이 없는 푸시알림이군요&quot;
  }</code></pre>
</li>
</ul>
</li>
</ol>
<ol start="6">
<li><p><code>application.yml</code> 내용 추가</p>
<p> 특정 파이어베이스 서버(=우리가 생성한 프로젝트)에 전송하기 위해 API 엔드포인트를 추가해줘야 하는데요, API 엔드포인트 형식은 공식문서에 따르면 아래와 같습니다.</p>
<pre><code class="language-json"> POST https://fcm.googleapis.com/v1/projects/{firebase-project-ID}/messages:send</code></pre>
<p> <img src="https://velog.velcdn.com/images/dev_tmb/post/b334996a-be3d-48b8-ae1d-e0b2984724d8/image.png" alt=""></p>
</li>
</ol>
<pre><code>- 참고

     https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ko&amp;authuser=0#update-the-server-endpoint

    ![](https://velog.velcdn.com/images/dev_tmb/post/b75caaf7-d20e-47f7-be4a-580806117446/image.png)



```yaml
fcm:
  key:
    path: firebase-adminsdk.json
    scope: https://www.googleapis.com/auth/cloud-platform
  api:
    url: **https://fcm.googleapis.com/v1/projects/{fcm-project-id}/messages:send**
  topic:
    &quot;sopt-topic&quot;
```

- IntelliJ에서 환경변수 관리하기

   ![](https://velog.velcdn.com/images/dev_tmb/post/c13e6ef9-a689-4487-ad87-a5ae148bb263/image.png)


    [Edit] &gt; [Modify Options] &gt; [Environment variables]

    ![](https://velog.velcdn.com/images/dev_tmb/post/e1b01533-9e15-47c9-9bc6-a699d8fad694/image.png)


    `key=value` 의 형태로 작성하고(따옴표, 괄호, 공백 없음), 코드에서 환경변수를 사용할 때는 키 값으로 적어주면 됩니다!

    ![](https://velog.velcdn.com/images/dev_tmb/post/333d584f-ed2b-4c13-8e32-8318b69f20b0/image.png)</code></pre><ol start="7">
<li><p>이제 FCMService에서 Firebase에 보낼 메시지 객체를 생성하고 보내봅시다!</p>
<p> 먼저 의존성 주입은 얘 정도만..  DTO → json 문자열로 매핑해서 String 형태로 반환하기 위해 사용할 거예요</p>
<pre><code class="language-java"> // service/FCMService.java
 private final ObjectMapper objectMapper;  // FCM의 body 형태에 따라 생성한 값을 문자열로 저장하기 위한 Mapper 클래스</code></pre>
<p> 위 설정파일에 내용을 추가해줬으니, 어딘가 사용이 되겠죠? </p>
<p> 바로 지금입니다. </p>
<pre><code class="language-java"> // service/FCMService.java
 @Value(&quot;${fcm.key.path}&quot;)
 private String SERVICE_ACCOUNT_JSON;
 @Value(&quot;${fcm.api.url}&quot;)
 private String FCM_API_URL;

 @Value(&quot;${fcm.topic}&quot;)
 private String topic;   </code></pre>
<p> <code>@Value</code> 어노테이션으로 application.yml의 내용을 가져와서 변수에 값을 넣어준다.</p>
<ul>
<li><p><code>topic</code> : 어플리케이션 내에서 관리하는 주제 (유저들이 구독 가능한)를 가져오면, 해당 주제를 구독하는 유저에게 일괄적으로 푸시 메시지 전송 가능</p>
</li>
<li><p><code>SERVICE_ACCOUNT_JSON</code> : 초기 설정과 마찬가지로 해당 파일 안에 있는 <strong>비공개 키</strong>를 읽어서 헤더에 실어야 요청을 정상적으로 보낼 수 있다!</p>
</li>
<li><p><code>FCM_API_URL</code> : 요청을 어디에 보낼 것인지 지정 → 파이어베이스 특정 서버 (미리 생성해둔 파이어베이스 프로젝트가 되겠쥬?)</p>
</li>
<li><p>얘네들은 어디서 쓰이냐구요?*</p>
<pre><code class="language-java">// 실제 파이어베이스 서버로 푸시 메시지를 전송하는 메서드
private void sendPushMessage(String message) {

   try {
       OkHttpClient client = new OkHttpClient();
       RequestBody requestBody = RequestBody.create(message, MediaType.get(&quot;application/json; charset=utf-8&quot;));
       Request httpRequest = new Request.Builder()
                   .url(**FCM_API_URL**)  // 요청을 보낼 위치 (to 파이어베이스 서버)
               .post(requestBody)
               .addHeader(HttpHeaders.AUTHORIZATION, &quot;Bearer &quot; + getAccessToken())
               .addHeader(HttpHeaders.CONTENT_TYPE, &quot;application/json; UTF-8&quot;)
               .build();

       Response response = client.newCall(httpRequest).execute();

       log.info(&quot;단일 기기 알림 전송 성공 ! successCount: 1 messages were sent successfully&quot;);
       log.info(&quot;알림 전송: {}&quot;, response.body().string());
   } catch (IOException e) {
       throw new IllegalArgumentException(&quot;파일을 읽는 데 실패했습니다.&quot;);
   }
}

// Firebase에서 Access Token 가져오기
private String getAccessToken() {

   try {
       GoogleCredentials googleCredentials = GoogleCredentials
                   .fromStream(new ClassPathResource(**SERVICE_ACCOUNT_JSON**).getInputStream())
               .createScoped(List.of(&quot;https://www.googleapis.com/auth/cloud-platform&quot;));
       googleCredentials.refreshIfExpired();
       log.info(&quot;getAccessToken() - googleCredentials: {} &quot;, googleCredentials.getAccessToken().getTokenValue());

       return googleCredentials.getAccessToken().getTokenValue();
   } catch (IOException e) {
       throw new IllegalArgumentException(&quot;파일을 읽는 데 실패했습니다.&quot;);
   }
}</code></pre>
<p>그리구, 요청 바디에는 정해진 형식대로 반드시 지켜서 보내야 한다고 했었죠!</p>
<p>json 문자열을 만들어주기 위해 아까 만들어둔 <strong><code>FCMMessage</code> 클래스</strong>를 이용할 것입니다</p>
<pre><code class="language-java">// service/FCMService.java

// 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [단일 기기]
private String makeSingleMessage(FCMPushRequestDto request) throws JsonProcessingException {

   try {
       FCMMessage fcmMessage = FCMMessage.builder()
               .message(FCMMessage.Message.builder()
                       .token(request.getTargetToken())   // 1:1 전송 시 반드시 필요한 대상 토큰 설정
                       .notification(FCMMessage.Notification.builder()
                               .title(request.getTitle())
                               .body(request.getBody())
                               .image(request.getImage())
                               .build())
                       .build()
               ).validateOnly(false)
               .build();

       return objectMapper.writeValueAsString(fcmMessage);
   } catch (JsonProcessingException e) {
       throw new IllegalArgumentException(&quot;JSON 처리 도중에 예외가 발생했습니다.&quot;);
   }
}

// 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [주제 구독]
private String makeTopicMessage(FCMPushRequestDto request) throws JsonProcessingException {
   try {
       FCMMessage fcmMessage = FCMMessage.builder()
               .message(FCMMessage.Message.builder()
                               .topic(topic)   // 토픽 구독에서 반드시 필요한 설정 (token 지정 x)
                               .notification(FCMMessage.Notification.builder()
                                       .title(request.getTitle())
                                       .body(request.getBody())
                                       .image(request.getImage())
                                       .build())
                               .build()
               ).validateOnly(false)
               .build();

       return objectMapper.writeValueAsString(fcmMessage);
   } catch (JsonProcessingException e) {
       throw new IllegalArgumentException(&quot;JSON 처리 도중에 예외가 발생했습니다.&quot;);
   }
}

// 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [다수 기기]
private static MulticastMessage makeMultipleMessage(FCMPushRequestDto request, List&lt;String&gt; tokenList) {
   MulticastMessage message = MulticastMessage.builder()
           .setNotification(Notification.builder()
                   .setTitle(request.getTitle())
                   .setBody(request.getBody())
                   .setImage(request.getImage())
                   .build())
           .addAllTokens(tokenList)
           .build();

   log.info(&quot;message: {}&quot;, request.getTitle() +&quot; &quot;+ request.getBody());
   return message;
}</code></pre>
<p>타겟팅에 따라 3가지 방식으로 구현이 가능하다고 했었는데요..! 3가지 방식의 작동하는 구현을 모두 소개하고, 실제 테스트는 단일 기기만 해보겠습니다 (시험 끝나고 또 나머지도 추가할게요,,)</p>
<p>→ [참고] <a href="https://www.notion.so/2fdec67c96a145db9ea914b8ae73998b?pvs=21"><strong>타겟팅에 따른 구현</strong></a> </p>
<h3 id="단일-기기-특정-이벤트-발생-시-1명의-기기-대상">단일 기기 (특정 이벤트 발생 시, 1명의 기기 대상)</h3>
<pre><code class="language-java">/**
* 단일 기기
* - Firebase에 메시지를 수신하는 함수 (헤더와 바디 직접 만들기)
*/
@Transactional
public String pushAlarm(FCMPushRequestDto request) throws IOException {

   String message = **makeSingleMessage**(request);
   sendPushMessage(message);
   return &quot;알림을 성공적으로 전송했습니다. targetUserId = &quot; + request.getTargetToken();
}</code></pre>
<h3 id="다수-기기-특정-이벤트-발생-시-2명-이상의-기기-대상">다수 기기 (특정 이벤트 발생 시, 2명 이상의 기기 대상)</h3>
<pre><code class="language-java">/**
* 다수 기기
* - Firebase에 메시지를 수신하는 함수 (동일한 메시지를 2명 이상의 유저에게 발송)
*/
public String multipleSendByToken(FCMPushRequestDto request, List&lt;User&gt; userList) {

   // User 리스트에서 FCM 토큰만 꺼내와서 리스트로 저장
   List&lt;String&gt; tokenList = userList.stream()
           .map(User::getFcmToken).toList();

   // 2명만 있다고 가정
   log.info(&quot;tokenList: {}🌈,  {}🌈&quot;,tokenList.get(0), tokenList.get(1));

   MulticastMessage message = makeMultipleMessage(request, tokenList);

   try {
       BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message);
       log.info(&quot;다수 기기 알림 전송 성공 ! successCount: &quot; + response.getSuccessCount() + &quot; messages were sent successfully&quot;);
       log.info(&quot;알림 전송: {}&quot;, response.getResponses().toString());

       return &quot;알림을 성공적으로 전송했습니다. \ntargetUserId = 1.&quot; + tokenList.get(0) + &quot;, \n\n2.&quot; + tokenList.get(1);
   } catch (FirebaseMessagingException e) {
       log.error(&quot;다수기기 푸시메시지 전송 실패 - FirebaseMessagingException: {}&quot;, e.getMessage());
       throw new IllegalArgumentException(ErrorType.FAIL_TO_SEND_PUSH_ALARM.getMessage());
   }
}</code></pre>
<h3 id="주제topic-구독-유저의-이벤트와-무관하게-사전에-구독한-알림-일괄-발송">주제(topic) 구독 (유저의 이벤트와 무관하게 사전에 구독한 알림 일괄 발송)</h3>
<pre><code class="language-java">/**
* 주제 구독 등록 및 취소
* - 특정 타깃 토큰 없이 해당 주제를 구독한 모든 유저에 푸시 전송
*/
@Transactional
public String pushTopicAlarm(FCMPushRequestDto request) throws IOException {

   String message = **makeTopicMessage(request)**;
   sendPushMessage(message);
   return &quot;알림을 성공적으로 전송했습니다. targetUserId = &quot; + request.getTargetToken();
}</code></pre>
<p>주제를 구독한 유저들이 존재하려면, 구독을 등록하거나 취소하는 로직도 필요하겠죠!</p>
<pre><code class="language-java">// Topic 구독 설정 - application.yml에서 topic명 관리
// 단일 요청으로 최대 1000개의 기기를 Topic에 구독 등록 및 취소할 수 있다.
public void subscribe() throws FirebaseMessagingException {
   // These registration tokens come from the client FCM SDKs.
   List&lt;String&gt; registrationTokens = Arrays.asList(
           &quot;YOUR_REGISTRATION_TOKEN_1&quot;,
           // ...
           &quot;YOUR_REGISTRATION_TOKEN_n&quot;
   );

   // Subscribe the devices corresponding to the registration tokens to the topic.
   TopicManagementResponse response = FirebaseMessaging.getInstance().subscribeToTopic(
           registrationTokens, topic);

   log.info(response.getSuccessCount() + &quot; tokens were subscribed successfully&quot;);
}

// Topic 구독 취소
public void unsubscribe() throws FirebaseMessagingException {
   // These registration tokens come from the client FCM SDKs.
   List&lt;String&gt; registrationTokens = Arrays.asList(
           &quot;YOUR_REGISTRATION_TOKEN_1&quot;,
           // ...
           &quot;YOUR_REGISTRATION_TOKEN_n&quot;
   );

   // Unsubscribe the devices corresponding to the registration tokens from the topic.
   TopicManagementResponse response = FirebaseMessaging.getInstance().unsubscribeFromTopic(
           registrationTokens, topic);

   log.info(response.getSuccessCount() + &quot; tokens were unsubscribed successfully&quot;);
}</code></pre>
<h4 id="✨-최종-fcmservice-클래스-코드">✨ 최종 <code>FCMService</code> 클래스 코드</h4>
<pre><code class="language-java">   package jjun.server.pushalarm.service;

   import com.fasterxml.jackson.core.JsonProcessingException;
   import com.fasterxml.jackson.databind.ObjectMapper;
   import com.google.auth.oauth2.GoogleCredentials;
   import com.google.firebase.messaging.BatchResponse;
   import com.google.firebase.messaging.FirebaseMessaging;
   import com.google.firebase.messaging.FirebaseMessagingException;
   import com.google.firebase.messaging.MulticastMessage;
   import com.google.firebase.messaging.Notification;
   import com.google.firebase.messaging.TopicManagementResponse;
   import java.io.IOException;
   import java.util.Arrays;
   import java.util.List;
   import jjun.server.pushalarm.common.exception.ErrorType;
   import jjun.server.pushalarm.domain.User;
   import jjun.server.pushalarm.dto.fcm.FCMMessage;
   import jjun.server.pushalarm.dto.fcm.FCMPushRequestDto;
   import jjun.server.pushalarm.repository.UserRepository;
   import lombok.RequiredArgsConstructor;
   import lombok.extern.slf4j.Slf4j;
   import okhttp3.MediaType;
   import okhttp3.OkHttpClient;
   import okhttp3.Request;
   import okhttp3.RequestBody;
   import okhttp3.Response;
   import org.springframework.beans.factory.annotation.Value;
   import org.springframework.core.io.ClassPathResource;
   import org.springframework.http.HttpHeaders;
   import org.springframework.stereotype.Service;
   import org.springframework.transaction.annotation.Transactional;

   @Slf4j
   @Service
   @RequiredArgsConstructor
   public class FCMService {

       private final ObjectMapper objectMapper;  // FCM의 body 형태에 따라 생성한 값을 문자열로 저장하기 위한 Mapper 클래스

       @Value(&quot;${fcm.key.path}&quot;)
       private String SERVICE_ACCOUNT_JSON;
       @Value(&quot;${fcm.api.url}&quot;)
       private String FCM_API_URL;
       @Value(&quot;${fcm.topic}&quot;)
       private String topic;

       /**
        * 단일 기기
        * - Firebase에 메시지를 수신하는 함수 (헤더와 바디 직접 만들기)
        */
       @Transactional
       public String pushAlarm(FCMPushRequestDto request) throws IOException {

           String message = makeSingleMessage(request);
           sendPushMessage(message);
           return &quot;알림을 성공적으로 전송했습니다. targetUserId = &quot; + request.getTargetToken();
       }

       /**
        * 다수 기기
        * - Firebase에 메시지를 수신하는 함수 (동일한 메시지를 2명 이상의 유저에게 발송)
        */
       public String multipleSendByToken(FCMPushRequestDto request, List&lt;User&gt; userList) {

           // User 리스트에서 FCM 토큰만 꺼내와서 리스트로 저장
           List&lt;String&gt; tokenList = userList.stream()
                   .map(User::getFcmToken).toList();

           // 2명만 있다고 가정
           log.info(&quot;tokenList: {}🌈,  {}🌈&quot;,tokenList.get(0), tokenList.get(1));

           MulticastMessage message = makeMultipleMessage(request, tokenList);

           try {
               BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message);
               log.info(&quot;다수 기기 알림 전송 성공 ! successCount: &quot; + response.getSuccessCount() + &quot; messages were sent successfully&quot;);
               log.info(&quot;알림 전송: {}&quot;, response.getResponses().toString());

               return &quot;알림을 성공적으로 전송했습니다. \ntargetUserId = 1.&quot; + tokenList.get(0) + &quot;, \n\n2.&quot; + tokenList.get(1);
           } catch (FirebaseMessagingException e) {
               log.error(&quot;다수기기 푸시메시지 전송 실패 - FirebaseMessagingException: {}&quot;, e.getMessage());
               throw new IllegalArgumentException(ErrorType.FAIL_TO_SEND_PUSH_ALARM.getMessage());
           }
       }

       /**
        * 주제 구독 등록 및 취소
        * - 특정 타깃 토큰 없이 해당 주제를 구독한 모든 유저에 푸시 전송
        */
       @Transactional
       public String pushTopicAlarm(FCMPushRequestDto request) throws IOException {

           String message = makeTopicMessage(request);
           sendPushMessage(message);
           return &quot;알림을 성공적으로 전송했습니다. targetUserId = &quot; + request.getTargetToken();
       }

       // Topic 구독 설정 - application.yml에서 topic명 관리
       // 단일 요청으로 최대 1000개의 기기를 Topic에 구독 등록 및 취소할 수 있다.

       public void subscribe() throws FirebaseMessagingException {
           // These registration tokens come from the client FCM SDKs.
           List&lt;String&gt; registrationTokens = Arrays.asList(
                   &quot;YOUR_REGISTRATION_TOKEN_1&quot;,
                   // ...
                   &quot;YOUR_REGISTRATION_TOKEN_n&quot;
           );

           // Subscribe the devices corresponding to the registration tokens to the topic.
           TopicManagementResponse response = FirebaseMessaging.getInstance().subscribeToTopic(
                   registrationTokens, topic);

           log.info(response.getSuccessCount() + &quot; tokens were subscribed successfully&quot;);
       }

       // Topic 구독 취소
       public void unsubscribe() throws FirebaseMessagingException {
           // These registration tokens come from the client FCM SDKs.
           List&lt;String&gt; registrationTokens = Arrays.asList(
                   &quot;YOUR_REGISTRATION_TOKEN_1&quot;,
                   // ...
                   &quot;YOUR_REGISTRATION_TOKEN_n&quot;
           );

           // Unsubscribe the devices corresponding to the registration tokens from the topic.
           TopicManagementResponse response = FirebaseMessaging.getInstance().unsubscribeFromTopic(
                   registrationTokens, topic);

           log.info(response.getSuccessCount() + &quot; tokens were unsubscribed successfully&quot;);
       }

       // 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [단일 기기]

       private String makeSingleMessage(FCMPushRequestDto request) throws JsonProcessingException {

           FCMMessage fcmMessage = FCMMessage.builder()
                   .message(FCMMessage.Message.builder()
                           .token(request.getTargetToken())   // 1:1 전송 시 반드시 필요한 대상 토큰 설정
                           .notification(FCMMessage.Notification.builder()
                                   .title(request.getTitle())
                                   .body(request.getBody())
                                   .image(request.getImage())
                                   .build())
                           .build()
                   ).validateOnly(false)
                   .build();

           return objectMapper.writeValueAsString(fcmMessage);
       }

       // 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [주제 구독]
       private String makeTopicMessage(FCMPushRequestDto request) throws JsonProcessingException {

           FCMMessage fcmMessage = FCMMessage.builder()
                   .message(FCMMessage.Message.builder()
                                   .topic(topic)   // 토픽 구독에서 반드시 필요한 설정 (token 지정 x)
                                   .notification(FCMMessage.Notification.builder()
                                           .title(request.getTitle())
                                           .body(request.getBody())
                                           .image(request.getImage())
                                           .build())
                                   .build()
                   ).validateOnly(false)
                   .build();

           return objectMapper.writeValueAsString(fcmMessage);
       }

       // 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [다수 기기]
       private static MulticastMessage makeMultipleMessage(FCMPushRequestDto request, List&lt;String&gt; tokenList) {
           MulticastMessage message = MulticastMessage.builder()
                   .setNotification(Notification.builder()
                           .setTitle(request.getTitle())
                           .setBody(request.getBody())
                           .setImage(request.getImage())
                           .build())
                   .addAllTokens(tokenList)
                   .build();

           log.info(&quot;message: {}&quot;, request.getTitle() +&quot; &quot;+ request.getBody());
           return message;
       }

       // 실제 파이어베이스 서버로 푸시 메시지를 전송하는 메서드
       private void sendPushMessage(String message) throws IOException {

           OkHttpClient client = new OkHttpClient();
           RequestBody requestBody = RequestBody.create(message, MediaType.get(&quot;application/json; charset=utf-8&quot;));
           Request httpRequest = new Request.Builder()
                   .url(FCM_API_URL)
                   .post(requestBody)
                   .addHeader(HttpHeaders.AUTHORIZATION, &quot;Bearer &quot; + getAccessToken())
                   .addHeader(HttpHeaders.CONTENT_TYPE, &quot;application/json; UTF-8&quot;)
                   .build();

           Response response = client.newCall(httpRequest).execute();

           log.info(&quot;단일 기기 알림 전송 성공 ! successCount: 1 messages were sent successfully&quot;);
           log.info(&quot;알림 전송: {}&quot;, response.body().string());
       }

       // Firebase에서 Access Token 가져오기
       private String getAccessToken() throws IOException {

           GoogleCredentials googleCredentials = GoogleCredentials
                   .fromStream(new ClassPathResource(SERVICE_ACCOUNT_JSON).getInputStream())
                   .createScoped(List.of(&quot;https://www.googleapis.com/auth/cloud-platform&quot;));
           googleCredentials.refreshIfExpired();
           log.info(&quot;getAccessToken() - googleCredentials: {} &quot;, googleCredentials.getAccessToken().getTokenValue());

           return googleCredentials.getAccessToken().getTokenValue();
       }
   }</code></pre>
</li>
</ul>
</li>
</ol>
<pre><code>&lt;aside&gt;</code></pre><p>  ⭐ 우리가 서비스에 위 로직을 적용하려면, <b>①특정 이벤트가 발생한 경우</b> 또는 <b>②주기적으로 알림을 보낼 경우</b> 등을 생각해볼 수 있겠죠!?</p>
<pre><code>푸시알림을 이때 보내야겠다! 하는 그 시점에서 위 메서드를 호출하는 거라고 이해하시면 됩니다!

&lt;/aside&gt;</code></pre><ol start="8">
<li><p>자 이제 파이어베이스에 보내는 부분까지 구현했으니, 테스트를 해볼까요?</p>
<ul>
<li><p><strong>[👨‍👩‍👧‍👦엄빠도 어렸다] 실제 적용 사례</strong></p>
<p>  <img src="https://velog.velcdn.com/images/dev_tmb/post/1b301bdb-85fc-4b93-9380-77371ea02363/image.png" alt=""></p>
</li>
</ul>
</li>
</ol>
<pre><code>    SOPT 32기 앱잼 ‘엄빠도 어렸다’에서는 위 알림 템플릿을 Enum 으로 관리하여 적용했습니다! 

    ```java
    public static FCMPushRequestDto sendTodayQna(String targetToken, String section, String topic) {

        return FCMPushRequestDto.builder()
                .type(MessageType.FIREBASE)
                .targetToken(targetToken)
                .title(&quot;📞&quot; + section + PushMessage.TODAY_QNA.getTitle())
                .body(&quot;&#39;&quot; + topic + PushMessage.TODAY_QNA.getBody())
                .build();
    }
    ```

    ```java
    @Getter
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    public enum PushMessage {

        // 새로운 주제가 도착했을 때
        TODAY_QNA(&quot;로부터 교신이 도착했어요&quot;,
                &quot;&#39;에 대한 질문에 답변하고 추억을 나눠보세요 ☺️(수신거부 : 설정 - 푸시알림 off)&quot;),

        private String title;
        private String body;
    }
    ```

    그래서 ! 우리는 간단한 테스트를 해볼 거니까 테스트용으로 만들어 보겠습니다


`FCMPushRequestDto` 클래스에 ***static*** 메서드를 정의해서 푸시알림을 보내는 시점에 바로 메시지가 들어간 객체로 메서드 호출을 할 수 있도록 합시다! (ENUM은 따로 구현하지 않겠습니다!)

```java
public static FCMPushRequestDto sendTestPush(String targetToken) {

    return FCMPushRequestDto.builder()
            .targetToken(targetToken)
            .title(&quot;💚DO SOPT SERVER💚&quot;)
            .body(&quot;서팟 앱잼 화이팅! FCM 화이팅!&quot;)
            .build();
}
```

API 호출을 통해 테스트하기 위해서, `FCMController` 클래스를 생성해줍시다!

```java
@RestController
@RequestMapping(&quot;/alarm&quot;)
@RequiredArgsConstructor
public class FCMController {

    private final FCMService fcmService;

    /**
     * 헤더와 바디를 직접 만들어 알림을 전송하는 테스트용 API (상대 답변 알람 전송에 사용)
     */
    @PostMapping
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity&lt;String&gt; sendNotificationByToken(@RequestBody FCMPushRequestDto request) throws IOException {

        fcmService.pushAlarm(request);
        return ResponseEntity.ok().body(&quot;푸시알림 전송에 성공했습니다!&quot;);
    }
}
```

또, 테스트용으로 만들어둔 유저 조회 API 호출 시마다 위 Push 메시지를 전송하는 코드를 추가해보겠습니다!

```java
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    // 푸시알림 활성화가 필요한 로직이라면, FCMService를 주입!
    private final FCMService fcmService;

    public GetUserResponse getUserById(Long userId) {
        User user = userRepository.findById(userId).orElseThrow(
                () -&gt; new EntityNotFoundException(&quot;존재하지 않는 회원의 아이디입니다.&quot;)
        );
        //== User 조회 API 호출 시 푸시 알림 전송! ==//
        fcmService.pushAlarm(FCMPushRequestDto.sendTestPush(user.getFcmToken()));
        return GetUserResponse.of(user);
    }
}
```

PostMan에서 `POST http://localhost:8080/alarm` 로 테스트해보면, 아래와 같이 전송에 성공했다는 로그를 볼 수 있어요! 클라이언트 측에서 앱을 해당 파이어베이스 프로젝트에 등록한다면 실제 기기에서 테스트 결과를 확인할 수 있습니다 !!!

![](https://velog.velcdn.com/images/dev_tmb/post/f4fa2f1a-c6b3-4f57-86b0-1c7fbaf6b171/image.png)

![](https://velog.velcdn.com/images/dev_tmb/post/46d51afc-5954-470e-8d42-c5c87a074a39/image.png)


→ 이 부분은 FCM 토큰 값이 유효하지 않기 때문에(아직 유저가 앱을 설치할 수 없으니까요!) 발생하는 문제이고, 여기까지 나오면 서버 측에서는 성공입니다 😁

![](https://velog.velcdn.com/images/dev_tmb/post/ba18717a-6fd1-4e50-b3cf-8da8f8fd1c64/image.png)</code></pre><p>전체 실습코드는 제 깃헙에 올려뒀으니 참고해주실 분들은 참고해주세요 !!!</p>
<p><a href="https://github.com/jun02160/fcm-practice.git">https://github.com/jun02160/fcm-practice.git</a></p>
<p>위 실습은 정말 일부분만 다루며 , 더 다양하고 좋은 레퍼런스들이 많아요..! 처음 접하시는 분들을 위해 최대한 빠르고 쉽게 구현할 수 있도록 준비한 자료이니 많은 도움 되셨으면 좋겠습니다 :&gt;</p>
<p><strong>궁금한 점, 오류사항 지적, 같이 토론하고 싶은 부분</strong> 모두 다 <strong><em>대 환 영</em></strong> 입니다 !!!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RestDocs+Swagger UI 사용 중 마주한 Swagger Fetch Error]]></title>
            <link>https://velog.io/@dev_tmb/RestDocsSwagger-UI-%EC%82%AC%EC%9A%A9-%EC%A4%91-%EB%A7%88%EC%A3%BC%ED%95%9C-Swagger-Fetch-Error</link>
            <guid>https://velog.io/@dev_tmb/RestDocsSwagger-UI-%EC%82%AC%EC%9A%A9-%EC%A4%91-%EB%A7%88%EC%A3%BC%ED%95%9C-Swagger-Fetch-Error</guid>
            <pubDate>Mon, 29 Jan 2024 04:37:42 GMT</pubDate>
            <description><![CDATA[<h2 id="오류-사항">오류 사항</h2>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/afa3d2ae-8d63-48ed-b337-01bb92477d4b/image.png" alt=""></p>
<p>openapi3을 이용해 RestDocs + Swagger UI를 이용한 API 문서 자동화를 세팅하였다. 이때, 컨트롤러 단위 테스트가 성공하면 build.gradle에서 설정해준 디렉터리 위치에 파일이 업데이트 되는 플로우이다. </p>
<pre><code class="language-java">// build.gradle

openapi3 {
    servers = [
            { url = properties[&quot;server.BASE_URL&quot;] },
            { url = &quot;http://localhost:8080&quot; }
    ]
    title = &quot;모티부 API 명세서&quot;
    description = &quot;Motivoo REST Docs with SwaggerUI&quot;
    version = &quot;v0.0.1&quot;
    format = &quot;json&quot;
    outputFileNamePrefix = &quot;open-api-3.0.1&quot;
    outputDirectory = &#39;build/resources/main/static/docs&#39;
}</code></pre>
<p>Swagger UI가 적용될 파일의 위치는 application.yml에서 아래와 같이 지정해주었다.</p>
<pre><code class="language-java">// application.yml 

springdoc:
  swagger-ui:
    path: /swagger   # 접속할 URL 경로
    url: /docs/open-api-3.0.1.json  # 파일 위치</code></pre>
<p>테스트를 추가하고 통과하면 정상적으로 파일에는 반영이 되었지만, 특정 시점부터 갑자기 동작하지 않았다. 이를 로컬에서 실행하여 <a href="http://localhost:8080/swagger">localhost:8080/swagger</a> 로 접속했을 때는 fetch error가 뜨며 파일을 읽어들이지 못하는 문제가 발생했다. </p>
<h2 id="문제-원인-및-해결-방안">문제 원인 및 해결 방안</h2>
<h3 id="try1-security-관련-설정-추가">TRY1. Security 관련 설정 추가</h3>
<pre><code class="language-java">private static final String[] *AUTH_WHITELIST* = {    
        &quot;/&quot;, &quot;/**&quot;, &quot;/oauth/**&quot;, &quot;/api/**&quot;, 
        &quot;/actuator/health&quot;,&quot;/withdraw&quot;, &quot;/mission/**&quot;, &quot;/home&quot;,    
        &quot;/swagger/**&quot;, &quot;/swagger-ui/**&quot;
};</code></pre>
<p>위와 같이 White List에 Swagger 관련 URL을 모두 넣어주고, 해당 url 접속에 대한 인증 필터를 거치지 않도록 했다. </p>
<p>하지만 이 방법은 아니었나 보다. Fail..!</p>
<h3 id="try2-controller-의-접근-제어-protected-이하-✅">TRY2. Controller 의 접근 제어 protected 이하 ✅</h3>
<p>Controller 단위 테스트이다 보니 문제원인을 파악하는 데 있어, 컨트롤러 클래스 단으로 다시 트래킹 해보았다. 
<img src="https://velog.velcdn.com/images/dev_tmb/post/25b01f43-d436-4c2c-ba8e-b8e8ef93c69f/image.png" alt=""></p>
<p>잘 살펴보니 생성자 주입을 위한 <code>@RequiredArgsConstructor</code> 의 접근 제어가 protected로 설정되어 있는 것을 확인할 수 있었다. 이는 하위 클래스에서만 컨트롤러에 접근 가능하고 외부에서는 접근을 못하도록 제한하는 것이므로 절대 좋지 않은 방식이라고 생각한다. </p>
<p>protected의 범위에 벗어나는 테스트 클래스에서는 당연히 위 Controller 클래스의 생성자에 접근할 수 없었고, 해당 클래스의 인스턴스를 생성하는 것이 불가능해졌다. 의존성 주입이 안 되니 Swagger 실행도 거부당한 것으로 예상된다. </p>
<h3 id="try3-webconfig---cors-설정-✅">TRY3. WebConfig - CORS 설정 ✅</h3>
<pre><code class="language-java">@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;)
            .allowedOrigins(&quot;*&quot;)
            .allowedOriginPatterns(&quot;*&quot;)
            .allowedMethods(&quot;*&quot;);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler(&quot;/swagger-ui/**&quot;)
            .addResourceLocations(&quot;classpath:/META-INF/resources/docs/&quot;);
    }
}</code></pre>
<p>참고 자료를 통해 CORS 에러로 인한 fetch error의 가능성이 있음을 열어두고, WebConfig 클래스를 추가하였다. </p>
<p>하지만 우리는 https 배포를 따로 설정해주지 않은 상태이므로, 사실상 CORS 관련 설정은 필요없는 상태였다. 따라서 controller의 접근 제어 변경과 WebConfig 파일 자체를 삭제해버리니 드디어 동작하였다!!!! 😭😹</p>
<h2 id="📑-참고-자료">📑 참고 자료</h2>
<p><a href="https://imksh.com/81">[Spring] Swagger ui Failed to load API definition 에러</a></p>
<p><a href="https://velog.io/@shwncho/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-Swagger-3-TypeError-Failed-to-fetch">[Swagger] Swagger 3 TypeError: Failed to fetch</a></p>
<p><a href="https://github.com/springdoc/springdoc-openapi/issues/361">404 not found on swagger UI · Issue #361 · springdoc/springdoc-openapi</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@WebMvcTest로 테스트 시 빈 등록이 이루어지지 않는 문제]]></title>
            <link>https://velog.io/@dev_tmb/WebMvcTest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%9C-%EB%B9%88-%EB%93%B1%EB%A1%9D%EC%9D%B4-%EC%9D%B4%EB%A3%A8%EC%96%B4%EC%A7%80%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@dev_tmb/WebMvcTest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%9C-%EB%B9%88-%EB%93%B1%EB%A1%9D%EC%9D%B4-%EC%9D%B4%EB%A3%A8%EC%96%B4%EC%A7%80%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Mon, 29 Jan 2024 04:00:43 GMT</pubDate>
            <description><![CDATA[<h2 id="오류-사항">오류 사항</h2>
<pre><code class="language-java">Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name &#39;globalExceptionHandler&#39; defined in file [/Users/jun/Desktop/Developer/Motivoo-Team/Motivoo-Server/build/classes/java/main/sopt/org/motivooServer/global/advice/GlobalExceptionHandler.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type &#39;sopt.org.motivooServer.global.util.slack.SlackUtil&#39; available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:801)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:942)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:608)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:310)
    at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$3(SpringBootContextLoader.java:137)
    at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58)
    at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46)
    at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1388)
    at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:545)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:137)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:108)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:187)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:119)
    ... 86 more</code></pre>
<h2 id="문제-원인">문제 원인</h2>
<p><code>throws Exception</code> 으로 테스트코드에서 예외를 던지는 부분이 포함되는데, 이때 <strong><em>ExceptionHandler</em></strong> 에 의해 예외가 감지된다. 하지만 해당 클래스에서 주입받는 <strong><em>SlackUtil</em></strong> 클래스의 컴포넌트 스캔에 실패에 줄줄이 스프링 빈으로 등록되지 못하게 되는데, 이로 인해 발생한 에러이다. </p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
@RestControllerAdvice
public class GlobalExceptionHandler {

    private final SlackUtil slackUtil;

    ...
}

@Component
@RequiredArgsConstructor
public class SlackUtil {
    ...
}</code></pre>
<p>테스트 클래스에 <code>@SpringBootTest</code> 를 사용했다면 기본적으로 <code>@Component</code> 어노테이션이 붙은 클래스들의 컴포넌트 스캔이 이루어지지만, 현재는 <code>**@WebMvcTest</code> 로 특정 컨트롤러만 빈으로 등록하도록 제한**해두었기 때문에 위와 같은 문제가 발생한 것!</p>
<h2 id="해결-방안">해결 방안</h2>
<blockquote>
<p>테스트 클래스에서 빈 등록이 가능하게 하자!</p>
</blockquote>
<p>→ Mock 객체를 사용하여 해결!</p>
<pre><code class="language-java">@AutoConfigureRestDocs
@WebMvcTest(value = HealthCheckController.class)
class HealthCheckControllerTest {

    @Autowired
    private MockMvc mockMvc;

    **@MockBean**
    private SlackUtil slackUtil;
}</code></pre>
<p><code>@MockBean</code> 어노테이션을 붙여서 실제 슬랙을 연동하지 않고도 Mock 객첼 대신하여 의존성 문제를 해결하였다</p>
<h3 id="🤫-springboottest와-webmvctest를-모두-쓰면">🤫 <code>@SpringBootTest</code>와 <code>@WebMvcTest</code>를 모두 쓰면?</h3>
<ul>
<li><p>에러 로그</p>
<pre><code class="language-java">  java.lang.IllegalStateException: Configuration error: found multiple declarations of @BootstrapWith for test class [sopt.org.motivooServer.global.healthcheck.HealthCheckControllerTest]: [@org.springframework.test.context.BootstrapWith(value=org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTestContextBootstrapper.class), @org.springframework.test.context.BootstrapWith(value=org.springframework.boot.test.context.SpringBootTestContextBootstrapper.class)]
      at org.springframework.test.context.BootstrapUtils.resolveExplicitTestContextBootstrapper(BootstrapUtils.java:194)
      at org.springframework.test.context.BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.java:150)
      at org.springframework.test.context.BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.java:126)
      at org.springframework.test.context.TestContextManager.&lt;init&gt;(TestContextManager.java:113)
      at org.junit.jupiter.engine.execution.ExtensionValuesStore.lambda$getOrComputeIfAbsent$4(ExtensionValuesStore.java:86)
      at org.junit.jupiter.engine.execution.ExtensionValuesStore$MemoizingSupplier.computeValue(ExtensionValuesStore.java:223)
      at org.junit.jupiter.engine.execution.ExtensionValuesStore$MemoizingSupplier.get(ExtensionValuesStore.java:211)
      at org.junit.jupiter.engine.execution.ExtensionValuesStore$StoredValue.evaluate(ExtensionValuesStore.java:191)
      at org.junit.jupiter.engine.execution.ExtensionValuesStore$StoredValue.access$100(ExtensionValuesStore.java:171)
      at org.junit.jupiter.engine.execution.ExtensionValuesStore.getOrComputeIfAbsent(ExtensionValuesStore.java:89)
      at org.junit.jupiter.engine.execution.ExtensionValuesStore.getOrComputeIfAbsent(ExtensionValuesStore.java:93)
      at org.junit.jupiter.engine.execution.NamespaceAwareStore.getOrComputeIfAbsent(NamespaceAwareStore.java:61)
      at org.springframework.test.context.junit.jupiter.SpringExtension.getTestContextManager(SpringExtension.java:294)
      at org.springframework.test.context.junit.jupiter.SpringExtension.beforeAll(SpringExtension.java:113)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeBeforeAllCallbacks$12(ClassBasedTestDescriptor.java:395)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeBeforeAllCallbacks(ClassBasedTestDescriptor.java:395)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:211)
      at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:84)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:148)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
      at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
      at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
      at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
      at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
      at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
      at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
      at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
      at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
      at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
      at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
      at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
      at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
      at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
      at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
      at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
      at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
      at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
      at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:119)
      at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:94)
      at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:89)
      at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62)
      at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
      at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
      at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      at java.base/java.lang.reflect.Method.invoke(Method.java:568)
      at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
      at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
      at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
      at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
      at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source)
      at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
      at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
      at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
      at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
      at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
      at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)
      at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)
      at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
      at worker.org.gra</code></pre>
<p>둘 중 하나만 사용하도록 주석처리하여 해결!</p>
</li>
</ul>
<h3 id="산-넘어-산-🗻">산 넘어 산 🗻</h3>
<p>위에 걸 해결하니 등장한 에러</p>
<ul>
<li><p>에러 로그</p>
<pre><code class="language-java">  java.lang.AssertionError: Status expected:&lt;200&gt; but was:&lt;401&gt;
      at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:59)
      at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:122)
      at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:637)
      at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:214)</code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/9de33beb-2f80-4a1f-b969-75a764dc7758/image.png" alt=""></p>
<p>콘솔에 출력은 잘 되나, 응답 코드가 성공의 200이 아닌, 권한이 없다는 401로 반환되었다. </p>
<p>→ 이는 Spring Security를 사용하고 있었기에 인증 필터에 걸린 문제임!
<img src="https://velog.velcdn.com/images/dev_tmb/post/bba8212b-7062-4b05-8f84-3c1ad84582cb/image.png" alt=""></p>
<p>위처럼 테스트 메서드에 인증된 모의(가짜) 사용자를 만드는 <code>@WithMockUser(roles = “USER”)</code>를 추가하면 정상적으로 테스트에 통과하는 것을 확인할 수 있다!</p>
<p>JWT를 이용한 인증/인가 구현이 완료된 후에는 해당 부분을 제외하고, principal 객체를 @MockBean으로 주입받는 방식으로 변경해주었다!</p>
<h2 id="📑-참고-자료">📑 참고 자료</h2>
<p><a href="https://mangkyu.tistory.com/242">[Spring] 스프링부트 테스트를 위한 의존성과 어노테이션, 애플리케이션 컨택스트 캐싱(@SpringBootTest, @WebMvcTest, @DataJpaTest)</a></p>
<p><a href="https://sanghye.tistory.com/24">[JUnit] Test code 작성시 DI(Dependencies Inject) 를 적용하는 방법</a></p>
<p><a href="https://smpark1020.tistory.com/216">[SpringSecurity] JUnit 테스트 코드에 시큐리티 적용하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Test에서의 의존성 주입 실패]]></title>
            <link>https://velog.io/@dev_tmb/Test%EC%97%90%EC%84%9C%EC%9D%98-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85-%EC%8B%A4%ED%8C%A8</link>
            <guid>https://velog.io/@dev_tmb/Test%EC%97%90%EC%84%9C%EC%9D%98-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85-%EC%8B%A4%ED%8C%A8</guid>
            <pubDate>Mon, 29 Jan 2024 03:52:43 GMT</pubDate>
            <description><![CDATA[<h1 id="오류-사항">오류 사항</h1>
<pre><code class="language-java">2024-01-10T13:27:51.341+09:00 ERROR 19502 --- [           main] o.s.b.web.embedded.tomcat.TomcatStarter  : Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name &#39;jwtAuthenticationFilter&#39; defined in URL [jar:file:/Users/jun/Desktop/Developer/Motivoo-Team/Motivoo-Server/build/libs/motivooServer-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/sopt/org/motivooServer/domain/auth/config/JwtAuthenticationFilter.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name &#39;jwtTokenProvider&#39; defined in URL [jar:file:/Users/jun/Desktop/Developer/Motivoo-Team/Motivoo-Server/build/libs/motivooServer-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/sopt/org/motivooServer/domain/auth/config/JwtTokenProvider.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name &#39;tokenRedisRepository&#39; defined in URL [jar:file:/Users/jun/Desktop/Developer/Motivoo-Team/Motivoo-Server/build/libs/motivooServer-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/sopt/org/motivooServer/domain/auth/repository/TokenRedisRepository.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name &#39;stringRedisTemplate&#39; defined in class path resource [org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class]: Unsatisfied dependency expressed through method &#39;stringRedisTemplate&#39; parameter 0: Error creating bean with name &#39;redisConfig&#39;: Injection of autowired dependencies failed</code></pre>
<p>서버에서 Controller 테스트를 통해 RestDocs로 문서화하는 작업을 자동화하기 위해서 BaseControllerTest라는 상위의 추상 클래스를 두고 하위 컨트롤러 각각에 대응하도록 테스트 파일을 만들어 상속받아 구현하도록 하였다. </p>
<p><a href="https://velog.io/@dev_tmb/WebMvcTest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%9C-%EB%B9%88-%EB%93%B1%EB%A1%9D%EC%9D%B4-%EC%9D%B4%EB%A3%A8%EC%96%B4%EC%A7%80%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C">@WebMvcTest로 테스트 시 빈 등록이 이루어지지 않는 문제</a> 에서 ExceptionHandler에 의해 컴포넌트 스캔이 제대로 되지 않는 문제가 있었는데, JWT 의존성을 머지한 이후 같은 문제가 발생했다. </p>
<h1 id="문제-원인-및-해결-방안">문제 원인 및 해결 방안</h1>
<p>결론적으로 하루 간 해당 이슈 해결에 골머리를 앓으며 찾은 문제 원인은 다음과 같이 정리할 수 있다. </p>
<h2 id="1-의존성-주입-문제">#1 의존성 주입 문제</h2>
<p>Controller-Service-Repository-Domain 계층의 클래스와 같이 내가 직접 만들고 어노테이션을 통해 빈 등록을 하며 의존성을 주입받도록 명시하는 클래스 외에 JWT, Slack, AWS, Redis, Firebase와 같이 외부 라이브러리에 존재하는 클래스를 빈으로 등록하고자 할 때, 테스트 Application 환경에서도 Main과 동일하게 설정파일이 구성되어 있어야 @Value 어노테이션으로 값을 읽어오는 등 정상적인 처리가 이루어질 수 있다. </p>
<h3 id="try1-redisconfig-빈-등록">TRY1. RedisConfig 빈 등록</h3>
<p><strong><em>#Keyword - RedisTemplate 주입, 테스트에서의 @Value 어노테이션, 테스트 실행 환경</em></strong></p>
<pre><code class="language-java">APPLICATION FAILED TO START

Description:
Field restTemplate in com.github.fabiomaffioletti.firebase.repository.DefaultFirebaseRealtimeDatabaseRepository required a bean of type &#39;org.springframework.web.client.RestTemplate&#39; that could not be found.

The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)

Action:
Consider defining a bean of type &#39;org.springframework.web.client.RestTemplate&#39; in your configuration.
Process finished with exit code 1</code></pre>
<p>위와 같은 에러 로그가 반복되어서 RestTemplate을 빈으로 등록하고 있는 <code>RedisConfig</code> 클래스를 뜯어보았다. </p>
<pre><code class="language-java">@Configuration
@EnableRedisRepositories
public class RedisConfig {
    @Value(&quot;${data.redis.host}&quot;)
    private String redisHost;

    @Value(&quot;${data.redis.port}&quot;)
    private int redisPort;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public RedisTemplate&lt;String, String&gt; redisTemplate() {
        RedisTemplate&lt;String, String&gt; redisTemplate = new RedisTemplate&lt;&gt;();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}</code></pre>
<ol>
<li><p>먼저, <code>@Value</code> 어노테이션으로 application.yml의 값을 잘 받아오고 있는가에 의문이 들었다. </p>
<p> 서버의 로컬/개발/운영 환경 분리를 위해 application-**.yml의 포맷으로 파일을 관리하고 있었다. </p>
<p> 각 테스트 클래스마다 상속받는 BaseControllerTest에서 특정 설정 파일을 읽도록 명시했지만, 해당 부분부터 읽는 데 문제가 발생하지 않았는지 의심이 갔다.</p>
<pre><code class="language-java"> @AutoConfigureMockMvc
 @AutoConfigureRestDocs
 @ExtendWith({RestDocumentationExtension.class})
 @WebMvcTest(properties = &quot;spring.config.location=classpath:/application.yml&quot;)
 public abstract class BaseControllerTest {
             ...
 }</code></pre>
<p> 클래스 단의 어노테이션 구성은 위와 같았고, <code>@WebMvcTest</code> 어노테이션의 속성으로 application.yml을 위와 같이 명시하고 테스트용 환경 설정 <strong><em>application.yml</em></strong> 파일을 생성하여 해결할 수 있었다. </p>
<p> 아래 참고자료에 의하면 @Value의 동작 시점이 의존관계 주입 시점인 원인도 고려해볼 수 있지만, 위처럼 application-test.yml의 형태가 아닌 기본 application.yml로 파일을 세팅하니 잘 동작하는 것을 확인하였다. </p>
<p> *참고 자료</p>
<p> <a href="https://kkambi.tistory.com/210">Mock 테스트에서 Spring 프로퍼티를 읽어올 수 없던 문제</a></p>
<p> <a href="https://lovon.tistory.com/163">단위테스트시 @value 값 주입 방법</a></p>
<p> <a href="https://growth-coder.tistory.com/176">[Spring] @Value 동작 방식 및 주의 사항</a></p>
</li>
<li><p>위 RedisConfig 클래스와 연관되는 곳은?</p>
<blockquote>
<p><code>*TokenRedisRepository*</code></p>
</blockquote>
<ul>
<li><p>기존 코드</p>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class TokenRedisRepository {

  private final StringRedisTemplate stringRedisTemplate;
      private final ValueOperations&lt;String, String&gt; valueOperations;

      public void saveRefreshToken(String refreshToken, String account) {
      String key = PREFIX_REFRESH + refreshToken;
      valueOperations.set(key, account);
      redisTemplate.expire(key, refreshTokenValidityInMilliseconds, TimeUnit.SECONDS);
  }
      ...
}</code></pre>
</li>
<li><p>개선 코드</p>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class TokenRedisRepository {

  private final RedisConfig redisConfig;

      public void saveRefreshToken(String refreshToken, String account) {
      RedisTemplate&lt;String, String&gt; redisTemplate = redisConfig.redisTemplate();
      ValueOperations&lt;String, String&gt; valueOperations = redisTemplate.opsForValue();

      String key = PREFIX_REFRESH + refreshToken;
      valueOperations.set(key, account);
      redisTemplate.expire(key, refreshTokenValidityInMilliseconds, TimeUnit.SECONDS);
  }
      ...
}</code></pre>
</li>
</ul>
</li>
</ol>
<p>위 코드에서 확인할 수 있는 기존 코드의 문제점은, RedisConfig에서 이미 redisTemplate에 대한 빈 등록을 하고 있는데 정작 TokenRedisRepository에서 등록하지 않은 다른 RedisTemplate을 사용하고 있는 것이었다. </p>
<p>따라서, 개선된 코드처럼 RedisConfig 자체를 주입받아 해당 클래스에서 등록한 redisTemplate을 그대로 가져다 쓰도록 변경하였다. </p>
<h3 id="try2-mockbean으로-가짜-빈-등록">TRY2. MockBean으로 가짜 빈 등록</h3>
<p><strong><em>keyword - @SpringBootTest VS @WebMvcTest</em></strong></p>
<p><code>@SpringBootTest</code> 가 아닌, <code>@WebMvcTest</code> 를 사용하므로 classpath에 존재하는 모든 컴포넌트를 스캔하는 것이 아니라 테스트하고자 하는 일부 컨트롤러에 관련된 클래스만 빈으로 등록할 수 있었다. </p>
<p>이는 스프링 컨텍스트에서 컨트롤러와 관련된 것들만 빈으로 등록하고 테스트를 한다. (나머지 Service, Repository와 같은 레이어는 의존성을 끊어버림)</p>
<p>따라서 모든 ControllerTest 클래스에서 상속받는 BaseControllerTest에 @MockBean으로 가짜 빈을 등록해주고 빌드 시 컴포넌트 스캔에서 오류가 발생하는 것을 막았다.</p>
<pre><code class="language-java">@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ExtendWith({RestDocumentationExtension.class})
@WebMvcTest(properties = &quot;spring.config.location=classpath:/application.yml&quot;)
public abstract class BaseControllerTest {

    @Autowired
    protected WebApplicationContext ctx;

    @Autowired
    protected ObjectMapper objectMapper;

    @Autowired
    protected MockMvc mockMvc;

    @MockBean
    private RedisConfig redisConfig;

    @MockBean
    private TokenRedisRepository tokenRedisRepository;

    @MockBean
    private JwtTokenProvider jwtTokenProvider;

    @MockBean
    private CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint;

    @MockBean
    private SlackService slackService;

    @MockBean
    private AWSConfig awsConfig;

    @MockBean
    private S3Service s3Service;

    @MockBean
    private FirebaseConfig firebaseConfig;

    @MockBean
    private FirebaseService firebaseService;

    @BeforeEach
    void setUp(final RestDocumentationContextProvider restDocumentation) {
        mockMvc = MockMvcBuilders.webAppContextSetup(ctx)
            .apply(documentationConfiguration(restDocumentation))
            .addFilters(new CharacterEncodingFilter(&quot;UTF-8&quot;, true))
            .alwaysDo(print())
            .build();
    }
}</code></pre>
<p>*다른 클래스에서는 아래와 같이 상속받아 사용</p>
<pre><code class="language-java">@WebMvcTest(UserController.class)
public class UserControllerTest extends BaseControllerTest {
}</code></pre>
<p>*참고 자료</p>
<p><a href="https://m.blog.naver.com/sosow0212/223076265261">[Spring] @Mock과 @MockBean의 차이점은 무엇일까?</a></p>
<h2 id="2-배포">#2 배포</h2>
<p>이렇게 어찌저찌 로컬에서 직접 <code>./gradlew clean build</code> 명령어를 실행해 빌드하고 jar파일을 실행했을 때 문제없이 동작하는 것을 확인했고, 이를 도커 이미지로 빌드하여 컨테이너를 실행시키니 바로 죽어버리는 문제가 또 발생하였다. </p>
<p>블루/그린 무중단 배포 전략을 가져가며 blue, green 각각의 도커 컨테이너를 띄우는 방식으로 구축했는데 두 컨테이너 모두 어플리케이션 실행에서 바로 죽어버려 빌드 과정의 배포 설정 파일에서 에러가 있음을 감지했다. </p>
<p>Docker log를 직접 확인하고, Redis 서버 관련 에러가 나는 것을 발견했다. 문제 원인은 Redis 역시 도커 컨테이너로 띄우는 과정에서 컨테이너 이름을 application.yml의 redis host 설정 부분에 동일하게 적어주지 않아서 발생한 문제였다. </p>
<pre><code class="language-java">data:
  redis:
    host: redis # 로컬에서 테스트 할 때는 localhost로 사용
    port: 6379</code></pre>
<p>위 부분에서 로컬 환경 테스트 시에는 localhost로, 배포 시에는 redis로 설정해줘야 정상적으로 빌드될 수 있었다.</p>
<p>그동안 <code>./gradlew clean build -x test</code>로 배포 스크립트를 작성하며, 테스트 빌드에 대해서 오류를 경험해본 적이 많지 않은데, 이번에 RestDocs 에 Swagger UI를 적용하도록 새로운 시도를 하며 테스트 환경에서 경험할 수 있는 다양한 에러 상황을 잦게 마주한 것 같다. </p>
<p>배포의 경우, 직접적인 테스트 의존성 주입 문제와는 관련 없지만 같은 실수를 반복하지 않도록 기록하고 공유하고자 한다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DO SOPT 6차 세미나 생각 과제]]></title>
            <link>https://velog.io/@dev_tmb/DO-SOPT-6%EC%B0%A8-%EC%84%B8%EB%AF%B8%EB%82%98-%EC%83%9D%EA%B0%81-%EA%B3%BC%EC%A0%9C</link>
            <guid>https://velog.io/@dev_tmb/DO-SOPT-6%EC%B0%A8-%EC%84%B8%EB%AF%B8%EB%82%98-%EC%83%9D%EA%B0%81-%EA%B3%BC%EC%A0%9C</guid>
            <pubDate>Mon, 29 Jan 2024 02:33:51 GMT</pubDate>
            <description><![CDATA[<h1 id="bcrypt">#BCrypt</h1>
<blockquote>
<p>단방향 암호화를 위해 만들어진 해시 함수</p>
</blockquote>
<p>*복호화는 불가능</p>
<h2 id="hash">Hash</h2>
<p>해시 함수 : 임의의 길이를 갖는 임의의 데이터에 대해 고정된 길이의 데이터로 매핑하는 함수 </p>
<p>→ 이 함수의 결과물이 <strong><em>Hash 값</em></strong></p>
<ul>
<li>장점 : 빠른 속도</li>
<li>단점 : 보안에 취약</li>
</ul>
<p>→ 해시함수의 취약점을 보완하기 위한 방안 : <strong><em>Salt, Key Streching</em></strong></p>
<p><em>이들을 구현한 해시함수 중 가장 널리 사용되는 것이 *</em>BCrypt**인 것!</p>
<aside>
✨ **암호화가 진행되는 과정**

<h3 id="사용자-비밀번호-→-salt-생성-→-hashing-→-암호화된-비밀번호-db에-저장">사용자 비밀번호 → Salt 생성 → Hashing → 암호화된 비밀번호 DB에 저장</h3>
</aside>

<h3 id="salting">Salting</h3>
<p>실제 비밀번호 이외에 추가적으로 랜덤한 데이터 값을 더해서 해시 값을 계산하는 방법</p>
<p>→ 유저의 비밀번호에 난수를 추가하여 해시함수의 입력값으로 넣는 것</p>
<h3 id="key-streching">Key Streching</h3>
<p>단방향 해시 값을 계산한 뒤, 그 해시 값을 해시하고, 또 해시하는 반복 과정</p>
<p><em>*동일 장비에서 비교가능한 횟수를 제한</em></p>
<h1 id="redisremote-dictionary-server">#Redis(Remote Dictionary Server)</h1>
<blockquote>
<p>Remote(원격)에 위치하고 프로세스로 존재하는 In-Memory 기반의 Dictionary(key-value) 구조 데이터 관리 Server 시스템</p>
</blockquote>
<p><strong>**key-value 구조 데이터</strong>란?*  비관계형 구조 (≠ MySQL과 같은 관계형 데이터) 
→ 단순하게 키-값의 형태로 저장하는 구조</p>
<p>⇒ 따라서 관계형 데이터베이스와 같이 쿼리 연산의 지원은 없지만, <strong>데이터의 고속 읽기와 쓰기에는 최적화</strong>되어 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/12d3f498-2d33-4552-9b78-1911a8bb01e6/image.png" alt=""></p>
<h2 id="redis--일종의-nosql">Redis = 일종의 NoSQL</h2>
<p>*NoSQL 데이터베이스는 단순 검색 및 추가 작업에 있어서 매우 최적화된 키-값 저장 기법을 사용하여 응답속도나 처리 효율 등에 있어 매우 뛰어난 성능을 보인다. </p>
<p>Redis는 인 메모리(In-Memory) 솔루션으로도 분류되고, 다양한 데이터 구조체를 지원함으로써 DB, Cache, Message Queue, Shared Memory 용도로 사용될 수 있다. 일반 DB와 같이 디스크(SSD)에 데이터를 쓰는 구조가 아니라 메모리(DRAM)에서 데이터를 처리하기 때문에 작업 속도가 상당히 빠르다. </p>
<h3 id="redis의-특징">Redis의 특징</h3>
<ul>
<li>NoSql DBMS(비관계형 데이터베이스)로 분류되며 In memory 기반의 Key - Value 구조를 가진 데이터 관리 시스템</li>
<li>메모리 기반이라 모든 데이터들을 메모리에 저장하고 조회에 매우 빠르다. (리스트형 데이터 입력과 삭제가 MySQL에 비해서 10배정도 빠름)</li>
<li>메모리에 상주하면서 서비스의 상황에 따라 데이터베이스로 사용될 수 있으며, Cache로도 사용될 수 있다.</li>
<li>Remote Data Storage로 여러 서버에서 같은 데이터를 공유하고 보고 싶을 때 사용할 수 있다.</li>
<li>다양한 자료구조 를 지원한다. (Strings, Set, Sorted-Set, Hashes, List ...)</li>
<li>쓰기 성능 증대를 위한 클라이언트 측 샤딩(Sharding)을 지원한다.<ul>
<li>Sharding : 같은 테이블 스키마를 가진 데이터(row)를 다수의 데이터베이스에 분산하여 저장하는 방법</li>
</ul>
</li>
<li>메모리 기반이지만 Redis는 영속적인 데이터 보존(Persistence)이 가능하다. (메모리는 원래 휘발성)</li>
<li>스냅샷 기능을 제공해 메모리 내용을 *.rdb 파일로 저장하여 해당 시점으로 복구할 수 있다.</li>
<li>여러 프로세스에서 동시에 같은 key에 대한 갱신을 요청하는 경우, 데이터 부정합 방지 Atomic 처리 함수를 제공한다(원자성)</li>
<li>Redis는 기본적으로 1개의 싱글 쓰레드로 수행되기 때문에, 안정적인 인프라를 구축하기 위해서는 <strong><a href="http://redisgate.kr/redis/configuration/replication.php">Replication(Master-Slave 구조)Visit Website</a></strong>가 필수이다.</li>
</ul>
<h3 id="redis-캐시-활용-사례">Redis 캐시 활용 사례</h3>
<p>소셜 네트워크 서비스(SNS) ‘인스타그램, 트위터, 페이스북’ 등에 있는 타임라인 기능은 팔로우 하는 사용자들의 최근 게시물을 확인할 수 있는 페이지이다. </p>
<p>타임라인은 실시간 활동 사용자의 요청이 다량으로 처리되어야 하므로, 일일이 데이터베이스를 접근하는 방식으로 처리한다면 속도 저하의 문제로 효율성이 떨어진다. </p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/62c4970c-7109-4e11-b83c-33c734657905/image.png" alt=""></p>
<p>이들은 이 문제를 해결하기 위해 메모리 기반 NoSQL 기술인 Redis를 사용한다. 이들 서비스의 데이터 센터에 존재하는 방대한 양의 Redis Cluster는 각 사용자의 타임라인마다 노출될 타임라인 정보를 리스트 형태로 약 800개 가량을 캐싱한다. </p>
<p>실시간으로 발생하는 타임라인 요청은 바로 데이터베이스에 접근하지 않고, Redis에 캐싱된 타임라인 정보를 먼저 가져와서 이를 토대로 쿼리를 단순화한 이후에 데이터베이스에 접근한다. </p>
<h2 id="redis-실습">Redis 실습</h2>
<h3 id="redis-자료구조">Redis 자료구조</h3>
<table>
<thead>
<tr>
<th>메소드명</th>
<th>반환 오퍼레이션</th>
<th>Redis 자료구조</th>
</tr>
</thead>
<tbody><tr>
<td>opsForValue()</td>
<td>ValueOperations</td>
<td>String</td>
</tr>
<tr>
<td>opsForList()</td>
<td>ListOperations</td>
<td>List</td>
</tr>
<tr>
<td>opsForSet()</td>
<td>SetOperations</td>
<td>Set</td>
</tr>
<tr>
<td>opsForZSet()</td>
<td>ZSetOperations</td>
<td>Sorted Set</td>
</tr>
<tr>
<td>opsForHash()</td>
<td>HashOperations</td>
<td>Hash</td>
</tr>
<tr>
<td>- <code>RedisTemplate</code>: RedisTemplate은 Thread-Safe 하며 재사용이 가능</td>
<td></td>
<td></td>
</tr>
<tr>
<td>- <code>JacksonJsonSerializer</code>: JSON 포맷으로 데이터를 저장하는 경우</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-%EA%B0%9C%EB%85%90-%EC%86%8C%EA%B0%9C-%EC%82%AC%EC%9A%A9%EC%B2%98-%EC%BA%90%EC%8B%9C-%EC%84%B8%EC%85%98-%ED%95%9C%EB%88%88%EC%97%90-%EC%8F%99-%EC%A0%95%EB%A6%AC?category=918728#Redis_%EC%BA%90%EC%8B%9C%EC%9D%98_%ED%99%9C%EC%9A%A9_%EC%82%AC%EB%A1%80">[REDIS] 📚 레디스 소개 &amp; 사용처 (캐시 / 세션) - 한눈에 쏙 정리</a></p>
<p><a href="https://devlog-wjdrbs96.tistory.com/375">[Spring] Spring Data Redis로 자료구조 사용해보기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DO SOPT 3차 세미나 생각 과제]]></title>
            <link>https://velog.io/@dev_tmb/0t7b1h9p</link>
            <guid>https://velog.io/@dev_tmb/0t7b1h9p</guid>
            <pubDate>Mon, 29 Jan 2024 02:29:47 GMT</pubDate>
            <description><![CDATA[<h1 id="join-종류">#JOIN 종류</h1>
<aside>
💡 <b>JOIN</b>

<blockquote>
<p>두 개의 테이블을 서로 묶어서 하나의 결과를 만들어 낼 때 사용</p>
</blockquote>
<p>Left 집합과 Right 집합 간의 조건 있는 결합</p>
<p>→ 결과는 Cartesian Product의 subset</p>
<p><em>참고 - Left *</em>속성값<strong>과 Right **속성값</strong> 간의 조건 있는 결합을 말한다. (instance 간에 이루어지는 연산)</p>
</aside>

<h3 id="inner-join내부-조인">INNER JOIN(내부 조인)</h3>
<blockquote>
<p>두 테이블을 조인할 때, 두 테이블에 모두 지정한 열의 데이터가 있어야 함</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/a0ebdb2c-807c-4787-a087-249754e0f1c0/image.png" alt=""></p>
<pre><code class="language-sql">SELECT &lt;select_list&gt;
FROM 테이블A
INNER JOIN 테이블B
ON A.key = B.key</code></pre>
<h3 id="outer-join외부-조인">OUTER JOIN(외부 조인)</h3>
<blockquote>
<p>두 테이블을 조인할 떄, 1개의 테이블에만 데이터가 있어도 결과가 나옴</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/e1453ce0-5a77-4184-b61d-9c08209bd6b5/image.png" alt=""></p>
<pre><code class="language-sql">SELECT &lt;select_list&gt;
FROM 테이블A
FULL OUTER JOIN 테이블B
ON A.key = B.key</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/dfc03a48-b7ed-4350-b0d3-ce1fb3b3c6ee/image.png" alt=""></p>
<h3 id="cross-join상호-조인">CROSS JOIN(상호 조인)</h3>
<blockquote>
<p>한쪽 테이블의 모든 행과 다른 쪽 테이블의 모든 행을 조인하는 기능</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/628b32c2-5338-499c-b68a-5ea20f685e59/image.png" alt=""></p>
<h3 id="self-join자체-조인">SELF JOIN(자체 조인)</h3>
<blockquote>
<p>자신이 자신과 조인한다는 의미로, 1개의 테이블을 사용함</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/b3dde042-cd5f-4642-a25f-3ce59eb61b4d/image.png" alt=""></p>
<p>*참고자료 - <a href="https://hongong.hanbit.co.kr/sql-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95-joininner-outer-cross-self-join/">https://hongong.hanbit.co.kr/sql-기본-문법-joininner-outer-cross-self-join/</a></p>
<h1 id="column-table-의-역할과-옵션">#@Column, @Table 의 역할과 옵션</h1>
<h3 id="column"><code>@Column</code></h3>
<blockquote>
<p>객체 필드를 테이블 컬럼에 매핑할 때 사용</p>
</blockquote>
<table>
<thead>
<tr>
<th>속성</th>
<th>기능</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>필드와 매핑할 테이블의 컬럼 이름</td>
<td>객체의 필드 이름</td>
</tr>
<tr>
<td>insertable (거의 사용X)</td>
<td>엔티티 저장 시 이 필드도 같이 저장<br/>- false : 읽기 전용일 때 사용 → 이 필드는 데이터베이스에 저장 X</td>
<td>true</td>
</tr>
<tr>
<td>updatable (거의 사용X)</td>
<td>엔티티 수정 시 이 필드도 같이 수정<br/>- false : 읽기 전용일 때 사용 → 이 필드는 데이터베이스에 수정X</td>
<td>true</td>
</tr>
<tr>
<td>table (거의 사용X)</td>
<td>하나의 엔티티를 두 개 이상의 테이블에 매핑할 때 사용<br/>→ 지정한 필드를 다른 테이블에 매핑할 수 있음</td>
<td>현재 클래스가 매핑된 테이블</td>
</tr>
<tr>
<td>nullable(DDL)</td>
<td>null 값의 허용 여부를 설정<br/>- false : DDL 생성 시 NOT NULL 제약조건이 붙음</td>
<td>true<br/>*자바 기본 타입에서는 null 값을 입력할 수 없어 JPA는 기본적으로 기본 타입에 NOT NULL 제약 조건을 추가해준다. <b>(but @Column을 명시하면 nullable=true의 기본값이 지정되므로 false로 바꿔주는 것이 안전)</b></td>
</tr>
<tr>
<td>unique(DDL)</td>
<td>@Table의 uniqueConstraints와 같지만, 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용<br/>*만약 두 컬럼 이상을 사용해서 유니크 제약조건을 사용하려면 클래스 레벨에서 @Table.uniqueConstraints를 사용해야 함</td>
<td></td>
</tr>
<tr>
<td>columnDefinition(DDL)</td>
<td>데이터베이스 컬럼 정보를 직접 지정 가능</td>
<td>필드의 자바 타입과 방언 정보를 사용해서 적절한 컬럼 타입을 생성</td>
</tr>
<tr>
<td>length(DDL)</td>
<td>문자 길이 제약조건, String 타입에만 사용</td>
<td>255</td>
</tr>
<tr>
<td>precision, scale(DDL)</td>
<td>- precision : 소수점을 포함한 전체 자릿수<br/>- scale : 소수의 자릿수<br/>BigDecimal 타입에서 사용 (BigInteger도 가능) → 아주 큰 숫자나 정밀한 소수를 다루어야 할 때만 사용<br/>*double, float 타입에는 적용X</td>
<td>precision=19, scale=2</td>
</tr>
</tbody></table>
<h2 id="table"><code>@Table</code></h2>
<blockquote>
<p>엔티티와 매핑할 테이블 지정 *생략 시 엔티티명=테이블명으로 지정됨</p>
</blockquote>
<h3 id="속성">속성</h3>
<table>
<thead>
<tr>
<th>속성</th>
<th>기능</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>매핑할 테이블 이름</td>
<td>엔티티 이름 사용</td>
</tr>
<tr>
<td>catalog</td>
<td>catalog 기능이 있는 데이터베이스에서 catalog를 매핑</td>
<td></td>
</tr>
<tr>
<td>schema</td>
<td>schema 기능이 있는 데이터베이스에서 schema를 매핑</td>
<td></td>
</tr>
<tr>
<td>uniqueConstraints(DDL)</td>
<td>DDL 생성 시에 유니크 제약조건을 만든다. 2개 이상의 복합 유니크 제약조건을 만들 수 있다. <br/>*참고 - 이 기능은 스키마 자동 생성 기능을 사용해서 DDL을 만들 때만 사용됨</td>
<td></td>
</tr>
</tbody></table>
<h1 id="양방향-연관관계">#양방향 연관관계</h1>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/3e4df7e9-867b-47a2-9ee5-5a97cacc3032/image.png" alt=""></p>
<p>회원과 팀은 다대일 관계, 팀과 회원은 일대다 관계이다. </p>
<p>일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다. </p>
<p><em>*JPA는 List, Collection, Map, Set 등의 다양한 컬렉션을 지원한다.</em> </p>
<p>양방향 관계를 매핑하려면 일대다 관계를 가지는 쪽의 엔티티에서 다음과 같이 필드를 추가할 수 있다.</p>
<p><strong>*[참고]</strong> 데이터베이스 테이블은 원래부터 외래키를 통해 양방향 관계를 가지므로, 데이터베이스에 따로 추가할 내용은 없다. </p>
<pre><code class="language-java">@OneToMany(mappedBy = &quot;member&quot;)  
private List&lt;Order&gt; orders = new ArrayList&lt;Order&gt;();</code></pre>
<ul>
<li><p><code>@OneToMany</code> : 일대다 관계 매핑</p>
<ul>
<li>mappedBy 속성 : 양방향 매핑일 때 사용 → 반대쪽 매핑의 필드명을 값으로 준다.</li>
</ul>
</li>
<li><p>일대다 컬렉션을 조회하려면 아래와 같이 접근하면 된다.</p>
<pre><code class="language-java">  Member member = em.find(Member.class, &quot;member1&quot;);
  List&lt;Order&gt; orders = member.getOrders();  // (Member -&gt; Order) 방향으로 연관관계 데이터를 조회하는 &#39;객체 그래프 탐색&#39;</code></pre>
</li>
</ul>
<h3 id="양방향-매핑의-규칙-연관관계의-주인">양방향 매핑의 규칙: 연관관계의 주인</h3>
<blockquote>
<p><strong>연관관계 주인(객체) = 외래 키 관리자(테이블)</strong></p>
</blockquote>
<p>엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없다. 단지 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 한 것 뿐이다. </p>
<p>테이블은 원래부터 양방향 연관관계를 가지고 있는데, 만약 객체의 연관관계를 양방향으로 연결하여 연관관계 관리 포인트가 2곳이 된다면 <strong>객체의 참조는 둘인데 외래 키는 하나인 꼴</strong>이 된다. 따라서 두 객체 연관관계 중 하나를 정해서 외래 키를 관리할 <strong>연관관계의 주인</strong>을 결정해야 한다. </p>
<p>→ 이는 <code>mappedBy</code> 속성을 사용하여 지정할 수 있다. (주인이 아닌 쪽에서 주인을 지정)</p>
<table>
<thead>
<tr>
<th>연관관계 주인</th>
<th>주인이 아닌 것</th>
</tr>
</thead>
<tbody><tr>
<td>@ManyToOne</td>
<td>@OneToMany(mappedBy=”필드명”)</td>
</tr>
<tr>
<td>[FK]외래 키가 있는 곳 → 데이터베이스 연관관계와 매핑되는 부분</td>
<td>[PK] 여기의 값을 기본적으로 가져야 한다. by 참조 무결성</td>
</tr>
<tr>
<td>자식 테이블</td>
<td>부모 테이블</td>
</tr>
<tr>
<td>진짜 매핑  <br/>ex. Member.team : 회원은 팀 변경이 가능</td>
<td>가짜 매핑   <br/>ex. Team.members : 팀은 회원 변경이 불가능(READ only. 객체 그래프 탐색만 가능)</td>
</tr>
<tr>
<td>1:N에서 “N”에 해당</td>
<td>1:N에서 “1”에 해당</td>
</tr>
</tbody></table>
<p>1:N관계에서 <em>“N쪽이 1쪽을 참조하다”</em></p>
<p>연관관계의 주인만이 데이터베이스 연관관계와 매핑되고, 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면, 주인이 아닌 쪽은 읽기만 할 수 있다.</p>
<p>위 예제 코드에서 Member와 Order 중에 주인은 Member일 것이고, Team과 Member 중에서는 Team이 될 것이다. </p>
<h3 id="저장">저장</h3>
<pre><code class="language-java">// 회원에 팀을 저장할 때 
member.setTeam(team)  // 연관관계 설정(연관관계의 주인)
// 팀에 회원을 저장할 때
team.getMembers().add(member);  // 무시(연관관계의 주인X)</code></pre>
<p><strong>양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다.</strong> 따라서 아래의 CASE는 데이터베이스(외래 키)에 영향을 주지 않으며, 데이터베이스에 저장할 때 무시된다. </p>
<p>그렇다고 해서 또 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 되는 것은 아니다. ORM은 객체와 관계형 데이터베이스 둘 다 중요하다. 따라서 순수한 객체로서의 연관관계까지 고려한다면, <strong>객체 관점에서는 위와 같이 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다</strong>. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다. </p>
<p>📍 위와 같이 양방향 연관관계를 설정해주는 코드로 작성하면 순수한 객체 상태에서도 동작하며, 테이블의 외래키도 정상 입력될 수 있다!</p>
<aside>
  🚨 <b>주의점</b>

<p>가장 흔히 하는 실수가 바로 주인이 아닌 곳에만 값을 입력하여 외래 키 값이 정상적으로 저장되지 않는 것이다. 이러한 문제가 발생하면 주인의 값을 입력했는지 확인해보는 것이 좋다. </p>
<p>→ 이와 같이 저장하면 데이터베이스 상의 테이블에는 null 값으로 들어가 있다. </p>
</aside>

<h3 id="연관관계-편의-메소드">연관관계 편의 메소드</h3>
<p>위 코드에서 각각 메서드를 호출하여 구현한다면 실수로 하나만 호출하여 양방향이 깨질 수도 있다는 위험이 있어, 양방향 관계에서 두 코드를 하나인 것처럼 사용하는 것이 안전하다.</p>
<pre><code class="language-java">public void setTeam(Team team) {

        // 연관관계 객체를 변경할 때, 기존의 관계를 제거하는 작업이 필요하다. 
        if (this.team != null) {
                this.team.getMembers().remove(this);
        }

        this.team = team;
        team.getMembers().add(this);
}</code></pre>
<p>기존의 관계가 삭제되지 않는 경우에도 이는 연관관계의 주인이 아닌 쪽에서 관계가 제거되지 않은 것이므로 DB에는 정상 반영되지만, 만약 영속성 컨텍스트가 아직 살아있는 상태에서 getMembers()를 호출한다면 기존의 제거되지 않은 관계의 객체가 반환될 것이다. 따라서 위와 같이 제거를 따로 처리해주는 것이 안전하다. </p>
<p>*실제 양방향 매핑을 구현하는 것은 비즈니스 로직의 필요에 따라 매우 복잡하다. 우선 단방향 매핑을 사용하고 반대 방향으로 객체 그래프 탐색 기능(JPQL 탐색 쿼리 포함)이 필요할 때 양방향을 사용하도록 코드를 추가해도 된다. </p>
<blockquote>
<p>☑️ 정리</p>
<ul>
<li>단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.</li>
<li>단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.</li>
<li>양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.</li>
</ul>
</blockquote>
<h1 id="cascadetype">#<em>CascadeType</em></h1>
<h2 id="영속성-전이-cascade">영속성 전이: CASCADE</h2>
<blockquote>
<p>특정 엔티티를 영속 상태로 만들 때 <strong>연관된 엔티티도 함께 영속 상태로 만들고 싶은 경우</strong>에 영속성 전이 기능을 사용할 수 있다. ⇒ 자식을 저장하려면 부모에 등록만 하면 된다.</p>
</blockquote>
<p>JPA는 CASCADE 옵션으로 영속성 전이를 제공한다. 즉, 영속성 전이를 사용하면 부모 엔티티 저장 시 자식 엔티티도 함께 저장할 수 있는 것이다. </p>
<pre><code class="language-java">// 부모 엔티티
@Entity
public class Parent {

    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = &quot;parent&quot;)
    private List&lt;Child&gt; children = new ArrayList&lt;&gt;();
}

// 자식 엔티티
@Entity
public class Child {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private Parent parent;
}</code></pre>
<h3 id="저장-1">저장</h3>
<ul>
<li><p>부모 1명에 자식 2명을 저장하는 경우</p>
<pre><code class="language-java">  private static void saveNoCascade(EntityManager em) {

      // 부모 저장
      Parent parent = new Parent();
      **em.persist(parent);**

      // 1번 자식 저장
      Child child1 = new Child();
      child1.setParent(parent);  // 자식 -&gt; 부모 연관관계 설정
      parent.getChildren().add(child1);  // 부모 -&gt; 자식
      **em.persist(child1);**

      // 2번 자식 저장
      Child child2 = new Child();
      child2.setParent(parent);  // 자식 -&gt; 부모 연관관계 설정
      parent.getChildren().add(child2);  // 부모 -&gt; 자식
      **em.persist(child2);**
  }</code></pre>
<p>  JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 하므로, 위와 같이 부모 엔티티를 영속 상태로 만든 뒤, 자식 엔티티도 각각 영속 상태로 만드는 과정이 필요하다. </p>
<p>  → 이때 영속성 전이(CASCADE)를 사용하면 부모만 영속 상태로 만들어도 나머지 연관된 자식까지 한 번에 영속 상태로 만들 수 있다. (3줄 → 1줄의 코드로 단순화!)</p>
</li>
<li><p><strong>영속성 전이를 활성화하자! → CASCADE 옵션(PERSIST) 적용</strong> 🌟</p>
<pre><code class="language-java">  @Entity
  public class Parent {

      @Id @GeneratedValue
      private Long id;

      **@OneToMany(mappedBy = &quot;parent&quot;, cascade = CascadeType.PERSIST)**
      private List&lt;Child&gt; children = new ArrayList&lt;&gt;();
  }</code></pre>
<p>  위와 같이 영속성 전이를 활성화하고 나면 저장 시, <code>setParent(parent)</code>만으로 연관관계가 추가됨과 동시에 부모를 persist 할 때 함께 영속 상태로 전이됨을 보장해준다. </p>
<p>  <img src="https://velog.velcdn.com/images/dev_tmb/post/d608c435-61d7-47b1-b2c0-f4be3a9dc36e/image.png" alt=""></p>
</li>
</ul>
<pre><code>*영속성 전이는 연관관계를 매핑하는 동작과는 아무 관련이 없고, 단지 엔티티 영속화 시 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다. 

⇒ 순서 상으로 1) 양방향 연관관계 추가 2) 영속 상태로 전이 로 이루어지는 것을 볼 수 있다!</code></pre><h3 id="삭제">삭제</h3>
<ul>
<li><p>위에서 저장한 부모와 자식 엔티티를 모두 제거하는 경우</p>
<pre><code class="language-java">  Parent findParent = em.find(Parent.class, 1L);
  Child findChild1 = em.find(Child.class, 1L);
  Child findChild2 = em.find(Child.class, 2L);

  // 각각의 엔티티를 조회하여 하나씩 제거한다. (자식 -&gt; 부모 순서로 ***by 외래 키 제약조건***)
  **em.remove(findChild1);
  em.remove(findChild2);
  em.remove(findParent);**</code></pre>
</li>
<li><p><strong>영속성 전이를 활성화하자! → CASCADE 옵션(REMOVE) 적용</strong> 🌟</p>
<pre><code class="language-java">  Parent findParent = em.find(Parent.class, 1L);
  em.remove(findParent); </code></pre>
<p>  위 코드를 실행하면 DELETE SQL을 3번 실행하고, 부모는 물론 연관된 자식도 모두 삭제한다. 삭제 순서는 영속성 전이 활성 이전과 같이 자식을 먼저 삭제한 후에 부모를 삭제한다. <strong><em>by 외래 키 제약조건</em></strong></p>
<p>  *CASCADE 옵션 없이 위 코드 실행 시, 부모 엔티티만 삭제되어 자식 테이블에 걸려 있는 외래 키 제약조건에 의해, 데이터베이스에서 외래 키 무결성 예외가 발생하게 된다.</p>
</li>
</ul>
<aside>
  🔎 <b>CASCADE의 종류</b>

<pre><code class="language-java">package javax.persistence;

public enum CascadeType {
    ALL,      // 모두 적용
    PERSIST,  // 영속
    MERGE,    // 병합 
    REMOVE,   // 삭제
    REFRESH,  // REFRESH
    DETACH;   // DETACH

    private CascadeType() {
    }
}</code></pre>
<p>*여러 속성을 같이 사용할 수도 있다.<br>ex. <code>cascade = { CascadeType.PERSIST, CascadeType.REMOVE }</code></p>
<p>→ 참고로, 이들은 em.persist(), em.remove() 실행 시 바로 전이가 발생하지 않고, <strong>플러시를 호출할 때</strong> 전이가 발생한다. </p>
</aside>]]></description>
        </item>
        <item>
            <title><![CDATA[DO SOPT 2차 세미나 생각 과제]]></title>
            <link>https://velog.io/@dev_tmb/DO-SOPT-2%EC%B0%A8-%EC%84%B8%EB%AF%B8%EB%82%98-%EC%83%9D%EA%B0%81-%EA%B3%BC%EC%A0%9C</link>
            <guid>https://velog.io/@dev_tmb/DO-SOPT-2%EC%B0%A8-%EC%84%B8%EB%AF%B8%EB%82%98-%EC%83%9D%EA%B0%81-%EA%B3%BC%EC%A0%9C</guid>
            <pubDate>Mon, 29 Jan 2024 02:22:09 GMT</pubDate>
            <description><![CDATA[<h1 id="http-method의-특징">#HTTP Method의 특징</h1>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/06ee7a50-796c-41e1-9d2d-4be9cfae6e28/image.png" alt=""></p>
<h2 id="1️⃣-멱등idempotent">1️⃣ 멱등(idempotent)</h2>
<blockquote>
<p><strong>f(f(x))=x;</strong> 몇 번을 호출해도 결과는 동일!</p>
</blockquote>
<ul>
<li><code>POST</code> 는 멱등 메서드 X</li>
</ul>
<aside>
  💡 <b>활용</b>

<ul>
<li>자동 복구 메커니즘</li>
<li>타임아웃으로 요청 실패 시, 다시 클라이언트가 재요청해도 되는가?</aside>

</li>
</ul>
<h2 id="2️⃣-캐시가능cacheability">2️⃣ 캐시가능(Cacheability)</h2>
<blockquote>
<p>응답 결과를 캐시해서 사용해도 되는가? → <strong><em>YES!</em></strong> : <code>GET</code>, <code>HEAD</code>, <code>POST</code>, <code>PATCH</code></p>
</blockquote>
<ul>
<li><code>POST</code>와 <code>PATCH</code> 의 경우, 본문 내용까지 캐시 키로 고려해야 하므로 거의 사용X</li>
</ul>
<h2 id="3️⃣-안전safe">3️⃣ 안전(Safe)</h2>
<blockquote>
<p>호출해도 리소스의 변경이 일어나지 않는다</p>
</blockquote>
<h1 id="responsebody-와-requestbody-의-역할">#@ResponseBody 와 @RequestBody 의 역할</h1>
<blockquote>
<p>직렬화(serialization) : Java Object → JSON 문자열 
역직렬화(deserialization) : JSON 문자열 → Java Object</p>
</blockquote>
<p>HTTP 요청 본문(=Body) 데이터는 Spring에서 제공하는 <em>HttpMessageConverter</em>를 통해 타입에 맞는 객체로 변환된다. </p>
<h2 id="responsebody--응답본문"><code>@ResponseBody</code> = 응답본문</h2>
<p><strong>Java 객체(응답DTO)를 HTTP 요청 본문으로 매핑하여 클라이언트에 전송</strong></p>
<h3 id="📌-restcontroller">📌 <code>RestController</code></h3>
<ul>
<li><p>객체를 반환하기만 하면 객체 데이터는 Json형식의 HTTP 응답을 직접 작성한다. ⇒ 리턴값에 자동으로 <code>@ResponseBody</code>가 붙는 효과</p>
</li>
<li><p>Data를 return하는 것이 주 용도이다.</p>
</li>
<li><p><code>@RestController</code>는 크게 <strong><code>@Controller</code> + <code>@ResponseBody</code></strong> 두 개의 어노테이션의 조합으로 볼 수 있다.</p>
<ul>
<li><p><code>@Controller</code> - @Component로 스프링이 이 클래스의 오브젝트를 알아서 생성하고 다른 오브젝트들과의 의존성을 연결한다는 의미</p>
</li>
<li><p><code>@ResponseBody</code> - 이 클래스의 메서드가 리턴하는 것이 웹 서비스의 ResponseBody라는 의미</p>
<p>⇒ 이 어노테이션을 사용하면 각 메소드마다 <code>@ResponseBody</code> 설정 할 필요 X</p>
</li>
</ul>
</li>
</ul>
<h3 id="📌-responseentity">📌 <code>ResponseEntity</code></h3>
<p>HTTP 응답의 바디뿐만 아니라 여러 다른 매개변수(status, header)를 조작하고 싶을 때 사용</p>
<h2 id="requestbody--요청본문"><code>@RequestBody</code> = 요청본문</h2>
<p><strong>HTTP 요청 본문을 그대로 전달받아 Java 객체(요청DTO)로 변환하여 매핑</strong></p>
<ul>
<li><p>@RequestBody를 사용하면 요청 본문의 JSON, XML, Text 등의 데이터가 적합한 HttpMessageConverter를 통해 파싱되어 Java 객체로 <strong>변환</strong>된다.</p>
</li>
<li><p>@RequestBody를 사용할 객체는 필드를 바인딩할 생성자나 setter 메서드가 필요없다.</p>
<ul>
<li><p>다만 직렬화를 위해 <strong><em>기본 생성자</em></strong> 는 필수다.</p>
</li>
<li><p>또한 데이터 바인딩을 위한 필드명을 알아내기 위해 getter나 setter 중 1가지는 정의되어 있어야 한다.</p>
<p>  <em>만약 getter나 setter 메서드가 모두 정의되어 있지 않으면, 실행 시 **</em>HttpMessageNotWritableException*** 예외가 발생한다. </p>
</li>
</ul>
</li>
</ul>
<h3 id="📌-requestparam">📌 <code>RequestParam</code></h3>
<p>Request의 Parameter를 가져오는, 즉 쿼리 파라미터를 파싱하는 역할 
→ 키와 값의 쌍으로 전송되는 단순 데이터에 유용</p>
<p><strong>*@RequestParam과의 차이?</strong></p>
<p><code>@RequestBody</code> 로 데이터를 받을 때는 메서드의 변수명이 상관 없었지만, <code>@RequestParam</code> 으로 데이터를 받을 때는 데이터를 저장하는 이름으로 메서드의 변수명을 일일이 지정해줘야 한다. </p>
<table>
<thead>
<tr>
<th></th>
<th>@RequestBody</th>
<th>@RequestParam</th>
</tr>
</thead>
<tbody><tr>
<td>객체 생성</td>
<td>가능</td>
<td>불가능</td>
</tr>
<tr>
<td>각 변수별로 데이터 저장</td>
<td>불가능</td>
<td>가능</td>
</tr>
</tbody></table>
<h1 id="java-record">#Java Record</h1>
<blockquote>
<p>불변 데이터 객체를 쉽게 생성할 수 있도록 하는 데이터 클래스</p>
</blockquote>
<h2 id="특징">특징</h2>
<p><em>“final 클래스가 전제된다”</em></p>
<ul>
<li><p><code>멤버변수</code></p>
<ul>
<li><p><em>private final</em> 로 선언</p>
</li>
<li><p>필드별 getter 자동 생성 → <code>@Getter</code> 필요 X</p>
<p>  ex. member<strong>.getName()</strong> → member<strong>.name()</strong></p>
</li>
<li><p>모든 필드를 인자로 하는 public 생성자 자동 생성 ⇒ <code>@AllArgsConstructor</code> 필요X</p>
<p>  <em>*Record는 인스턴스 필드 수정이 불가능하다는 차이점</em></p>
</li>
</ul>
</li>
</ul>
<pre><code>**⇒ 컴파일 타임에 필드 캡슐화를 자동으로 구현**</code></pre><ul>
<li><p><code>메서드</code></p>
<ul>
<li><p><em>getter(), equals(), hashcode(), toString()</em> 구현을 기본으로 제공</p>
</li>
<li><p>생성자를 작성하지 않아도 됨!</p>
</li>
<li><p><em>⇒ 컴파일 타임에 생성자 메서드와 기본 메서드를 자동으로 구현*</em></p>
</li>
</ul>
</li>
</ul>
<h3 id="✨이점">✨이점</h3>
<ul>
<li>불변 데이터를 개체 간에 전달하는 작업에 대해 간단하게 처리 가능</li>
<li>적은 양의 코드로 명확한 의도 표현</li>
<li>기존 자바의 DTO 클래스로 적합!</li>
</ul>
<h3 id="🚨한계점">🚨한계점</h3>
<ul>
<li><p>상속(<em>extends</em>)이 불가능함</p>
<p>  → but, 인터페이스 구현<em>(implements)</em>은 가능</p>
</li>
<li><p>추상 클래스(<em>abstratct</em>) 선언 불가</p>
</li>
<li><p>엔티티 클래스로는 사용 불가</p>
<p>  → JPA의 지연로딩에서 프록시 객체를 생성하는 부분이 불가능하기 때문</p>
</li>
</ul>
<h1 id="oop와-rdb에는-어떠한-패러다임의-불일치가-있는가">#OOP와 RDB에는 어떠한 패러다임의 불일치가 있는가?</h1>
<blockquote>
<p>객체와 관계형 DB의 데이터 표현방식과 처리 방법이 달라서 발생하는 현상</p>
</blockquote>
<h2 id="객체-모델과-연관관계">객체 모델과 연관관계</h2>
<p>객체는 참조를 사용하여 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다면, 테이블은 외래키를 사용하여 다른 테이블과의 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다. </p>
<p>괸계형 데이터베이스에서는 조인 기능을 지원하고 있어 외래키의 값을 그대로 보관할 수 있다. </p>
<table>
<thead>
<tr>
<th></th>
<th>객체 모델</th>
<th>테이블</th>
</tr>
</thead>
<tbody><tr>
<td>외래키</td>
<td>X</td>
<td>O</td>
</tr>
<tr>
<td>참조</td>
<td>O</td>
<td>X</td>
</tr>
</tbody></table>
<p>객체가 DB에 의존적이지 않게 하기 위해서는, 양쪽의 불일치를 해결해줘야 한다!</p>
<p>ORM은 이들 둘 사이에서 매핑(연결)해주는 역할을 담당함으로써, 연관관계에 관련된 <strong><em>패러다임의 불일치 문제</em></strong> 를 해결해준다. </p>
<h2 id="객체지향의-특성이-db에는-없다">객체지향의 특성이 DB에는 없다!</h2>
<p>객체지향에는 상속, 추상화, 다형성 등의 고유한 특성이 있지만, DB에는 이러한 특성이 존재하지 않는다. 즉, 이 또한 표현방식과 처리, 기능이 서로 다르다고 볼 수 있다. </p>
<p>위와 같은 내용을 모두 <strong>OOP-RDB 간의 패러다임 불일치</strong>라고 한다. </p>
<h1 id="generatedvalue-의-옵션생성-전략">#<code>@GeneratedValue</code> 의 옵션(생성 전략)</h1>
<h2 id="identity-전략--기본-키-생성을-데이터베이스에-위임한다"><strong>IDENTITY 전략</strong> : 기본 키 생성을 데이터베이스에 위임한다.</h2>
<blockquote>
<p>데이터베이스에 엔티티를 저장해서 식별자 값을 획득한 후, 영속성 컨텍스트에 저장한다.</p>
</blockquote>
<pre><code class="language-java">@Entity
public class Board {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)  // &#39;ID INT NOT NULL AUTO_INCREMET PRIMARY KEY&#39;
        private Long id;
        ...
}</code></pre>
<p>데이터베이스에 값을 저장할 때 ID 컬럼을 비워두면 데이터베이스가 순서대로 값을 채워준다. </p>
<p>⇒ 데이터를 데이터베이스에 INSERT한 후에 기본 키 값을 조회 가능. 따라서 엔티티에 식별자 값을 할당하려면 JPA는 추가로 데이터베이스를 조회해야 한다. </p>
<aside>
  🚨 <b>주의</b>

<p>엔티티가 영속 상태가 되려면 식별자가 반드시 필요한데, IDENTITY 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 INSERT 쿼리가 DB로 전달된다. </p>
<p><strong>⇒ 이는 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.</strong> </p>
</aside>

<h2 id="sequence-전략--데이터베이스-시퀀스를-사용해서-기본-키를-할당한다"><strong>SEQUENCE 전략</strong> : 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.</h2>
<blockquote>
<p>데이터베이스 시퀀스에서 식별자 값을 획득한 후, 영속성 컨텍스트에 저장한다.</p>
</blockquote>
<p><em>*데이터베이스 시퀀스란? 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트</em></p>
<pre><code class="language-java">@Entity
@SequenceGenerator(  // 이는 @GeneratedValue 옆에 사용해도 된다. 
        name = &quot;BOARD_SEQ_GENERATOR&quot;,
        sequenceName = &quot;BOARD_SEQ&quot;,   // 매핑할 데이터베이스 시퀀스 이름
        initialValue = 1, allocationSize = 1)
public class Board {
        @Id
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = &quot;BOARD_SEQ_GENERATOR&quot;)  // &#39;CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1&#39;
        private Long id;
        ...
}</code></pre>
<p>사용할 데이터베이스 시퀀스를 매핑하기 위해서 <code>@SequenceGenerator</code>로 BOARD_SEQ_GENERATOR라는 시퀀스 생성기를 등록하고, 이 이름으로 지정한 sequenceName을 이용해 JPA가 실제 데이터베이스의 BOARD_SEQ  시퀀스와 매핑할 수 있다. </p>
<p>키 생성 전략을 GeneratedType.SEQUENCE로 설정 후에, 등록한 시퀀스 생성기를 선택하면 이제부터 id 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당하게 되는 것이다. </p>
<p><code>@SequenceGenerator</code> 의 속성</p>
<table>
<thead>
<tr>
<th>속성명</th>
<th>기능</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>식별자 생성기 이름</td>
<td>필수</td>
</tr>
<tr>
<td>sequenceName</td>
<td>데이터베이스에 등록되어 있는 시퀀스 이름</td>
<td>hibernate_sequence</td>
</tr>
<tr>
<td>initialValue</td>
<td>DDL 생성 시에만 사용됨. 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정</td>
<td>1</td>
</tr>
<tr>
<td>allocationSize</td>
<td>시퀀스 한 번 호출에 증가하는 수 ⭐ 성능 최적화에 사용됨</td>
<td>50 → 시퀀스를 호출할 때마다 값이 50씩 증가 (데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다.)</td>
</tr>
<tr>
<td>catalog, schema</td>
<td>데이터베이스 catalog, schema 이름</td>
<td></td>
</tr>
</tbody></table>
<aside>
  📍 <b>IDENTITY 전략과의 차이점은?</b>

<p>사용 코드는 두 전략이 유사하지만, SEQUENCE 전략은 em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회하고, 조회한 식별자를 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다. </p>
<p>따라서 트랜잭션 커밋 후 플러시가 일어나면 엔티티를 데이터베이스에 저장한다. → 이 부분의 <strong>식별자 값을 할당받는 시점 및 순서와 트랜잭션을 지원하는 쓰기 지연 동작 여부에서 차이점이 있다.</strong> </p>
</aside>

<ul>
<li><p>IDENTITY는 데이터베이스에 저장 후, 식별자 값을 할당하기 위해 추가로 조회하는 과정에서 생성 시 데이터베이스와 2번 통신한다고 볼 수 있다.</p>
</li>
<li><p>SEQUENCE는 데이터베이스 시퀀스를 통해 식별자를 조회하는 추가 작업을 필요로 하여 이 과정에서 데이터베이스와 2번 통신한다.</p>
<pre><code class="language-sql">  # 1. 식별자를 구하려고 데이터베이스 시퀀스를 조회한다.
  SELECT BOARD_SEQ.NEXTVAL FROM DUAL
  # 2. 조회한 시퀀스를 기본 키 값으로 사용해 데이터베이스에 저장한다. 
  INSERT INTO BOARD ...</code></pre>
  <aside>
  💡 **시퀀스 접근 횟수를 줄이는 전략 : allocationSize**

<p>  JPA는 이 시퀀스에 접근하는 횟수를 줄이기 위해 <code>@SequenceGenerator.allocationSize</code> 를 사용한다. 여기에 설정한 값만큼 한 번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당한다. </p>
<p>  ex.  allocationSize=50일 때, 시퀀스를 한 번에 50 증가시킨 다음에 1<del>50까지는 메모리에서 식별자를 할당한다. 그리고 51이 되면 시퀀스 값을 100으로 증가시킨 다음, 51</del>100까지 메모리에서 식별자를 할당한다. → <strong>여러 JVM이 동시에 동작해도 각각이 시퀀스 값을 선점하므로 기본 키 값 충돌 X</strong></p>
  </aside>


</li>
</ul>
<h2 id="table-전략--키-생성-테이블을-사용한다"><strong>TABLE 전략</strong> : 키 생성 테이블을 사용한다.</h2>
<blockquote>
<p>데이터베이스 시퀀스 생성용 테이블에서 식별자 값을 획득한 후, 영속성 컨텍스트에 저장한다.</p>
</blockquote>
<p>키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략</p>
<p>→ 모든 데이터베이스에 적용 가능 (의존 X)</p>
<pre><code class="language-java">@Entity
@TableGenerator(  // 테이블 키 생성기 등록
        name = &quot;BOARD_SEQ_GENERATOR&quot;,
    table = &quot;MY_SEQUENCES&quot;,   // 키 생성용 테이블 매핑
        pkColumnValue = &quot;BOARD_SEQ&quot;, allocationSize = 1)
public class Board {

        @Id
        **@GeneratedValue(strategy = GeneratedType.TABLE, generator = &quot;BOARD_SEQ_GENERATOR&quot;)**  // 테이블 키 생성기 지정
        private Long id;
        ...
}</code></pre>
<p>이렇게 생성기를 등록해주고 나면, id 식별자 값은 BOARD_SEQ_GENERATOR 테이블 키 생성기가 할당한다. </p>
<p><code>@TableGenerator</code> 의 속성</p>
<table>
<thead>
<tr>
<th>속성명</th>
<th>기능</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>식별자 생성기 이름</td>
<td>필수</td>
</tr>
<tr>
<td>table</td>
<td>키 생성 테이블명</td>
<td>hibernate_sequence</td>
</tr>
<tr>
<td>pkColumnName</td>
<td>시퀀스 컬럼명</td>
<td>sequence_name</td>
</tr>
<tr>
<td>valueColumnName</td>
<td>시퀀스 값 컬럼명</td>
<td>next_val</td>
</tr>
<tr>
<td>pkColumnValue</td>
<td>키로 사용할 값 이름</td>
<td>엔티티 이름</td>
</tr>
<tr>
<td>initialValue</td>
<td>초기값. 마지막으로 생성된 값이 기준</td>
<td>0</td>
</tr>
<tr>
<td>allocationSize</td>
<td>시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨)</td>
<td>50</td>
</tr>
<tr>
<td>catalog, schema</td>
<td>데이터베이스 catalog, schema 이름</td>
<td></td>
</tr>
<tr>
<td>uniqueConstraints(DDL)</td>
<td>유니크 제약 조건을 지정할 수 있다.</td>
<td></td>
</tr>
<tr>
<td>- TABLE 전략은 값을 조회하며서 SELECT 쿼리를 사용하고 다음 값으로 증가시키기 위해 UPDATE 쿼리를 사용한다. 이 전략은 SEQUENCE 전략보다 DB와 한 번 더 통신한다는 단점이 있다.</td>
<td></td>
<td></td>
</tr>
<tr>
<td>- 최적화하는 방법은 SEQUENCE 전략과 동일하게 allocationSize를 사용하는 것이다.</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h2 id="auto-전략"><strong>AUTO 전략</strong></h2>
<p>선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다.</p>
<p>ex. Oracle - <code>SEQUENCE</code>, MySQL - <code>IDENTITY</code></p>
<p>이는 자동적으로 선택을 해주는 것이기 때문에 데이터베이스를 변경해도 코드를 수정할 필요가 없다는 장점이 있다. </p>
<p>SEQUENCE나 TABLE 전략이 선택된 경우에는 시퀀스나 키 생성용 테이블을 미리 만들어 두어야 한다. </p>
<p>*스키마 자동 생성 기능을 사용한다면 하이버네이트가 기본값을 사용해서 적절한 시퀀스나 키 생성용 테이블을 만들어준다.</p>
<p>*Oracle의 시퀀스, MySQL의 AUTO_INCREMENT 기능과 같이 데이터베이스 벤더마다 키 자동 생성을 지원하는 방식이 다르기 때문에 IDENTITY와 SEQUENCE 전략은 사용하는 데이터베이스에 의존한다. </p>
<h1 id="url-uri-용어-정리">#URL, URI 용어 정리</h1>
<blockquote>
<p><strong>URI ?</strong> 네트워크 상 자원을 가리키는 일종의 고유 식별자(ID)</p>
<ul>
<li><strong>U</strong>niform - 리소스를 식별하는 통일된 방식</li>
<li><strong>R</strong>esource - 자원 → URI로 식별할 수 있는 모든 것 (제한 無)</li>
<li><strong>I</strong>dentifier - 다른 항목과 구분하는 데 필요한 정보</li>
</ul>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/fa93e817-4138-4763-8d58-5752850a2ec7/image.png" alt=""></p>
<p>⭐URN, URL은 URI에 포함, 즉 URI의 종류</p>
<ul>
<li>UR<strong>L</strong>(Location) : 리소스의 위치<ul>
<li>리소스의 위치 변경 시 함께 변경필요. 즉, 영구적이지 않다</li>
<li><em>“여기 찾아가면 원하는 데이터가 있다!”</em></li>
</ul>
</li>
<li>UR<strong>N</strong>(Name) : 리소스의 이름<ul>
<li>리소스의 위치정보가 아닌 실제 리소스의 고유한 이름으로 일정하게 유지됨</li>
<li><em>“데이터의 이름 그 자체” →이름만으로는 실제 리소스를 찾기가 어려움</em></li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/25396328-6ec8-4364-8b3c-dfde50a6c8f6/image.png" alt=""></p>
<h1 id="path-parameter-query-string">#Path Parameter, Query String</h1>
<p>RESTful API는 상황에 따라 아래와 같은 방식으로 통신할수 있다. <strong>“데이터(자원)”</strong> 관점에서 요청과 응답이 이루어지는 메커니즘을 살펴보자</p>
<p>이때, 주의할 점은 API 설계 시 페이지 단위로 생각하지 않는다는 것이다</p>
<h2 id="path-parameter">Path Parameter</h2>
<blockquote>
<p>요청을 경로로 구분하여 전달하는 방식</p>
</blockquote>
<ul>
<li><p>정제되지 않은 <strong>데이터</strong> 호출</p>
</li>
<li><p>원하는 조건의 데이터들, 하나의 데이터에 대한 정보를 받아올 때</p>
</li>
<li><p>필요한 상황, 정보에 따라 <strong>URI를 다르게</strong> 요청</p>
<p>  ex. GET /members/1 , GET /members , GET /members/1/posts</p>
</li>
</ul>
<p>_**    🚨 Worst Case
**  _  </p>
<pre><code>GET /members/1/detail

GET /members/1/posts_filter

GET /members/1/posts_search

GET /members/1/withdraw_detail</code></pre><h2 id="query-string">Query String</h2>
<blockquote>
<p>? 뒤에 변수에 값을 담아 전달하는 방식</p>
</blockquote>
<ul>
<li><p>정제된 결과물</p>
</li>
<li><p>보다 정교하고 복잡한 조건으로 요청 가능 <em>(Best Case👇🏻)</em></p>
<ul>
<li><p>페이지네이션</p>
<p>  ex. GET /members?offset=0&amp;limit=100</p>
</li>
<li><p>정렬</p>
<p>  ex. GET /members?ordering=-id (내림차순)</p>
</li>
<li><p>검색</p>
</li>
<li><p>필터링</p>
</li>
</ul>
</li>
</ul>
<p>*참고 - <a href="https://velog.io/@haileeyu21/Session-RESTful-API-%EB%9E%80-Path-parameters-Query-string">https://velog.io/@haileeyu21/Session-RESTful-API-란-Path-parameters-Query-string</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DO SOPT 1차 세미나 생각 과제]]></title>
            <link>https://velog.io/@dev_tmb/DO-SOPT-1%EC%B0%A8-%EC%84%B8%EB%AF%B8%EB%82%98-%EC%83%9D%EA%B0%81-%EA%B3%BC%EC%A0%9C</link>
            <guid>https://velog.io/@dev_tmb/DO-SOPT-1%EC%B0%A8-%EC%84%B8%EB%AF%B8%EB%82%98-%EC%83%9D%EA%B0%81-%EA%B3%BC%EC%A0%9C</guid>
            <pubDate>Mon, 29 Jan 2024 02:15:40 GMT</pubDate>
            <description><![CDATA[<h1 id="java-generic">#Java Generic</h1>
<h3 id="generic-programming이란">Generic Programming이란?</h3>
<p><strong>데이터 형식에 의존하지 않고</strong>, 하나의 값이 여러 다른 데이터 타입을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그래밍 방식</p>
<ul>
<li>어떤 타입에 대해 알고리즘이 기술되어 있는 것 → 어떤 타입? <em>in terms of types to-be-<strong>specified</strong>-later</em>(특정되어 있지 않고, 나중에 필요에 따라 결정!)</li>
<li>데이터 타입이 정해지지 않음</li>
</ul>
<p><strong>*️⃣ Generic한 변수/자료구조</strong></p>
<pre><code class="language-java">Event ev;   // OneDayEvent oneday; 로 선언한 객체보다 더 제네릭하다! 
Event[] events = new Event[capacity];
Object obj; // Object : Java 내 모든 클래스의 최상위 클래스 -&gt; 모든 타입을 포괄할 수 있음</code></pre>
<p><strong>*️⃣ Generic한 알고리즘(method)</strong></p>
<pre><code class="language-java">Arrays.sort(shapes, 0, n);</code></pre>
<p>어떤 타입의 배열에 대해서도 똑같이 정렬 기능을 적용할 수 있음 ⇒ 알고리즘을 제네릭하게 구현한 것도 일종의 제네릭 프로그래밍에 포함된다!</p>
<p><strong>*️⃣ Generic 클래스</strong></p>
<blockquote>
<p>Generics</p>
</blockquote>
<p>클래스 자체를 Generic한 Type으로, Type Indenpendent하게 구현하는 것</p>
<p>→ 클래스를 구현할 때, 멤버변수와 메소드의 타입을 미리 정의하지 않고 실제 그 클래스를 사용할 때(= 실제로 객체를 생성할 때) 타입을 우리가 지정할 수 있도록 작성하는 방식</p>
<p><em>*C++의 Class Template과 같은 개념</em></p>
<p>int, double과 같이 하나의 타입으로 특정하지 않고 T라는 가상의 타입을 가정하여 클래스를 구현할 수 있다. </p>
<pre><code class="language-java">public class Box&lt;T&gt; {
        private T t;
        public void set(T t) { this.t = t; }
        public T get() { return t; }
}</code></pre>
<p>→ T라는 가상의 타입으로 지정해줌으로써, 원하는 경우에 따라 int를 저장하는 용도, Event 객체를 저장하는 용도, 문자열을 저장하는 용도 등 다양하게 활용할 수 있다. </p>
<pre><code class="language-java">public class Pair&lt;K, V&gt; {
        private K key;
        private V value;
        public void set(K key, V value) { this.key = key; this.value = value; }
        public K getKey() { return key; }
}</code></pre>
<p>→ 원하는 조합의 객체도 자유롭게 만들 수 있다!</p>
<h3 id="class-object-vs-generics">class Object VS Generics</h3>
<pre><code class="language-java">public class Box {
        private Object t;
        public void set(Object t) { this.t = t; }
        public Object get() { return t; }
}

public class Box&lt;T&gt; {
        private T t;
        public void set(T t) { this.t = t; }
        public T get() { return t; }
}</code></pre>
<p>모든 클래스의 최상위 클래스인 Object로 선언하고 필요에 따라 타입을 지정해주는 방식이나, 가상의 타입인 T를 사용하여 구현하는 방식이나 모두 같은 기능을 하지만 굳이 T를 사용하는 이유는 ..</p>
<blockquote>
<p>Object로 선언할 경우, 부모 클래스의 멤버를 자식 클래스가 참조해야 하는 상황마다 Type casting이 추가적으로 따른다는 단점이 있다. → type casting이 많은 프로그램은 절대 좋은 프로그램일 수 없다!</p>
</blockquote>
<h1 id="자바-제어자와-접근제어자">#자바 제어자와 접근제어자</h1>
<h2 id="자바의-제어자modifier">자바의 제어자(Modifier)</h2>
<blockquote>
<p>클래스, 변수, 메서드의 선언부에 함께 사용되어 부가적인 의미를 부여</p>
</blockquote>
<p><strong>*용도 - 클래스, 멤버변수와 메서드에 대한 Visibility와 Accessibility를 컨트롤하기 위함!</strong></p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/291ae7b0-18a4-46d2-aab3-64209fd3bc16/image.png" alt=""></p>
<ul>
<li>접근 제어자 - <strong><em>public, protected, default, private</em></strong></li>
<li>그 외 - <strong><em>static, final, abstract, native, transient, synchronized, volatile, strictfp</em></strong></li>
</ul>
<h3 id="접근-제어자">접근 제어자</h3>
<table>
<thead>
<tr>
<th>접근 제어자</th>
<th>설명</th>
<th>대상</th>
</tr>
</thead>
<tbody><tr>
<td>public</td>
<td>접근 제한 無</td>
<td>클래스, 메서드, 멤버변수</td>
</tr>
<tr>
<td>protected</td>
<td>같은 패키지 내 + 다른 패키지의 자손 클래스에서 접근 가능</td>
<td>메서드, 멤버변수</td>
</tr>
<tr>
<td>default</td>
<td>같은 패키지 내에서만 접근 가능</td>
<td>클래스, 메서드, 멤버변수</td>
</tr>
<tr>
<td>private</td>
<td>같은 클래스 내에서만 접근 가능</td>
<td>메서드, 멤버변수</td>
</tr>
<tr>
<td>- 지역 변수에는 접근 제어자를 지정하지 않는다</td>
<td></td>
<td></td>
</tr>
<tr>
<td>- 객체지향 개념의 <strong>캡슐화</strong>에 해당!</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<aside>
  💡 <b>생성자의 접근 제어자</b>

<p>private로 지정하여 외부에서의 인스턴스 생성을 제한할 수 있다</p>
<p>→ 이때, public static 메서드를 통해서 이 클래스의 인스턴스를 사용하게끔 할 수 있다</p>
</aside>

<h3 id="static"><em>static</em></h3>
<blockquote>
<p>인스턴스 생성 없이 바로 접근 가능하도록, 모든 객체가 공유하는 멤버</p>
</blockquote>
<ul>
<li>클래스 당 하나만 생성</li>
<li>클래스 로딩 시에 생성 <em>*non-static은 객체 로딩 시에 생성</em></li>
<li>메모리 상에서 전역변수와 같은 <strong>Data 영역</strong>에 할당</li>
</ul>
<aside>
  ❗ <i><b>어디에 사용?</b></i>

<p>멤버변수, 메서드, 초기화 블럭</p>
</aside>

<h3 id="final"><em>final</em></h3>
<blockquote>
<p>한 번 정해지면 변경 불가능하도록 제한!</p>
</blockquote>
<ul>
<li>클래스 : 상속 불가 ⇒ 확장될 수 없는 클래스</li>
<li>메서드 : 오버라이드로 재정의 불가 ⇒ 반드시 처음 정의된 대로만</li>
<li>멤버변수 : 값을 변경할 수 없는 상수 → <code>public **static final** 변수명 = 값;</code></li>
</ul>
<aside>
  ❗ <i><b>어디에 사용?</b></i>

<p>클래스, 메서드, 멤버변수, 지역변수</p>
</aside>

<h3 id="abstract"><em>abstract</em></h3>
<ul>
<li>클래스 : 클래스 내에 추상 메서드가 선언되어 있음</li>
<li>메서드 : 선언부만 있고, 구현부가 없는 메서드</li>
</ul>
<p>extends와 implements가 가진 의미를 혼합한 개념으로, abstract로 정의된 메서드를 하나라도 가지면 추상 클래스라고 한다. 이는 부모의 특징을 연장해서 사용하는 동시에 몇 개는 새롭게 만들어 사용하는 것으로 생각하면 된다. </p>
<ul>
<li>*️⃣ 추상 클래스와 인터페이스의 차이는?</li>
</ul>
<pre><code>|  | 추상 클래스 | 인터페이스 |
| --- | --- | --- |
| 사용 키워드 | abstract | interface |
| 사용 가능 변수 | 제한 X | static final(상수) |
| 사용 가능 접근 제어자 | 제한 X (public, private, protected, default) | public |
| 사용 가능 메서드 | 제한 X | abstract method, default method, static method, private method |
| 상속 키워드 | extends | implements |
| 다중상속 가능 여부 | 불가능 | 가능 &lt;br/&gt;- 클래스에 다중 구현&lt;br/&gt;- 인터페이스끼리 다중 상속 |

인터페이스를 극단적인 추상 클래스로 볼 수 있지만, 이를 굳이 구분하는 이유는 다음과 같다. 

1. 사용 의도

    클래스 상속의 기본은 의미있는 연관 관계를 구축하고자 함에 있다. 즉, 구현의 강제화 외에 추상 클래스의 목적에는 클래스 간 명확한 계층 구조를 필요로 한다는 점에서도 찾을 수 있다. 

    - **IS-A 관계** (”~이다”)   **✅ 상속**

        일반적인 개념 - 구체적인 개념의 관계로, 자식 클래스가 부모 클래스에 종속되는 관계에서만 사용하는 것이 좋다. 

        ex. 사과는 과일이다. 강아지는 동물이다. 

    - **HAS-A 관계** (”~을 할 수 있는”)

        일반적인 포함 개념의 관계로, 다른 클래스의 기능(멤버)을 받아들여 사용한다. 

        ex. 차는 엔진을 가지고 있다. 


    인터페이스는 **상속에 구애 받지 않는 상속**이 가능하다는 특징이 있다. 타입끼리 묶이는 것이 자유로워 서로 논리적이지 않고 관련이 적은 클래스끼리 부모-자식의 관계가 아닌, 형제 관계처럼 묶을 수 있다. 즉, 인터페이스를 사용하는 이유는 **자유로운 타임 묶음을 통한 추상화를 이루는 것**에 있다!

2. 공통 기능 사용 여부

   모든 클래스가 인터페이스로만 구현이 가능하다면, 같은 기능을 하는 여러 클래스에 똑같은 코드를 반복해서 작성하는 번거로움이 따를 것이다. **공통된 기능을 가진 메서드나 멤버가 있는 경우**에는 추상 클래스를 이용하여 일반 메서드로 구현한 후, 구현의 의무를 부여할 메서드만 abstract로 선언해주면 된다. 


*참고 자료 - [https://inpa.tistory.com/entry/JAVA-☕-인터페이스-vs-추상클래스-차이점-완벽-이해하기](https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-vs-%EC%B6%94%EC%83%81%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0)</code></pre><h3 id="제어자의-조합">제어자의 조합</h3>
<ul>
<li><code>메서드</code><ul>
<li>static과 abstract를 <strong>함께</strong> 사용할 수 없다 → static 메서드는 몸통이 있는 메서드에만 가능</li>
<li>private과 final을 <strong>같이</strong> 사용할 필요는 없다 (둘 다 어차피 오버라이딩은 못하기 때문)</li>
<li>abstract 메서드의 접근 제어자가 private일 수 없다</li>
</ul>
</li>
<li><code>클래스</code><ul>
<li>final과 abstract를 <strong>동시에</strong> 사용할 수 없다<ul>
<li>abstract는 상속을 통해 완성 ↔ final은 상속을 제한 (모순적)</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>*참고 자료 - <a href="https://velog.io/@kongsub/Modifier">https://velog.io/@kongsub/Modifier</a></p>
<h1 id="싱글톤singleton">#싱글톤(Singleton)</h1>
<blockquote>
<p><em>클래스의 인스턴스가 딱 한 개만 생성되는 것을 보장하는 디자인 패턴</em></p>
</blockquote>
<pre><code class="language-java">public class Sopt {

        private static Sopt instance = new Sopt();

        private Sopt() {
                // 생성자의 접근 제어를 private으로 지정하여 외부에서의 객체 생성을 막는다
        }

        public static Sopt getInstance() {
                return instance;
        }

        public void 메서드() {
                    // Sopt.getInstance().메서드()로 이 클래스의 멤버함수 호출 가능!
        }
}</code></pre>
<p>Spring의 특징 중 <strong>제어의 역전(IoC)</strong>을 가능하게 한다!</p>
<p>스프링이 모든 의존성 객체에 대해 실행 시에 만들어주고 필요한 곳에 주입시켜줌으로써, 기본적으로 <strong>싱글톤 패턴</strong>으로 동작하는 Bean들의 작업이 스프링에 의해 처리될 수 있다.</p>
<h2 id="✨-benefit">✨ benefit</h2>
<ul>
<li><p>메모리 측면의 효율</p>
<p>  최초 한번의 new 로 객체를 생성하여 고정된 메모리 영역을 사용 ⇒ 이후부터 해당 객체에 접근할 때 메모리 낭비를 방지할 수 있음</p>
</li>
<li><p>데이터 공유가 쉬움</p>
<p>  전역으로 사용되어, 다른 클래스 인스턴스들이 접근하여 사용 가능</p>
</li>
</ul>
<h2 id="🚨-problem">🚨 problem</h2>
<ul>
<li><p>많은 양의 코드가 필요</p>
<p>  → 멀티스레딩 환경에서의 객체 생성 중 발생할 수 있는 동시성 문제를 해결할 수 있어야 함</p>
</li>
<li><p>테스트가 어려움</p>
<p>  격리된 환경에서 테스트를 수행하려면, 매번 인스턴스의 상태를 초기화시켜야 하는 번거로움이 따름</p>
</li>
<li><p>클라이언트가 구체 클래스에 의존하는 형태</p>
<p>  SOLID 중 DIP와 OCP를 위반할 가능성이 높음</p>
</li>
</ul>
<p>→ 이들 간의 <strong><em>trade-off</em></strong> 관계를 잘 따져가며, 싱글톤 패턴의 적용 여부를 결정해야 한다.</p>
<h1 id="spring의-각각의-의존성-주입-방법-특징-및-생성자-주입-방식의-장점">#Spring의 각각의 의존성 주입 방법 특징 및 생성자 주입 방식의 장점</h1>
<h2 id="1️⃣-생성자-주입">1️⃣ 생성자 주입</h2>
<pre><code class="language-java">public class Server {

    private Leader partLeader;

        public Server(Leader leader) {
                this.partLeader = leader;
        }
}</code></pre>
<aside>
  ⭐ <b>장점은?!</b>

<ul>
<li><p>의존관계의 내용을 외부로 노출시켜야 함 ⇒ 컴파일 타임에 오류를 잡아낼 수 있다</p>
<p>  → final 키워드로</p>
</li>
<li><p>객체의 불변성 확보 → 객체 생성 시 1회만 호출됨이 보장 (setter 주입의 단점 해결)</p>
</li>
<li><p>테스트 코드 작성의 편리함</p>
<ul>
<li>null을 주입하지 않는 한 NullPointerException 발생 X</li>
</ul>
</li>
<li><p>순환 참조 에서 방지 → 컴파일 시점에 발견 가능</p>
</li>
<li><p><code>@Autowired</code> 로 의존성 주입을 남발하여 발생하는 의존성, 결합에 대한 문제 방지 (필드 주입의 단점 해결)</p>
</aside>

</li>
</ul>
<h2 id="2️⃣-setter-주입-수정자-주입">2️⃣ setter 주입 (수정자 주입)</h2>
<pre><code class="language-java">public void setPartLeader(Leader leader) {
        this.partLeader = leader;
}</code></pre>
<p>이와 같은 방식을 사용하면, 한 클래스를 수정할 때 다른 클래스까지 수정하지 않아도 된다는 장점이 있다. 즉, 의존성을 주입하지 않았을 때보다 <strong>코드의 수정이 용이</strong>하다. </p>
<h2 id="3️⃣-필드-주입---autowired--✅-spring-사용">3️⃣ 필드 주입 - @Autowired  <strong>✅ Spring 사용</strong></h2>
<p>→ 변수, 생성자, setter 메서드, 일반 메서드 등에 적용 가능</p>
<p>스프링에서 Bean 인스턴스가 생성된 이후, <code>@Autowired</code>로 설정된 메서드가 자동으로 호출되고, 인스턴스가 자동으로 주입된다. 즉, 스프링이 관리하는 Bean을 해당 변수와 메서드에 자동으로 매핑해주는 역할을 한다. </p>
<h1 id="jar-war-차이">#JAR, WAR 차이</h1>
<p><strong>*JAR ⊃ WAR</strong></p>
<h2 id="jarjava-archive">JAR(Java Archive)</h2>
<p>#Spring boot에서 가이드하는 표준</p>
<blockquote>
<p>Java 어플리케이션이 동작할 수 있도록 자바 프로젝트를 압축한 파일</p>
</blockquote>
<ul>
<li>Class(리소스, 속성 파일), 라이브러리 파일을 포함</li>
<li>JRE(Java Runtime Environment)만 있어도 실행 가능!</li>
</ul>
<h2 id="warweb-application-archive">WAR(Web Application Archive)</h2>
<blockquote>
<p>Servlet/JSP 컨테이너에 배치할 수 있는 웹 어플리케이션 전체를 패키징 하기 위한 압축파일 포맷</p>
</blockquote>
<ul>
<li>웹 관련 자원을 포함 - JSP, Servlet, JAR, Class, XML, HTML, Javascript</li>
<li>사전 정의된 구조를 사용 - WEB-INF, META-INF</li>
<li>별도의 웹 서버 or 웹 컨테이너 필요</li>
</ul>
<aside>
  💡 <b>결론</b>

<p>어플리케이션 리소스를 패키징 하는 방법에 차이가 있을 뿐, 모두 <code>java -jar 프로젝트명.jar</code> 을 통해 쉽게 배포하고 동작시킬 수 있다</p>
</aside>

<p>*참고 자료 - <a href="https://velog.io/@mooh2jj/JAR-vs-WAR-%EB%B0%B0%ED%8F%AC%EC%9D%98-%EC%B0%A8%EC%9D%B4">https://velog.io/@mooh2jj/JAR-vs-WAR-배포의-차이</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[예외처리 전략]]></title>
            <link>https://velog.io/@dev_tmb/%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@dev_tmb/%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Thu, 18 Jan 2024 11:17:18 GMT</pubDate>
            <description><![CDATA[<h1 id="오류error와-예외exception의-차이">오류(Error)와 예외(Exception)의 차이</h1>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/b2e8f76b-b934-41d0-aea3-e377c9bfaf87/image.png" alt=""></p>
<h3 id="오류error">오류(Error)</h3>
<blockquote>
<p>시스템이 종료되어야 할 수준의 상황과 같이 수습할 수 없는 심각한 문제</p>
</blockquote>
<p>이미 복구가 불가능한 상태의 예외 - 메모리 부족, 시스템 오류 등</p>
<p>→ 이는 개발자가 잡을 수 없음  ex. <strong><em>StackOverflowError, OutOfMemoryError</em></strong></p>
<p>애플리케이션 로직은 <code>Throwable</code> 예외도 잡아서는 안되며, <code>Exception</code>부터 잡는 게 적합하다.</p>
<h3 id="예외exception">예외(Exception)</h3>
<p><strong><em>#체크_예외</em></strong></p>
<blockquote>
<p>특정 부적절한 상황에 던져지는/던질 수 있는 예외</p>
</blockquote>
<p><strong>애플리케이션 로직에서 사용할 수 있는</strong> 실질적인 최상위 예외</p>
<p>→ 모두 컴파일러가 체크하는 체크 예외 (<code>RuntimeException</code> 제외) ex. <strong><em>NullPointerException, IllegalArgumentException</em></strong></p>
<h3 id="runtimeexception"><code>RuntimeException</code></h3>
<p><strong><em>#언체크_예외, 런타임_예외</em></strong></p>
<p>컴파일러가 체크하지 않는 언체크 예외로, 이를 포함한 하위 예외는 모두 런타임 예외에 해당한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/6c2954eb-3532-4011-90c3-6bea7d4db2d7/image.png" alt=""></p>
<p>*Exception과 Error 모두에 상속관계를 가지는 <code>Throwable</code> 클래스는 getMessage(), printStackTrace()라는 공통 메서드를 가지며, 이를 통해 오류나 예외의 메시지를 담는다. </p>
<h3 id="결론-error와-exception의-발생-상황은-명확히-구분되어야-한다">[결론] Error와 Exception의 발생 상황은 명확히 구분되어야 한다!</h3>
<h2 id="checked-exception--vs--unchecked-exception의-처리">Checked Exception  VS  Unchecked Exception의 처리</h2>
<blockquote>
<p>예외 = 폭탄 돌리기</p>
</blockquote>
<aside>
  <b>🌟  이것만은 지키자!  [예외 기본 규칙]</b>

<ol>
<li>예외는 잡아서 처리하거나 던져야 한다.</li>
<li>예외를 잡거나 던질 때 지정한 예외 뿐만 아니라 그 예외의 자식들도 함께 처리된다.<ul>
<li><code>catch</code> : 그 하위 예외들까지 모두 잡기</li>
<li><code>throws</code> : 그 하위 예외들까지 모두 던지기</aside>

</li>
</ul>
</li>
</ol>
<h3 id="던진-예외들을-처리하지-않으면"><em>던진 예외들을 처리하지 않으면?</em></h3>
<p>예외에 의해 시스템 오류가 발생해서는 안 되므로, 이를 WAS가 잡아서 개발자가 설정한 <strong>서버 오류 페이지</strong>를 보여주는 식으로 처리한다. 즉, 이때는 서버가 죽지 않으므로 예외 상황들을 던지는 게 훨씬 안전!</p>
<table>
<thead>
<tr>
<th></th>
<th>Checked Exception</th>
<th>Unchecked Exception</th>
</tr>
</thead>
<tbody><tr>
<td>처리 방법</td>
<td>체크 예외는 잡아서 처리하거나, 또는 밖으로 던지도록 선언해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.⇒ 무조건 잡아서 던지거나 처리하기</td>
<td>컴파일러가 예외를 처리하지 않는다.  *예외를 던지는 throws를 선언하지 않고, 생략할 수 있다. 이 경우 자동으로 예외를 던진다.</td>
</tr>
<tr>
<td>장점</td>
<td>*가장 좋은 오류는 컴파일 오류!  개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡을 수 있다</td>
<td>신경쓰고 싶지 않은 언체크 예외 모두 무시 가능!</td>
</tr>
<tr>
<td>단점</td>
<td>① 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리(throws)해야 하기 때문에 너무 번거로워진다. ② 의존관계에 따른 단점 존재 (여러 계층에 걸친 throws)  ③ 복구 불가능한 예외 (DB, 외부 네트워크에서 발생한 예외는 따로 처리할 방법이 없다)</td>
<td>개발자가 실수로 예외를 누락할 수 있다</td>
</tr>
</tbody></table>
<br/>
<aside>
🌟 체크 예외 VS 언체크 예외 차이점

<p><strong><em>“예외를 처리할 수 없을 때 예외를 밖으로 던지는 부분(<code>throws 예외</code>)”을 필수로 선언해야 하는가 생략할 수 있는가!</em></strong></p>
</aside>

<h2 id="체크언체크-예외-활용">체크/언체크 예외 활용</h2>
<h3 id="체크언체크-예외는-언제-사용할까"><em>체크/언체크 예외는 언제 사용할까?</em></h3>
<p>*기본적으로 언체크(런타임) 예외가 더 좋다!</p>
<ol>
<li><p>기본적으로 언체크(런타임) 예외를 사용하자</p>
<p> <code>@Transactional</code> 로 선언적 트랜잭션 처리를 할 때, 체크 예외는 롤백 되지 않고, 언체크 예외는 롤백이 된다</p>
</li>
<li><p>체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용하자</p>
<p> [예시] 결제 시 포인트 부족, 계좌 이체 실패, 로그인 ID/PW 불일치 등</p>
<p> → 반드시 해당 예외를 잡아서 처리해야 하는 문제일 때만 사용! (개발자가 실수로 놓칠 수도 있는 부분)</p>
</li>
</ol>
<h1 id="예외처리-전략">예외처리 전략</h1>
<h3 id="1-예외-복구">1. 예외 복구</h3>
<p>예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것</p>
<p>→ 예외로 어떤 작업의 처리가 불가능하다면 다르게 작업을 처리하도록 유도</p>
<h3 id="2-예외-처리-회피">2. 예외 처리 회피</h3>
<p><strong>예외를 복구할 수 없는 경우,</strong> 예외 처리를 직접 처리하지 않고, 자신을 호출한 곳으로 던져버리는 것</p>
<p>→ 다른 메서드로 책임 전가</p>
<h3 id="3-예외-전환">3. 예외 전환</h3>
<p><strong>예외를 복구할 수 없는 경우,</strong> 적절한 예외로 변환하여 던지는 것</p>
<p>→ 런타임 예외로 포장하는 것이 이에 해당 ☑️</p>
<ul>
<li>의미 있고 추상화된 예외로 전환</li>
<li>불필요한 처리를 줄여줌</li>
</ul>
<p>⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️</p>
<h2 id="예외-포함과-스택-트레이스">예외 포함과 스택 트레이스</h2>
<blockquote>
<p>예외를 전환할 때는 기존 예외를 반드시 포함해야 한다!</p>
</blockquote>
<p>실무에서는 항상 로그를 사용하는 것이 좋다. 로그를 통해 잡힌 예외를 확인하고 원인 분석을 통한 개선이 가능하기 떄문이다. </p>
<p>이때, 스택 트레이스를 출력할 수 있는데 <code>e.printStackTrace()</code> 를 사용하거나 <code>log.info(”ex”, e)</code>와 같이 로그를 찍어주면, 예외가 로그에 출력된다</p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/725cd17f-4c3d-48d5-a79c-bc45d23548da/image.png" alt=""></p>
<p>→ Cause By - Cause By 로 어떤 예외를 원천을 발생했는지 꼬리를 무는 형식으로 원인을 찾을 수 있다.</p>
<h3 id="결론-checked-exception은-unchecked인-runtimeexception으로-대신-던지도록-하자">[결론] Checked Exception은 Unchecked인 RuntimeException으로 대신 던지도록 하자!</h3>
<p>자바에는 체크 예외가 더 많고, 이들 중 복구할 수 없는 예외가 정말 많다. <code>throws</code> 를 덕지덕지 붙이거나, 아예 <code>Exception</code>을 던져버리는 선택도 하는데 이는 절대 권장하지 않는 방법이다. </p>
<p>이를 해결할 수 있는 방법은 <strong>런타임 예외로 대신 던지도록</strong> 하는 것이다. </p>
<p>→ 이들을 문서화하거나 코드에 <code>throws 런타임 예외</code>로 명시해줌으로써 개발자가 놓치지 않도록 한다.</p>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://mangkyu.tistory.com/152">[Java] 체크 예외(Check Exception)와 언체크 예외/런타임 예외 (Uncheck Exception, Runtime Exception)의 차이와 올바른 예외 처리 방법</a></p>
<p>Inflearn - 김영한 ‘스프링 DB 1편 - 데이터 접근 핵심 원리’ 강의</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2023년 돌아보기 ✨]]></title>
            <link>https://velog.io/@dev_tmb/2023%EB%85%84-%EB%8F%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dev_tmb/2023%EB%85%84-%EB%8F%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 06 Jan 2024 17:57:18 GMT</pubDate>
            <description><![CDATA[<p>2023.12.31. 
정말 많이 성장했다고 느낀 소중한 2023년 한 해의 마무리를 더욱 의미 있게 보내고자 인간으로서의 회고와 개발자로서의 회고로 오늘 하루를 보내고 있다. </p>
<p>꽤 애정하고 20살부터 꾸준히 야금야금 쌓아오고 있는 Velog의 공간에 개발자로서 돌아본 나의 한 해를 정리해보고자 한다. </p>
<h2 id="1-서버-개발자로서의-성장">1. 서버 개발자로서의 성장</h2>
<h3 id="1-spring-boot에-대한-이해도">1) Spring Boot에 대한 이해도</h3>
<p>재작년~작년 즈음에 스프링에 입문하기 위해 인프런 ‘김영한’ 강사님의 강의를 보며 온전히 이해하지 못한 채로 코드를 따라치고, 프로젝트를 진행하며 구현해야 할 기능들을 블로그에서 본 코드 그대로 따라치며 그야말로 “막”코딩을 했던 적이 있다. 하지만 이후 다시 인프런 강의를 복습하니까 마구 따라치고 구현만 성공했던 코드들이 얼마나 깔끔하지 못하고 스프링의 장점을 살리지 못했던 코드인지 깨달았다. </p>
<p>결론적으로, 프로젝트를 새로 진행할 때마다 더 좋은 설계에 대한 고민을 할 줄 아는 개발자가 되었다. 이전에는 스프링 기본 개념 없이 특정 어노테이션의 사용, 내가 자바를 사용한다는 인식 없이 전혀 객체지향을 고민하지 않은 코드 작성을 했다면, 이제는 내 코드에 대한 이유를 설명하고, 코드 한 줄이라도 클린코드와 유지보수에 용이한 코드, 좋은 아키텍쳐를 고민하는 사고를 가지게 되었다. </p>
<p>서버에 관한 지식이 쌓일 수록 부하가 덜 들고 기능 단위보다 계층 단위, 전체 프로젝트, 더 나아가 서버 전반적으로 넓은 시야에서 바라보고 생각하는 개발자가 됨으로써 ‘스프링 부트’라는 특정 스택에만 구애되지 않고 ‘서버’ 개발을 이해하고 어떤 스택이든 적용시킬 수 있을 거라는 자신감 또한 생겼다. </p>
<h3 id="2-기록의-중요성">2) 기록의 중요성</h3>
<p>올해는 특히 가장 많은 프로젝트와 스터디를 진행한 한 해이다. 동아리, 해커톤 등을 통해 많은 경험을 쌓았고,</p>
<img src="https://velog.velcdn.com/images/dev_tmb/post/13005f20-c32c-4fdf-894b-3ece00707a6e/image.png" width=500 />

<p>올해는 노션을 더 꼼꼼하고 잘 활용하고자 다짐했고, 특히 개발 시에 노션의 활용도가 높았다고 자부한다. 프로젝트를 진행할 때 나의 생각을 글로 정리하며, 해야 할 일들을 명시한다는 자체로 머릿속의 복잡한 생각들이 정리되었고 이를 더 예쁘게, 통일성 있게 정리하고자 하며 내 사고과정이 온전히 담긴 아티클이 완성되었다. </p>
<p>이를 통해, 다른 누군가에게 나의 인사이트와 경험들을 공유하는 의미 있는 글쓰기로 이어졌고, 다시 내 글을 열어보며 실력적으로도 큰 성장을 이루었다. </p>
<p>에러 상황에 대한 원인 분석, 해결방법, 시도한 트래킹 흔적들을 모두 남겨놓는 ‘트러블 슈팅’과 특정 기술을 새로 공부하거나 새로운 기능을 구현하는 과정에서 하나하나 모두 기록하는 ‘공부방’의 공간, 뿐만 아니라 스터디를 진행할 때 활용한 책이나 강의들을 조금이라도 기록해두면 그 순간에서 이해하고 끝내는 것이 아닌 나중에까지 지식이 지속되는 느낌을 주었다. </p>
<p>내년에도 반드시 가져가고 싶은 습관이 바로 기록하는 습관이다. 기록을 통해 더 큰 성장을 이루었다고 자신하며, 앞으로의 나도 개발 뿐만 아닌 일상까지 모두 하나하나 기록할 것이다.  (노션 더 잘 활용해봐야지 ㅎㅎ)</p>
<h3 id="3-내-코드의-why를-찾는-과정">3) 내 코드의 WHY를 찾는 과정</h3>
<p>기한이 정해져 있다는 것은 중요하지만, 조금만 여유를 가지고 천천히 내 코드를 바라볼 때 더 음미할 수 있다고 생각한다. 올해는 조급함을 버리고 조금 더디더라도 내 코드의 WHY를 찾는 데만 집중했다. 그러다보니 자연스레 서버 계층 구조와 전반적인 플로우가 이해됐고, 또 다른 것을 깊숙히 파볼 수 있었다. </p>
<p>올해 크게 성장했다고 느끼는 부분이자, 작년과 비교했을 때 가장 많이 바뀐 태도라고 생각한다. </p>
<h3 id="4-몰입하는-개발의-즐거움">4) 몰입하는 개발의 즐거움</h3>
<p>개발에 몰입하는 내 모습을 인지할 때, 그 순간이 매우 값지고 뿌듯하게 느껴진다.  </p>
<p>SOPT라는 대학생 연합 IT 벤처 창업 동아리에서 장기 해커톤인 ‘APP-JAM’을 진행하며 몰입하는 개발을 특히 더 진하게 느낄 수 있었고, 정말 다같이 프로덕트 자체를 만들어 간다는 설렘을 안고 개발을 즐기며 몰입하는 모습이 인상 깊게 남는다. </p>
<p>이 경험을 통해 개발자로의 진로를 더 확신하게 되었고, 개발에 대한 애정이 아주 많이 커진 올 한 해였다. 앞으로 개발을 임하는 나의 자세가 몰입하며 그 순간을 더 즐겼으면 하고, 순간에 대한 몰입으로 얻어가는 인사이트가 많아졌으면 한다. </p>
<h3 id="5-devops에-관심을-갖게-되며">5) DevOps에 관심을 갖게 되며..</h3>
<p>올해 초에 진행한 스프링부트 프로젝트 ‘심야식당’에서는 Docker+Jenkins를 이용한 CI/CD 구축을 실패한 경험이 있다. 하지만 이후 서버 개발의 탄탄한 지식을 쌓고 CI/CD라는 개념을 명확히 다지고 나니 Docker를 이용한 배포는 어렵지 않게 성공해냈다. 이렇게 시행착오를 겪고 나니 기능 개발 뿐만 아닌 배포 작업과 같이 서버를 띄운 이후의 관리에도 관심이 커졌고, 흥미를 느끼고 있다. 2024년에는 DevOps에 관한 공부에도 Deep Dive하여 여러 선택지 중에 고민하여 선정할 줄 아는 개발자가 될 것이다. </p>
<h2 id="2-프로젝트를-통해-얻은-가치-있는-경험">2. 프로젝트를 통해 얻은 가치 있는 경험</h2>
<h3 id="1-심야식당-프로젝트에서의-경험">1) ‘심야식당’ 프로젝트에서의 경험</h3>
<p>UMC 동아리 3기 때 진행한 ‘심야식당’ 프로젝트는 협업에서의 다양한 경험과 성장을 이루게 해준 프로젝트이다. 좋은 사람들과 함께 했고 새로운 시도를 많이 접해보며 처음으로 제대로 된 스프링부트 프로젝트를 진행한 경험이다. 이때 릴리즈를 못한 점은 매우 아쉽지만 ‘데모데이’라는 것을 처음 진행해보며 내가 구현한 기능과 사용한 기술을 설명하는 능력, 효율적인 협업 방식과 소통하는 방법을 많이 배워갔다고 생각한다. </p>
<h3 id="2-sopt-합동세미나--솝커톤">2) SOPT 합동세미나 &amp; 솝커톤</h3>
<p>클라이언트, 디자인, 기획 등 다른 파트원과 같은 서버 파트원과 다수의 협업 경험들을 SOPT 동아리 정규활동 중에 경험하며, &quot;협업&quot;에 대한 의미를 다시금 생각해보게 되었다. 나는 어떤 개발자와 함께 일하고 싶은지, 내가 누구나 함께 일하고 싶어하는 개발자가 되기 위해 어떠한 노력을 하고 자세를 갖춰야 하는지 등 개발 실력 뿐만 아닌 소통하는 방법에 대해 정말 많이 배웠다. </p>
<h3 id="3-합숙과-함께한-엄빠도-어렸다">3) 합숙과 함께한 ‘엄빠도 어렸다’</h3>
<p>‘엄빠도 어렸다’ 서비스는 내가 진심으로 프로덕트의 기획부터 애정을 가지며 어플 심사를 올리는 과정까지 함께 하고 현재 다음 스프린트 개발을 위해 현재 진행 중인 프로젝트이다. 처음으로 제3자의 실 유저의 유입을 직접 확인하며 트래픽 처리와 과부하에 대한 고민까지 이어지게 했으며, 이는 개발에 몰입하는 것에 대한 진정한 의미를 찾게 했으며 개발에 대한 애정이 매우 커지게 했다. 릴리즈 경험을 원하던 과거의 내가 왜 릴리즈를 해보고 싶었는지, 이를 통해 얻고자 했던 것을 전부 가져가게 한 소중한 경험이다. 개발자의 꿈을 가지며 다른 사람들에게 의미 있는 순간과 가치를 전해주고 싶다는 나의 가치관은 이 서비스를 통해 더 확고해졌고, 구체화 되었으며 꿈에 한 발짝 더 가까워졌다. </p>
<h2 id="3-용기있던-도전과-후회-없는-선택">3. 용기있던 도전과 후회 없는 선택</h2>
<h3 id="1-sopt-지원">1) SOPT 지원</h3>
<img src="https://velog.velcdn.com/images/dev_tmb/post/24730bd2-1092-48e4-90d5-9ef4fae4db8a/image.png" width=100/>

<img src="https://velog.velcdn.com/images/dev_tmb/post/b2092ec3-bfdc-428f-89ee-c1a95880ebb9/image.png" width=100/>



<p>올해 1~2월에 동아리 지원에 대한 고민을 많이 했다. 그동안 쉴 틈 없이 동아리로 학기를 채워서 이제까지 했던 나의 경험들을 돌아볼 필요가 있지 않을까, 정확히 알지 못했던 것을 짚고 넘어가야 하지 않을까 하는 마음에 일단 상반기에 모집하는 동아리를 리스트업 해둔 뒤 한참을 고민했다. </p>
<p>그러다가, 말로만 듣던, 정말 들어가기 어려울 것만 같고 대학생활 워너비였던 SOPT라는 동아리에 지원하고자 고민 끝에 용기있게 도전했다. 그때의 주저 없는 ‘일단 해보자’의 마인드는 지금 2023년을 돌아보고 있는 나에게 감히 인생 최고의 선택이라고 말해주고 있다. 도전하기로 마음을 먹은 뒤로 약 35331개 단어 수로 이루어진 서류&amp;면접 준비 노션 페이지를 채울 만큼 정말 열심히 준비했다. 이 과정에서 내 개발 경험과 대학생활, 스스로의 성격, 장단점 등을 모두 돌아보는 계기가 되어 정말 값진 순간이었다고 생각한다. 고3 대학 합격을 희망하는 마음보다 훨씬 큰 간절함과 노력을 들이며 SOPT 지원에 내 2월 말~3월을 바쳤다. 그때의 노력은 빛을 발하여 14:1이라는 경쟁률을 뚫고 SOPT의 부원이 되는 감사한 결과로 이어졌다. </p>
<p>2023년 SOPT에서의 행복한 기억들과 열정으로 앞으로의 미래를 그려나갈 수 있을 만큼 많은 사람들로부터 긍정적인 에너지를 얻었으며, 개발에 진심으로 푹 빠져서 애정을 갖게 해준 SOPT에서의 경험이 정말 소중하다. </p>
<p>32기 서버파트 YB부터 33기 서버파트 OB까지 매순간 진심을 다해 성실하게 임했고, 최선을 다해 함께 성장하고 행복하기를 소망했다. 그 덕에 정말 열정적이고 멋지고 좋은 사람들을 많이 만나며 내 인생에서 최고로 꼽을 수 있는 한 해, 2023년을 성장+행복으로 채우며 보냈다고 자부하며 앞으로도 몇 십년 간은 잊지 못할 해로 기억될 것 같다. </p>
<h3 id="2-우테코-프리코스-지원">2) 우테코 프리코스 지원</h3>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/df024954-823c-4221-ac65-ead133736ad4/image.png" alt=""></p>
<p>나의 용기있는 도전들은 늘 의미있는 결과를 안겨줬다. </p>
<p>따라서 어떤 순간이든 후회없이 도전하고 지원하며 뛰어들었다. 올해 우아한 테크코스 6기 역시 당차게 도전했고, 길다면 긴 한 달 간의 프리코스 여정을 완주하였다. 결과는 아쉽게도 불합격이었지만, 프리코스를 통해 나의 부족한 점, 학습법에 대한 개선점과 확신 등을 얻었으며 다른 여러 프리코스 지원자들의 열정으로 다시 사기를 충전하는 계기가 되어 도전만으로 의미 있었다고 생각한다. </p>
<h2 id="4-보다-넓은-시야를-가진-개발자를-위해">4. 보다 넓은 시야를 가진 개발자를 위해..</h2>
<h3 id="1-문제상황에-부딪히기를-즐기며-새로운-방식을-시도하라">1) 문제상황에 부딪히기를 즐기며, 새로운 방식을 시도하라</h3>
<p>최근 같이 협업하는 팀원의 말 중, 트러블 슈팅을 두려워하지 말고 직접 부딪혀라! 라는 말이 인상적으로 남는다. </p>
<p>내가 하고 있는 것을 제대로 알고 넘어가는 것이 중요하다는 것은 아주 많이 깨달았다. 올해 Velog 활성화, 노션 활용, TIL 작성 등 나의 꾸준한 노력을 지속하기 위한 여러 개의 시도를 마구 했지만 이 중 오래 지키고 있는 것은 전부가 아니라는 한계점이 있었다. 스스로 반성을 해야 할 부분이기도 하지만, 한 번에 많은 좋다는 것들을 시도하기 보다 일단 꾸준히 잘 하고 있고, 잘 할 수 있는 것을 먼저 한 후에 그 다음 단계를 고려하고 살을 붙여나가는 것이 나에게 적합한 방식이라는 교훈을 주기도 한다. </p>
<p>인생에 있어 어떠한 문제 상황들을 마주하기에 두려움이 앞서기 마련인데, 그런 거 다 생각하다가 시도도 못해보고 끝나지 말고 일단 도전하며 맞닥뜨린 문제들을 해결하는 자체만으로 즐겨버리자.</p>
<p>문제상황에 직면하여 이를 해결하는 과정에서 개선해야 할 부분들을 고치고, 새로운 것들을 추가로 도입해도 늦지 않다는 것이다. 이렇게 직접 문제에 파고들어 느끼며 해결하고 공부한 개념과 기술들은 완전한 내 것이 되어 더 멋진 개발자로 성장하게끔 만드는 것 같다. 2024년에는 개발에 두려움 없이 뛰어드는 자세를 가질 것이다. </p>
<h3 id="2-오늘-다짐할-일을-내일로-미루지-말자">2) 오늘 다짐할 일을 내일로 미루지 말자</h3>
<p>지난 한 해동안 내가 새로 다짐하며 사기를 얻는 말이 있다.</p>
<blockquote>
<p><em>매일의 꾸준한 노력들이 끊임없이 성장하는 나를 만들어간다</em></p>
</blockquote>
<p>게으른 태도를 버리고 부지런히 나의 꿈을 쫓는 실천적인 자세가 중요하다고 생각한다. 자기합리화로 의미 없이 흘려보낸 하루를 줄이는 것이 2024 신년 목표이다. 당장 해야 할 일을 나중으로 미루지 않는 태도를 계속해서 의식하며 더 알차고 성장한 한 해를 보내고자 한다.</p>
<p>긴 글, 두서 없이 써 내려갔지만 나름 의미 있는 2023년의 마지막 날을 보낸 것 같아 뿌듯하다. (역시 사람은 블로그를 꾸준히 써 버릇해야 돼..) 가장 의미있고 행복했던 2023년! 💖 2024년도 더 멋지고 빛나게 살아보자~!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 1665번: 가운데를 말해요 - Python]]></title>
            <link>https://velog.io/@dev_tmb/%EB%B0%B1%EC%A4%80-1665%EB%B2%88-%EA%B0%80%EC%9A%B4%EB%8D%B0%EB%A5%BC-%EB%A7%90%ED%95%B4%EC%9A%94-Python</link>
            <guid>https://velog.io/@dev_tmb/%EB%B0%B1%EC%A4%80-1665%EB%B2%88-%EA%B0%80%EC%9A%B4%EB%8D%B0%EB%A5%BC-%EB%A7%90%ED%95%B4%EC%9A%94-Python</guid>
            <pubDate>Mon, 28 Aug 2023 05:19:24 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_tmb/post/aaaa9b3e-39cc-494d-a28a-fea9492edefb/image.png" alt=""></p>
<p>오랜만에! 알고리즘 문제 풀이를 업로드해보려 한다. 확실히 그냥 문제를 주석달며 푸는 것과 블로그에 글을 작성하며 푸는 것은 차이가 있다. 문제를 어떻게 하면 쉽게 이해하고 풀이를 효율적으로 공유할 수 있을지 더 고민하게 되기 때문인 것 같다. </p>
<p>갑자기 급하게 파이썬으로 코딩테스트를 볼 일이 생겨서 파이썬 문법도 상기시킬 겸 본격적인 알고리즘 공부 시작 전에 백준 문제풀이를 조금씩 실천해보려 한다!</p>
<p><em>GOGOGOGO</em> 🏃🏻‍♀️💨</p>
<h2 id="🔗-문제">🔗 문제</h2>
<blockquote>
<p><a href="https://www.acmicpc.net/problem/1655">BOJ 1655 : 가운데를 말해요</a></p>
</blockquote>
<p>난이도 - 골드 2</p>
<p>알고리즘 분류 - 자료구조, 우선순위 큐</p>
<h2 id="💬-풀이">💬 풀이</h2>
<h3 id="단순하게-접근했던-나의-풀이">단순하게 접근했던 나의 풀이..</h3>
<pre><code class="language-python">T = int(input())

def mid_num(nums):
    nums.sort()
    # len: 3 -&gt; 1 = 3 // 2 4 -&gt;1
    if len(nums) % 2 == 0:
        idx = len(nums) // 2 - 1
    else:
        idx = len(nums) // 2
    return nums[idx]
nums = []
result = []
for i in range(T):
    nums.append(int(input()))
    result.append(mid_num(nums))

for i in range(T):
    print(result[i])</code></pre>
<p>파이썬 리스트 등의 문법을 정말 활용 못하는 티가 나는 코드이다. 말 그대로 리스트 정렬 후, 중간 인덱스 값을 가져오는 문제인데 대충 몇 개 수로 이루어진 리스트에서 인덱스가 짝수/홀수일 때에 따라 어떤 계산이 필요한지 유추해보고 if-else 조건식으로 구현한 것이다. </p>
<h3 id="우선순위-큐를-이용하라">&#39;우선순위 큐&#39;를 이용하라!</h3>
<p>우선순위 큐를 구현할 때 주로 <strong>힙 자료구조</strong>를 사용할 수 있다. 
힙이란, <strong>힙 순서 속성</strong>을 만족하는 완전 이진 트리이다!</p>
<h4 id="힙-순서-속성은-뭔데-기본-rule">힙 순서 속성은 뭔데? [기본 Rule]</h4>
<blockquote>
<p>부모 노드가 <strong>자식 노드보다 작거나 같은</strong> 순서 규칙만 지키면 OK</p>
</blockquote>
<p>우선순위 큐로 구현할 때 사용 → 어떻게 넣든 삽입 순서에 상관없이, 우선순위대로 출력</p>
<p>즉, 작은 순서로 데이터 출력이 가능 (Min Heap)</p>
<p><em>cf. 데이터가 큰 순서로 출력하려면? <strong>최대힙(Max Heap)</strong> (자식이 부모보다 항상 큰 값을 가지는 규칙)</em></p>
<p>*최소 힙을 최대 힙처럼 사용하려면? 단순히 숫자에 -1을 곱해서 사용하면 된다.</p>
<h4 id="힙을-주로-어디에-쓰는가">힙을 주로 어디에 쓰는가?</h4>
<blockquote>
<p><strong>우선순위 큐(Priority Queue)</strong>
들어온 순서와 상관없이 우선 순위가 높은 데이터가 먼저 나오는 구조</p>
</blockquote>
<p>*보통 더 높은 우선순위를 더 낮은 값으로 표현</p>
<p>in Python...</p>
<ul>
<li><p>heapq를 사용</p>
<p>  *이게 조금 더 저수준 (우선순위 큐가 힙을 기반으로 만들어진 자료구조이므로)</p>
<p>  출력 순서는 트리로 그려서 표현하고 순회하는 것으로 해석하면 된다!</p>
<ul>
<li><code>heapq.heapify(heap)</code> 트리 순서 규칙에 따라 힙 트리를 만든다.</li>
<li>작은 순서대로 출력하려면? <code>heapq.heappop(heap)[1])</code></li>
</ul>
</li>
<li><p>PriorityQueue를 사용</p>
</li>
</ul>
<h4 id="우선순위-큐를-이용한-것과-단순-구현을-비교해보자">우선순위 큐를 이용한 것과 단순 구현을 비교해보자!</h4>
<p>우선순위 큐의 특성에 따르면, 삽입 순서와는 무관하게 새로운 값을 추가할 때마다 정렬이 유지되어야 한다. 이때 최소 힙과 최대 힙은 각각 오름차순 정렬, 내림차순 정렬에 대응된다고 볼 수 있다. 
(즉 위 코드에서 for문을 돌며 매번 sort()로 정렬한 부분을 우선순위 큐 자료구조로 더 간단하게 구현할 수 있는 것이다.)</p>
<p>힙 트리를 사용함으로써 조회 속도를 빠르게 하려면 루트에 있는 원소를 꺼낼수록 유리하다. 접근할 트리의 깊이가 얕을수록 성능이 좋아지는 것이다. </p>
<p>따라서, 중간값을 가져와야 하는 본 문제에서는 최소 힙과 최대 힙을 모두 사용하여 중간값이 루트에 있도록 임의대로 정렬한 후에 결과를 출력하고자 한다. </p>
<p><strong>💡 정렬 전략</strong></p>
<blockquote>
<p>홀수와 짝수로 구분 </p>
</blockquote>
<p>짝수는 최대 힙에, 홀수는 최소 힙에 일단 push를 한 뒤에 루트끼리 비교했을 때 최대 힙의 루트원소가(-max_heap[0]) 더 작으면 힙의 루트원소를 서로 교체해준다. </p>
<p>   힙을 파이썬 배열로 구현하므로, 최대 힙의 특성에 부합하게 음수로 저장하고 최소 힙은 양수로 저장한다.</p>
<p>   <strong>최대힙의 루트원소=최댓값, 최소힙의 루트원소=최솟값</strong>
   *높은 우선순위를 주고 싶은 것에 음수값 return</p>
<h2 id="💻-코드">💻 코드</h2>
<pre><code class="language-python">import heapq

n = int(input())

max_heap = []
min_heap = []

for i in range(n):
    x = int(input())
    if i % 2 == 0:
        heapq.heappush(max_heap, -x)
    else:
        heapq.heappush(min_heap, x)

    if max_heap and min_heap and -max_heap[0] &gt; min_heap[0]:
        maxnum = -heapq.heappop(max_heap)
        minnum = heapq.heappop(min_heap)

        heapq.heappush(max_heap, -minnum) 
        heapq.heappush(min_heap, maxnum)

    print(-max_heap[0])

</code></pre>
<h2 id="📊-정리">📊 정리</h2>
<h3 id="📍-오늘의-메모">📍 오늘의 메모</h3>
<ul>
<li><p>정말 오랜만에 제대로 난이도 높은 문제 풀려고 하다보니 문제접근을 어떻게 하는지 완전히 잊었나보다!</p>
</li>
<li><p>단순하게 문제 그대로 해석하여 코드에 옮기는 &#39;억지기법&#39; 식의 풀이도 좋지만, 대부분 통과를 못할 가능성이 크다. 이럴때는 알고리즘 분류에서 힌트 얻기 </p>
</li>
<li><p>Python에서 시간초과 발생 시 TIP!</p>
<pre><code class="language-python">  import sys

  input = sys.stdin.readline
  n = int(input())</code></pre>
<p>입력받을 때 그냥 <code>input()</code> 대신 <code>sys.stdin.readline()</code>을 사용하자!</p>
<p>➔ <code>sys.stdin.readline()</code>이 더 빠른 이유</p>
<p>input()에서는 다음과 같은 중간과정을 거치기에 속도가 비교적 느리다. </p>
<ol>
<li>매개변수로 문자열이 들어오면 프롬프트 메시지로 출력</li>
<li>입력받은 값의 개행 문자를 삭제시키고 반환</li>
</ol>
</li>
</ul>
<h3 id="📍-참고자료">📍 참고자료</h3>
<ul>
<li><a href="https://yeomss.tistory.com/120">Python sys.stdin.readline() 사용 / 알고리즘 입력 받기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[트랜잭션 커밋이 제대로 이루어지지 않는 문제 in 스케줄링 작업]]></title>
            <link>https://velog.io/@dev_tmb/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%BB%A4%EB%B0%8B%EC%9D%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%EB%A3%A8%EC%96%B4%EC%A7%80%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C-in-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81-%EC%9E%91%EC%97%85</link>
            <guid>https://velog.io/@dev_tmb/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%BB%A4%EB%B0%8B%EC%9D%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%EB%A3%A8%EC%96%B4%EC%A7%80%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C-in-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81-%EC%9E%91%EC%97%85</guid>
            <pubDate>Thu, 17 Aug 2023 16:19:04 GMT</pubDate>
            <description><![CDATA[<h2 id="ᑒ-오류">ᑒ 오류</h2>
<pre><code class="language-java">o.h.engine.jdbc.spi.SqlExceptionHelper   : Lock wait timeout exceeded; try restarting transaction</code></pre>
<pre><code class="language-java">org.springframework.transaction.**IllegalTransactionStateException**: Transaction is already completed - do not call commit or rollback more than once per transaction
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.rollback(AbstractPlatformTransactionManager.java:804) ~[spring-tx-5.3.29.jar:5.3.29]
    at sopt.org.umbba.api.service.scheduler.ScheduleService.lambda$schedulePushAlarm$1(ScheduleService.java:108) ~[main/:na]
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.3.29.jar:5.3.29]
    at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:95) ~[spring-context-5.3.29.jar:5.3.29]
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[na:na]
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
    at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]</code></pre>
<h2 id="✐-문제-원인-및-해결-방안">✐ 문제 원인 및 해결 방안</h2>
<h3 id="sqlexceptionhelper"><code>SqlExceptionHelper</code></h3>
<p>데이터베이스에서 트랜잭션을 수행하는 동안, 다른 트랜잭션으로부터 요청된 락이 해제되지 않아 발생한 문제 ⇒ <strong><em>락 충돌 발생 !</em></strong></p>
<h3 id="illegaltransactionstateexception"><strong><code>IllegalTransactionStateException</code></strong></h3>
<ul>
<li><p>트랜잭션 상태가 부적절할 때 발생 in 스프링 트랜잭션의 예외 클래스</p>
</li>
<li><p>트랜잭션이 이미 완료된 상태에서 2번 이상의 커밋 또는 롤백이 시도된 경우</p>
<p>  → <strong>하나의 트랜잭션 내에서는 한 번의 커밋 또는 롤백만이 호출되어야 한다.</strong> </p>
  <br/>

</li>
</ul>
<h2 id="ミ⛧-결론">ミ⛧ 결론</h2>
<p>⇒ <strong>[분석 결과]</strong> 트랜잭션 처리가 중복 호출되고 있다. 수동으로 관리해줘야 하는 부분과 자동으로 관리되는 부분의 경계를 찾아 제대로 트랜잭션 처리가 이루어지도록 해야 하고, 이를 위해서 로직과 경계를 수정해주자!</p>
<h3 id="💡-적절한-트랜잭션-범위는">💡 <strong>적절한 트랜잭션 범위는?</strong></h3>
<h4 id="좁게-가져가는-것이-좋다">좁게 가져가는 것이 좋다!</h4>
<ul>
<li><p>데이터 일관성 유지</p>
<p>  한 번에 처리되는 작업이 적으면 데이터 변경 사항을 추적하고 관리하기 쉬우므로, 데이터의 일관성을 더 쉽게 유지할 수 있음</p>
</li>
<li><p>동시성 제어</p>
<p>  여러 사용자 또는 프로세스가 동시에 작업을 수행할 때 트랜잭션 간 충돌을 덜 발생시킬 수 있음</p>
</li>
<li><p>데이터베이스 락 최소화</p>
<p>  적은 범위의 트랜잭션은 락을 더 적게 사용하므로 다른 작업에 대한 차단 가능성이 줄어듦</p>
</li>
</ul>
<h4 id="넓게-가져가는-것이-좋다">넓게 가져가는 것이 좋다!</h4>
<ul>
<li><p>복잡한 비즈니스 로직</p>
<p>  하나의 트랜잭션 범위 내여야만 비즈니스 로직이 완전히 처리되는 경우</p>
</li>
<li><p>성능 고려</p>
<p>  트랜잭션 시작~커밋 자체가 성능에 영향을 미칠 수 있음</p>
</li>
<li><p>거대한 데이터 조작</p>
<p>  많은 양의 데이터를 한번에 처리해야 하는 경우, 트랜잭션 범위를 확장하여 DB 작업을 최소화 하는 편이 더 효율적임</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DB 커넥션 풀 누수 문제]]></title>
            <link>https://velog.io/@dev_tmb/DB-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80-%EB%88%84%EC%88%98-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@dev_tmb/DB-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80-%EB%88%84%EC%88%98-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Thu, 17 Aug 2023 16:15:09 GMT</pubDate>
            <description><![CDATA[<h2 id="ᑒ-오류">ᑒ 오류</h2>
<pre><code class="language-java">java.lang.Exception: **Apparent connection leak detected**
    at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-4.0.3.jar:na]
    at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:122) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
    at org.hibernate.internal.NonContextualJdbcConnectionAccess.obtainConnection(NonContextualJdbcConnectionAccess.java:38) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.acquireConnectionIfNeeded(LogicalConnectionManagedImpl.java:108) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getPhysicalConnection(LogicalConnectionManagedImpl.java:138) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getConnectionForTransactionManagement(LogicalConnectionManagedImpl.java:276) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.begin(LogicalConnectionManagedImpl.java:284) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
    at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.begin(JdbcResourceLocalTransactionCoordinatorImpl.java:246) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
    at org.hibernate.engine.transaction.internal.TransactionImpl.begin(TransactionImpl.java:83) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.beginTransaction(HibernateJpaDialect.java:164) ~[spring-orm-5.3.29.jar:5.3.29]
    at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:421) ~[spring-orm-5.3.29.jar:5.3.29]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:400) ~[spring-tx-5.3.29.jar:5.3.29]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373) ~[spring-tx-5.3.29.jar:5.3.29]
    at sopt.org.umbba.api.service.scheduler.ScheduleService.lambda$schedulePushAlarm$1(**ScheduleService.java:62**) ~[main/:na]
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.3.29.jar:5.3.29]
    at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:95) ~[spring-context-5.3.29.jar:5.3.29]
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[na:na]
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
    at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]</code></pre>
<h2 id="✐-문제-원인-및-해결-방안">✐ 문제 원인 및 해결 방안</h2>
<p>데이터베이스와의 연결이 끊긴 누수 문제 → 과연 Dead Lock이 발생한 것일까?</p>
<h4 id="해당-에러의-원인으로-의심되는-상황">해당 에러의 원인으로 의심되는 상황!</h4>
<ol>
<li><p>메모리 한계치를 초과하여 SELECT 쿼리를 보낸 경우 - <a href="https://cocococo331.tistory.com/29">https://cocococo331.tistory.com/29</a></p>
</li>
<li><p>하나의 쓰레드 내에서 같은 엔티티 매니저를 공유하는 경우 </p>
<p> <strong><em>*하나의 EntityManager는 하나의 쓰레드에서만 사용가능하다! EntityManager 자체가 데이터베이스 연결과의 트랜잭션 상태를 유지하며 공유나 재사용이 불가능한 리소스이기 때문.</em></strong></p>
<p> → 쓰레드 풀 내에서 병렬로 이루어지는 작업들이 공유할 확률이 있나..?</p>
<p> 답변은 NO!</p>
<p> 위에서 엔티티 매니저를 주입받을 때 <code>@PersistenceContext</code> 어노테이션을 사용한다. 이는 각각의 쓰레드에 대해 별도의 엔티티 매니저가 생성됨을 보장한다. </p>
<p> 또한 쓰레드에서 이루어지는 작업은 엔티티 매니저 내부에서 독립적으로 처리되기 떄문에 쓰레드 풀 내에서 병렬로 이루어지는 다른 작업들과 공유될 확률은 거의 없다고 보면 된다. </p>
<p> 위에서 검출된 바로는 역시 스케줄링 작업 곳곳에서 누수가 발생하는 듯 보였고, EntityManager를 따로 주입받지 않는 부분에서도 같은 에러가 발생되어 명시적으로 예외가 발생되면 커넥션을 종료하도록 로직을 추가하였다. </p>
<pre><code class="language-java"> import org.aspectj.lang.annotation.AfterThrowing;
 import org.aspectj.lang.annotation.Aspect;
 import org.springframework.stereotype.Component;

 import java.sql.Connection;

 @Aspect
 @Component
 public class ConnectionClosingAspect {

     @AfterThrowing(pointcut = &quot;execution(* sopt.org.umbba.notification.service.*.*(..))&quot;, throwing = &quot;ex&quot;)
     public void closeConnectionOnException(Exception ex) {
         Connection conn =  // DB 연결 정보로 connection 객체 가져오기

                 if (conn != null &amp;&amp; !conn.isClosed()) {
             conn.close();
         }

     }
 }</code></pre>
<p> 하지만.. 일부 커넥션에만 누수가 발생한 것이라 새로운 Connection 객체를 만들었다가 종료시키는 건 크게 의미가 없었다고 한다..</p>
</li>
<li><p>스케줄링 작업 내에서 <code>Thread.sleep(1000)</code> 사용</p>
<p> 쓰레드를 1초동안 잠시 멈추게 하는 sleep() 메서드를 호출하는 것은 불필요한 블로킹 작업일 수 있다. 잠시 멈추게 한다는 행위 자체가 쓰레드 풀의 리소스를 잠깐동안 차지하는 것이기 떄문에, 특히 여러 번 동시에 실행되는 이 스케줄링 작업에서는 꼭 필요하지 않다면 빼는 것을 권장한다. </p>
<ul>
<li><p><strong>sleep(1000)을 빼면?</strong></p>
<pre><code class="language-yaml">  java.util.concurrent.TimeoutException: null
      at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:204) ~[na:na]
      at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer.waitForRunningQueuesToStop(SimpleMessageListenerContainer.java:162) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
      at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer.doStop(SimpleMessageListenerContainer.java:141) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
      at org.springframework.cloud.aws.messaging.listener.AbstractMessageListenerContainer.stop(AbstractMessageListenerContainer.java:352) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
      at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer.stop(SimpleMessageListenerContainer.java:46) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
      at org.springframework.cloud.aws.messaging.listener.AbstractMessageListenerContainer.destroy(AbstractMessageListenerContainer.java:365) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
      at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer.destroy(SimpleMessageListenerContainer.java:46) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
      at org.springframework.beans.factory.support.DisposableBeanAdapter.destroy(DisposableBeanAdapter.java:213) ~[spring-beans-5.3.29.jar:5.3.29]
      at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroyBean(DefaultSingletonBeanRegistry.java:587) ~[spring-beans-5.3.29.jar:5.3.29]
      at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingleton(DefaultSingletonBeanRegistry.java:559) ~[spring-beans-5.3.29.jar:5.3.29]
      at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingleton(DefaultListableBeanFactory.java:1163) ~[spring-beans-5.3.29.jar:5.3.29]
      at sopt.org.umbba.notification.config.GracefulShutdown.lambda$stopSqsListeners$0(GracefulShutdown.java:54) ~[main/:na]
      at java.base/java.util.LinkedHashMap$LinkedKeySet.forEach(LinkedHashMap.java:559) ~[na:na]
      at sopt.org.umbba.notification.config.GracefulShutdown.stopSqsListeners(GracefulShutdown.java:54) ~[main/:na]
      at sopt.org.umbba.notification.config.GracefulShutdown.stop(GracefulShutdown.java:48) ~[main/:na]
      at sopt.org.umbba.notification.config.GracefulShutdown.stop(GracefulShutdown.java:37) ~[main/:na]
      at org.springframework.context.support.DefaultLifecycleProcessor.doStop(DefaultLifecycleProcessor.java:235) ~[spring-context-5.3.29.jar:5.3.29]
      at org.springframework.context.support.DefaultLifecycleProcessor.access$300(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.29.jar:5.3.29]
      at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.stop(DefaultLifecycleProcessor.java:374) ~[spring-context-5.3.29.jar:5.3.29]
      at org.springframework.context.support.DefaultLifecycleProcessor.stopBeans(DefaultLifecycleProcessor.java:207) ~[spring-context-5.3.29.jar:5.3.29]
      at org.springframework.context.support.DefaultLifecycleProcessor.onClose(DefaultLifecycleProcessor.java:130) ~[spring-context-5.3.29.jar:5.3.29]
      at org.springframework.context.support.AbstractApplicationContext.doClose(AbstractApplicationContext.java:1070) ~[spring-context-5.3.29.jar:5.3.29]
      at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.doClose(ServletWebServerApplicationContext.java:174) ~[spring-boot-2.7.14.jar:2.7.14]
      at org.springframework.context.support.AbstractApplicationContext.close(AbstractApplicationContext.java:1024) ~[spring-context-5.3.29.jar:5.3.29]
      at org.springframework.boot.SpringApplicationShutdownHook.closeAndWait(SpringApplicationShutdownHook.java:145) ~[spring-boot-2.7.14.jar:2.7.14]
      at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
      at org.springframework.boot.SpringApplicationShutdownHook.run(SpringApplicationShutdownHook.java:114) ~[spring-boot-2.7.14.jar:2.7.14]
      at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]</code></pre>
</li>
</ul>
</li>
</ol>
<ol start="4">
<li><p>누수 검출 옵션 ON 으로 원인 추적하기</p>
<pre><code class="language-yaml"> spring:
     datasource:
         hikari: leak-detection-threshold: 2000  *# default: 0(이용X)*</code></pre>
<p> → 로그에서 “<strong>ProxyLeakTask</strong>”를 찾아보면, 어디서 누수가 발생하는지 검출할 수 있다!</p>
<p> 배포 서버 로그를 뒤져보니.. 매번 다른 데서 발생해서 일관되지 않다는 것을 알았다..!</p>
<p> <br/><br/></p>
<h4 id="다시-생각해보자">다시 생각해보자</h4>
<p>그렇다면, 검출 시간이 너무 짧아서 예민하게 반응하는 건 아닐까?</p>
</li>
</ol>
<p>2000 → 60000으로 확 늘려줘보니까 바로 누수 관련 에러 및 경고는 뜨지 않았다..</p>
<p>*참고 - <a href="https://stackoverflow.com/questions/73688119/hikaripool-1-fill-pool-skipped-pool-is-at-sufficient-level">https://stackoverflow.com/questions/73688119/hikaripool-1-fill-pool-skipped-pool-is-at-sufficient-level</a></p>
<p>⇒ <strong>[결론]</strong> 처음에 hikariCP 관련 옵션 설정에서 누수 검출 시간을 초과할 때까지 연결이 되지 않으면 누수로 감지하고 WARN을 날리는 시간을 2000 (2초)로 설정해뒀는데, 이게 아무래도 너무 짧았던 모양이다. 또 이렇게 연결에 지연이 발생한 부분은 아무래도 트랜잭션 관리를 수동으로 해주는 부분이지 않을까 강한 의심이 든다. 계속 누수를 검출했다고 뜨는 부분도 트랜잭션 매니저를 가져오는 부분이었으니까!</p>
<pre><code class="language-java">TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Thread.sleep()에서 발생한 InterruptedException]]></title>
            <link>https://velog.io/@dev_tmb/Thread.sleep%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-InterruptedException</link>
            <guid>https://velog.io/@dev_tmb/Thread.sleep%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-InterruptedException</guid>
            <pubDate>Thu, 17 Aug 2023 16:11:35 GMT</pubDate>
            <description><![CDATA[<h2 id="ᑒ-오류">ᑒ 오류</h2>
<pre><code class="language-java">java.lang.RuntimeException: java.lang.**InterruptedException**: sleep interrupted
    at sopt.org.umbba.api.service.scheduler.FCMScheduler.lambda$schedulePushAlarm$4(FCMScheduler.java:81) ~[main/:na]
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.3.29.jar:5.3.29]
    at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:95) ~[spring-context-5.3.29.jar:5.3.29]
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[na:na]
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
    at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]
Caused by: java.lang.InterruptedException: sleep interrupted
    at java.base/java.lang.Thread.sleep(Native Method) ~[na:na]
    at sopt.org.umbba.api.service.scheduler.FCMScheduler.lambda$schedulePushAlarm$4(FCMScheduler.java:79) ~[main/:na]</code></pre>
<h2 id="✐-문제-원인-및-해결-방안">✐ 문제 원인 및 해결 방안</h2>
<blockquote>
<p><em>Keyword</em>: 쓰레드 풀, 동시성 문제, 병렬처리</p>
</blockquote>
<h3 id="interruptedexception"><strong><code>InterruptedException</code></strong></h3>
<p>스케줄링 작업에서 <strong>쓰레드 풀</strong>을 미리 10개로 확보해둔 후에, ① 매번 연결 요청이 오가거나 ② 한번에 여러 개의 작업을 병렬처리할 때의 부하를 방지할 수 있었다. </p>
<ul>
<li>① 작업 요청이 들어올 때마다 쓰레드를 생성하고 작업을 할당한다면, 오버헤드가 너무 크다!</li>
<li>② API 요청 작업 뒤에서 정해진 시간마다 비동기적으로 처리되고 있었기에, 쓰레드 수를 제한하지 않으면 쓰레드 폭증으로 인한 애플리케이션 성능 저하가 발생할 위험이 컸기 때문이다.</li>
</ul>
<aside>
  🧐 <b>쓰레드 풀이 뭔데요?! (DB 커넥션 풀이랑 또 다른건가..)</b>

<p><img src="https://velog.velcdn.com/images/dev_tmb/post/06ae1a22-0b43-4a86-8c10-dfe2b7df947a/image.png" alt=""></p>
<blockquote>
<p>여러 작업들을 병렬로 처리하기 위해 미리 생성된 쓰레드들을 관리하는 기술</p>
</blockquote>
<p>DBCP와 유사한 개념으로, 데이터베이스 커넥션 풀을 여러 개 미리 할당해둔 후 쿼리를 날릴 때 연결 과정을 생략한 채로 빠르게 사용이 가능한 것처럼!</p>
<p><strong>쓰레드를 미리 생성하고, 작업 요청이 발생할 때마다 미리 생성된 쓰레드로 해당 작업을 처리하는 방식</strong>을 의미한다. </p>
</aside>

<pre><code class="language-java">@Bean
public TaskScheduler scheduler() {
    scheduler = new ThreadPoolTaskScheduler();
    **scheduler.setPoolSize(POOL_SIZE);  // 10**
    scheduler.setThreadNamePrefix(&quot;현재 쓰레드 풀-&quot;);
    scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
    scheduler.initialize();
    return scheduler;
}

// 스케줄러 중지 후 재시작 (초기화)
public static void resetScheduler() {
    scheduler.shutdown();
    FCMService.clearScheduledTasks();
    **scheduler.setPoolSize(POOL_SIZE);  // 10**
    scheduler.setThreadNamePrefix(&quot;현재 쓰레드 풀-&quot;);
    scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
    scheduler.initialize();
}</code></pre>
<p>위 코드는 내가 스케줄러를 사용하기 위해 설정해준 코드인데, 여기서 초기화를 쓴 이유는 새로운 작업을 예약할 때마다 기존에 예약된 모든 작업을 초기화한 후에 처음부터 다시 모든 작업을 예약하는 방식으로 구현했기 떄문이다. </p>
<p>초기화에서 사용한 <code>shutdown()</code>은 현재 쓰레드가 처리 중인 작업과 작업 큐에 대기하고 있는 작업을 모두 마친 뒤에 쓰레드 풀을 종료하는 메서드이다. (아마도 쓰레드 풀을 10으로 초기화해도 계속해서 번호는 증가하는 이유가 작업 큐에 채워지는 순서대로 번호가 붙는 것인 걸로 예상된다 → 서버가 돌아갈 때마다 쓰레드 수가 100, 200까지 계속 증가하는 현상을 볼 수 있었거든요..)</p>
<p>작업을 예약할 때마다 쓰레드 풀에 들어오는 작업들을 <strong>작업 큐</strong>에 채우고, 쓰레드 별로 할당하여 처리할 것이다. 이때, 쓰레드 간 충돌되거나 한꺼번에 많은 작업을 요청하는 경우를 고려해 실행 중인 일정 기간동안 중지시키는 <code>sleep(1000)</code>을 사용했다. </p>
<p><em>→ sleep() 메서드를 사용할 때는 반드시 InterruptException에 대한 예외처리를 하도록 되어 있다.</em></p>
<p><strong><em>InterruptException</em></strong>은 결국 1초간 sleep()을 수행하다가 <strong>입출력이나 특정 로직의 수행이 블로킹 당해서</strong> 발생한 것인데, 이는 쓰레드 풀이 꽉 차 있어서 쓰레드를 강제 종료 시키려 한 것 또는 쓰레드 풀 내의 작업을 종료시키다가 쓰레드가 반응하지 않은 경우로 예상해볼 수 있다. 후자로 의심이 가는 부분은 <code>clearScheduledTasks()</code>에서 완전히 모든 작업의 예약을 취소하기 전까지는 실질적으로 초기화의 효력을 제대로 보지 못해 작업이 계속 쌓이고 있을 가능성이 크다. </p>
<p>기본적으로, 쓰레드 풀 내의 작업을 종료시키기 위한 cancel(), shutdownNow() 등의 메서드들은 쓰레드의 인터럽트 메커니즘에 의존하므로 <strong>인터럽트에 반응하지 않으면 작업을 종료할 수 없다</strong>. 즉, 작업을 종료했는데 중간에 인터럽트를 무시해버린다면 계속해서 작업이 쌓이고 풀이 꽉 차게 되어버리는 것이다. </p>
<p>(문제들이 돌고 도네..) </p>
<p>그래서 쓰레드 풀 작업에 대한 sleep()을 사용할 때 아래와 같이 작성하는 것을 권장한다고 한다.</p>
<pre><code class="language-java">try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();  // 권장
    // throw new RuntimeException(e); -&gt; 현재 구현된 코드 
}</code></pre>
<p>인터럽트를 시도했으면, 끝까지 인터럽트를 수행하도록!  → InterruptException이 발생했다는 것은 상태가 interrupt로 인식되는 데 실패했음을 의미하므로 수동으로 상태를 바꿔주고 작업이 종료되도록 해야 한다.</p>
<h2 id="͙·☽-참고-자료">.͙·☽ 참고 자료</h2>
<p><a href="https://hudi.blog/java-thread-pool/">자바에서 쓰레드 풀 다뤄보기</a></p>
<p><a href="http://happinessoncode.com/2017/10/09/java-thread-interrupt/">Java InterruptedException은 어따 쓰는겨?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS에서 스케줄링 작업을 예약하려면?!]]></title>
            <link>https://velog.io/@dev_tmb/AWS%EC%97%90%EC%84%9C-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81-%EC%9E%91%EC%97%85%EC%9D%84-%EC%98%88%EC%95%BD%ED%95%98%EB%A0%A4%EB%A9%B4</link>
            <guid>https://velog.io/@dev_tmb/AWS%EC%97%90%EC%84%9C-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81-%EC%9E%91%EC%97%85%EC%9D%84-%EC%98%88%EC%95%BD%ED%95%98%EB%A0%A4%EB%A9%B4</guid>
            <pubDate>Thu, 17 Aug 2023 15:39:54 GMT</pubDate>
            <description><![CDATA[<p>서버에서 주기적으로 특정 로직을 수행하거나, 특정 시간에 푸시 예약을 발송하는 기능 등을 구현할 때 Scheduler를 사용한다. 이러한 기능들은 API 호출 또는 사용자의 이벤트 시에 처리하는 것들이 아니므로 모두 비동기적으로 작업이 이루어지는데, 이들 간에 작업이 쌓이거나 앞단에서 이루어지는 API 호출 등의 작업과 동시적으로 이루어져 병렬 수행할 수 있는 크기의 최대치를 넘어가게 되면 서버가 급격히 느려지거나 장애가 발생하게 되는 것이다. </p>
<p>현재는 스프링부트 내에 있는 Spring Scheduler 라이브러리를 사용해 cron으로 예약을 일괄적으로 걸도록 구현된 상태이지만, 이러한 작업을 효율적으로 최적화하기 위해 <strong>메시지 큐</strong>를 사용할 수도 있다.</p>
<p>Spring Scheduler를 이용할 때는 cron 표현식을 각각의 부모자식 마다 지정해둔 뒤, Trigger로 예약한 작업을 수행하는 방식으로 구현을 했다. </p>
<p>이때 부모와 자식 모두 답변을 완료했다는 특정 조건을 충족했을 떄만 파이어베이스로 알림을 보내는 함수를 호출한다. </p>
<p>다른 스케줄링 작업 외에 푸시알림을 구현한 부분은 SQS 대기열에 메시지를 추가하기 위해 SqsPrducer에서 정의한 <code>produce()</code> 를 호출한다. 그렇다면! 스케줄링 작업 역시 이와 마찬가지로 예약된 시간+특정 조건에 달성하면 <code>produce()</code> 호출과 같이 SQS로 보내주면 되는 것이다.</p>
<p>어차피 SQS에 추가한 이후부터는 다른 푸시 메시지와 동일하게 리스너를 통해 consume하면 되는 것이므로 <strong><em>어떻게 SQS 대기열로 보내느냐</em></strong>가 관건인 것이다. </p>
<p>그렇다면 크게 두 가지로 나눠서 해야할 일을 정리해볼 수 있겠다.</p>
<ol>
<li>특정 시간마다 특정 조건을 만족했는지 검사</li>
<li>검사했다면 SQS 대기열로 푸시</li>
</ol>
<aside>
  👨‍👩‍👧‍👦 <b>도입가능한 기술 스택</b>

<ul>
<li><p>AWS Lambda</p>
<p>  <a href="https://velog.io/@dev_tmb/AWS-Lambda%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90">AWS Lambda에 대해 알아보자</a> </p>
<p>  서버리스로 트리거할 작업을 정의 → 테이블의 각 필드마다 예약 작업을 처리하는 Lambda 함수를 적용 가능</p>
<p>  지난 ‘오늘의 질문 알림’이 푸시된 이후에 부모와 자식 모두 답변을 한 경우, 푸시알림 전송</p>
<p>  → but, 이는 ec2와 같이 하나의 서비스를 운영할 수 있는 함수 단위이므로 단순히 일부 기능(<strong><em>부모, 자식이 모두 답변을 했는가?</em></strong>)에 대한 트리거를 위해 람다를 사용하는 건 투머치라고 생각함. </p>
</li>
<li><p>AWS EventBridge</p>
<p>  <a href="https://velog.io/@dev_tmb/AWS-EventBridge%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90">AWS EventBridge에 대해 알아보자</a> </p>
<p>  AWS의 스케줄러 서비스 </p>
<ul>
<li>일정 - cron 표현식으로 지정할 수 있어 가장 기존과 유사하게 구현할 수 있지만, 푸시알림 시간이 부모자식마다 다르므로 각각에 대한 작업 예약을 어떻게 구현할지는 더 알아봐야 함</li>
<li>규칙 - 외부 애플리케이션과의 연동이 단순 API 호출만으로 가능할지 모르겠다!</li>
</ul>
</li>
</ul>
<p>⇒ 결론적으로 도입 가능한 방식</p>
<h3 id="1-aws-lambda-함수로-해당-스케줄링푸시알림-기능-별도의-서비스로-관리">1. AWS Lambda 함수로 해당 스케줄링+푸시알림 기능 별도의 서비스로 관리</h3>
<ul>
<li>ParentchildRepository에서 알림 시간을 읽어와 Lambda로 전송</li>
<li>Lambda에서는 계속해서 RDS의 답변여부 필드를 읽으며 체크</li>
<li>예약된 시간이 되었을 때, 조건이 충족되었다면 SQS로 푸시알림 전송</li>
<li>SQSListener가 대기열에 있는 메시지를 폴링하여 서버에서 파이어베이스로 전송</li>
</ul>
<h3 id="2-eventbridge로-sqs-대기열에-메시지를-보내는-작업-스케줄링">2. EventBridge로 SQS 대기열에 메시지를 보내는 작업 스케줄링</h3>
<ul>
<li>전체 Parentchild를 조회하는 것은 서버에서 실행</li>
<li>작업 예약을 위해 이벤트 버스 규칙에 맞게 요청 API 호출</aside>

</li>
</ul>
<p>최후의 방법은.. 가장 별로라고 생각되지만 현재 API 서버에 있는 SqsProducer의 역할을 알림서버에도 함께 두는 방식이다. API 서버와 알림 서버의 역할을 명확히 분리해야 하는데, ①스케줄러를 API 서버에 옮기거나 ②SQS 대기열에 추가하는 SqsProducer를 알림 서버로 옮기거나 둘 중 하나를 포기하는 게 최선의 방법인 것 같다. 하지만 이렇게 하면 단순히 파이어베이스에 보내는 작업을 병렬처리 하는 것을 SQS 대기열에 추가하는 것으로 바꾸는 것밖에 안되기 때문에 실질적으로 동시다발적으로 발생하여 부하를 최소화 하고자 했던 본래 스케줄링 작업의 변화는 없는 것이다. </p>
<p>그럼에도 스케줄링 작업을 SQS와 연동하여 처리함으로써 보장되는 장점은 <strong>비동기 작업 처리 및 안정성을 확보하면서 시스템의 확장성을 향상시킬 수 있다</strong>고 한다..</p>
<h3 id="결론적으로--내가-선택한-방식은">결론적으로 , 내가 선택한 방식은?</h3>
<p>스케줄러 자체가 항상 알림에만 적용되지 않을 수 있다! 즉, 파이어베이스로 푸시 메시지를 보내는 작업 외에는 모두 DB를 조회해서 특정 조건에 대한 검사를 반복적으로 수행하는 것이므로 엄연히 말하자면 API 서버에 두는 것이 더 적절하다고 판단했다</p>
<hr>
<h3 id="⭐️-이후-변동사항">⭐️ 이후 변동사항</h3>
<p>→ 비동기로 이루어지는 스케줄링 작업과 일반 API 작업이 같은 쓰레드 풀을 공유함으로써 풀이 꽉 차거나 누수가 발생하는 문제가 빈번히 발생했다. </p>
<p>결국, 멀티모듈과 서버 분리의 이점을 극대화하기 위해서는 스케줄링 작업을 알림서버에 두는 것이 최선이었고 쓰레드 풀을 API 서버와 알림서버 각각에 두고 사용하다보니 훨씬 서버의 부하가 적게 들었다.</p>
<p>현재는 스케줄링 작업을 활성시키기 위한 신호를 API 서버에서 SQS로 보내고, 이 신호를 알림 서버의 Listener가 대기열에서 가져오면 작업을 예약하는 방식으로 구현되어 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EventBridge에 대해 알아보자]]></title>
            <link>https://velog.io/@dev_tmb/AWS-EventBridge%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@dev_tmb/AWS-EventBridge%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 17 Aug 2023 15:33:07 GMT</pubDate>
            <description><![CDATA[<h2 id="eventbridge란">EventBridge란?</h2>
<blockquote>
<p>다양한 소스의 데이터와 애플리케이션을 연결하는 데 사용할 수 있는 서버리스 이벤트 버스 서비스 (⇒ <em>AWS의 Scheduler</em> )</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/35e48d90-fdc9-4ced-bb72-75697baff222/image.png" alt=""></p>
<p><strong>이벤트 수신, 필터링, 변환, 라우팅 및 전송</strong>에 대한 규칙을 적용하는데, 이 규칙은 <strong>이벤트 패턴</strong>이라고 하는 이벤트 구조 or 일정에 따라 <strong>이벤트를 대상에 일치</strong>시킨다. </p>
<p>예를 들어, EC2 인스턴스 생성 및 삭제에 대한 이벤트가 감지되면 이에 따른 특정 이벤트를 수행하게 할 수 있다. </p>
<h2 id="eventbridge의-흐름">EventBridge의 흐름</h2>
<ol>
<li>EventBridge는 이벤트를 수신하고, 이벤트를 대상으로 라우팅하는 규칙을 적용한다</li>
<li>이때 수신한 모든 이벤트는 이벤트 버스와 연결된다.</li>
</ol>
<pre><code>- 이벤트 버스의 형태
![](https://velog.velcdn.com/images/dev_tmb/post/3d78dc3f-a334-4f79-a858-080dd3a13e3a/image.png)


→ 이벤트 버스 = **이벤트를 수신하는 파이프라인**</code></pre><ol start="3">
<li>이벤트 버스와 연결된 규칙에 따라, 도착한 이벤트가 규칙과 일치하는지 확인 및 평가한다. </li>
</ol>
<h2 id="eventbridge-직접-사용해보기">EventBridge 직접 사용해보기!</h2>
<p>스케줄러로 이용 가능한 것은 &quot;규칙&quot;과 &quot;일정&quot;이 있다. 둘은 사용 목적이 조금씩 다른데, 비교하자면 아래와 같다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Amazon EventBridge 규칙</th>
<th>Amazon EventBridge Scheduler</th>
</tr>
</thead>
<tbody><tr>
<td>cron 표현식</td>
<td>UTC로 작성</td>
<td>Asia/Seoul로 작성</td>
</tr>
<tr>
<td>이벤트 대상</td>
<td>이벤트 대상에 파라미터 전달 X</td>
<td>이벤트 대상에 파라미터 전달 O</td>
</tr>
</tbody></table>
<h3 id="규칙">규칙</h3>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/c50d85d5-1806-465b-9482-fa6a33cd21ae/image.png" alt=""></p>
<p>여기서는 다른 AWS 서비스의 이벤트 또는 외부 애플리케이션의 이벤트 등을 감지하여 이벤트 버스로 들어온 모든 이벤트에 동일한 규칙을 적용하도록 설정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/e79548bd-3637-4665-891c-3c036f069557/image.png" alt=""></p>
<p>→ 샘플 이벤트 항목들을 보면 <code>AWS API Call via CloudTrail(SQS)</code>라는 탭이 있긴 하지만, 우리가 구현해야 할 부분은 SQS의 이벤트를 감지하는 것이 아닌, 애플리케이션에서의 이벤트 감지 시 SQS로 보내는 이벤트 작업을 수행하는 것이므로 여기서는 무관하다. </p>
<p>즉, EC2 인스턴스의 중지, 시작 등의 이벤트와 같이 SaaS 유형의 AWS 서비스와 연동하여 트리거하는 서비스라고 생각된다. </p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/d1f476f9-f0e1-4bbc-becb-a87b74f3e7e5/image.png" alt=""></p>
<p>외부 서비스와의 연동을 위해서는 사용자 지정 json 형식으로 요청 API를 스프링부트 서버 내에서 호출하는 방식으로 이벤트 버스에 전송하는 것을 고려해볼 수도 있을 것 같다. </p>
<h3 id="일정">일정</h3>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/64f442f0-b25f-4bf8-ad40-a17f96c38c16/image.png" alt=""></p>
<p>일정에서는 Spring Scheduler로 구현한 것과 같이 cron 표현식을 이용할 수 있다. 원하는 스케줄링 작업 시간대를 설정하고 일정을 생성하면 아래와 같이 대상 API를 선택할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/ac5d6484-a7bc-49ec-a285-d66afc98b159/image.png" alt=""></p>
<p>이는 스케줄러 이벤트가 발생되었을 때, 발생 대상을 어디에 둘지를 세팅하는 것으로, 우리는 SQS 대기열에 메시지를 보내야 하므로 <strong>Amazon SQS의 SendMessage</strong>를 선택해준다. </p>
<p>이후 대기열로 보낼 메시지 형식을 작성하면 되는데, 우리는 Firebase로 바로 보내줄 것이므로 파이어베이스 메시지에 필요한 정보 (FCMRequestDto)대로 구성하여 메시지 형식을 지정해주면 된다. </p>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://kim-jong-hyun.tistory.com/148">Amazon EventBridge Scheduler에 대해 알아보기</a></p>
<p><a href="https://devvkkid.tistory.com/290">AWS SQS와 EventBridge를 활용한 스케줄러 만들기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS Lambda에 대해 알아보자]]></title>
            <link>https://velog.io/@dev_tmb/AWS-Lambda%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@dev_tmb/AWS-Lambda%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 17 Aug 2023 15:29:28 GMT</pubDate>
            <description><![CDATA[<h2 id="aws-lambda-소개">AWS Lambda 소개</h2>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/4b6f83bf-61d6-40f3-b18e-54c2c311465e/image.png" alt=""></p>
<blockquote>
<p>AWS의 ServerLess 컴퓨팅 FaaS 상품</p>
</blockquote>
<p><strong><em>Lambda 함수가 호출되면 Lambda는 함수의 인스턴스를 할당하여 이벤트를 처리하고, 실행이 마치면, 다른요청을 처리할 수 있다.</em></strong></p>
<h3 id="serverless란"><em>ServerLess란?</em></h3>
<p>“서버가 없다”는 의미로, 개발자가 서버를 관리할 필용 없이 애플리케이션을 빌드하고 실행할 수 있도록 하는 클라우드 네이티브 개발 모델</p>
<p>클라우드 제공업체가 서버 인프라에 대한 프로비저닝, 유지 관리 등을 대신 처리해줌으로써, 모든 유형의 애플리케이션이나 백엔드 서비스에 대한 코드를 별도의 관리 없이 실행할 수 있다. 개발자들은 비즈니스 로직 작성에만 더욱 집중할 수 있게 한다. </p>
<ul>
<li><p>파일 처리
<img src="https://velog.velcdn.com/images/dev_tmb/post/bccf5ef1-2a97-4e2e-b352-074c99de8869/image.png" alt=""></p>
</li>
<li><p>웹 애플리케이션
<img src="https://velog.velcdn.com/images/dev_tmb/post/cd93fca4-9efa-442e-83f6-5d4abe96c3a6/image.png" alt=""></p>
</li>
</ul>
<p>또한, 다른 AWS 서비스들과 연동이 용이하여 다른 AWS 서비스로부터 코드를 자동으로 트리거하도록 설정할 수도 있다. </p>
<p>ex. 이미지를 S3에서 읽어와 람다 함수를 통해 크기를 resize 하는 작업</p>
<p>코드를 업로드만 하면 다른 AWS 서비스로부터의 트리거나 웹 또는 모바일(SaaS)에서의 직접 호출 등의 방식을 통해 Lambda에서 높은 가용성으로 이를 실행시켜준다. </p>
<p>람다는 EC2 인스턴스와 마찬가지로 가용성이 높은 하나의 컴퓨터 환경을 제공해주는 것이므로, 하드웨어 측면에서 스펙에 제한이 있다. 
<img src="https://velog.velcdn.com/images/dev_tmb/post/b58a30a7-68af-4bc4-a55a-087d1efb1c3d/image.png" alt=""></p>
<h3 id="언제-람다를-쓸까">언제 람다를 쓸까?</h3>
<blockquote>
<p><strong>특정한 시기에만</strong> 코드를 실행하는 경우</p>
</blockquote>
<ol>
<li>서버를 띄우지 않고 간단한 코드를 실행시키고 싶은 경우</li>
<li>특정 기간 또는 특정 주기로 코드를 실행시켜야 하는 경우</li>
<li>트리거가 실행될 때만 코드를 실행시키고 싶은 경우</li>
</ol>
<h2 id="aws-lambda-특징">AWS Lambda 특징</h2>
<h3 id="장점">장점</h3>
<ol>
<li><p>비용 절감</p>
<p> 항상 서버를 켜두고 있지 않고도 필요할 때만 함수를 호출하는 것이 가능하므로 비용을 절약할 수 있다</p>
</li>
<li><p>인프라 운영관리 부담절감</p>
<p> 서버 관리를 자체적으로 지원해준다 → ex. 트래픽 증가 시 자동으로 오토스케일링 수행</p>
</li>
<li><p>빠른 개발 배포</p>
<p> 람다를 이용하면 AWS 자체에서 지원하는 기능이 많아, 쉽게 배포가 가능하고 API 연동도 매우 쉽게 할 수 있다</p>
</li>
</ol>
<h3 id="단점">단점</h3>
<ol>
<li><p>리소스 제한</p>
<p> 직접 서버에서 돌리는 것보다는 <strong>메모리, 처리시간</strong> 등의 스펙이 턱없이 부족하다 </p>
<ul>
<li><p>메모리: 128MB ~ 10000MB (2020년 기준 10GB로 향상)</p>
</li>
<li><p>처리시간: 최대 900초 (람다 함수를 최대 15분 동안만 실행할 수 있다는 의미)</p>
<p>⇒ 즉, 하나의 함수가 한 번 호출될 때, 최대 10GB의 메모리까지 사용이 가능하며, 처리시간은 최대 15분이라는 의미이다. </p>
</li>
</ul>
</li>
<li><p>Stateless (무상태성)</p>
<p> 함수가 호출될 때마다 새로운 컨테이너를 띄우는 방식이므로, 별도의 상태를 저장하지 않는다</p>
<p> ⇒ 즉, Lambda가 이벤트에 의해 트리거 될 때마다 완전히 새로운 환경에서 실행된다는 것을 의미한다. </p>
</li>
<li><p>ColdStart</p>
<p> 람다는 리소스를 효율적으로 사용하기 위해 오래 사용하지 않는 동안에는 잠시 컴퓨팅 파워를 꺼두고 있다가, 호출 시에 다시 컨테이너를 띄우는데, 이때 실행환경을 구성하기 위한 시간 딜레이가 발생할 수 있다</p>
<p> *EC2의 경우, WarmStart로, 항상 가동된 상태에서 요청을 받을 준비가 되어 있어 딜레이가 없다. </p>
</li>
<li><p>동시성 제한</p>
<p> 람다는 각 리전별로 동시에 실행할 수 있는 람다함수의 개수를 <strong>최대 1000개로 제한</strong>하고 있다. 따라서 이 요청 수를 넘어가면 람다가 수행되지 않는 문제가 발생할 수 있다. </p>
</li>
</ol>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://inpa.tistory.com/entry/AWS-%F0%9F%93%9A-%EB%9E%8C%EB%8B%A4Lambda-%EA%B0%9C%EB%85%90-%EC%9B%90%EB%A6%AC">https://inpa.tistory.com/entry/AWS-📚-람다Lambda-개념-원리</a></p>
<p><a href="https://aws.amazon.com/ko/lambda/?nc2=type_a">https://aws.amazon.com/ko/lambda/?nc2=type_a</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 커넥션 풀(DBCP)]]></title>
            <link>https://velog.io/@dev_tmb/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80DBCP</link>
            <guid>https://velog.io/@dev_tmb/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80DBCP</guid>
            <pubDate>Sun, 13 Aug 2023 19:42:09 GMT</pubDate>
            <description><![CDATA[<h2 id="dbcpdatabase-connection-pool이란">DBCP(Database Connection Pool)이란?</h2>
<blockquote>
<p>최초 Pool 내에서 연결(Connection)들을 하여 HTTP 요청에 따라 응답을 제공하고 반환받으며 이를 재사용하는 것</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/08778f96-d4ea-418a-8ad3-de781b0fcd74/image.png" alt=""></p>
<p>사용자로부터 웹 애플리케이션에 요청이 들어올 때마다 데이터베이스 연결을 수립하고 해제하는 것은 매우 비효율적이다. </p>
<p>→ 이를 해결하기 위해 등장한 것이 바로 DBCP!</p>
<p>미리 여러 개의 데이터베이스 커넥션을 생성하여 연결해두고, 필요할 때마다 꺼내서 쓰는 방식으로, 매번 DB 요청이 들어올 때마다 새롭게 연결을 하는 것이 아닌 항상 <strong>연결을 열린 상태로</strong> 유지한다. </p>
<p>여기서 커넥션 풀에 있는 커넥션들은 이미 TCP/IP로 DB와 연결되어 있는 상태를 유지하므로, 언제든지 즉시 SQL을 DB에 전달할 수 있는 것이다. </p>
<p>이는 애플리케이션을 시작하는 시점에 필요한 만큼 미리 확보하여 풀에 보관을 하는데, 이때 크기를 지정해줄 수 있다. (기본값은 10개!)</p>
<h3 id="dbcp의-과정">DBCP의 과정</h3>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/c1fad2f8-fb38-4b85-bb9a-aae4979ce22b/image.png" alt=""></p>
<ol>
<li>WAS가 실행되면서 Pool 내에 Connection들을 생성</li>
<li>HTTP의 요청에 따라 Pool 내에서 Connection 객체를 가져가 사용</li>
<li>사용이 완료된 Connection 객체는 Pool 내에 반환</li>
</ol>
<aside>
  🤫 <b>커넥션 풀을 이용하지 않고 연결할 때는요?</b>

<p>데이터베이스는 웹 애플리케이션과 분리되어 있기 때문에, 데이터베이스 드라이버를 이용하여 따로 연결해주는 작업이 필요하다. </p>
<ol>
<li>DB Driver를 사용하여 데이터베이스 연결 Open</li>
<li>데이터를 읽고 쓰기 위해 TCP 소켓 Open</li>
<li>TCP 소켓을 이용하여 데이터 통신</li>
<li>데이터베이스 연결 Close</li>
<li>TCP 소켓 Close</li>
</ol>
<p>→ 이것이 바로 <strong><em>데이터베이스 연결의 lifecycle</em></strong> 이다. </p>
</aside>

<h3 id="dbcp의-장점">DBCP의 장점</h3>
<ol>
<li><p>WAS와 데이터베이스와의 연결(Connection)을 줄임으로써 비용 절감</p>
</li>
<li><p>Pool 내에 미리 연결되어 있기 때문에 매번 생성하지 않아도 OK</p>
</li>
<li><p>Connection에 대한 조정 가능   <strong><em>인프라의 DB Connection 수도 함께 고려해야 함</em></strong></p>
<p> *Connection Pool의 크기 설정</p>
<ul>
<li><strong>大?</strong> 메모리 소모가 큰 대신, 많은 사용자의 대기시간이 줄어든다</li>
<li><strong>小?</strong> 사용자의 대기시간이 길어진다</li>
</ul>
</li>
</ol>
<h2 id="hikaricp는-뭔데">HikariCP는 뭔데?</h2>
<p>데이터베이스 커넥션 풀 프레임워크에는 대표적으로 Apache Commons DBCP, Tomcat DBCP, HikariCP, Oracle UCP 등이 있다. </p>
<p>그중에서도 HikariCP는 스프링부트에 기본으로 내장되어 있는 <strong>JDBC 데이터베이스 커넥션 풀링 프레임워크</strong>이다. 가장 우선적으로 HikariCP를 사용하고, 이를 사용할 수 없는 상황에서는 Tomcat DBCP → Apache Commons DBCP → Oracle UCP 순으로 선택한다고 한다. </p>
<p>스프링부트 서버를 처음 실행할 때 아래와 같은 로그를 볼 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/ead0632f-44e8-45b9-8191-edff9e509032/image.png" alt=""></p>
<p>이는 서버의 실행과 동시에 HikariCP가 시작되고 있다는 의미이다. </p>
<ul>
<li><p>HikariCP를 사용하는 이유는! 바로 벤치마크의 결과 가장 성능이 좋은 것으로 나타났기 때문!</p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/ac916ec4-3db3-417d-9f45-b98aea90682b/image.png" alt=""></p>
</li>
</ul>
<pre><code>HikariCP는 바이트코드 수준까지 극단적으로 최적화되어 있다고 한다. 또한, Java의 컬렉션 프레임워크 사용에서 최적화가 잘 되어 있어 위와 같이 좋은 성능을 띄는 것으로 보인다.</code></pre><ul>
<li><p>HikariPool에 대한 로깅을 수행하려면?</p>
<pre><code class="language-yaml">  logging:
      level:
          com.zaxxer.hikari.pool.HikariPool: debug</code></pre>
</li>
<li><p>빌드 환경의 버전을 맞춰주어야 한다!</p>
<p>  Java Version에 호환되는 HikariCP가 각각 존재하는데, 자동으로 맞춰주지 않을 경우 또는 버전 호환성 문제로 에러가 발생한 경우 아래를 참고하여 설정해주자</p>
<pre><code class="language-xml">  Artifacts
  Java 11+ maven artifact:

  &lt;dependency&gt;
     &lt;groupId&gt;com.zaxxer&lt;/groupId&gt;
     &lt;artifactId&gt;HikariCP&lt;/artifactId&gt;
     &lt;version&gt;5.0.0&lt;/version&gt;
  &lt;/dependency&gt;
  Java 8 maven artifact (maintenance mode):

  &lt;dependency&gt;
     &lt;groupId&gt;com.zaxxer&lt;/groupId&gt;
     &lt;artifactId&gt;HikariCP&lt;/artifactId&gt;
     &lt;version&gt;4.0.3&lt;/version&gt;
  &lt;/dependency&gt;
  Java 7 maven artifact (maintenance mode):

  &lt;dependency&gt;
     &lt;groupId&gt;com.zaxxer&lt;/groupId&gt;
     &lt;artifactId&gt;HikariCP-java7&lt;/artifactId&gt;
     &lt;version&gt;2.4.13&lt;/version&gt;
  &lt;/dependency&gt;
  Java 6 maven artifact (maintenance mode):

  &lt;dependency&gt;
     &lt;groupId&gt;com.zaxxer&lt;/groupId&gt;
     &lt;artifactId&gt;HikariCP-java6&lt;/artifactId&gt;
     &lt;version&gt;2.3.13&lt;/version&gt;
  &lt;/dependency&gt;</code></pre>
</li>
</ul>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://hudi.blog/dbcp-and-hikaricp/">데이터베이스 커넥션 풀 (Connection Pool)과 HikariCP</a></p>
<p><a href="https://adjh54.tistory.com/73">[Java/Library] HikariCP 이해하고 적용하기 (with. MyBatis)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS SQS  vs  SNS]]></title>
            <link>https://velog.io/@dev_tmb/AWS-SQS-vs-SNS</link>
            <guid>https://velog.io/@dev_tmb/AWS-SQS-vs-SNS</guid>
            <pubDate>Sun, 13 Aug 2023 19:37:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_tmb/post/e714f66c-787d-449e-97a0-5752162746e1/image.png" alt="">
*이미지 출처 - <a href="https://beabetterdev.com/">https://beabetterdev.com/</a></p>
<h1 id="개념">개념</h1>
<p>SNS와 SQS는 AWS에서 제공하는 메시지 전달 서비스로, 각각이 가진 특징으로 효율적이고 안정적인 시스템 구축이 가능하다. </p>
<h2 id="aws-snssimple-notification-service">AWS SNS(Simple Notification Service)</h2>
<blockquote>
<p>애플리케이션 대 애플리케이션(A2A) 및 애플리케이션 대 개인(A2P) 통신에 사용되는 분산 pub/sub 시스템</p>
</blockquote>
<p><strong>pub/sub 이란?</strong></p>
<p><strong>메세지를 공급하는 주체</strong>와 <strong>소비하는 주체</strong>를 <strong>분리</strong>해 제공하는 메세징 방법</p>
<p>→ 우체통(Topic)에 집배원(Publisher)이 신문을 배달하는 행위가 있고, 우체통에 신문이 배달되기를 기다렸다가 빼서 보는 구독자(Subscriber)의 행위가 있다. </p>
<aside>
  💡 이를 <b>푸시 알림</b>에 적용해보면, 다음과 같이 이해할 수 있다.

<ul>
<li>서버에서 메시지 전송 - Topic 메시지 게시(pub)</li>
<li>사용자의 특정 이벤트 - Topic 가입(sub) 및 메시지 푸시</aside>

</li>
</ul>
<aside>
  💡 이를 <b>채팅 서비스</b>에 적용해보면, 다음과 같이 이해할 수 있다.

<ul>
<li>채팅방 생성 - pub/sub 구현을 위한 Topic이 생성됨</li>
<li>채팅방 입장 - Topic 구독</li>
<li>채팅방에서 메세지를 송수신 - 해당 Topic으로 메세지를 송신(pub) / 수신(sub)</aside>

</li>
</ul>
<p>SNS를 사용하면, 다양한 유형의 Subscriber에게 메시지를 전송할 수 있다. </p>
<h3 id="a2a">A2A</h3>
<ul>
<li><strong>AWS SQS 대기열</strong></li>
<li>AWS Lambda 함수</li>
<li>HTTP endpoint</li>
</ul>
<h3 id="a2p">A2P</h3>
<ul>
<li>end-user 장치<ul>
<li>SMS 메시지</li>
<li>전자 메일</li>
<li>푸시 알림</li>
</ul>
</li>
</ul>
<p>위와 같이 여러 프로토콜을 지원한다는 장점이 있고, 이들과 결합하여 더욱 복잡한 시스템의 구축이 가능하다.</p>
<aside>
👍🏻 SNS의 장점!

<p><strong>확장성</strong>의 측면에서, 자동으로 메시지 전달 처리를 확장해주기 떄문에 시스템의 규모가 커질 때 별도의 구성이나 관리가 필요없다. 또한 여러 가용 영역에 메시지가 저장되어 서비스 중단이나 데이터 손실이 거의 없는 <strong>높은 내구성</strong>을 가진다. </p>
</aside>


<p><br/><br/></p>
<h2 id="aws-sqssimple-queue-service">AWS SQS(Simple Queue Service)</h2>
<blockquote>
<p>마이크로서비스, 분산 시스템 및 서버리스 애플리케이션을 위한 완전관리형 메시지 대기열(Queue)</p>
</blockquote>
<p>대부분의 메시징 미들웨어와 같은 구성 요소로 구성된다. </p>
<table>
<thead>
<tr>
<th>종류</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td>Producers</td>
<td>처리할 작업 메시지를 SQS에 등록</td>
</tr>
<tr>
<td>Queue</td>
<td>메시지를 저장하는 큐</td>
</tr>
<tr>
<td>Consumers</td>
<td>큐 대기열에 있는 메시지 목록을 조회하여 받아옴(pull)</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/2c548e94-5a46-4d71-a797-a358aab24395/image.png" alt=""></p>
<aside>
👍🏻 SNS의 장점!

<p><strong>확장성</strong>의 측면에서, 자동으로 큐를 확장하고 관리해주기 때문에 시스템 성장 시 별도의 구성이나 관리가 필요하지 않다. SNS와 마찬가지로 여러 가용 영역에 메시지가 저장되어 서비스 중단이나 데이터 손실 역시 거의 없는 <strong>높은 내구성</strong>을 가진다. </p>
<p>또한, <strong>비동기적으로 작업을 처리</strong>하여 시스템의 응답 시간을 개선하고 다양한 작업을 동시에 처리할 수 있다는 장점이 있다. </p>
</aside>



<aside>
  👨‍👩‍👧‍👦 우리 <b>엄빠도 어렸다</b> 는 지금!

<ul>
<li>동기 방식: 스케줄링된 작업이 수행되는 동안 해당 작업이 끝날 때까지 기다리는 방식<ul>
<li>푸시알림을 보내는 작업이 스케줄링될 때마다 그 작업이 끝날 때까지 기다리는 경우</li>
<li>스케줄링된 작업이 끝나기 전까지 다음 작업 실행 X</li>
</ul>
</li>
<li><strong>비동기 방식</strong>: 스케줄링된 작업이 완료되기를 기다리지 않고 다음 작업을 계속 실행하는 방식 ☑️<ul>
<li>푸시알림을 보내는 스케줄링 작업이 백그라운드에서 실행</li>
<li>다음 작업과는 독립적으로 처리됨</aside>

</li>
</ul>
</li>
</ul>
<h1 id="차이점은">차이점은?!</h1>
<table>
<thead>
<tr>
<th></th>
<th>SNS</th>
<th>SQS</th>
</tr>
</thead>
<tbody><tr>
<td>통신 형태</td>
<td>A2A, A2P</td>
<td>A2A</td>
</tr>
<tr>
<td>메시지 처리</td>
<td>- 푸시 메커니즘: 사용자에게 메시지를 전송(push)한다 <br/>- Fanout: 같은 메시지를 여러 방향으로 발행</td>
<td>- 긴 폴링(풀) 메커니즘: 사용자가 대기열에서 메시지를 가져온다(pull) <br/>- Decouple: 여러 서비스로 분리하여 병렬식 비동식 처리</td>
</tr>
<tr>
<td>시스템 종류</td>
<td>pub/sub 시스템</td>
<td>큐잉 시스템</td>
</tr>
<tr>
<td>메시지 보관</td>
<td>메시지를 받는 사용자가 없으면 몇 번 시도 후에 삭제된다 (메시지 지속X)</td>
<td>메시지는 일정 기간동안 유지된다 (메시지 지속 가능: 1분~14일)</td>
</tr>
<tr>
<td>활용</td>
<td>실시간 알림이 필요한 애플리케이션에 사용 → 하나의 메시지를 사용자들이 각각 다른 방식으로 활용 <br/><br/>[EX] <br/>① 최종 사용자에게 이메일, SMS 및 푸시알림을 실시간으로 전송(Apple, Googlr, FireOS, Windows 장치 등) <br/>② 여러 가입자에게 메시지 브로드캐스트(모든 사용자에게 동일한 푸시알림 출력)<br/>③ SNS를 사용하여 분산된 앱 간에 이벤트를 전달하거나, 다양한 비즈니스 시스템에서 레코드를 업데이트<br/>④ 실시간 경고 및 애플리케이션 모니터링</td>
<td>메시지 처리에 사용 → 하나의 메시지를 사용자들이 동일한 방식으로 활용<br/><br/>[EX]<br/>① 비동기 처리 워크플로우<br/>② 여러 SQS 대기열을 사용하여 메시지를 병렬로 처리<br/>③ 마이크로서비스, 분산 시스템 및 서버리스 애플리케이션의 분리 및 확장<br/>④ 신뢰할 수 있는 메시징 보증(주문, 정확한 1번의 처리)을 통해 이벤트 전송, 저장 및 수신</td>
</tr>
</tbody></table>
<h2 id="→-이들을-결합하면-더-강력해지겠지">→ 이들을 결합하면 더 강력해지겠지?</h2>
<blockquote>
<p>SNS와 SQS를 함께 사용하면 이벤트에 대한 즉각적인 알림이 필요한 애플리케이션에 메시지를 전달할 수 있음과 동시에, 다른 애플리케이션이 나중에 처리할 수 있도록 SQS 대기열에 지속될 수도 있다.</p>
</blockquote>
<p>메시지가 Topic에 게시되는 정확한 순서로 구독된 SQS FIFO 대기열에 메시지를 배달하는 SNS Topic을 사용할 수 있다. SQS FIFO 큐의 소비자는 큐로 전송되는 것과 </p>
<h1 id="우리들의-이야기">우리들의 이야기</h1>
<h3 id="이러한-플랫폼을-사용하면-좋은-점은-무엇일까">이러한 플랫폼을 사용하면 좋은 점은 무엇일까?</h3>
<p>단순 푸시알림 구현에는 FCM 만 사용해도 되겠지만, 당장 트랜잭션 처리나 동시성 문제 등으로 하나의 서버 내에서 API와 푸시알림 스케줄링이 모두 이루어지는 것이 서버가 점점 무거워지는 이유 중 하나라고 생각했다. </p>
<p>사용자 수가 늘어남에 따라 특정 시간에 보내는 푸시알림이 대용량으로 처리되어야 하는 순간이 필요할 수 있다. 서버에서 단순히 FCM으로 구현한 현 상황에서 대용량의 푸시를 보낸다면 사실상 <strong>비동기가 아닌 동기처리 방식</strong>으로 보내면서 푸시의 대기 요청이 쌓이게 될 것이다. <em>(현재는 쓰레드 풀이 10으로 설정되어 한번에 10개까지 비동기로 보낼 수 있다)</em></p>
<p>아마 이러한 이유로 다른 API 호출 작업과 겹쳐지면서 서버가 다운되거나 복불복으로 알림이 오는 등의 문제가 발생했을 가능성이 크다 🚨</p>
<h3 id="이것이-바로-비동기-메시지-처리-방식인-queue를-활용해야-하는-이유이다">이것이 바로 <strong>비동기 메시지 처리 방식</strong>인 Queue를 활용해야 하는 이유이다.</h3>
<p>또한, SNS와 SQS 중 어떤 것을, 혹은 둘 다 사용해야 하는가에 대한 질문에서는
FCM이라는 A(Application)과의 통신을 해야 하므로, A2A가 지원되는 것을 기본으로 하면서 여러 소비자에게 동일한 메시지를 보내는 것(ex. 전체 공지 등)보다 하나의 소비자가 이벤트를 발생시킬 때마다 보내는 메시지가 적합하다고 생각한다. 물론 우리 서비스의 특성상 부모자식 두 유저가 한 쌍으로 움직여서 이를 다수의 소비자로 고려해야 할지, 단일 소비자로 고려해야 할지에 의문이 남아있지만 일단은 SQS 하나만 운영해도 괜찮다고 생각한다. </p>
<p>현재는 푸시알림 기능을 MVP로 최소한의 구현만이 이루어진 상태이지만, 서비스의 확장성을 생각해보면 알림에 대한 항목과 그 메시지들이 점점 쌓일 때 감당해야 할 서버의 부하를 미리 고려하는 것도 좋은 방법일 것 같다!</p>
<h3 id="aws-sqs를-사용하면-단순-푸시-메시지-전송뿐만-아니라-스케줄링-작업도-효율적으로-처리할-수-있다">AWS SQS를 사용하면 단순 푸시 메시지 전송뿐만 아니라 스케줄링 작업도 효율적으로 처리할 수 있다!</h3>
<p>AWS SQS는 마이크로서비스, 분산 시스템 및 서버리스 애플리케이션을 위한 완전관리형 메시지 대기열(Queue)라고 앞서 설명했다. </p>
<p>즉, 분산된 아키텍처에서 비동기 메시지 전달을 지원할 수 있기에 스케줄링 작업과 같이 분산되어 처리되어야 할 작업에 적합한 것이다! </p>
<h4 id="⚡-aws-sqs를-사용하여-스케줄링-작업을-효율적으로-구현하는-방법">⚡ AWS SQS를 사용하여 스케줄링 작업을 효율적으로 구현하는 방법</h4>
<ol>
<li><strong>스케줄링 작업을 메시지로 전달</strong></li>
</ol>
<p>   특정 시간 또는 주기적으로 실행되어야 하는 작업을 메시지로 전달 가능</p>
<pre><code> → *메시지를 큐에 보내는 것은 특정 작업을 예약하는 것*</code></pre><ol start="2">
<li><p><strong>Worker 서버 구성</strong></p>
<p> SQS에서 메시지를 받아 작업을 처리하는 Worker 서버 구성 가능 </p>
<p> → Worker 서버에서는 메시지를 직접 꺼내와 작업을 처리하고 결과를 반환할 수 있다</p>
<ol start="3">
<li><p><strong>가용성 및 확장성</strong></p>
<ul>
<li>여러 Worker 서버를 사용하여 작업을 병렬 처리</li>
<li>필요에 따라 SQS 큐의 용량을 조절하여 처리량을 관리</li>
</ul>
</li>
<li><p><strong>지연 및 실패 처리</strong></p>
<p>메시지의 Delayed Delivery 기능을 사용하여 메시지를 일정 시간 동안 대기시키는 지연 시간 설정 가능</p>
<p>→ 특정 시간 후에 작업을 실행하거나, 예상치 못한 실패 시 다시 시도하는데 유용</p>
</li>
<li><p><strong>비동기성과 분리</strong></p>
<p>스케줄링 작업을 SQS를 통해 비동기적으로 처리함으로써 애플리케이션의 주요 로직과 분리 
⇒ 애플리케이션의 응답 시간을 최적화하고 병목 현상을 줄일 수 있습니다.</p>
</li>
<li><p><strong>모니터링과 로깅</strong></p>
<p>각 메시지의 상태 및 처리 이력을 추적</p>
</li>
<li><p><strong>Dead Letter Queue(DLQ)</strong></p>
<p>작업 처리 중에 예외가 발생하는 경우, SQS의 Dead Letter Queue를 활용하여 실패한 작업을 따로 관리하고 분석할 수 있음</p>
</li>
<li><p><strong>스케일 아웃</strong></p>
<ul>
<li>작업 부하가 증가하면 필요에 따라 Worker 서버를 추가하여 확장가능</li>
<li>또한 AWS에서 제공하는 관리형 서비스인 Amazon ECS, AWS Lambda 등과 연계하여 더욱 유연한 확장이 가능<br/>

</li>
</ul>
</li>
</ol>
</li>
</ol>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://dev-minjeong.tistory.com/39">AWS SNS vs. SQS (feat. Lambda)</a></p>
<p><a href="https://kanoos-stu.tistory.com/90">[AWS] AWS SNS와 SQS를 사용한 이벤트 처리 구현하기 (w.Spring Boot)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQS를 스프링부트에서 사용해보자!]]></title>
            <link>https://velog.io/@dev_tmb/SQS%EB%A5%BC-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@dev_tmb/SQS%EB%A5%BC-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 13 Aug 2023 19:27:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_tmb/post/57e70e57-84fe-43e7-8e3d-ccc36e450ea9/image.png" alt=""></p>
<h1 id="step1-대기열-생성">STEP1. 대기열 생성</h1>
<p>자, 먼저 AWS의 SQS 페이지로 들어가 [대기열 생성]을 클릭하자!
<a href="https://ap-northeast-2.console.aws.amazon.com/sqs/v2/home?region=ap-northeast-2#/">AWS SQS 대기열 생성하러 가봅시다 <del>!</del>!~!</a></p>
<h2 id="1-1-세부-정보">1-1. 세부 정보</h2>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/dfe15b4b-584e-4736-8127-dc09cf2e02f4/image.png" alt=""></p>
<h3 id="표준-대기열">표준 대기열</h3>
<ul>
<li><p>속도 - 무제한</p>
<ul>
<li>초당 호출 수 제한 거의 X</li>
</ul>
</li>
<li><p>최대한 도착한 순서대로 처리하도록 정렬하지만 완전한 보장은 X</p>
</li>
<li><p>높은 처리량을 위해 설계</p>
</li>
<li><p>메시지 큐에 쌓여 있는 메시지를 최소 1회 전송하지만, 가끔 한 개 이상의 메시지 사본이 배달될 수도 있다</p>
<p>  ⇒ 중복 메시지 발생 가능</p>
</li>
</ul>
<h3 id="fifo-대기열">FIFO 대기열</h3>
<ul>
<li><p>속도 - 300~3000msg/s의 제한 有</p>
<ul>
<li>API - 초당 최대 300개</li>
<li>배치함수 - 초당 최대 3000개의 메시지 처리 가능</li>
</ul>
</li>
<li><p>비즈니스 로직에서 메시지 발행 순서가 반드시 보장되어야 하는 경우에 사용</p>
</li>
<li><p>정확히 1회만 전송</p>
<p>  ⇒ 중복 메시지 발생 X</p>
</li>
</ul>
<p>→ 필요에 따라 <strong>유형</strong>을 선택한 후, 대기열의 <strong>이름</strong>을 지정해주자</p>
<aside>
  👨‍👩‍👧‍👦 <b>엄빠도 어렸다</b>에서는요?

<p>일단 메시지를 보내는 순서가 유지되어야 한다는 개념이 각 부모자식 관계마다 일정시간에 알림을 예약해두는 스케줄링 작업에도 적용되는지 확실하지 않아, </p>
<p>표준 대기열로 선택을 해보겠다 </p>
</aside>

<h2 id="1-2-구성-설정">1-2. 구성 설정</h2>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/36763008-6af9-4137-aca2-e667d2b5d049/image.png" alt=""></p>
<ul>
<li><p>표시 제한 시간</p>
<p>  한 메시지 소비자가 대기열에서 푸시 받은 메시지를 다른 메시지 소비자에게는 보이지 않는 시간 ⇒ 즉, 하나의 소비자에게만 메시지가 푸시되는 동안 다른 소비자에게는 푸시하지 않도록 간격을 보장한다. </p>
</li>
<li><p>메시지 보존 기간</p>
<p>  SQS가 삭제되지 않은 메시지를 보관하는 시간 ⇒ 일정 시간 동안 보관해두었다가 사라진다.</p>
</li>
<li><p>전송 지연</p>
<p>  대기열에 추가된 각 메시지의 첫 번째 전송에 대한 지연시간 </p>
<p>  *일부러 전송을 늦추고 싶다면 이 부분을 바꿔주면 된다. </p>
</li>
<li><p>최대 메시지 크기</p>
<p>  전송하려는 메시지의 최대 크기</p>
<p>  *256KB가 최대이며, 이를 넘어가는 경우에는 Amazon SQS Extended Client Library를 사용해 S3와 함께 사용할 수 있다.</p>
</li>
<li><p>메시지 수신 대기 시간</p>
<p>  Polling이 메시지를 사용할 수 있을 때까지 기다리는 최대 시간</p>
</li>
</ul>
<p>→ 일단 default로 지정된 값대로 아무것도 건드리지 않고 생성해보겠다!</p>
<h2 id="1-3-암호화-및-권한-설정">1-3. 암호화 및 권한 설정</h2>
<p><em>여기서는 *</em>암호화만 비활성화** 해두고, 기본 설정 그대로 건드리지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/fde0d955-40a6-4d61-8855-7abf668380b7/image.png" alt=""></p>
<p>서버 측 암호화(SSE, Server Side Encryption)를 활성화하면 SQS는 대기열에 들어오는 모든 메시지들을 암호화한다. 따라서 권한을 가진 소비자에게 전송되는 경우에만 메시지 해독이 가능하다. </p>
<p>따라서, 일단은 암호화를 <strong>비활성화</strong> 하고 넘어가도록 하자</p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/b6be1790-b592-4607-933d-dadd25be0991/image.png" alt=""></p>
<p>대기열에 접근할 수 있는 액세스 정책에 대한 내용인데, 이는 기본 설정값 그대로 유지하고, IAM 쪽에서 AWS의 접근권한을 설정하는 것으로 하겠다. </p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/0f089567-c34e-4f73-a670-bd77d461b21c/image.png" alt=""></p>
<p>배달 못한 편지 = <strong>DLQ</strong>는 메시지를 소비할 수 없을 때 사용되며, 이를 통해 문제가 발생한 메시지를 격리하여 실패 원인을 분석할 수 있다. </p>
<p>이렇게 대기열을 생성하고, <strong>메시지 전송 및 수신</strong> 테스트를 해서 성공하면 정상적으로 SQS가 생성된 것이다. </p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/fa2aaaca-d67b-4610-8b0c-5c6bd3e139ef/image.png" alt=""></p>
<p>아무런 문자열이나 입력한 후(json, string 형식 모두 가능) [메시지 전송]을 클릭하면 아래와 같이 [메시지 폴링] 버튼이 활성화되고,</p>
<p>클릭하면 대기열에 쌓여있던 메시지가 폴링된 것을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/182d59fa-cc52-462a-961e-f7e6f68740fb/image.png" alt=""></p>
<h1 id="step2-iam-사용자-생성-및-키-발급">STEP2. IAM 사용자 생성 및 키 발급</h1>
<p><a href="https://us-east-1.console.aws.amazon.com/iamv2/home?region=ap-northeast-2#/users">AWS IAM 사용자 생성 Go~!</a></p>
<h2 id="2-1-사용자-세부-정보-지정">2-1. 사용자 세부 정보 지정</h2>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/38bd9e95-b3db-44df-9e77-c1f805f20fab/image.png" alt=""></p>
<p><strong>AWS IAM &gt; 사용자 &gt;</strong> <strong>[사용자 추가]</strong>를 클릭하여 SQS에 권한을 갖는 IAM 사용자를 생성한다.</p>
<h2 id="2-2-권한-설정">2-2. 권한 설정</h2>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/6abc79dd-4555-4db2-aedf-62d1fa434096/image.png" alt=""></p>
<p><strong>‘직접 정책 연결’</strong> 선택 후, <strong>AmazonSQSFullAccess 권한을 적용</strong>한다. </p>
<h2 id="2-3-보안-자격-증명--액세스-키-발급">2-3. 보안 자격 증명 &gt; 액세스 키 발급</h2>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/76205081-8ebb-425d-ae52-46bef90c804c/image.png" alt=""></p>
<p>액세스 키 만들기를 클릭하면, 아래와 같은 <strong>액세스 키 모범 사례 및 대안</strong>정보 선택사항이 뜨는데 아래와 같이 설정해주고 넘어간다. </p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/c19061ff-0155-4796-a607-5d0a3c020440/image.png" alt=""></p>
<p>액세스 키를 생성하면 아래와 같이 액세스 키, 비밀 액세스 키가 뜨는데 이는 다시 볼 수 없으니 꼭 다른 데 까먹지 않게 기록해두자!</p>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/48c0880c-527a-43e6-9bab-023850c139be/image.png" alt=""></p>
<h1 id="step3-스프링부트-프로젝트-설정">STEP3. 스프링부트 프로젝트 설정</h1>
<p><a href="https://docs.awspring.io/spring-cloud-aws/docs/3.0.0/reference/html/index.html#sqs-integration">Spring Cloud AWS(스프링 3.0 버전 이상에 적용되는 공식문서)</a></p>
<h2 id="buildgradle에-의존성-추가">build.gradle에 의존성 추가</h2>
<pre><code class="language-java">// AWS SQS
implementation group: &#39;org.springframework.cloud&#39;, name: &#39;spring-cloud-aws-messaging&#39;, version: &#39;2.2.6.RELEASE&#39;
implementation group: &#39;org.springframework.cloud&#39;, name: &#39;spring-cloud-aws-autoconfigure&#39;, version: &#39;2.2.6.RELEASE&#39;

// Spring 3.0 버전 이상이라면 아래로 설정
implementation platform(&quot;io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1&quot;)
implementation &#39;io.awspring.cloud:spring-cloud-aws-starter-sqs&#39;</code></pre>
<h2 id="applicationyml">application.yml</h2>
<pre><code class="language-yaml">cloud:
  aws:
    region:
      static: ap-northeast-2
    credentials:
      access-key: {IAM 액세스 키}
      secret-key: {IAM 시크릿 액세스 키}
    sqs:
      queue-name: {SQS 대기열 이름}
    stack:
      auto: false</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_tmb/post/89269fdd-d3cd-46d0-8589-fa4d0688aa1e/image.png" alt=""></p>
<h2 id="테스트용-클래스">테스트용 클래스</h2>
<p>공식문서와 블로그 자료를 참고해서 테스트용 서비스, 컨트롤러 클래스를 생성하였다.</p>
<h3 id="awssqsconfigjava"><code>AwsSQSConfig.java</code></h3>
<pre><code class="language-java">@Import(SqsBootstrapConfiguration.class)
@Configuration
public class AwsSQSConfig {

    @Value(&quot;${spring.cloud.aws.credentials.access-key&quot;)
    private String AWS_ACCESS_KEY;

    @Value(&quot;${spring.cloud.aws.credentials.secret-key&quot;)
    private String AWS_SECRET_KEY;

    @Value(&quot;${spring.cloud.aws.region.static&quot;)
    private String AWS_REGION;

    // 클라이언트 설정: region과 자격증명
    @Bean
    public SqsAsyncClient sqsAsyncClient() {
        return SqsAsyncClient.builder()
                .credentialsProvider(() -&gt; new AwsCredentials() {
                    @Override
                    public String accessKeyId() {
                        return AWS_ACCESS_KEY;
                    }

                    @Override
                    public String secretAccessKey() {
                        return AWS_SECRET_KEY;
                    }
                })
                .region(Region.of(AWS_REGION))
                .build();
    }

    // Listener Factory 설정 (Listener 쪽)
    @Bean
    public SqsMessageListenerContainerFactory&lt;Object&gt; defaultSqsListenerContainerFactory() {
        return SqsMessageListenerContainerFactory.builder()
                .sqsAsyncClient(sqsAsyncClient())
                .build();
    }

    // 메시지 발송을 위한 SQS 템플릿 설정 (Sender 쪽)
    @Bean
    public SqsTemplate sqsTemplate() {
        return SqsTemplate.newTemplate(sqsAsyncClient());
    }
}</code></pre>
<pre><code class="language-java">@Configuration
public class AwsSQSConfig {

    @Value(&quot;${cloud.aws.credentials.accessKey}&quot;)
    private String accessKey;

    @Value(&quot;${cloud.aws.credentials.secretKey}&quot;)
    private String secretKey;

    @Value(&quot;${cloud.aws.region.static}&quot;)
    private String region;

    @Primary
    @Bean
    public AmazonSQSAsync amazonSQSAsync() {
        BasicAWSCredentials basicAwsCredentials = new BasicAWSCredentials(accessKey, secretKey);
        return AmazonSQSAsyncClientBuilder.standard()
            .withRegion(region)
            .withCredentials(new AWSStaticCredentialsProvider(basicAwsCredentials))
            .build();
    }
}</code></pre>
<h3 id="sqstransferlistenerjava"><code>SqsTransferListener.java</code></h3>
<pre><code class="language-java">import io.awspring.cloud.sqs.annotation.SqsListener;
import org.springframework.stereotype.Component;

@Component
public class SqsTransferListener {

    @SqsListener(&quot;${spring.cloud.aws.sqs.queue-name}&quot;)
    public void messageListener(String message) {
        System.out.println(&quot;Listener: &quot; + message);
    }
}</code></pre>
<h3 id="sqsmessagesenderjava"><code>SqsMessageSender.java</code></h3>
<pre><code class="language-java">import io.awspring.cloud.sqs.operations.SendResult;
import io.awspring.cloud.sqs.operations.SqsTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.services.sqs.SqsAsyncClient;

@Component
public class SqsMessageSender {

    private final SqsTemplate queueMessagingTemplate;

    @Value(&quot;${spring.cloud.aws.sqs.queue-name}&quot;)
    private String QUEUE_NAME;

    public SqsMessageSender(SqsAsyncClient sqsAsyncClient) {
        this.queueMessagingTemplate = SqsTemplate.newTemplate(sqsAsyncClient);
    }

    public SendResult&lt;String&gt; sendMessage(String message) {
//        Message&lt;String&gt; newMessage = MessageBuilder.withPayload(message).build();
        System.out.println(&quot;Sender: &quot; + message);
        return queueMessagingTemplate.send(to -&gt; to
                .queue(QUEUE_NAME)
                .payload(message));
    }
}</code></pre>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://lannstark.tistory.com/88">AWS SQS 들이파기</a></p>
<p><a href="https://devbksheen.tistory.com/entry/Spring-boot-3x-Amazon-SQS-%EC%A0%81%EC%9A%A9%EA%B8%B0">Spring boot 3.x + Amazon SQS 적용기</a></p>
]]></description>
        </item>
    </channel>
</rss>