<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>유릉이의 개발 블로그</title>
    <link>https://yunchan97.tistory.com/</link>
    <description>CS 지식 및 알고리즘 풀이</description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 10:22:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yunchan^.^</managingEditor>
    <image>
      <title>유릉이의 개발 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/5953424/attach/9b03cefe03c44b38af2e1d360613c4ad</url>
      <link>https://yunchan97.tistory.com</link>
    </image>
    <item>
      <title>FCM 푸시 알림, SQS + Lambda로 리팩토링</title>
      <link>https://yunchan97.tistory.com/100</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 FCM 서비스 로직 흐름&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;알림 생성 요청&lt;/b&gt;: 스케줄러나 특정 이벤트에 의해 FCM 푸시 알림을 전송할 필요가 있을 때, FcmService에서 푸시 알림 전송 로직이 호출됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;FCM 메시지 생성&lt;/b&gt;: FcmService에서 FCM 푸시 알림을 전송하기 위해 메시지를 생성합니다. 이 메시지는 FirebaseMessaging 객체를 통해 전송됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;멀티캐스트 전송&lt;/b&gt;: FcmService는 여러 개의 FCM 토큰을 한 번에 전송할 수 있는 multiSendMessage 메서드를 사용하여 배치로 푸시 알림을 전송합니다. 이 과정에서 Firebase Admin SDK가 사용되며, FCM 서버와 직접 통신하여 푸시 알림을 전송합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;결과 처리&lt;/b&gt;: 전송된 푸시 알림에 대한 결과를 받아 성공 여부를 확인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;기존 코드의 문제점&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;서버 부하&lt;/b&gt;: FCM 푸시 알림을 직접 전송할 때, 특히 많은 사용자에게 동시에 푸시 알림을 전송할 경우, Spring 애플리케이션 서버에 부하가 걸릴 수 있습니다. 이는 추후에 MAU가 높아졌을 때 서버의 성능 저하로 이어질 수 있으며, 서버가 다른 중요한 요청을 처리하는 데 영향을 줄 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;확장성 문제&lt;/b&gt;: FCM 푸시 알림 전송 로직이 Spring 애플리케이션에 내장되어 있기 때문에, 트래픽이 급격히 증가하는 경우 이를 효율적으로 처리하기 어려울 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;실패 처리 문제&lt;/b&gt;: 배치 전송 중 일부 메시지가 실패할 경우, 전체 배치가 실패한 것처럼 처리될 가능성이 있습니다. &amp;rarr; 배치가 실패하면 하나의 배치로 묶인 메시지가 날아가는 큰 문제점이 존재합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;따라서, FCM 알림 서비스를 Spring 애플리케이션에서 분리하여 SQS + Lambda를 활용해 처리하기로 결정했습니다. 전체적인 로직의 흐름은 아래와 같습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SQS + Lambda 로직 흐름&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;알림 생성 요청&lt;/b&gt;: 스케줄러나 특정 이벤트에 의해 FCM 푸시 알림을 전송할 필요가 있을 때, SqsMessageService를 통해 SQS 큐에 메시지를 전송합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SQS 큐로 메시지 전송&lt;/b&gt;: FCM 푸시 알림 요청이 SQS 큐로 전송됩니다. 각 FCM 메시지는 SQS 큐에 메시지로 저장됩니다. SQS 큐는 높은 확장성을 가지고 있으며, 여러 메시지를 효율적으로 처리할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Lambda 트리거&lt;/b&gt;: SQS 큐에 메시지가 들어오면, Lambda 함수가 트리거되어 큐에서 메시지를 가져와 처리합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Lambda에서 FCM 메시지 전송&lt;/b&gt;: Lambda 함수는 큐에서 받은 메시지를 바탕으로 FCM 푸시 알림을 생성하고, 이를 Firebase Admin SDK를 통해 전송합니다. Lambda는 자동으로 확장되어 동시에 많은 메시지를 처리할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;결과 처리&lt;/b&gt;: Lambda 함수 내에서 메시지 전송 결과를 처리하고, 실패한 메시지에 대한 로그를 남기거나 재시도 로직을 구현할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SQS + Lambda 사용의 장점&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;확장성&lt;/b&gt;: SQS는 매우 확장 가능하고 안정적인 메시징 서비스를 제공하며, Lambda는 필요에 따라 자동으로 확장되어 많은 양의 푸시 알림 요청을 동시에 처리할 수 있습니다. 이로 인해 트래픽이 급증하는 상황에서도 안정적으로 푸시 알림을 전송할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;서버 부하 감소&lt;/b&gt;: FCM 푸시 알림 전송 작업이 Spring 애플리케이션 서버가 아닌 Lambda에서 처리되므로, 애플리케이션 서버의 부하가 크게 줄어듭니다. 이는 서버가 다른 중요한 작업을 더 효과적으로 처리할 수 있도록 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;비용 효율성&lt;/b&gt;: Lambda는 사용한 만큼만 비용을 지불하는 구조로, 특정 시점에만 푸시 알림을 많이 전송하는 경우 비용을 절감할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;에러 처리&lt;/b&gt;: SQS와 Lambda를 사용하면 메시지의 실패 시에도 자동으로 재시도할 수 있는 메커니즘을 설정할 수 있으며, 배치 전송 중 일부 메시지가 실패해도 나머지 메시지에는 영향을 미치지 않게 처리할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같은 장점들로 인해, 기존 코드를 SQS + Lambda 구조로 리팩토링하기로 결정했습니다. 이를 위해, 우선, SQS와 Lambda를 연결해서 Event를 처리하는 서버를 구현해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;SQS 인스턴스 생성&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;lambda 함수 생성&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;lambda에 SQS 권한 부여&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;생성된 lambda에 트리거로 SQS추가&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같이 설정을 하면 lambda와 SQS가 잘 연결되었는지 테스트 코드를 통해 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이벤트 JSON형식은 lambda 함수에서 코드를 생성한 형식에 맞춰서 테스트를 진행해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring 애플리케이션에서 FCM 푸시 요청을 처리하기 위해서는 Lambda 함수에서 해당 요청을 받아 처리하는 코드가 필요합니다. 이러한 작업을 처리하기 위해 주로 Python이나 Node.js와 같은 언어가 많이 사용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;두 언어 모두 AWS Lambda에서 지원되며, 간결하고 효율적인 비동기 처리가 가능하기 때문에 이러한 장점을 고려하여, 저는 Python을 사용하여 Lambda 함수를 작성했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Python은 간결하고 직관적인 문법을 제공하며, AWS SDK (boto3)와 같은 라이브러리 지원이 잘 되어 있어 AWS 서비스와의 통합이 용이합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작성한 lambda_function.py 코드를 순서에 맞게 설명하면&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Lambda 함수 호출 (lambda_handler 실행)&lt;/b&gt;:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SQS에서 메시지가 도착하면 AWS Lambda가 자동으로 lambda_handler 함수를 호출합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 SQS 메시지의 내용이 event 매개변수로 전달됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Firebase 초기화 (initialize_firebase)&lt;/b&gt;:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Lambda 함수가 실행될 때 Firebase가 이미 초기화되지 않았다면 initialize_firebase 함수가 호출되어 Firebase가 초기화됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 과정에서 AWS Secrets Manager에서 Firebase 자격 증명을 가져와 Firebase SDK를 설정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SQS 메시지 처리&lt;/b&gt;:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;lambda_handler 함수에서 event['Records']를 순회하면서 SQS 메시지들을 하나씩 처리합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 메시지의 body를 JSON으로 파싱하고, title, body, token 정보를 추출합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;FCM 알림 전송 (send_fcm_update_notification)&lt;/b&gt;:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추출된 정보(제목, 내용, 디바이스 토큰)를 바탕으로 FCM 푸시 알림 메시지를 생성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;send_fcm_update_notification 함수를 통해 Firebase Cloud Messaging을 사용하여 해당 디바이스로 알림을 전송합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;결과 반환&lt;/b&gt;:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 메시지가 성공적으로 처리되면 lambda_handler는 상태 코드 200과 함께 성공 메시지를 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처리 중 오류가 발생하면 오류 메시지를 출력하고 상태 코드 500을 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import json
import boto3
import firebase_admin
from firebase_admin import credentials, messaging

def get_firebase_credentials():
    secret_name = &quot;firebase-dev/serviceAccount&quot;  # Secrets Manager에 저장한 비밀의 이름
    region_name = &quot;ap-northeast-2&quot;  # Secrets Manager가 위치한 리전

    # Secrets Manager 클라이언트 생성
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    try:
        # Secrets Manager에서 비밀 가져오기
        get_secret_value_response = client.get_secret_value(SecretId=secret_name)
        secret = get_secret_value_response['SecretString']
        return json.loads(secret)
    except Exception as e:
        print(f&quot;Error retrieving secret: {e}&quot;)
        raise e

def initialize_firebase():
    # 기본 Firebase 앱이 이미 초기화되었는지 확인
    if not firebase_admin._apps:
        # Firebase 자격 증명 가져오기
        credentials_data = get_firebase_credentials()
        cred = credentials.Certificate(credentials_data)
        firebase_admin.initialize_app(cred)

def send_fcm_multicast_notification(title, body, tokens):
    try:
        # FCM 메시지 생성 및 전송 로직
        message = messaging.MulticastMessage(
            notification=messaging.Notification(
                title=title,
                body=body
            ),
            tokens=tokens  # 여러 개의 디바이스 토큰을 사용
        )
        
        response = messaging.send_multicast(message)
        print('Successfully sent message:', response)
    except messaging.ApiCallError as e:
        print(f&quot;API call error sending message: {e}&quot;)
    except Exception as e:
        print(f&quot;Error sending message: {e}&quot;)

def lambda_handler(event, context):
    try:
        # Firebase 초기화
        initialize_firebase()
        
        # SQS 메시지 처리
        tokens = []
        title = None
        message_body = None
        
        for record in event['Records']:
            # SQS 메시지 본문 가져오기
            body = json.loads(record['body'])
            title = body.get('title')
            message_body = body.get('body')
            token = body.get('token')
            tokens.append(token)

        if tokens:
            print(f&quot;Sending notification to tokens: {tokens}&quot;)
            send_fcm_multicast_notification(title, message_body, tokens)
        
        return {
            'statusCode': 200,
            'body': json.dumps('Messages sent successfully')
        }
    except Exception as e:
        print(f&quot;Error in lambda_handler: {e}&quot;)
        return {
            'statusCode': 500,
            'body': json.dumps(f&quot;Error: {str(e)}&quot;)
        }

&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리팩토링 된 코드를 확인하려면 다음 Github를 확인해주세요!&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;전체 코드 링크&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/depromeet/WalWal-server&quot;&gt;GitHub - depromeet/WalWal-server: 세상 모든 반려동물을 한 자리에서! 왈왈  백엔드&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;PR 링크&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/depromeet/WalWal-server/pull/238&quot;&gt;FCM 알림 서비스 SQS + Lambda 리팩토링 by dbscks97 &amp;middot; Pull Request #238 &amp;middot; depromeet/WalWal-server&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리팩토링 하면서 삭제된 코드와 이유&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;삭제 된 내용&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FcmService: 해당 클래스는 Spring 애플리케이션에서 FCM 푸시 알림을 전송하기 위해 사용되었습니다. 이 서비스는 multiSendMessage와 같은 메서드를 통해 여러 개의 FCM 푸시 알림을 배치로 전송하는 로직을 포함하고 있었습니다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 FCM 푸시 알림은 SQS(Lambda)를 통해 전송되도록 변경되었습니다. 따라서 Spring 애플리케이션에서 직접 FCM 메시지를 전송하는 기능이 더 이상 필요하지 않습니다. FcmService와 관련된 모든 로직은 Lambda 함수로 대체되었으므로, 이 클래스는 불필요하게 되어 졌습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;삭제 된 내용&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FcmConfig : 해당 클래스는 Spring 애플리케이션에서 FCM을 사용하기 위한 설정 파일로, Firebase Admin SDK를 초기화하여 FCM 푸시 알림을 전송할 수 있는 FirebaseMessaging 빈을 설정하는 역할을 했습니다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring 애플리케이션이 더 이상 FCM 푸시 알림을 직접 관리하지 않기 때문에 이 설정 파일은 필요 없게 되었습니다. FCM 푸시 알림 전송이 Lambda에서 처리되므로, Spring 애플리케이션 내에서 Firebase Admin SDK를 초기화하거나 사용하는 작업이 없어졌습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <author>yunchan^.^</author>
      <guid isPermaLink="true">https://yunchan97.tistory.com/100</guid>
      <comments>https://yunchan97.tistory.com/100#entry100comment</comments>
      <pubDate>Mon, 2 Sep 2024 21:10:30 +0900</pubDate>
    </item>
    <item>
      <title>정적 팩토리 메서드 패턴의 중요성</title>
      <link>https://yunchan97.tistory.com/99</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정적 팩토리 메서드&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #373e48; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;패턴은 개발자가 구성한 Static Method를 통해 간접적으로 생성자를 호출하는 객체를 생성하는 디자인 패턴이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #373e48; text-align: left;&quot;&gt;&lt;span&gt;우리는 지금까지 객체를 인스턴스화 할때 직접적으로 생성자(Constructor)를 호출하여 생성하였는데, 별도의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;객체 생성의 역할을 하는 클래스 메서드&lt;/b&gt;를 통해 간접적으로 객체 생성을 유도하는 것이다. 이 패턴은 다양한 이점이 있어 많이 사용되는데, 그 특징과 장점은 다음과 같다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #373e48; text-align: left;&quot;&gt;&lt;span&gt;특징&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;정적 메서드 사용&lt;/b&gt;: 객체 생성을 위해 정적 메서드를 사용합니다. 이 메서드는 일반적으로 new 키워드를 사용하여 객체를 생성하는 생성자와 달리 클래스 자체의 메서드로 호출됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명명 가능&lt;/b&gt;: 메서드 이름을 통해 객체 생성의 의도를 명확하게 할 수 있습니다. 예를 들어, from 또는 of 같은 이름을 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연한 객체 반환&lt;/b&gt;: 항상 새로운 객체를 반환하는 것이 아니라, 기존에 생성된 객체를 반환하거나, 서브클래스의 객체를 반환할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;이름을 통한 가독성 향상&lt;/b&gt;: 메서드 이름을 통해 객체 생성의 목적을 명확히 할 수 있어 코드의 가독성이 향상됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐싱&lt;/b&gt;: 동일한 객체가 자주 요청되는 경우, 생성된 객체를 캐싱하여 반환할 수 있어 성능을 향상시킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다양한 반환 타입&lt;/b&gt;: 반환 타입을 유연하게 정의할 수 있어 필요에 따라 다양한 서브클래스의 객체를 반환할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;객체 생성 로직 캡슐화&lt;/b&gt;: 객체 생성 로직을 메서드 내부에 캡슐화하여 코드의 응집도를 높이고, 생성 로직 변경 시 클라이언트 코드에 영향을 최소화할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 특징과 장점들을 사용해 멀쩡한 생성자를 냅두고 번거롭게 한단계 거쳐 정적 팩토리 메서드를 통해 객체를 생성하는지에 대한 실용성에 대해 예시를 들어 나타내겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 명명 가능성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자는 클래스 이름과 동일하게 정해져 있어서 어떤 객체를 생성하는지 명확히 드러내기 어렵습니다. 반면, 정적 팩토리 메서드는 명명 가능하기 때문에 메서드 이름을 통해 어떤 타입의 객체를 생성하는지, 어떤 특징을 가지는 객체를 생성하는지 명확하게 나타낼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721207108846&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Car {
    private String brand;
    private String color;

    // private 생성자
    private Car(String brand, String color) {
        this.brand = brand;
        this.color = color;
    }

    // 정적 팩토리 메서드 (매개변수 하나는 from 네이밍)
    public static Car brandBlackFrom(String brand) {
        return new Car(brand, &quot;black&quot;);
    }

    // 정적 팩토리 메서드 (매개변수 여러개는 of 네이밍)
    public static Car brandColorOf(String brand, String color) {
        return new Car(brand, color);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &quot;of&quot;는 매개변수 여러개를 나타내고, &quot;from&quot;은 매개변수 한개를 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 캐싱을 통한 성능 향상&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 생성 비용이 크거나, 동일한 객체가 자주 요청되는 경우, 정적 팩토리 메서드를 사용하면 생성된 객체를 캐싱하여 재사용할 수 있습니다. 이는 성능을 향상시키고 메모리 사용을 최적화할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721207189447&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Boolean {
    private final boolean value;
    private static final Boolean TRUE = new Boolean(true);
    private static final Boolean FALSE = new Boolean(false);

    private Boolean(boolean value) {
        this.value = value;
    }

    public static Boolean valueOf(boolean value) {
        return value ? TRUE : FALSE;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Boolean.valueOf 메서드는 true나 false에 대해 이미 생성된 객체를 반환합니다. 이를 통해 불필요한 객체 생성을 피할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 반환 타입의 유연성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 팩토리 메서드는 반환 타입을 유연하게 할 수 있어, 실제 클래스의 하위 타입을 반환할 수 있습니다. 이는 인터페이스 기반 설계나 상속 구조를 사용할 때 매우 유용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721207226713&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Animal {
    void sound();
}

public class Dog implements Animal {
    public void sound() {
        System.out.println(&quot;Bark&quot;);
    }
}

public class Cat implements Animal {
    public void sound() {
        System.out.println(&quot;Meow&quot;);
    }
}

public class AnimalFactory {
    public static Animal createAnimal(String type) {
        if (type.equals(&quot;Dog&quot;)) {
            return new Dog();
        } else if (type.equals(&quot;Cat&quot;)) {
            return new Cat();
        }
        throw new IllegalArgumentException(&quot;Unknown animal type&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AnimalFactory.createAnimal 메서드는 입력에 따라 Dog나 Cat 객체를 반환할 수 있습니다. 이는 유연한 설계를 가능하게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 인스턴스화 제한&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 팩토리 메서드를 사용하면 생성자를 private으로 선언하여 클래스의 인스턴스화를 제한할 수 있습니다. 이는 불변 클래스(Immutable Class)를 만들거나 싱글턴 패턴(Singleton Pattern)을 구현할 때 유용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721207312807&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
        // private 생성자
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시에서 Singleton 클래스는 getInstance 메서드를 통해서만 인스턴스에 접근할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정적 팩토리 메서드 네이밍 규칙&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정적 팩토리 메서드 와 다른 정적 메서드와 역할을 구분짓기 위해 독자적인 네이밍 컨벤션&lt;span&gt;(Convention)&lt;/span&gt;이 존재한다. 단, 정적 팩토리 메서드에서의 네이밍은 단순히 선호도 의미를 넘어서 거의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;법칙 정도로 사용&lt;/b&gt;되는 것이니, 각 네이밍의 역할에 대해 알아두는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;개념을 아는 것만큼 중요&lt;/b&gt;하다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle; background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;from&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 하나의 매개 변수를 받아서 객체를 생성&lt;/li&gt;
&lt;li&gt;of&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 여러개의 매개 변수를 받아서 객체를 생성&lt;/li&gt;
&lt;li&gt;getInstance | instance&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음&lt;/li&gt;
&lt;li&gt;newInstance | create&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 항상 새로운 인스턴스를 생성&lt;/li&gt;
&lt;li&gt;get[OrderType]&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음&lt;/li&gt;
&lt;li&gt;new[OrderType]&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 항상 다른 타입의 새로운 인스턴스를 생성&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>static</category>
      <category>정적 팩토리 메서드</category>
      <author>yunchan^.^</author>
      <guid isPermaLink="true">https://yunchan97.tistory.com/99</guid>
      <comments>https://yunchan97.tistory.com/99#entry99comment</comments>
      <pubDate>Wed, 17 Jul 2024 18:12:32 +0900</pubDate>
    </item>
    <item>
      <title>Error creating bean with name 'jpaAuditingHandler' 에러 해결</title>
      <link>https://yunchan97.tistory.com/98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트 테스트관련 코드에서 테스트코드를 실행했더니 Error creating bean with name 'jpaAuditingHandler' 이와 같은 오류가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706794779276&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaAuditingHandler': Cannot resolve reference to bean 'jpaMappingContext' while setting constructor argument
	at app//org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:377)
	at app//org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:135)
	at app//org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:689)
	at app//org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:513)
	at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1334)
	at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1164)
	at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561)
	at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521)
	at app//org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
	at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at app//org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
	at app//org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975)
	at app//org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:960)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인이 무엇인지 확인해보니까&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;프로젝트 내에서 Spring 컨테이너를 활용하는 테스트를 진행할 때, 가장 기본이 되는 Application 클래스가 항상 로드가 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;2. &lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;Application 클래스에 &lt;/span&gt;@EnableJpaAuditing&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt; 어노테이션이 추가되어 있어, 모든 테스트가 JPA 관련 Bean들을 필요로 하는 상태가 되버린 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;3. &lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;통합 테스트의 경우, 전체 컨텍스트를 로드하고 JPA를 포함한 모든 Bean들을 주입받기 때문에 문제가 발생하지 않습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;4. &lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;그러나 &lt;/span&gt;@WebMvcTest&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;를 사용하는 단위 테스트는 JPA 관련 Bean을 전혀 로드하지 않기 때문에, 여기서 에러가 발생합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;이를 해결하기 위해서는 아래와같은 어노테이션을 달아주는 &lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;간단한 방법이 있지만 이 방법으로는 &lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;Bean을 목 객체로 대체하여 사용하는 방법이기때문에 다른방법으로 문제를 해결해보겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@MockBean(JpaMetamodelMappingContext.class)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 방법으로는 @Configuration 분리하는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706795144736&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 @EnableJpaAuditing 관련 클래스를 따로 빼서 작성해두면 단위테스트 때 마다@MockBean(JpaMetamodelMappingContext.class) 이런 어노테이션을 안달아도 해결이 된다 !&lt;/p&gt;</description>
      <category>Spring</category>
      <author>yunchan^.^</author>
      <guid isPermaLink="true">https://yunchan97.tistory.com/98</guid>
      <comments>https://yunchan97.tistory.com/98#entry98comment</comments>
      <pubDate>Thu, 1 Feb 2024 22:46:43 +0900</pubDate>
    </item>
    <item>
      <title>멀티모듈 서비스 레이어 분리</title>
      <link>https://yunchan97.tistory.com/97</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;멀티모듈의 패키지 구성요소에 따라서 서비스 레이어를 분리해서 &lt;span style=&quot;color: #374151; text-align: start;&quot;&gt; 서비스 계층의 책임을 재분배하는 리팩토링을 거칠려고한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;그 전에는 필요한 의존성만 가져와서 써야하는데 계층의 책임이 맞지 않게 가져다 사용해서 여러 문제가 발생했다..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;우선 CustomOAuth2UserService 부터 변경해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;기존 코드는&amp;nbsp;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1706783719233&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sellyourunhappiness.core.user.application;

import static sellyourunhappiness.core.user.domain.enums.SocialType.*;

import java.util.Collections;
import java.util.Map;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import sellyourunhappiness.core.security.CustomOAuth2User;
import sellyourunhappiness.core.user.domain.User;
import sellyourunhappiness.core.user.domain.enums.SocialType;
import sellyourunhappiness.core.user.infrastructure.UserRepository;

@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService&amp;lt;OAuth2UserRequest, OAuth2User&amp;gt; {
	private final UserRepository userRepository;

	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		OAuth2UserService&amp;lt;OAuth2UserRequest, OAuth2User&amp;gt; delegate = new DefaultOAuth2UserService();
		OAuth2User oAuth2User = delegate.loadUser(userRequest);

		String Accesstoken = userRequest.getAccessToken().getTokenValue();
		System.out.println(&quot;Accesstoken = &quot; + Accesstoken);
		String registrationId = userRequest.getClientRegistration().getRegistrationId();
		SocialType socialType = getSocialType(registrationId);
		String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
		OAuthAttributes extractAttributes = OAuthAttributes.of(socialType, userNameAttributeName, oAuth2User.getAttributes());
		Map&amp;lt;String, Object&amp;gt; attributes = oAuth2User.getAttributes();

		User createdUser = getUser(extractAttributes, socialType);

		return new CustomOAuth2User(
			Collections.singleton(new SimpleGrantedAuthority(createdUser.getRole().getKey())),
			attributes,
			extractAttributes.getNameAttributeKey(),
			createdUser.getEmail(),
			createdUser.getRole()
		);
	}

	private SocialType getSocialType(String registrationId) {
		if (&quot;google&quot;.equals(registrationId)) {
			return GOOGLE;
		}
		throw new IllegalArgumentException(&quot;Unknown SocialType: &quot; + registrationId);
	}


	private User getUser(OAuthAttributes attributes, SocialType socialType) {
		User findUser = userRepository.findBySocialTypeAndEmail(socialType,
			attributes.getOauth2UserInfo().getEmail()).orElse(null);

		if(findUser == null) {
			return saveUser(attributes, socialType);
		}
		return findUser;
	}

	private User saveUser(OAuthAttributes attributes, SocialType socialType) {
		User createdUser = attributes.toEntity(socialType, attributes.getOauth2UserInfo());
		return userRepository.save(createdUser);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;위와 같았는데 해당 코드에서는 loadUser만 사용하므로 UserService와 CustomOAuth2UserService로 분리해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706783782289&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sellyourunhappiness.global.dto;

import static sellyourunhappiness.core.user.domain.enums.SocialType.*;

import java.util.Collections;
import java.util.Map;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import sellyourunhappiness.api.user.application.UserBroker;
import sellyourunhappiness.core.security.CustomOAuth2User;
import sellyourunhappiness.core.user.application.OAuthAttributes;
import sellyourunhappiness.core.user.domain.User;
import sellyourunhappiness.core.user.domain.enums.SocialType;

@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService&amp;lt;OAuth2UserRequest, OAuth2User&amp;gt; {

	private final UserBroker userBroker;

	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		OAuth2UserService&amp;lt;OAuth2UserRequest, OAuth2User&amp;gt; delegate = new DefaultOAuth2UserService();
		OAuth2User oAuth2User = delegate.loadUser(userRequest);

		String registrationId = userRequest.getClientRegistration().getRegistrationId();
		SocialType socialType = getSocialType(registrationId);
		String userNameAttributeName = userRequest.getClientRegistration()
			.getProviderDetails()
			.getUserInfoEndpoint()
			.getUserNameAttributeName();
		OAuthAttributes extractAttributes = OAuthAttributes.of(socialType, userNameAttributeName,
			oAuth2User.getAttributes());
		Map&amp;lt;String, Object&amp;gt; attributes = oAuth2User.getAttributes();

		User createdUser = userBroker.getUser(extractAttributes, socialType);

		return new CustomOAuth2User(
			Collections.singleton(new SimpleGrantedAuthority(createdUser.getRole().getKey())),
			attributes,
			extractAttributes.getNameAttributeKey(),
			createdUser.getEmail(),
			createdUser.getRole()
		);
	}

	private SocialType getSocialType(String registrationId) {
		if (&quot;google&quot;.equals(registrationId)) {
			return GOOGLE;
		}
		throw new IllegalArgumentException(&quot;SocialType이 일치하지 않습니다.: &quot; + registrationId);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;이건 CustomOAuth2UserService이다. 원래는 하위모듈에서 사용되었지만 로그인과정에서 사용되는 서비스이므로 상위 모듈로 필요한 코드들만 가지고 옮겼다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706783830226&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sellyourunhappiness.core.user.application;

import java.util.Optional;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import sellyourunhappiness.core.user.domain.User;
import sellyourunhappiness.core.user.domain.enums.SocialType;
import sellyourunhappiness.core.user.infrastructure.UserRepository;

@Service
@RequiredArgsConstructor
public class UserService {
	private final UserRepository userRepository;

	public Optional&amp;lt;User&amp;gt; findByEmail(String email) {
		return userRepository.findByEmail(email);
	}

	public User saveUser(OAuthAttributes attributes, SocialType socialType) {
		User createdUser = attributes.toEntity(socialType, attributes.getOauth2UserInfo());
		return userRepository.save(createdUser);
	}

	public Optional&amp;lt;User&amp;gt; findBySocialTypeAndEmail(SocialType socialType, String email) {
		return userRepository.findBySocialTypeAndEmail(socialType, email);
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;이건 UserService이다. 기존에는 계층이 안나눠져있어서 다른 코드들의 의존성을 받아 사용했지만 변경 후에는 UserRepository에만 의존하게 수정했다. UserController에서 사용하기전에 UserBroker를 추가해 책임을 더 분리했다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1706783902035&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sellyourunhappiness.api.user.application;

import java.util.Map;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import sellyourunhappiness.core.user.application.JwtService;
import sellyourunhappiness.core.user.application.OAuthAttributes;
import sellyourunhappiness.core.user.application.UserService;
import sellyourunhappiness.core.user.domain.User;
import sellyourunhappiness.core.user.domain.enums.SocialType;

@RequiredArgsConstructor
@Service
public class UserBroker {
	private final UserService userService;
	private final JwtService jwtService;

	public User getUserByEmail(String email) {
		return userService.findByEmail(email)
			.orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;유저를 찾을 수 없습니다.: &quot; + email));
	}

	public Map&amp;lt;String, String&amp;gt; refreshAccessToken(String refreshToken) {
		return jwtService.refreshAccessToken(refreshToken);
	}

	public User getUser(OAuthAttributes attributes, SocialType socialType) {
		User findUser = userService.findBySocialTypeAndEmail(socialType, attributes.getOauth2UserInfo().getEmail())
			.orElseGet(() -&amp;gt; userService.saveUser(attributes, socialType));
		return findUser;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;UserBroker는 UserService만 가져와서 사용하기때문에 각 서비스마다 책임이 분리되어서 사용되는 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 UserService와 관련된 코드들을 분리해서 나눠봤습니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <author>yunchan^.^</author>
      <guid isPermaLink="true">https://yunchan97.tistory.com/97</guid>
      <comments>https://yunchan97.tistory.com/97#entry97comment</comments>
      <pubDate>Thu, 1 Feb 2024 19:39:06 +0900</pubDate>
    </item>
    <item>
      <title>ApplicationTests &amp;gt; contextLoads() FAILED 에러 해결</title>
      <link>https://yunchan97.tistory.com/96</link>
      <description>&lt;pre id=&quot;code_1706763055203&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SellyourunhappinessApiApplicationTests &amp;gt; contextLoads() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
        Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:802
            Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:802
                Caused by: org.springframework.beans.factory.BeanCreationException at AutowiredAnnotationBeanPostProcessor.java:514
                    Caused by: java.lang.IllegalArgumentException at PropertyPlaceholderHelper.java:180

OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

&amp;gt; Task :sellyourunhappiness-api:test FAILED&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 테스트를 작성하고 전체 프로젝트 빌드를 했더니 위와 같은 에러가 발생했다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 원인이 문제인지 확인해보려고하는데 찾아봐야 할 부분이 너무 많아서 계속 삽질 중이다,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;가장 일반적인 이유 중 하나는 프로퍼티 설정과 관련된 문제라고합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PropertyPlaceholderHelper.java:180&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;를 보면 프로퍼티 관련 문제일 가능성이 있습니다. 이 경우, Spring의 프로퍼티 설정이나 &lt;/span&gt;@Value&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt; 어노테이션을 사용하여 프로퍼티 값을 주입하려고 할 때, 해당 프로퍼티가 정확하게 설정되지 않았거나 읽을 수 없는 경우에 발생할&lt;/span&gt; 것이라해서 좀 더 자세한 에러로그를 확인하기 위해&amp;nbsp;./gradlew test -i로 스택을 찍어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706762987148&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Caused by:
        org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityConfig' defined in file [/Users/bag-yunchan/Desktop/sell-your-unhappiness-back/sellyourunhappiness-api/build/classes/java/main/sellyourunhappiness/global/config/SecurityConfig.class]: Unsatisfied dependency expressed through constructor parameter 1: Error creating bean with name 'customOAuth2LoginSuccessHandler' defined in URL [jar:file:/Users/bag-yunchan/Desktop/sell-your-unhappiness-back/sellyourunhappiness-core/build/libs/sellyourunhappiness-core-0.0.1-SNAPSHOT-plain.jar!/sellyourunhappiness/core/security/handler/CustomOAuth2LoginSuccessHandler.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtService': Injection of autowired dependencies failed
            at app//org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:802)
            at app//org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:241)
            at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1354)
            at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1191)
            at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561)
            at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521)
            at app//org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
            at app//org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
            at app//org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
            at app//org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
            at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975)
            at app//org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:960)
            at app//org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:625)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 찍어보니까 securityConfig 관련 빈 생성 문제와 CustomOAuth2LoginSuccessHandler, jwtService 의존성 및 프로퍼티설정이 잘못됐다고 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706767379459&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityConfig' defined in file
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'customOAuth2LoginSuccessHandler' defined in URL
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtService': Injection of autowired dependencies failed
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'jwt.token.key' in value &quot;${jwt.token.key}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마도 @WebMvcTest를 사용해서 UserController에서 필요한 의존성들을 주입하면서 관련 에러들이 발생한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 우리 프로젝트는 멀티모듈을 사용하고있기때문에 상위 모듈의 application-api.yml 과 하위모듈의 application-core.yml이 둘다 잘 로드 되는지 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 두개의 yml 을 로드해야하기때문에 SpringApplication에 다음과 같이 설정해줬습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1706767422210&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableJpaAuditing
@SpringBootApplication
public class SellyourunhappinessApiApplication {
    public static void main(String[] args) {
        System.setProperty(&quot;spring.config.name&quot;,&quot;application-api,application-core&quot;);
        SpringApplication.run(SellyourunhappinessApiApplication.class, args);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버거를 통해 로그를 찍어보니까 프로퍼티들은 둘 다 잘 로드되고있는 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706767432289&quot; class=&quot;roboconf&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Properties: {PATH=/opt/homebrew/opt/openjdk@17/bin:/usr/local/opt/openjdk@17/bin:/usr/local/opt/openjdk@17/bin:/opt/local/bin:/opt/local/sbin:/opt/homebrew/bin:/opt/homebrew/sbin:/Library/Frameworks/Python.framework/Versions/3.11/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin:/usr/local/mysql/bin, MANPATH=/opt/local/share/man:/opt/homebrew/share/man:/opt/homebrew/share/man:::, HOMEBREW_PREFIX=/opt/homebrew, COMMAND_MODE=unix2003, LOGNAME=bag-yunchan, HOMEBREW_REPOSITORY=/opt/homebrew, PWD=/Users/bag-yunchan/Desktop/sell-your-unhappiness-back, XPC_SERVICE_NAME=application.com.jetbrains.intellij.4745075.4745799, INFOPATH=/opt/homebrew/share/info:/opt/homebrew/share/info:, __CFBundleIdentifier=com.jetbrains.intellij, SHELL=/bin/zsh, PAGER=less, LSCOLORS=Gxfxcxdxbxegedabagacad, HOMEBREW_CELLAR=/opt/homebrew/Cellar, OLDPWD=/, USER=bag-yunchan, ZSH=/Users/bag-yunchan/.oh-my-zsh, TMPDIR=/var/folders/7r/62c0y0q9671_t6f8040167lr0000gn/T/, SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.GWsYzy9b7Z/Listeners, XPC_FLAGS=0x0, __CF_USER_TEXT_ENCODING=0x1F5:0x3:0x33, LESS=-R, LC_CTYPE=ko_KR.UTF-8, LS_COLORS=di=1;36:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43, HOME=/Users/bag-yunchan}
Property Source: random
Properties: java.util.Random@54d0d561
Property Source: Config resource 'class path resource [application-core.yml]' via location 'optional:classpath:/'
Properties: {spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver, spring.datasource.url=jdbc:mysql://database-test.cttv6gkl7biw.ap-northeast-2.rds.amazonaws.com:3306/test?serverTimezone=Asia/Seoul&amp;amp;characterEncoding=UTF-8, spring.datasource.username=admin, spring.datasource.password=qwer1234, spring.jpa.hibernate.ddl-auto=create, spring.jpa.properties.hibernate.show_sql=true, spring.jpa.properties.hibernate.format_sql=true, spring.jpa.properties.hibernate.default_batch_fetch_size=100, spring.jpa.open-in-view=false, spring.profiles.active=local, logging.level.org.hibernate.sql=trace, jwt.secret=vbZrJPe/dxRnrun2HoO16XFQEHwF5r72e6HBoAF4+u43V9fwua9Ktu5xULvwsdQTuo70xGOhljXv1LqNt1ZoCg==, jwt.access.expiration=360000, jwt.access.header=Authorization, jwt.refresh.expiration=1209600000, jwt.refresh.header=Authorization-refresh}
Property Source: Config resource 'class path resource [application-api.yml]' via location 'optional:classpath:/'
Properties: {spring.h2.console.enabled=true, spring.h2.console.path=/h2-console, spring.security.oauth2.client.registration.google.client-id=330207848617-5q422g8mp74jdfavrlfon95cv488thuf.apps.googleusercontent.com, spring.security.oauth2.client.registration.google.client-secret=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, spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google, spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code, spring.security.oauth2.client.registration.google.scope[0]=profile, spring.security.oauth2.client.registration.google.scope[1]=email, sellyourunhappiness.slack.token=T06C30DKWR1/B06EHSJA9GV/EmcTfUsZbizVV5hvtcZ92IzT}
Disconnected from the target VM, address: '127.0.0.1:60780', transport: 'socket'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 도대체 application-core.yml에 설정되있는 jwt.secret 을 @Value에서 못받아갈까? 애플리케이션을 실행할땐 문제가없는데 빌드시에만 문제가 발생하고있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 해결책들을 찾아보니까&amp;nbsp; build.gradle에서 아래와 같은 부분을 지우는 방법은 근본적인 해결책을 찾는 방법들이 아니기 때문에 근본적인 원인을 고쳐야될 것 같다..&lt;/p&gt;
&lt;pre id=&quot;code_1706767483995&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tasks.named('test') {
    useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1706767530783&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void contextLoads() {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드에서 해당 빈들을 의존성 주입을 해주면서 해결이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사용하지 않는 빈들을 의존성 주입해서 적용하는 방법은 좋지 않은 방법이니까 해당 빈들을 주입안해도되게 코드를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩토링해보자..&lt;/p&gt;</description>
      <category>Spring</category>
      <author>yunchan^.^</author>
      <guid isPermaLink="true">https://yunchan97.tistory.com/96</guid>
      <comments>https://yunchan97.tistory.com/96#entry96comment</comments>
      <pubDate>Thu, 1 Feb 2024 18:48:08 +0900</pubDate>
    </item>
    <item>
      <title>[TIL]23.11.12(일)</title>
      <link>https://yunchan97.tistory.com/95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;벌써 크래프톤 정글을 수료한 지 3개월이 지났다.. 시간이 너무 빠르다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9월부터 약 2개월정도 코딩 테스트 준비하고 자기소개서 쓰고 프로젝트하느라 블로그 글 쓸 시간이 없었네요..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 취업준비ing이긴하지만 좀 더 부족한 포트폴리오, 경험 등을 채우기 위해 프로젝트를 진행하고 있고 IT동아리도 지원해보고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나태하게 아무것도 안하기엔 시간이 너무 빠르게 지나가서 뭐라도 하면서 보람 있게 지내봐야지.. 원래 목표는 이번 하반기 취업이 목표였지만 그렇게 쉽지만은 않기에 빠르면 좋지만 내년 상반기에는 취업한다는 생각으로 열심히 살아야지..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>일상</category>
      <author>yunchan^.^</author>
      <guid isPermaLink="true">https://yunchan97.tistory.com/95</guid>
      <comments>https://yunchan97.tistory.com/95#entry95comment</comments>
      <pubDate>Sun, 12 Nov 2023 16:54:21 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] N으로 표현 [Python]</title>
      <link>https://yunchan97.tistory.com/94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;994&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQ3tk7/btstMBxBCkR/HrD9ki9DVlzucSOeKRJNm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQ3tk7/btstMBxBCkR/HrD9ki9DVlzucSOeKRJNm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQ3tk7/btstMBxBCkR/HrD9ki9DVlzucSOeKRJNm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQ3tk7%2FbtstMBxBCkR%2FHrD9ki9DVlzucSOeKRJNm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;627&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;994&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lX31A/btstHaALkPx/O58mgrOVk5PTlLWNGcTKM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lX31A/btstHaALkPx/O58mgrOVk5PTlLWNGcTKM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lX31A/btstHaALkPx/O58mgrOVk5PTlLWNGcTKM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlX31A%2FbtstHaALkPx%2FO58mgrOVk5PTlLWNGcTKM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;440&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1694502306651&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def solution(N, number):
    if N == number:
        return 1
    
    answer = -1
    arr = [set() for _ in range(8)]
    
    for i in range(len(arr)):
        arr[i].add(int(str(N)*(i+1)))
    
    for i in range(1,8):
        for j in range(i):
            for op1 in arr[j]:
                for op2 in arr[i-j-1]:
                    arr[i].add(op1+op2)
                    arr[i].add(op1-op2)
                    arr[i].add(op1*op2)
                    if op2 != 0:
                        arr[i].add(op1//op2)
        if number in arr[i]:
            answer = i+1
            break
    
    return answer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;풀이방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 동적 계획법을 사용하는 문제로 주어진 N을 가지고 사칙 연산만 사용하여 number를 만들 수 있는 최소 횟수를 구하는 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 이 문제를 봤을 때 어떻게 동적계획법으로 문제를 풀어나가야 하는지 몰라서 헤매다가 유튜브 풀이법을 참고하여 풀이를 작성했습니다. 동적 계획법을 적응하려면 많은 문제를 풀어야 할 것 같네요..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 set() 자료구조를 사용해서 8개를 만들어 줍니다. 8개를 만드는 이유는 8개보다 크면 -1을 return 하는 조건 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;set()을 만들고 set 자료구조에 주어진 N을 i+1갯수만큼 값을 넣어줍니다. 그러면 다음과 같이 set()에 값이 들어가게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-12 오후 4.09.43.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpbq9Y/btstSA5nxPY/ohzktz1GA9BhuLpAflO3Q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpbq9Y/btstSA5nxPY/ohzktz1GA9BhuLpAflO3Q0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpbq9Y/btstSA5nxPY/ohzktz1GA9BhuLpAflO3Q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpbq9Y%2FbtstSA5nxPY%2Fohzktz1GA9BhuLpAflO3Q0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1196&quot; height=&quot;56&quot; data-filename=&quot;스크린샷 2023-09-12 오후 4.09.43.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;56&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러고 나서 op1과 op2를 반복문을 통해서 사칙연산을 반복하게 되는데 이 과정이 동적계획법의 과정이라고 생각하면 될 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;i가 0부터 증가하면서 set자료구조 안에 경우의 수를 구해서 저장해 두고 i값이 커질수록 op1, op2를 불러와서 연산만 하는 것이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적계획법을 사용되었습니다. 그리고 만약 내가 원하는 결과 값 Number가 현재 구한 arr [i]에 존재하게 된다면 그 횟수가 최소 횟수가 되기 때문에 break를 통해서 값을 return 하면 원하는 결과를 얻을 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 어떻게 해야할지 접근방법에 대해서 고민을 많이 했지만 동적계획법을 사용하는 문제를 많이 풀어보지 못해서 부족하다는 것을 느끼고 유튜브를 보고 참고하여 접근하는 방법에 대해서 알 수 있게 되었습니다. 아래는 제가 참고한 유튜브 링크입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;#Reference&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=ZsVVTEfZee8&quot;&gt;https://www.youtube.com/watch?v=ZsVVTEfZee8&lt;/a&gt;&lt;/p&gt;</description>
      <category>알고리즘/프로그래머스</category>
      <category>dp</category>
      <category>N으로 표현</category>
      <category>동적계획법</category>
      <category>파이썬</category>
      <category>프로그래머스</category>
      <author>yunchan^.^</author>
      <guid isPermaLink="true">https://yunchan97.tistory.com/94</guid>
      <comments>https://yunchan97.tistory.com/94#entry94comment</comments>
      <pubDate>Tue, 12 Sep 2023 16:15:21 +0900</pubDate>
    </item>
    <item>
      <title>[TIL]23.09.08(금) - 구름톤 챌린지 마무리</title>
      <link>https://yunchan97.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;8월 14일부터 시작된 구름톤 1일 1 알고리즘 챌린지가 오늘로 마무리를 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하루에 한 문제라서 보기에는 쉬워 보이지만 꾸준히 해야 하는 점에서 은근 힘이 든다는 점.. 그리고 뒤로 갈수록 난이도가 조금씩 상승해서 푸는데 시간이 쫌 걸렸네요. 그래도 블록을 쌓아가면서 성취감도 느끼고 알고리즘 실력도 성장하는 것 같아서 재미가 있었어요. 블로그에 구름톤 챌린지 관련 문제를 작성하면 추후에 네이버페이 포인트도 줘서 일석이조입니다. 물론 나는 1주 차 때 몰라서 작성을 못썼지만.. ㅎㅎ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크래프톤 정글을 수료하고 벌써 3주차가 넘어가네요 시간이 참 빠르지만 9월에 공채도 많고 협력사 관련 채용프로세스도 있어서 할 게 너무 많아서 9월도 금방 지나갈 것 같다. 그래도 빨리 취업하기 위해 이것저것 도움이 되는 것들을 찾아보면서 하다 보면 좋은 일이 생기지 않을까라는 생각을 갖고 열심히 시간을 보내봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 구름톤 챌린지에서 18일이상 블록을 모은 사람들에게 오프라인 대회에 참가 우선권을 준다고 해서 열심히 푼다고 했는데 과연 오프라인 대회에도 참가할 수 있을지는 모르겠네요.. 알고리즘 대회에 나가봐서 다른 사람들과 경쟁을 해보면 좋은 기회가 될 것 같습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-08 오후 3.16.20.png&quot; data-origin-width=&quot;2826&quot; data-origin-height=&quot;1244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdQqBj/btsts0EdvXP/uw2qPK1ZtUT4bmkwBPCYK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdQqBj/btsts0EdvXP/uw2qPK1ZtUT4bmkwBPCYK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdQqBj/btsts0EdvXP/uw2qPK1ZtUT4bmkwBPCYK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdQqBj%2Fbtsts0EdvXP%2Fuw2qPK1ZtUT4bmkwBPCYK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;352&quot; data-filename=&quot;스크린샷 2023-09-08 오후 3.16.20.png&quot; data-origin-width=&quot;2826&quot; data-origin-height=&quot;1244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>TIL</category>
      <author>yunchan^.^</author>
      <guid isPermaLink="true">https://yunchan97.tistory.com/93</guid>
      <comments>https://yunchan97.tistory.com/93#entry93comment</comments>
      <pubDate>Fri, 8 Sep 2023 15:23:09 +0900</pubDate>
    </item>
    <item>
      <title>구름톤 챌린지 4주 차 학습 일기(Day 02)</title>
      <link>https://yunchan97.tistory.com/92</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;플레이어는 1번부터 N번까지의 번호가 붙은 N개의 도시와 M개의 도로가 있는 나라에 살고 있다. 각 도로는 서로 다른 두 도시를 양방향으로 연결하고 있고, 주어진 도로만을 이용해 임의의 두 도시 사이를 이동하는 것이 가능하다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;플레이어는 차를 타고 S번 도시에서 E번 도시로 이동하려고 한다. 플레이어가 두 도시 사이를 이동할 때는 항상 가장 작은 수의 도시를 거치는 경로를 따라 이동한다. 예를 들어 아래 그림과 같이 도시와 도로가 주어지고, 플레이어가 1번 도시에서 4번 도시로 이동하려고 할 때는 항상 1 &amp;rarr; 3 &amp;rarr; 4의 경로를 따라 이동한다. 이 경우에는 출발 도시와 도착 도시를 포함해 총 세 개의 도시를 거쳐 이동할 수 있다. 1 &amp;rarr; 5 &amp;rarr; 2 &amp;rarr; 4의 경로로 이동하는 것은 출발 도시와 도착 도시를 포함해 네 개의 도시를 거치는 경로이므로, 플레이어는 해당 경로로는 이동하지 않을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mrGLp/btstfeD25lR/xe7mmwTtfJz8kOIjbkdNik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mrGLp/btstfeD25lR/xe7mmwTtfJz8kOIjbkdNik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mrGLp/btstfeD25lR/xe7mmwTtfJz8kOIjbkdNik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmrGLp%2FbtstfeD25lR%2Fxe7mmwTtfJz8kOIjbkdNik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;336&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;항상 가장 작은 수의 도시를 거치는 경로가 유일하지 않을 수 있다. 아래 그림과 같이 도시와 도로가 주어지고, 3번 도시에서 1번 도시로 이동하고자 할 때 가장 작은 수의 도시를 거치는 경로는 3 &amp;rarr; 2 &amp;rarr; 1과 3 &amp;rarr; 4 &amp;rarr; 1의 두 개가 있다. 이런 경우에 플레이어는 두 경로 중 아무 경로나 택해서 이동한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5IxJQ/btstfbUQTpA/WISZaHiNyuX1yrzo1ORjm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5IxJQ/btstfbUQTpA/WISZaHiNyuX1yrzo1ORjm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5IxJQ/btstfbUQTpA/WISZaHiNyuX1yrzo1ORjm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5IxJQ%2FbtstfbUQTpA%2FWISZaHiNyuX1yrzo1ORjm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;438&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;플레이어가 사는 나라에서는 자주 공사를 한다. 일 뒤에는 번 도시에서 하루 동안 공사를 할 예정이다. 어떤 도시에서 공사를 하고 있으면, 그 도시에 연결된 모든 도로를 일시적으로 사용할 수 없다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;어떤 도시에서 공사를 하느냐에 따라 플레이어가 이동해야 하는 경로가 달라질 수 있다. 앞으로 N일 동안 매일 플레이어는 S번 도시에서 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;E&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;번 도시로 이동한다고 할 때, 각 날마다 플레이어가 이동하는 경로에 포함되는 도시의 수를 구해서 출력해 보자. 단, 출발 도시와 도착 도시에서 공사를 하거나, 두 도시 사이를 이동할 수 없는 경우에는 -1을 대신 출력한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;코드&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1694067801791&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from sys import stdin as s
from collections import deque

def bfs(city, start, end, visited):
    global leng
    if city == start:
        return
        
    q = deque([[start]])
    
    while q:
        path = q.popleft()
        node = path[-1]
        
        if node == end:
            length = len(path)
            leng = min(leng, length)
        else:
            for to in arr[node]:
                if to == city or to in path or visited[to]:
                    continue
                new_path = list(path)
                new_path.append(to)
                q.append(new_path)
                visited[to] = True

    if leng == 10000:
        ans[city].append(-1)
    else:
        ans[city].append(leng)

if __name__ == '__main__':
    N, M, S, E = map(int, s.readline().split())
    arr = [[] for _ in range(N + 1)]
    ans = [[] for _ in range(N + 1)]

    for _ in range(1, M + 1):
        a, b = map(int, s.readline().split())
        arr[a].append(b)
        arr[b].append(a)

    for i in range(1, N + 1):
        if i==S or i==E:
                continue
        visited = [False] * (N + 1)  
        leng = 10000
        bfs(i, S, E, visited)
    
    for i in range(1, N + 1):
        if ans[i]:
            print(ans[i][0])
        else:
            print(-1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;느낀점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 이문제를 봤을 때 최단 경로를 구하는 문제여서 다익스트라인가?라는 생각이 들었다. 하지만 가중치나 비용 같은 걸 따지지 않고 단지 경로의 길이를 가지고 계산을 하는 문제이다 보니까 bfs로 풀어도 되겠다는 생각을 해서 bfs로 풀었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 문제의 핵심요소는 S번 도시부터 시작해서 E번 도시까지 방문했을 때 모든 경우의 수를 구하고 가장 경로가 짧은 것을 출력하는 것입니다. 제가 문제를 풀 때 막혔던 부분은 경로가 나눠질 때 어떻게 그걸 따로따로 가지고 있을까 라는 생각을 하는 것이 어려웠던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;new_path로 경로가 나눠질 때 그 케이스 별로 경우의 수를 구해준 다음에 길이를 비교하는 과정은 생각보다 쉬웠고 제출했을 때 시간초과와 런타임 에러가 발생했는데 이걸 해결하기 위해 처음에는 leng의 초기값을 inf로 잡아두고 방문표시를 체크 안 하고 모든 노드를 계속 탐색하니까 에러가 발생했다고 생각했습니다. 방문처리와 leng초기값을 적당한 10000으로 잡아두고 풀이하니 모든 케이스가 통과됐습니다.&lt;/p&gt;</description>
      <category>알고리즘/구름톤</category>
      <author>yunchan^.^</author>
      <guid isPermaLink="true">https://yunchan97.tistory.com/92</guid>
      <comments>https://yunchan97.tistory.com/92#entry92comment</comments>
      <pubDate>Thu, 7 Sep 2023 15:28:21 +0900</pubDate>
    </item>
    <item>
      <title>구름톤 챌린지 4주 차 학습 일기(Day 01)</title>
      <link>https://yunchan97.tistory.com/91</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;이 세상에는 수많은 컴퓨터들이 통신망을 통해 서로 연결되어 정보를 교류하고 있다. 오늘 플레이어는 이 거대한 통신망 중 한 구역을 조사하고자 한다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;플레이어가 조사할 구역에는 N개의 컴퓨터가 있고, M개의 통신 회선이 있다. 각 컴퓨터는 1번부터 N번까지 번호가 붙어 있고, 통신 회선은 서로 다른 두 컴퓨터를 양방향으로 연결하고 있다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;컴퓨터들은 연결 여부에 따라 여러 개의 컴포넌트로 나뉜다. 어떤 두 컴퓨터가 통신 회선만을 이용해서 연결되어 있다면 두 컴퓨터는 같은 컴포넌트에 속한다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;플레이어는 여러 개의 컴포넌트 중, 가장 밀도가 높은 컴포넌트를 조사하려고 한다. 컴포넌트의 밀도는 그 컴포넌트에 포함된 통신 회선의 개수를 컴퓨터의 수로 나눈 값이다. 주어진 통신 구역을 분석해서, 가장 밀도가 높은 컴포넌트의 정보를 출력해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;예제 설명&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;첫 번째 예제에서 주어진 통신 구역에는 두 개의 컴포넌트가 있다. 한 컴포넌트는 (1, 3, 5, 7)번 컴퓨터로 이루어져 있고, 다른 컴포넌트는 (2, 4, 6)번 컴포넌트로 이루어져 있다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;(1, 3, 5, 7)번 정점으로 구성된 컴포넌트에는 네 개의 통신 회선이 존재하므로, 이 컴포넌트의 밀도는 4/4 = 1이다. (2, 4, 6)번 컴퓨터로 구성된 컴포넌트에는 두 개의 통신 회선이 존재하므로, 이 컴포넌트의 밀도는 2/3이다. 먼저 주어진 컴포넌트의 밀도가 더 크므로, 1 3 5 7을 답으로 출력해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32; text-align: start;&quot;&gt;코드&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1693894730515&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from collections import deque
from sys import stdin as s

N,M = map(int,s.readline().split())
graph = [[] for _ in range(N+1)]
ans = [[] for _ in range(N+1)]
answer = []
v = [0]*(N+1)

for _ in range(M):
	a,b = map(int,s.readline().split())
	graph[a].append(b)
	graph[b].append(a)
	
for i in range(1,N+1):
	if v[i]:
		continue
	
	q = deque([i])
	v[i] = 1
	ans[i].append(i)
	result=0
	
	while q:
		now = q.popleft()
		
		for to in graph[now]:
			if now in graph[to]:
				result+=1
				if not v[to]: 
					ans[i].append(to)
					q.append(to)
					v[to]=1
	ans[i].append(result//2)
density = 0

for an in ans:
    if an:
        dens = an[-1] / (len(an)-1)
        if dens &amp;gt; density:
            answer = an[:(len(an)-1)]
            density = dens
print(*sorted(answer))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;느낀점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 이 문제를 봤을 때는 구름톤 16일차 문제와 비슷한 느낌의 BFS라는걸 느꼈습니다. 하지만 원래 처음에 짯던 코드에서는 조건문을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693894873875&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while q:
    now = q.popleft()

    for to in graph[now]:
        if not v[to] and now in graph[to]: 
        result+=1
        ans[i].append(to)
        q.append(to)
v[to]=1
	ans[i].append(result)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드로 작성을해서 예제 1번의 경우 이미 컴포넌트가 4개의 통신으로 이루어져야하는데 이미 방문처리가 되어있어서 갯수를 세는데 문제가 생겼습니다. 그래서 정답코드처럼 코드를 바꾼후에 밀도를 비교해서 높은 밀도를 가진 배열을 출력하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFS는 방문처리를 어떻게 하냐에따라서 문제가 달라지는걸 느꼈습니다.&lt;/p&gt;</description>
      <category>알고리즘/구름톤</category>
      <category>BFS</category>
      <category>구름톤 17일차</category>
      <category>그래프의 밀집도</category>
      <category>통신망 분석</category>
      <author>yunchan^.^</author>
      <guid isPermaLink="true">https://yunchan97.tistory.com/91</guid>
      <comments>https://yunchan97.tistory.com/91#entry91comment</comments>
      <pubDate>Tue, 5 Sep 2023 15:23:30 +0900</pubDate>
    </item>
  </channel>
</rss>