FCM 푸시 파헤치기

Hits

thumbnail.png

안녕하세요 현재 줌인터넷 핀테크 개발팀 서버개발자로 근무하고있는 김의빈입니다.

이번 포스팅에서는 입사 이후 진행하였던 파일럿프로젝트에 대한 회고와 FCM의 푸시에 대한 이야기를 풀어내 보려고합니다. 해당 글의 내용이 처음 푸시를 접하시는 분들이나 현재 구현을 진행중이신 분들에게 도움이 될 수 있도록 한번 정리해보았습니다. 😊

이번 포스팅에서 알아갈 수 있는 내용은 전반적으로 다음과 같습니다.

목차

  1. 시작
  2. 푸시컴포넌트
    2.1 푸시란 무엇일까?
    2.2 FCM 이란?
    2.3 FCM의 TOKEN이란?
    2.4 FCM의 TOPIC이란?
    2.5 서버가 해야할 모범 사례
  3. 아키텍처 요구사항
    3.1 파일럿 프로젝트
      3.1.1 요구사항
      3.1.2 최종 아키텍처 설계
      3.1.2 최종 아키텍처 선택
    3.2 실제 프로젝트
      3.2.1 요구사항
      3.2.2 애플리케이션 아키텍처
      3.2.3 푸시 스키마 테이블 설계
      3.2.4 서버 내, 여러개 Firebase App 관리
  4. 푸시 발송
    4.1 토큰을 이용한 푸시발송
    4.2 토픽을 이용한 푸시발송
    4.3 토픽 구독과 구독 취소
    4.4 푸시 발송 비동기처리 메서드 callAsync()
  5. 시연 화면
  6. 발생한 이슈
  7. FCM의 한계점
  8. 마치며

1. 시작


줌 인터넷에 입사한 이후, 바로 실무에 투입하지 않고 전반적인 프로젝트의 흐름과 파트장님의 온보딩을 통해 해당 프로젝트에 대한 전반적인 이해와 사용되는 기술스택들을 파악하는 시간을 가졌습니다.

실제 서비스에 접목시킬 내용을 토대로 파일럿프로젝트를 전달받았었고 실제 서비스의 접목시 고려해야할 사항들과 서비스들을 토대로 설계를 진행하였습니다.

그렇다면 FCM부터 한번 살펴보도록 하겠습니다.

2. 푸시 컴포넌트

2.1 푸시란 무엇일까?


이번 포스팅에서는 FCM을 활용한 유저 디바이스 푸시발송을 주제로 이야기를 풀어갑니다.

그렇다면, 여기서 말하는 푸시란 무엇인지 짚고 넘어가 보겠습니다.

그렇다면 해당 포스팅에서 이야기하고자하는 FCM을 활용한 푸시발송FCM부터하나씩 살펴보겠습니다.

2.2 FCM 이란?


FCM-PUSH-03.png

FCM ( Firebase - Cloud - Messaging ) [ 공식문서 LINK ]

FCM의 공식문서에서는 FCM이 제공하는 주요 기능을 다음과 같이 이야기해주고 있었습니다. [ 공식문서 LINK ]

FCM을 이용해 각 유저들에게 푸시메시지를 전송하기 위해선 TOKEN , TOPIC을 활용해 푸시 메시지를 보낼 수 있습니다. 그렇다면 이 두가지가 어떤것일까요? 한번 같이 살펴보겠습니다 😊

2.3 FCM의 TOKEN 이란?


FCM에서 푸시발송 시 사용되는 TOKEN에 대해 설명해보자면 다음과 같습니다.

FCM-PUSH-04.png

즉, Token은 Firebase에서 관리하는 프로젝트별 접속하는 기기의 고유 ID로 볼 수 있습니다.

2.4 FCM의 TOPIC 이란?


FCM에서 푸시 발송 시 사용되는 TOPIC에 대해 설명해보면 다음과 같습니다.

FCM-PUSH-05.png

FCM에서 토픽을 선정할때 유의해야하는 사항들은 다음과 같습니다.

2.5 서버가 해야할 모범 사례


FCM의 공식 레퍼런스에서는 FCM을 사용하는 서버의 경우 다음과 같은 사항을 지켜야함을 명시해주고 있었습니다.

FCM-PUSH-06.png

모범 사례를 통해 알 수 있듯이, Firebase에서 발급된 토큰의 경우 발급 이후의 토큰관리를 하고있지 않기에, 이를 서버에서 따로 관리를 해줘야 함을 알 수 있습니다.

이때 토큰에서 위와 같은 사례를보며 토픽 또한, 서버에서 구독 이후 따로 관리를 해줘야 함을 알 수 있었습니다.

위 사항들을 미루어보았을때 서버에서는 Firebase의 각 디바이스 TOKEN 값들과 FCM TOPIC을 구독한 이후 해당 값들을 가지고 있어야 하고 관리해 줘야함을 알 수 있었습니다.

3. 아키텍처 요구사항

3.1 파일럿 프로젝트


3.1.1 요구사항

파일럿 프로젝트를 진행할 당시 전달받았던 요구사항은 다음과 같습니다.

< 요구사항 1. 채팅시스템이 있다는 점들을 염두해야한다. >

현재 GET STOCK에서는 채팅 시스템을 통해 유저분들께 보다 편리하고 다양한 서비스를 제공드리기 위한 기획 속 채팅 시스템 도입시 유연한 푸시 서비스를 제공해야 함을 전달 받았었고 당시 사용 기획중에 있던 Kafka를 생각하여 아키텍처를 구성해보았습니다.

Kafka 를 활용하여 생각했던 플로우는 다음과 같습니다.

FCM-PUSH-07.png

채팅의 경우 실시간으로 짧은 시간 내, 신속하게 푸시 메시지가 발송 및 수신이 진행되어야 합니다.

또한 채팅 푸시의 경우는 채팅의 특성상 짧은 시간 내 많은 양의 푸시메시지가 요구된다고 생각했었습니다.

그렇기에, 기존 사용 계획에 포함되어 있던 Kafka를 활용해 각 파티션별로 채팅과, 마케팅 커뮤니티 알림등 서비스의 관련된 큐를 분리하여 메시지를 처리하게된다면 해당 부분에 대한 Latency를 최소화시킬 수 있을지 않을까? 라는 생각을 하였습니다.

< 요구사항 2. TOKEN 푸시 발송 외 TOPIC발송 구현과 TOPIC에 대한 주제선정하기 >

현재 GET STOCK에서 사용하는 FCM 푸시에는 TOPIC의 관련된 정책이 존재하지 않았었습니다. 그렇기에, FCM에서 간편하게 그룹발송을 할수 있는 TOPIC을 활용해 주제를 선정할 필요가 있었습니다.

제가 주제를 선청할때 기준은 다음과 같았습니다.

FCM-PUSH-08.png

TOPIC 발송의 경우 유튜브의 구독시스템과 비슷한 구조라고 생각할 수 있습니다.

유튜브를 이용하며 경험하였던 정책들 중, 내가 구독을 누른 유튜버가 생방송을 시작하거나 새로운 영상이 올라왔을 때 다음과 같이 푸시알림을 보내주고 있었습니다.

FCM-PUSH-09.png

해당 부분에서 영감을 받아, 서비스를 이용하시는 고객분들이 관심있어하는 주제 혹은 많은 유저분들께 푸시알림을 보낼때는 위 이야기한 사항들을 고려해 TOPIC 주제 선정을 진행하였습니다.

3.1.2 최종 아키텍처 설계

위 사항들을 고려해 당시 진행할 파일럿 프로젝트 아키텍처로 두가지를 선정해 보았습니다.

FCM-PUSH-10.png

저는 처음 다음과 같이 아키텍처를 구상하였었습니다. KafkaRedis를 생각했던 이유는 다음과 같습니다.

FCM-PUSH-11.png

3.1.3 최종 아키텍처 선택

위와 같이, 두가지의 아키텍처를 제안하였고 이때 두번째 아키텍처가 선정되어 파일럿 프로젝트에 접목하게 되었습니다. 접목된 이유는 다음과 같습니다.

위와 같은 이유로 두번째 아키텍처를 적용하여 파일럿 프로젝트를 진행하였습니다.

3.2 실제 프로젝트


3.2.1 요구사항

파일럿 프로젝트가 마무리 된후, 최종적으로 마주했던 요구사항은 다음과 같습니다.

파일럿 당시 FCM의 대한 이해실제 프로젝트의 사용될 기술스택들의 대한 도메인 공부를 진행하며 구현에 집중했었기에, 해당 서버에서는 하나의 프로젝트만 관리할 수 없었고 이는 요구사항에서 이야기했던 다양한 프로젝트에서의 사용이 불가했습니다.

그렇기에, 다음과 같은 부분을 염두하며 실제 프로젝트에 접목하고자 하였습니다.

해당 요구사항에 맞춰 어떻게 진행했는지 하나씩 살펴보겠습니다.

3.2.2 애플리케이션 아키텍처

푸시를 구현하며 구현했던 아키텍처는 다음과 같습니다.

FCM-PUSH-12.png

푸시 서버의경우 멀티모듈로 진행을 하였고 해당 부분은 푸시 발송 시, 핵심인 Service module 내, 아키텍처 입니다.

FCM-PUSH-13.png

푸시 발송의 경우 다양한 발송 서비스가 필요로 했습니다.

그렇기에, 푸시 발송 유형을 인터페이스로 분리하여 진행하였습니다.

FCM-PUSH-14.png

구독 서비스의 경우도 푸시발송 서비스와 마찬가지로 다양한 상황이 있을 것입니다.

해당 부분또한 공통적인 ‘구독’에 대한 인터페이스를 분리하여 각 서비스 분리를 진행하였습니다.

해당 부분에 대해 분리를 진행했던 이유는 다음과 같습니다.

3.2.3 푸시 스키마 테이블 설계

푸시 스키마 테이블 설계의 경우 다음과 같은 사항들을 염두하여 설계해보고자 하였습니다.

FCM-PUSH-15.png

위 사진은 스키마 설계 중 일부분 입니다.

위에서 언급했던것과 같이 다양한 프로젝트에서 해당 서비스를 이용하기 위해선 각 프로젝트 별로 구분하여 푸시 발송 메시지와 토픽을 구분해야 했습니다.

그렇기에, 다음과 같이 프로젝트별 관리 테이블을 만든 후, topic 데이터를 적재할 시 각 프로젝트 별로 주제를 관리할 수 있도록 설계를 진행하였습니다.

3.2.4 서버 내, 여러개 Firebase App 관리

현재 푸시 발송시 FCM의 공식문서에서 제안하는것 처럼 @PostConstruct 어노테이션을 활용하여 최초 실행 시, 푸시 서버내, FirebaseApp의 SDK를 가져와 initializeApp을 실행하는 구조로 구성이 되어 있었습니다.

아래의 코드를 살펴보겠습니다.

public class FirebaseConfig {

   @Value("Sdk json파일 경로")
   private Resource resource;

   @PostConstruct
   public void initFirebase() {
      try {
         // Service Account를 이용하여 Fireabse Admin SDK 초기화
         FileInputStream serviceAccount = new FileInputStream(resource.getFile());
         FirebaseOptions options = new FirebaseOptions.Builder()
                 .setCredentials(GoogleCredentials.fromStream(serviceAccount))
                 .build();
         FirebaseApp.initializeApp(options);

      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

위와 같은 구조로 생성할 시, 하나의 FirebaseApp 내부 프로젝트들의 권한을 서버에서 취득할 수 있지만, 추후 방향성에 맞춘 다양한 서비스에서의 푸시서비스 제공의 취지와는 맞지 않았고 지원할 수 없는 구조였습니다.

이러한 사항을 어떻게 해결할 수 있을지 고민을 해보았고 다음과 같이 내부 수정을 진행해 보았습니다.

@Component
public class FirebaseAppProvider {

   private static final String JSON_TYPE_SUFFIX = ".json";

   private final Map<String, FirebaseApp> firebaseApps = new HashMap<>();

   @PostConstruct
   public void initFirebase() {

      List<ClassPathResource> resources = getFirebaseResources();

      for (ClassPathResource resource : resources) {

         String projectName = getProjectName(resource);

         try (InputStream inputStream = resource.getInputStream()) {

            FirebaseOptions options = FirebaseOptions.builder()
                    .setCredentials(GoogleCredentials.fromStream(inputStream))
                    .build();
            FirebaseApp.initializeApp(options, projectName);
            firebaseApps.put(projectName, FirebaseApp.getInstance(projectName));

         } catch (IOException e) {
            throw new RuntimeException(e);
         }
      }
   }
}

등록한 프로젝트에 푸시 알림을 보낼때는 다음과 같이 사용하였습니다.

public FirebaseApp getFirebaseApp(String projectName) {
        return firebaseApps.get(projectName);
    }

그렇다면 해당 메서드의 사용시점은 언제일까요?

바로, 푸시 발송시점에 해당 메서드가 사용됩니다. 그림으로 살펴보면 다음과 같습니다.

FCM-PUSH-16.png

FCM-PUSH-17.png

그렇기에 해당 메서드를 통해 푸시를 보내는 시점에 특정 프로젝트를 타겟팅 해줘 정상적으로 전달이 이뤄지도록 하기위해 사용합니다.

FCM-PUSH-18.png

4. 푸시 발송

4.1 토큰을 이용한 푸시발송

메시지 발송 시, FCM에서 제공하는 발송 메서드들은 다음과 같습니다. 공식문서Link

@Override
    public void sendMessage(PushNotificationRequestDTO request) {
        FirebaseApp firebaseApp = firebaseAppProvider.getFirebaseApp(request.getProjectName());
        MulticastMessage messages = request.buildSendMessageToToken(request);

        FirebaseMessaging.getInstance(firebaseApp).sendMulticastAsync(messages);
    }

다음은 푸시 서버에서 FCM으로의 메시지 발송 시, 일부분 입니다.

해당 메서드는 다중 타겟에게 푸시 메시지를 요청하기 위한 푸 시발송 메서드로서, 보낼 대상의 Project FirebaseApp에 푸시 메시지를 비동기로 전송을 할 수 있는 메서드 입니다.

다른 푸시 발송 메서드의 경우도 위와 같이 끝에 Async가 붙는 비동기 처리를 하는데, 어떻게 구성이 되어있는지 위 코드를 기반으로 내부를 살펴보겠습니다.

public static synchronized FirebaseMessaging getInstance(FirebaseApp app) {
    FirebaseMessagingService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID,
        FirebaseMessagingService.class);
    if (service == null) {
      service = ImplFirebaseTrampolines.addService(app, new FirebaseMessagingService(app));
    }
    return service.getInstance();
  }

다음은 위 내용중 getInstance에 관련된 메서드 입니다.

서버에서는 FCM의 요청 시, 해당 메시지와 종류, 보낼 대상의 Firebase 인스턴스를 설정해야함을 알 수 있습니다.

...
public ApiFuture<BatchResponse> sendAllAsync(
      @NonNull List<Message> messages, boolean dryRun) {
    return sendAllOp(messages, dryRun).callAsync(app);
  }

다음은 위 메서드 중, Firebase에서 제공하는 sendMulticastAsync의 내부로직입니다.

FCM을 하기 위한, 제공되는 Firebase-admin 라이브러리에서는 다음과 같이 비동기 요청도 제공을 하고 있습니다.

메시지 전송시 Async를 붙인 메시지 전송 요청은 다음과 같이 끝에 callAsync를 통해 FirebaseApp에 요청을 보내줌을 확인할 수 있었습니다.

메시지 발송의 비동기 처리를하는 callAsync의 내부는 밑의 푸시 발송 비동기처리 메서드 callAsync() 파트에서 함께 살펴보겠습니다.

4.2 토픽을 이용한 푸시발송

실 프로젝트에서는 토큰을 통한 발송만 현재 구현되어있는 상황이었고, 이와 더해 토픽 발송도 추가하여 보다 다양한 푸시발송 서비스를 제공하고자 하였습니다.

위 서론에서 토픽의 대한 정의는 전달하였으니, 토픽 발송시 이뤄지는 플로우에 대해 이전 토픽에 사용했던 그림으로 다시한번 살펴보겠습니다.

FCM-PUSH-05.png

위 그림을 보면 알 수 있듯, 각 토픽별로 그룹을 묶어 FCM에서 관리를 하고 메시지 발송 요청 시, 발송 대상의 토픽을 포함하면 해당 토픽을 구독한 유저들에게 메시지를 발송하게 되어집니다.

토픽 발송의 경우도 Multicast, sendAll 메서드와 동일하게 각 토픽 그룹별 FCM에서 1000건까지의 발송이 이뤄지지 않기에, 만약 해당 토픽의 구독자 수가 1000명 이상이라면 구독자별로 나눠 발송을 하거나, 1000명 별로 여러개의 토픽을 생성하여 구독자를 분리하는 방법을 취해야합니다.

4.3 토픽 구독과 구독 취소

토픽 구독과 구독 취소의 경우는 어떻게 구성하였는지 코드를 통해 살펴보면 다음과 같습니다.

... 

// 구독 요청 시
public void subScribe(FirebaseApp firebaseApp, String topicName, List<String> memberTokenList) {
        FirebaseMessaging.getInstance(firebaseApp).subscribeToTopicAsync(
                memberTokenList,
                topicName
        );
    }

.... 

// 구독 취소
public void unSubscribe(FirebaseApp firebaseApp, String topicName, List<String> memberTokenList) {
        FirebaseMessaging.getInstance(firebaseApp).unsubscribeFromTopicAsync(
                memberTokenList,
                topicName
        );
    }

....

다음은 FCM으로의 구독과 구독취소 요청 메서드로서 위에서 소개한 푸시발송 메서드와 비슷한 구조로 이뤄짐을 알 수 있습니다.

그렇다면 내부는 어떻게 이뤄져 있는지 내부도 살펴보겠습니다.

// 구독 요청시 

/**
   * Subscribes a list of registration tokens to a topic.
   *
   * @param registrationTokens A non-null, non-empty list of device registration tokens, with at
   *     most 1000 entries.
   * @param topic Name of the topic to subscribe to. May contain the {@code /topics/} prefix.
   * @return A {@link TopicManagementResponse}.
   */
  public TopicManagementResponse subscribeToTopic(@NonNull List<String> registrationTokens,
      @NonNull String topic) throws FirebaseMessagingException {
    return subscribeOp(registrationTokens, topic).call();
  }

....

// 구독 취소 요청시 
/**
   * Similar to {@link #unsubscribeFromTopic(List, String)} but performs the operation
   * asynchronously.
   *
   * @param registrationTokens A non-null, non-empty list of device registration tokens, with at
   *     most 1000 entries.
   * @param topic Name of the topic to unsubscribe from. May contain the {@code /topics/} prefix.
   * @return An {@code ApiFuture} that will complete with a {@link TopicManagementResponse}.
   */
  public ApiFuture<TopicManagementResponse> unsubscribeFromTopicAsync(
      @NonNull List<String> registrationTokens, @NonNull String topic) {
    return unsubscribeOp(registrationTokens, topic).callAsync(app);
  }

각 주석을 통해 알 수 있듯, Token값이 비어져 있지 않은 경우, 최대 한번의 요청에 1000건까지 구독및 취소 요청이 가능함을 알 수 있었습니다.

4.4 푸시 발송 비동기처리 메서드 callAsync()

위 푸시의 내부를 살펴보면 비동기처리를 진행할때 callAsync라는 메서드를 사용하고 있음을 확인할 수 있었습니다.

그렇다면, 이 callAsync 내부는 어떻게 구성이 되어 있을까요? 한번 살펴보겠습니다.

... 
/*
Run this operation asynchronously on the main thread pool of the specified FirebaseApp.
매개변수: app – A non-null FirebaseApp.
반환: An ApiFuture.
*/
  public final ApiFuture<T> callAsync(@NonNull FirebaseApp app) {
    checkNotNull(app);
    return ImplFirebaseTrampolines.submitCallable(app, this);
  }

해당 내부는 다음과 같은 구조로 진행됨을 확인할 수 있었습니다.

5. 시연화면


지금까지 푸시관련된 이야기를 한번 풀어보았습니다.

그렇다면 푸시 서버에서 요청 전송 시, 어떻게 발송이 될까요?

해당 포스팅의 구현화면에서는 TOKEN을 이용한 발송, TOPIC을 활용한 발송 두가지 화면을 첨부해 보고자합니다.

[ Android 테스트 ]

FCM-PUSH-19.png

MulticastAsync를 통한 메시지 발송시

FCM-PUSH-20.png

TOPIC을 통한 메시지 발송전송시 테스트

[ iOS 테스트 ]

FCM-PUSH-21.png

MulticastAsync를 통한 메시지 발송시

FCM-PUSH-22.png

TOPIC을 통한 메시지 발송전송시 테스트

요청 Body의 해당되는 내용들은 민감사항일수 있기에, 따로 첨부하지 않았습니다.

위처럼 토픽을 구독 후 요청을 보내거나, 혹은 토큰을 이용한 푸시메시지 발송시 해당 유저의 기기에서 다음과 같이 푸시 메시지가 발송됨을 확인할 수 있었습니다.

6. 발생한 이슈

현재까지 적용을 진행하며 발생한 이슈는 다음과 같았습니다.

FCM을 통해 발송할 시, Android와 iOS(APNs)에 대한 Config 세팅을 진행해야하는데, 해당부분에 문제가 생겨 발송되지 않음을 확인할 수 있었습니다.

// Android 세팅 
public AndroidConfig TokenAndroidConfig(PushNotificationRequestDTO request) {
        return AndroidConfig.builder()
                .setCollapseKey(request.getCollapseKey())
                .setNotification(AndroidNotification.builder()
                        .setTitle(request.getTitle())
                        .setBody(request.getMessage())
                        .build())
                .build();
    }

	// APNs 세팅 ( iOS ) 
    public ApnsConfig TokenApnsConfig(PushNotificationRequestDTO request) {
        return ApnsConfig.builder()
                .setAps(Aps.builder()
                        .setAlert(
                                ApsAlert.builder()
                                        .setTitle(request.getTitle())
                                        .setBody(request.getMessage())
                                        .setLaunchImage(request.getImgUrl())
                                        .build()
                        )
                        .setCategory(request.getCollapseKey())
                        .setSound("default")
                        .build())
                .build();
    }

위 코드 중, APNs에서 필수적으로 Config 세팅이 필요한것은 다음과 같이 세팅을 진행해야 합니다.

    public ApnsConfig TokenApnsConfig(PushNotificationRequestDTO request) {
        return ApnsConfig.builder()
                .setAps(
									..........
                                ApsAlert.builder()
                                        .setTitle(request.getTitle())
                                        .setBody(request.getMessage())
                                        .setLaunchImage(request.getImgUrl())
                                        .build()
                        )
									..........
    }

iOS 발송시, 안드로이드와는 다르게 푸시 발송 시, 위와 같이 Alert에 세팅을 진행해줘야 하는데 이를 누락하여 발생했던 이슈가 있었습니다.

물론 간단하게 위 내용처럼 세팅을 통해 이슈를 해결할 수 있었지만, 안드로이드와 비교 시 setBody 를 통해 이미지또한 간편하게 넣을 수 있었던 점과는 다르게 APNs에서는 이미지의 관련된 설정을 따로 진행해야 함을 알 수 있었습니다.

느낀점이라면 iOS의 경우 하나하나 주어진 양식별로 세팅을 진행해 줘야한다!? 라는 느낌을 많이 받았던 것 같습니다.

7. FCM의 한계점


지금까지 FCM을 리서치를 진행하고 구현까지 진행해보며 느낀 한계점에 대해 한번 이야기를 꺼내볼까 합니다.

FCM의 경우 무료와 오픈소스이기에, 아무래도 한계점이 있을 수 밖에 없었지 않았나 라는 생각이 들었습니다.

그렇기에, 발송건수가 적을 경우는 괜찮을 수 있지만 점차 발송건수가 커질수록 다른 모색책을 생각해봐야 할수도 있겠다라는 생각을 하였습니다.

8. 마치며


입사 이후 푸시에 대해 접하고 이에대한 레퍼런스를 찾았을때 많이 나오지 않아 공식레퍼런스를 하나씩 살펴보며 찾아보았던 기억이 새록새록합니다.

정말 부족한 글일수도있지만 푸시를 구현하시는 분들에게 조금이나마 도움이 되었으면 좋겠다라는 생각을 하며 글을 작성해보았습니다.

글을 작성하며 그동안의 구현한 내용과 나의 부족한점들을 돌아볼 수 있었던 좋은 시간이었습니다.

해당 포스팅이 FCM을 이용해 푸시를 구현하고자 하는 팀과 회사 그리고 글을 보시는 분들에게 도움되는 내용이 있다면 글로서 한번 더 풀어보고 싶다라는 욕심을 이번기회에 더 가지게되었습니다.

처음은 미약하지만 끝은 창대하리라 라는 말이 있듯 조그마한 이야기에서 시작해 더 도움되고 수준높은 글을 쓸 수 있는 날을 기대하며 이만 글을 마치고자합니다.

끝까지 읽어주셔서 너무나 감사합니다 😊 보시는 분들에게 항상 좋은일만 가득하시길 기원합니다.