태그 : Event
2024 GTC 이벤트 실시간 랭킹: GraphQL Subscription 활용법
By 김수진래블업은 2024년 GTC 이벤트를 기념하여 특별한 이벤트를 개최했다. 참가자들은 래블업이 제공한 LLM 모델을 이용하여 주어진 이미지와 유사한 이미지를 생성했고, 높은 점수를 받은 참가자 중에서 추첨을 통해 무려 NVIDIA RTX 4090 그래픽 카드를 증정했다. 🫢
이번 포스트에서는 이벤트 페이지 중 참가자들의 점수를 실시간으로 확인할 수 있게 해주는 리더 보드 페이지에 사용된 GraphQL의 subscription 기능에 대해 알아보고자 한다.GTC24 이벤트 페이지
Subscription 이란?
클라이언트가 서버 측 이벤트 스트림으로부터 데이터를 구독하는 메커니즘이다.
데이터가 실시간으로 바뀌는 경우, 예를 들어 실시간 로그나 채팅 어플리케이션 등을 구현할 때, 서버에서 업데이트를 푸시해주면 바로 반영할 수 있다.
subscription은 필요한 정보가 서버에서 변경될 때만 데이터를 보내준다. 따라서 데이터 변경이 빈번하지 않은 경우, subscription은 데이터 트래픽을 줄이고, 이에 따른 비용 절감 효과도 있을 수 있다.
비슷한 개념으로 GraphQL 의
network-only
fetchPolicy 옵션을 주고 Query 를 요청해서 매번 최신 정보를 가져올수 있지만 subscription과 차이가 있다. Query 는 클라이언트가 데이터를 필요로 할 때마다 항상 서버에 요청하며 항상 최신 데이터를 보장하지만, 각 요청에 대한 네트워크 비용을 수반한다. 그래서 어떤 버튼을 클릭했을 때 항상 최신의 결과를 보여주도록 보장하기 위해 fetchPolic 를 network-only 로 설정하는 것은 괜찮지만, 주식 거래 창과 같이 업데이트가 빈번한 데이터를 가져오기 위해 query를 사용한다면 네트워크 비용이 상당해진다.결론적으로, 목표 응용 프로그램의 요구 사항, 사용자 수, 데이터의 업데이트 빈도 등에 따라 subscription 을 사용할지, query 를 사용할지 결정해야 한다.
사용 방법
subscription 정의하기
사용 방법은 query 와 유사한데, 키워드만
subscription
을 사용해주면 된다.const leaderboardSubscriptions = graphql` subscription Ranking_leaderboardSubscription { leaderboard { submissions { id name score imageUrl } lastUpdatedAt } } `;
leaderboard
스트림에서 이벤트가 발생할 때마다 애플리케이션에 알림이 전송되고, 클라이언트에서는 업데이트된 결과를 얻을 수 있다.그럼 다음과 같은 결과를 얻을 수 있다.
leaderboard: { submissions: [ { "id": "76293167-e369-4610-b7ac-4c0f6aa8f699", "name": "test", "score": 0.5910864472389221, "imageUrl": "<IMAGE_URL>" }, ], lastUpdatedAt: 1710176566.493705 }
subscribe
실시간 랭킹을 보여주기 위해 해당 페이지에 들어갈 때 subscribe 를 호출하고, 다른 페이지로 넘어갈 경우, dispose 를 호출하여 unsubscribe 하기 위해
useEffect
를 사용했다.import { useEffect } from 'react'; import { requestSubscription } from 'react-relay'; useEffect(() => { const subscriptionConfig = { subscription: leaderboardSubscriptions, variables: {}, onNext: (response: any) => { setLeaderboard(response.leaderboard.submissions); // 미리 정의된 state }, onError: (error: any) => { console.error('Leaderboard subscription error', error); }, }; const { dispose } = requestSubscription( RelayEnvironment, // 아래 '설정 방법' 참고 subscriptionConfig, ); return () => { dispose(); }; }, []); // 빈 의존성 배열을 통해 컴포넌트가 마운트되거나 언마운트될 때만 이 부분이 실행되도록 함
requestSubscription
- 메소드는 반환 값으로
Disposable
오브젝트를 제공한다. - 이
Disposable
오브젝트에는 구독을 취소하는 dispose 메서드가 포함되어 있다.
onNext
subscription
으로 데이터가 업데이트되면, 미리 정의해두었던 state 를 업데이트하여 실시간 랭킹을 보여주도록 하였다.onNext
,onError
외에도, subscription 이 끝날 때 호출되는onCompleted
, 서버 응답을 기반으로 메모리 내 릴레이 저장소를 업데이트를 위한updater
등 다양한 설정들이 있다. 자세한 설명은 이 링크를 참고하길 바란다.
dispose
useEffect
hook 내에서 반환하는 cleanup 함수를 통해 컴포넌트가 언마운트될 때 dispose 메소드를 호출하여 구독을 종료하게 된다.
설정 방법 (+Relay)
Relay document 에 따르면, GraphQL subscriptions 은 WebSockets 으로 통신하며, graphql-ws를 사용해서 network를 설정하는 방법은 다음과 같다. (subscriptions-transport-ws를 사용하는 방법도 있지만 deprecated 되었으니 패스하기로 한다.)
import { ExecutionResult, Sink, createClient } from 'graphql-ws'; import { Environment, Network, RecordSource, Store, SubscribeFunction, RelayFeatureFlags, FetchFunction, Observable, GraphQLResponse, } from 'relay-runtime'; import { RelayObservable } from 'relay-runtime/lib/network/RelayObservable'; import { createClient } from 'graphql-ws'; const wsClient = createClient({ url: GRAPHQL_SUBSCRIPTION_ENDPOINT, connectionParams: () => { return { mode: 'cors', credentials: 'include', }; }, }); const subscribeFn: SubscribeFunction = (operation, variables) => { return Observable.create((sink: Sink<ExecutionResult<GraphQLResponse>>) => { if (!operation.text) { return sink.error(new Error('Operation text cannot be empty')); } return wsClient.subscribe( { operationName: operation.name, query: operation.text, variables, }, sink, ); }) as RelayObservable<GraphQLResponse>; }; // Export a singleton instance of Relay Environment // configured with our network function: export const createRelayEnvironment = () => { return new Environment({ network: Network.create(fetchFn, subscribeFn), store: new Store(new RecordSource()), }); }; export const RelayEnvironment = createRelayEnvironment();
wsClient
- url 에는 GraphQL 서버의 웹소켓 URL 을 입력한다.
- credentials 설정은
connectionParams
를 통해 가능하다.
subscribeFn
- Observable의 구독 동작을 정의한다.
if (!operation.text) { ... }
에서 쿼리 문자열의 유효성을 확인하여 유효하지 않은 경우, 오류를 발생시키고 실행을 중단한다.- 마지막으로
return wsClient.subscribe( ... )
코드는 웹소켓 클라이언트를 사용하여 실제로 subscription을 구독하고, GraphQL operation의 payload를 sink (즉, Observer) 에게 전달한다. - 간단히 말해, 이 함수는 GraphQL subscription 요청을 처리하고, subscription 이벤트가 발생할 때마다 해당 결과를 Observable 스트림에 push하는 역할을 한다고 볼 수 있다.
createRelayEnvironment
- 새로운 Relay Environment 를 생성하고 반환한다.
- Relay의 Environment는 다른 고수준 Relay 객체들과 네트워크 계층, 캐시등을 관리하는 컨테이너이다.
- GraphQL query/mutation 요청을 처리하는 함수를
fetchFn
, subscription 요청을 처리하는 함수를subscribeFn
에 할당한 상태이다. - 캐시 데이터를 저장하고 관리하는 Relay Store를 생성하기 위해
RecordSource
저장소를 사용했다.
RelayEnvironment
createRelayEnvironment
함수를 호출함으로써 RelayEnvironment 를 초기화하고, 이를 추후 다른 곳에서 임포트해 사용할 수 있게 내보내는 역할을 한다.- 이렇게 구성된
RelayEnvironment
는 주로QueryRenderer
,useLazyLoadQuery
,commitMutation
등에서 사용된다.
CORS 에러
처음에 GraphQL 서버의 웹소켓 URL을 설정하기 위해 서버측에서 사용하는
config.toml
파일을 읽어와서 주소를 설정했다. 그런데 자꾸 CORS 에러가 나면서 요청 보낼 때마다 Unauthorized 가 뜨는 것이다. 그래서 이것저것 삽질을 한 결과, 동료분의 도움으로 해결할 수 있었다. (정말 감사합니다 🥹🙏)해결 방법은 바로
http-proxy-middleware
를 사용해setupProxy
를 설정하는 것!create-react-app manual에서도 알 수 있듯이, 일반적으로 프론트엔드와 백엔드가 분리된 개발 환경에서 CORS 이슈를 방지하기 위한 설정이나, 개발 서버에서 실제 서버의 특정 경로에 대한 요청을 프록시하기 위해
setupProxy
를 설정할 수 있다.코드는 다음과 같다.
const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function (app) { app.use( createProxyMiddleware('/graphql', { target: 'http://127.0.0.1:9220', changeOrigin: true, followRedirects: true, ws: true, }), ); };
createProxyMiddleware('/graphql', { ... })
- '/graphql'에서 발생하는 모든 HTTP 요청을 미들웨어가 처리하도록 설정한다.
target: 'http://127.0.0.1:9220'
- 프록시 된 요청이 전달될 서버의 주소를 설정한다. 여기선 9220번 포트로 설정했다.
changeOrigin: true
- 요청의 호스트 헤더를 target의 호스트로 변경한다. CORS 이슈를 해결하기 위해 사용한다.
followRedirects: true
- 이 설정은 서버가 요청에 대해 리다이렉트 응답을 보냈을 때 그 리다이렉트를 프록시가 따르도록 한다.
ws: true
- 이 설정은 웹소켓 프록시를 활성화한다. 클라이언트와 서버 간의 웹소켓 연결도 이 프록시를 통해 전달되며, subscribe를 위해
true
로 설정하였다.
리더보드 페이지
기나긴 삽질 끝에 마침내 완성한 리더보드 페이지! 🎉 참여해 주신 모든 분들께 깊은 감사를 드립니다. 🙇🏻♀️
결론
GraphQL 의 subscription 을 사용하여 실시간 랭킹 같은 기능을 구현할 수 있었다. CORS 때문에 설정 방법에 애를 먹긴 했지만, 사용 방법은 query 를 쓸 때와 크게 다르지 않아 어렵지 않았다.
subscription 은 실시간 업데이트와 효율성이 가장 큰 장점이 아닐까 생각한다. 서버로부터 실시간으로 데이터를 수신하므로 사용자는 항상 최신 상태를 볼 수 있으며, 필요한 데이터가 변경될 때만 업데이트를 받기 때문에, 자주 변경되지 않은 데이터에 대해서는 서버 요청을 최소화할 수 있다.
하지만 웹소켓 또는 유사한 실시간 프로토콜을 구현해야 하며, 클라이언트와 서버 사이의 연결 상태를 관리하는 로직도 필요하기에 복잡하긴 하다. 이 글에서 다루진 않았지만, subscription 을 위해 서버측에서 추가 작업이 필요하다. 그리고 실시간 연결을 필요로 하기 때문에 그에 따른 서버 자원과 클라이언트의 리소스 소모가 있을 수 있다.
따라서 어떠한 방법이 비용이나 성능 면에서 더 효율적인지는 애플리케이션의 특성, 데이터의 갱신 빈도, 사용자의 동시 접속자 수 등 여러 요소에 따라 달라질 수 있으니 적절히 판단하여 사용하길 바란다.
references
- https://relay.dev/docs/v10.1.3/subscriptions/
- https://relay.dev/docs/guided-tour/updating-data/graphql-subscriptions/#configuring-the-network-layer
- https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
- https://github.com/enisdenjo/graphql-ws
- https://github.com/apollographql/subscriptions-transport-ws
- https://graphql.org/blog/subscriptions-in-graphql-and-relay
- https://create-react-app.dev/docs/proxying-api-requests-in-development
28 March 2024
- 메소드는 반환 값으로
NVIDIA GTC 2024에서 만나요! 래블업이 AI 기술의 최전선을 보여드립니다
By 래블업 주식회사안녕하세요, 래블업입니다. 오는 3월 18일부터 21일까지 미국 새너제이에서 열리는 NVIDIA GTC 2024 컨퍼런스에 래블업이 참가합니다. 5년 만에 열리는 이번 대면 행사에서 래블업은 실버 스폰서로서 그 동안 개발해온 최신 AI 기술과 제품을 선보일 예정입니다.
About GTC 2024
GTC는 NVIDIA가 주최하는 AI 분야 최대 규모의 기술 컨퍼런스입니다. 30만 명 이상이 온오프라인으로 참여할 것으로 예상되는 이번 행사에서는 NVIDIA CEO Jensen Huang의 기조연설을 비롯해 900여 개의 세션과 300여 개의 전시 부스, 20여 개의 기술 워크숍 등 다양한 프로그램이 마련되어 있습니다. 제너러티브 AI를 포함한 최신 AI 기술 트렌드를 한눈에 파악할 수 있는 최고의 기회가 될 것입니다.
Lablup at GTC 2024
래블업은 GTC에서 전시 부스(#1233)를 운영하며, APAC 지역 유일의 NVIDIA DGX-Ready 소프트웨어인 Backend.AI Enterprise 플랫폼을 시연합니다. Backend.AI는 NVIDIA DGX 시스템을 비롯한 GPU 인프라의 성능을 극대화하고 사용성을 개선해주는 AI 인프라 운영 플랫폼입니다.
또한 래블업의 MLOps 솔루션인 FastTrack도 함께 선보입니다. FastTrack을 통해 생성형 AI 모델의 전체 개발 과정을 간소화하고 자동화할 수 있습니다. 특히 파운데이션 모델을 다양한 산업 분야에 맞춰 자동으로 파인튜닝하고 챗봇 등으로 활용하는 데모도 준비되어 있습니다.
Sessions at GTC
래블업은 두 가지 주제로 GTC 세션 발표에도 참여합니다.
첫 번째 세션에서는 "Idea to Crowd: Manipulating Local LLMs at Scale" 이라는 제목으로, 개인용 GPU부터 대규모 데이터 센터까지 다양한 규모에서 로컬 LLM을 파인튜닝하고 운영하는 기술과 사례를 소개합니다.
특히 래블업의 GPU 유동화 기술을 활용해 다양한 크기의 거대언어모델(LLM)과 이미지 생성 모델을 함께 멀티 GPU에 효율적으로 적재하는 방법에 대해 심도 있게 다룰 예정입니다. LLM 로딩 후 불가피하게 발생하는 GPU 메모리 낭비를 최소화하고, 모델별 성능 저하와 전체 성능 향상의 trade-off를 분석한 실험 결과도 공유합니다. 이를 통해 온프레미스 환경에서 AI 모델 서빙을 보다 비용 효율적으로 수행하는 방안을 제시할 것입니다.
두 번째 세션에서는 Personalized Generative AI라는 주제로, 개인용 GPU를 활용해 가정에서도 손쉽게 생성형 AI 모델을 구동하고 개인화하는 방법을 다룹니다. PC나 가정용 서버 등 작은 규모의 하드웨어에서 생성형 AI를 자동으로 운영하고 파인튜닝하는 기술을 소개하고, 이를 통해 개인 맞춤형 AI 비서가 우리 삶에 더욱 밀접하게 스며들 미래를 전망해볼 것입니다.
곧 만나요!
짧게나마 래블업이 이번 GTC에서 선보일 기술과 비전을 소개해드렸습니다. 3월 새너제이에서 열리는 GTC 2024에 참석하시는 분들께서는 래블업 부스(#1233) 에 꼭 들러주시기 바랍니다. 최신 AI 기술을 직접 체험하고 래블업 팀과 직접 소통하실 수 있습니다.
온라인으로 참여하시는 분들도 래블업 세션 발표를 통해 로컬 LLM과 개인화된 생성형 AI의 현재와 미래를 만나보실 수 있을 것입니다. 래블업은 앞으로도 AI 기술의 최전선에서 기업과 개인이 AI를 보다 쉽게 활용할 수 있도록 노력하겠습니다. 감사합니다!
15 March 2024
2023 Lablup DevOps Summer 회고
By 이규봉이번 포스팅에서는 래블업에서 지난 9개월 동안 개발자로 경험한 이야기를 다뤄보았다.
Table of Contents
- 지원 동기
- 인턴에서 DevOps로!
- rraft-py 개발
- 오픈소스 컨트리뷰션 아카데미 지역 스프린트 Backend.AI 멘토링
- 다양한 컨퍼런스 참여
- 2023 오픈소스 컨트리뷰션 아카데미
- 파이콘 발표
- 결론
지원 동기
개인적으로 래블업에 입사하기 전부터 취미로든, 업무 시간에서든 내가 개발한 프로그램을 통해 다른 사람들에게 도움을 주는 일을 지속적으로 할 수 있는 직업을 갖고 싶다는 생각을 해 왔다.
특히 오픈 소스는 내가 작성한 코드가 다른 사람들에게 도움을 줄 수 있을 뿐 아니라, 원하는 사람이 있다면 코드를 자유롭게 수정하고 활용할 수 있다는 점에서 더욱 더 매력적으로 느껴졌다.
졸업 프로젝트로 Arvis라는 개인 프로젝트를 진행해보고 나서야 깨달은 사실 중 하나는, 프로젝트 규모가 계속 커지면서 단순히 내가 좋아하는 일이라는 이유로 프로젝트를 지속하는 것이 현실적으로 쉽지 않다는 점이었다. 나름대로 초기부터 프로젝트를 신중하게 계획하고 진행해 보았지만, 결론적으로 프로젝트 유지에 필요한 시간과 노력을 간과한 것 같다는 생각이 들었다.
그런 점에서 오픈 소스 관련 활동들을 적극적으로 권장하고 지원해주며, 심지어 소스 코드의 핵심 부분을 오픈 소스로 개발하는 래블업은 내가 꿈꾸던 회사였다.
인턴에서 DevOps로!
필자의 래블업 OSSCA 인턴십에서 마지막 3주차 동안 했었던 일은 분산 시스템 관련된 공부 및 조사였다. 특히 Raft 알고리즘 구현에 관련된 내용이 대부분이었다. 직무 이름은 인턴에서 DevOps로 변경되었지만 인턴십에서 맡았던 이슈를 해결하기 위해 계속 Raft 등 인턴십 때 공부했던 내용들을 확장한다는 느낌으로 진행하게 되었다.
물론 이외에도 아래에서 언급할 다양한 활동 들에도 참여했지만, 현재까지 회사에서한 작업들 중 가장 주된 작업은 rraft-py 작성 등 기존 분산 락으로 작성되어 있는 구조를 대체하기 위해 Raft 알고리즘 구현체의 파이썬 바인딩을 작성하고, Backend.AI와 어떻게 통합시킬 수 있을지에 대해 고민하는 것이었다.
rraft-py 개발
rraft-py는
tikv/raft-rs
의 파이썬 바인딩 구현체로, 이에 대한 자세한 내용은 GitHub Readme / Wiki를 참고하면 된다. 또한 다음 달 2023 파이콘 KR 발표에서 해당 주제의 기술적인 세부사항들을 발표할 예정이므로 관심이 있다면 참고하면 좋을 것 같다.여기선 rraft-py를 개발하면서 배운 기술적인 내용들은 일단 차치하고 Lablup 개발자로서의 경험에 초점을 맞춰 보려고 한다.
rraft-py는 단순히 Backend.AI의 한 이슈를 해결하는 것 뿐 아니라, 별개의 프로젝트를 만들고 그 프로젝트를 Backend.AI와 통합시키기 위한 프로젝트까지 진행해야 했기 때문에 많은 고민을 하며 진행했다.
전체적으로 프로젝트에 몇 가지 마일 스톤들이 있었는데 매 마일 스톤을 통과할 때 마다 좀 더 안정된 느낌으로 프로젝트를 진행할 수 있었던 것 같다. 분명히 매번 높은 성취감이 있었지만 나중에 가서야 처음 작성했던 코드가 의도한 대로 돌지 않는다는 것을 확인하고 좌절할 때도 많았다. 하지만 래블업에서 이런 삽질들을 할 수 있는 시간을 허용해줬고, 이전에 그냥 "삽질이었네" 하고 치부하고 넘어가면서 배운 것들을 통해 비로소 지금까지 올 수 있었다고 생각한다.
rraft-py 예제 코드 실행 결과
rraft-py를 Backend.AI에 통합하기 위해선 아직도 가야할 길이 더 남아 있지만, 결론적으로 느낀 것은 스스로 생각하고 판단하면서 프로젝트를 계속 고도화 시켜보는 경험을 할 수 있어서 좋았고, 이런 부류의 경험을 좋아하는 개발자들에게 래블업은 최고의 선택지 중 하나가 될 수 있을 것이라는 점이다.
오픈소스 컨트리뷰션 아카데미 지역 스프린트 Backend.AI 멘토링
예상했던 것 보다 많은 시간을 요구했기 때문에 rraft-py 개발을 가장 메인으로 진행했지만, 이것 외에도 다양한 성격의 업무를 진행해 볼 수 있는 기회가 있었다.
그 중 가장 인상 깊었던 업무중 하나가 바로 제 1회 대구 오픈 소스 컨트리뷰션 아카데미 지역 스프린트에 Backend.AI 멘토로 참여해 본 경험이었다.
사실 Backend.AI에 대한 깊은 이해 없이 멘토로 참여하게 되었는데, 설상가상으로 스프린트 기간도 고작 2일 밖에 주어지지 않아 여러모로 많은 걱정을 갖고 참여했었다.
멘티 분들이 하나라도 배우고 최대한 만족하고 돌아 가실 수 있도록 하기 위해 Backend.AI를 전혀 모르는 분들에게 어떤 식으로 설명 해 드릴지, 다른 플랫폼에서 어떻게 개발 환경을 구축해야 하는지 등 (개인적으로 평소엔 macOS + docker desktop 환경에서만 개발했었는데, 멘티 분들은 Windows 환경에서 진행하시는 분도 있으셔서 개발 환경 구축하는 과정에서 삽질 하기도 했다.) 여러 가지들을 생각해보고 준비해보아야 했다.
결론적으로 이런 과정들에 익숙하지 못했던 내가 오히려 더 많은 것들을 배울 수 있었고, 멘티 분들도 생각보다 워낙 잘 따라와 주셔서 다들 한 개 이상씩 PR들을 만드실 수 있었던 뜻 깊은 시간이 된 것 같아 좋았다.
제 1회 대구 오픈 소스 컨트리뷰션 아카데미 지역 스프린트
다양한 컨퍼런스 참여
AI Expo, AWS Summit, Next Rise 등 아래와 같은 다양한 컨퍼런스, 전시회들에 부스로 참여해 볼 수 있었다. 이런 곳들에 참여하면서 Backend.AI를 여러 부류의 사람들에게 설명하는 법을 배울 수 있었던 것도 좋았고, 다른 회사들의 다앙한 기술들을 직접 접할 수 있었던 점도 매력적이었다.
AI EXPO KOREA 2023
2023 오픈소스 컨트리뷰션 아카데미
래블업은 오픈 소스 문화를 지향하는 회사 답게 매년 오픈소스 컨트리뷰션 아카데미에 적극적으로 참여하고 있다. 올해에도 오픈소스 컨트리뷰션 아카데미에 참여했는데, Backend.AI 팀 외에도 다른 다양한 프로젝트들에 참여할 수 있게 장려하고 있어서 필자는 GlueSQL 멘티로 참여해 진행해 보고 있다.
이런 자유로운 문화는 성장하고 싶어 하는 욕구가 강한 개발자들에게 상당히 매력적인 포인트가 될 것이라 생각한다.
(필자 외에도 2023 컨트리뷰션 아카데미 다른 프로젝트들에 두 명이나 더 참여하고 있다.)
파이콘 발표
회사에서의 rraft-py 개발 경험을 토대로 2023 파이콘 KR에서 발표해 볼 수 있는 기회를 얻을 수도 있었다.
개인적으로 대외 발표가 처음이라서 다소 얼떨떨한 느낌이지만 나름대로 최선을 다해 준비해보고 있다. 발표에 관심을 가지는 분들에게 발표 자료 뿐만 아니라 GitHub을 통해 소스 코드나 작업 내역까지 모두 공유할 수 있다는 점이 기대된다.
결론
래블업은 오픈 소스 문화를 지향하는 회사로, 오픈 소스 컨트리뷰션 아카데미나 파이콘 등 다양한 오픈 소스, 커뮤니티 관련 행사들에 참여할 수 있도록 장려하며 개발자에게 주도적으로 작업을 진행해 보게 하는 기회를 제공한다.
래블업에서 앞으로 더 꾸준히 다양한 성격의 오픈 소스 활동들에 참여하고, 배우고 성장하며, 기여해보고 싶다.
18 July 2023