실용주의 데브옵스 for MSA

안녕하세요, 콘텐츠서비스개발팀에서 카카오TV 동영상 빅데이터 수집 및 분석 업무와 동영상 추천 시스템을 개발하고 있는 Knight 라고 합니다.

실용주의 데브옵스 for MSA’는 저희가 카카오TV 실서비스를 오픈하기 전에 진행한 데브옵스 시스템 구축에 대한 이야기입니다.  카카오TV가 데브옵스 시스템을 구축하게 된 배경과 여러 운명의 갈림길에서 의사 결정을 한 과정, 이전 시 발생한 문제와 이 과정 중에 얻게 된 베스트 프랙티스에 대해 소개하려고 합니다.

  • 쿠버네티스 클러스터 만들기 
  • 쿠버네티스 CI/CD 시스템 구축하기
  • 인프라서비스 설치 및 관리하기

 


 

카카오TV가 데브옵스 시스템을 구축하게 된 배경

 

저희가 MSA에 관심을 가지게 된 건 2018년 9월경이었습니다. 당시, 스프링 5.0 마이크로서비스에 대한 책을 읽으면서 장단점들을 파악하는 스터디를 진행했습니다. 이후 본격적으로 MSA를 도입할 것인가를 주제로 팀 내 워크숍을 진행했고, 최종적으로 MSA를 도입하기로 결정했습니다. 

그러고 나서 도커, 쿠버네티스, 헬름(Helm), 스프링부트(Webflux), 센트리(Sentry), 스프링 클라우드 슬루스(Sleuth)와 같이 도입이 필요한 기술을 찾고, 앱들을 기동할 클러스터를 구축하기로 했습니다.

 

첫 번째 의사 결정: 개발 단계(Phase) 별로 클러스터를 어떻게 분리할까? 

카카오는 Dev Zone과 Production Zone으로 존이 분리되어 있고, 개발 단계도 일반적으로 Dev, Sandbox, CBT, Production으로 구분하고 있습니다. 

여러 책을 읽어 보면, 하나의 클러스터 내에서 여러 개발 환경을 네임스페이스로 논리적으로 구분해서 사용 가능하다고 이야기하고 있습니다. 하지만 저희는, 하나의 클러스터에 모든 개발 단계를 포함하게 되면 운영과 관리의 요소는 줄겠지만 서비스 안정성은 떨어질 수 있다고 판단했습니다. 그래서, 운영 및 관리의 편의성을 위해 Dev와 Sandbox는 통합하고, CBT와 Production은 서비스 안정성을 고려해 논리적 뿐 아니라 물리적으로도 분리하기로 결정했습니다.

물론 노드 어피니티(Node Affinity) 기능을 통해서도 물리적으로 분리하는 게 가능하지만, 이렇게 하면 별도 설정을 해주는 일이 추가되기 때문에 클러스터 자체를 별도 구성하는 방식으로 원천적으로 물리적으로 분리했습니다.

 

두 번째 의사 결정: Ingress(쿠버네티스의 전통적 게이트웨이) NGINX VS Istio(서비스 메시)

쿠버네티스 전환 시에 대부분의 분들이 고민하는 부분일 것입니다. 저희도 처음에는 Istio를 생각했습니다. Istio가 규모가 있는 서비스에 적합하다고 생각하고 사내 인프라팀 멤버의 도움도 받아 집중적으로 진행하다가, 결국은 Ingress NGINX로 선회하게 되었습니다. 

이유는, 당시에 아직 Istio가 초기 버전이라 버그가 많았고, 쿠버네티스 설정을 변경해야 하는 문제, 사내 쿠버네티스와의 버전 호환성 문제 등이 있었습니다. 이 외에도, 저희가 서비스를 당시에 Strangler(스트랭글러) 패턴으로 구축 중에 있어서, 이를 통해 Istio의 필요 기능(트래픽 라우팅, SSL 오프로딩 등)을 어느 정도 대체 가능해 Istio까지는 필요 없겠다고 판단했습니다. 

 

– Strangler(스트랭글러) 패턴이란?

시스템이 정상적으로 돌아가는 상태에서 점진적으로 MSA 아키텍처로 코드를 분리해 나가는 디자인 패턴입니다. 

레거시 모놀리식 서비스를 MSA 화해서 옮길 때 앞단에 두는 프록시를 일반적으로 Strangler라고 부릅니다. 트래픽을 바로 라우팅하게 되면 장애 위험성이 크기 때문에 처음에는 레거시와 MSA 서비스를 미러링을 해서 MSA가 제대로 동작하는지 확인하고, 트래픽 비율을 조정하면서 점차 이전하는 방식을 통해 모든 모놀리식 기능들을 이동시킵니다. 최종적으로는, 모놀리식 서비스와 모놀리식과 MSA 간 가교 역할을 했던 Strangler는 제거하게 됩니다.

 

단, 최근에 저희 팀에서 MLOps화(머신러닝 오퍼레이션 자동화)를 진행하고 있고, ML 모델을 서빙할 때 Istio를 도입해 사용하고 있는 부분이 있습니다. 또한, 카카오 내의 많은 다른 팀과 조직에서 Istio를 도입해 잘 사용하고 있습니다. 따라서, 각 상황에 맞게 신중하게 결정하시길 추천드립니다.

 

추가적 의사 결정: Production 클러스터를 몇 개 만들까?

의아한 질문일 수 있습니다. IDC 이중화 하면 되는 거 아닌가 생각할 수 있습니다. 저희가 처음 쿠버네티스 클러스터를 구축할 때 디플로이먼트에 리소스 리미트(Limit)를 설정하지 않고 배포한 적이 있습니다. 이 때 리소스 메모리 누수(Memory leak)가 발생해 파드가 하나 죽더니, 주변 파드도 하나씩 영향을 받기 시작해 노드 다운, 이어서 결국은 클러스터 전체가 다운되는 사건이 발생했습니다. 그래서 잘못하면 쿠버네티스 클러스터를 통째로 망가뜨릴 수도 있겠구나 하는 두려움에 삼중화를 결정했습니다. 운영해 본 결과, 그 사이에 쿠버네티스 버전 업그레이드와 안정화가 상당히 진행되어 한 번도 클러스터에 장애가 발생한 적이 없습니다. 따라서 지금 시점에서는 이중화면 충분하다고 생각합니다. 

 

쿠버네티스 CI/CD 시스템 구축하기

 

클러스터만 제대로 만들어 놓으면 쿠버네티스와 MSA를 바탕으로 별문제없이 지낼 수 있을 거라고 생각을 했었지만, 여전히 문제는 있었습니다.

 

DRY 원칙 위배가 가져온 고통을 동반한 데브옵스

첫 번째로 MSA 프로덕트를 한 개 만들 때마다 고정적으로 해야 하는 일들이 있었습니다.

당시에 Helm으로 배포를 했었습니다. 즉, 우선 기존의 Helm 차트를 복사하기&붙여 넣기한 다음 이름, 네임스페이스 등을 변경하여 사용하는 것입니다. 그 이후에는 Jenkins Job을 복사해서 역시나 필요한 부분을 수정해 사용합니다. 도커 이미지의 경우 사내 도커 이미지 저장소(이미지 빌드 포함)인 D2Hub에서 빌드하고, 여기에 브랜치 트리거 등을 설정해 놓습니다.

결국 DRY(Don’t Repeat Yourself) 원칙에 위배되는 거라, 이로 인한 후폭풍이 오게 되어 있습니다.

예를 들어, Sentry(앱 모니터링 소프트웨어)가 사내의 앱 모니터링 솔루션인 Matrix로 변경이 되면, 이 부분에 대한 통합 설정이 Jenkins에 이미 다 포함되어 있을 것이기 때문에, 모든 MSA 프로덕트가 만든 Jenkins Job을 다 수정해야 합니다. 또한, 쿠버네티스 클러스터 버전이 업그레이드되면 리소스 스펙이 변경되기도 합니다. 이 중에 사용 중인 리소스가 있다면 Helm차트를 수정해야 합니다. D2Hub가 업데이트되면 등록해 놓은 브랜치 트리거를 다시 설정해야 할 수도 있습니다.

결론은, MSA에는 어느 정도 DRY 원칙 위배가 허용될지 몰라도, 그 MSA를 위한 데브옵스는 DRY 원칙을 위배하면 손이 매우 많이 간다는 것입니다.

 

Jenkins 운영에 있어서의 개발자 리소스 낭비 

다음으로, Jenkins 운영 관리에 대한 문제가 있었습니다. 지금까지 경험한 Jenkins는 처음에 설치하는 사람이 Jenkins 담당자가 되는 제품이었습니다. 즉, Jenkins를 처음 설치한 사람의 희생이 전제되어야 합니다. 또한, 싱글머신에 운영하다 보면 스케일아웃도 힘듭니다(슬레이브를 이용하면 되지만 이 역시 추가 작업이 필요하죠). 사이사이에 데이터 백업도 해야 합니다. 

개발자 리소스를 소중하게 생각한다면, Jenkins에 들어가는 관리 및 운영 리소스를 줄여야겠다는 생각에 이르게 됐습니다. 

 

세 번째 의사 결정: CI와 CD 분리 결정, GitOps 방식과 Argo CD 채택

또한, 기존에는 Jenkins가 CI와 CD를 둘 다 담당하고 있었는데, 외부에 CD 관련 다양한 좋은 툴이 나오기 시작했고 Jenkins도 비대화되고 있어서 CI와 CD를 분리하기로 결정했습니다. 

CI 도구는 기존과 동일하게 Jenkins로 가기로 했습니다. CI에서 Jenkins의 지위가 독보적이기도 하고 기존에 짜놓은 Jenkins 스크립트가 많고, 파이프라인에 대한 사내 레퍼런스(카카오페이: https://tv.kakao.com/channel/3693125/cliplink/413991843 참고)도 있어서 이를 재활용하고 싶었습니다. 

CD 도구로는 전통적 강자인 Spinnaker(스핀에이커)와 신흥 강자로 부상 중인 Argo(아르고) CD가 있었습니다. 

이 중 Argo의 장점은 GitOps(깃옵스) 방식을 사용한다는 점이었습니다. 

GitOps란, 배포의 형상을 선언형으로 기술하여 Config 리포지토리에서 관리하고 이를  운영 환경 간의 상태 차이가 없도록 유지하는 방식을 말합니다. 즉, 배포할 쿠버네티스 리소스를 Git 리포지토리에 올려놓으면 실제 운영 중인 클러스터와 차이가 발생하게 되고 이를 감지하고 클러스터에 동기화해주는 어플리케이션이 동기화를 수행하여 지속적으로 배포가 이루어지도록 하는  방식입니다. 

GitHub 등의 웹 UI에 들어가면 배포된 상태를 편하게 확인할 수 있고 단일 진실 공급원(Single source of truth)으로  신뢰성도 담보됩니다. 또한, Git 커밋을 통해 관리되기 때문에 별도 설정이나 시스템 구축 없이도 배포 이력이나 감사 로그를 쌓을 수 있습니다. 롤백도 Git의 특정 리비전을 참고하여 동기화하는 것이라, 기존 배포 결과가 OK 상태였다고 하면 해당 리비전으로 롤백 했을 때 배포에 문제가 발생하지 않습니다.

GitOps 방식에 매료되어 CD 도구는 Argo CD로 결정했습니다.

Spinnaker를 통해서도 배포 빈도나 속도 상승, 생산성 향상, 신속한 오류 대처가 가능하겠지만, Argo는 이에 더해 GitOps의 추가 장점도 있었기 때문입니다.

또한 Argo는 Stateless 한 애플리케이션이고, 데이터가 다 Git에 올라가 있으므로 관리가 필요한 요소가 거의 없습니다. 반면, Spinnaker는 데이터도 미니오(Minio)에 저장해야 하기 때문에 데이터를 따로 관리해야 합니다.

 

Git 브랜치 전략 수정: Git Flow(깃 플로)에서 GitHub Flow(깃허브 플로)로 단순화

기존에 모놀리식 서비스를 개발하고 운영할 때에는 Git Flow 방식을 사용했습니다. 마스터(master) 브랜치와 디벨롭(develop) 브랜치를 두고, 개발이 필요할 때 디벨롭 브랜치에서 피처(feature) 브랜치를 따서 디벨롭 브랜치에 머지 시키고, 배포가 필요할 때는 릴리즈(release) 브랜치를 따서 마스터 브랜치에 머지 시키는 방식으로 배포했습니다. 급한 이슈가 있을 때는 마스터에서 핫픽스(hotfixes)를 따고 이를 마스터에 다시 머지 해서 배포하는 조금은 복잡한 Git Flow 방식을 쓰고 있었습니다.

그런데 MSA로 전환해 보니 MSA에서는 단순한 GitHub Flow만으로도 충분하겠다는 생각이 들었습니다. 마스터에서 브랜치를 바로 따서 코드 개발해서 추가하고 개발이 어느 정도 완료되면 PR(Pull Request)을 요청해 해당 PR을 머지 할지 여부를 논의하고 충분히 성숙되면 바로 마스터에 머지 하는 방식입니다. 마스터에 머지가 되면 자동으로 배포가 이루어집니다. 

이미지 출처: https://guides.github.com/introduction/flow/

 

이런 GitHub Flow의 배포 전략에 맞도록 인터페이스 요구 사항은, 개발자가 Git CLI 명령과 GitHub 버튼만으로 손쉽게 배포가 가능하도록 하자! 로 정했습니다. 

인터페이스가 이렇게 구성되면 신입 개발자라고 하더라도 별도의 러닝커브 없이 배포 작업을 할 수 있게 될 것으로 생각되었습니다.

이것으로 중요한 의사 결정들은 어느 정도 마무리되었습니다.

이제부터는 개발이 필요했던 요소에 대해 말씀드리겠습니다.

  • 사용자 설정 스펙: 인그레스 도메인, 도메인 이름, 레플리카 개수, 메모리,  CPU 등 사용자가 설정할 수 있는 스펙과 방법 필요 
  • Jenkins 파이프라인 스크립트: 사용자 설정 스펙을 바탕으로 CI/CD에서 비즈니스 로직을 수행할 파이프라인 스크립트 필요 
  • GitOps용 리포지토리: Argo CD가 바라볼 GitOps용 리포지토리 필요
  • 관련 인프라 설치: Jenkins와 Argo CD 설치

 

설정 스펙의 경우, yaml 파일로 기술하고 시스템 스펙과 사용자 스펙을 분리한 다음, 사용자 스펙이 시스템 스펙 중 필요한 부분만 오버라이드해서 쉐도잉해서 가져다 쓸 수 있도록 했습니다. 이렇게 하면, 설정 스펙을 중앙화해서 관리하기도 편하고, 사용자 입장에서도 필요한 부분만 수정해서 간단하게 사용할 수 있는 등 다양한 요구 사항을 자유롭게 반영해 자유롭게 구성할 수 있습니다.  

Jenkins 파이프라인의 경우, 스크립티드 문법을 사용해서 그루비(Groovy) 언어로 작성했습니다. Jenkins 파이프라인을 만들 때 선언형(Declarative) 문법과 스크립티드 문법을 둘 다 허용합니다. 가독성은 선언형이 더 높지만, 선언형의 경우, 작업을 하다 보면 선언형에서 지원하지 않는 부분이 있어 결국은 선언형과 스크립티드를 혼합해서 사용하게 되는 한계가 있기 때문에 처음부터 일관성있게 스크립티드 단일 문법을 사용하였습니다.  

파이프라인 스테이지별로 독립적으로 모듈화해서 구현을 해서 스테이지 추가, 삭제가 편리하도록 코드를 짰고, 단정문을 많이 사용해서 파이프라인 스크립트 실수로 인해 잘못 배포되는 경우가 최대한 없도록 구현했습니다. 

GitOps용 리포지토리의 경우, 먼저 GitOps 리포지토리를 생성하고,  MSA 스탠다드 디플로이란 곳에 디렉토리를 만들어서 ingress, service, deployment 조합의 Helm 차트를 넣었습니다. Helm 차트는 일반적인 MSA WAS들이 공유해서 쓰는 것으로, Helm은 배포가 아닌 템플릿화(베이킹) 용도로만 사용했습니다.  

사용자 설정과 CI/CD 스크립트를 통해서 배포하려는 대상에 맞는 values.yaml 파일이 생성되고, 이를 시스템이 GitOps용 리포지토리에 커밋(푸시) 해 주는 부분까지 구현했습니다.

 

Production 배포의 구현 결과는 다음과 같습니다.

구현된 CI/CD를 통해 프로덕트를 최초로 배포하기 위해서는 등록 과정이 필요합니다. 먼저 GitOps 리포지토리의 특정 yaml 파일에 배포하길 원하는 프로덕트의 리포지토리명을 추가한 다음, 이를 마스터에 머지 시킵니다. 이후, 각 리포지토리의 Jenkins 파일에 어떤 브랜치의 Jenkins 파이프라인을 사용할지 등을 기입합니다. 마지막으로, 개별 설정이 들어간 workflow.yaml를 작성해서 각 MSA Git 리포지토리에 넣고 마스터에 머지 하면 구현된 CI/CD를 사용하기 위한 준비 과정은 완료됩니다. 

workflow.yaml의 예

 

이후, 브랜치에 커밋이 푸쉬되거나 머지 되면 자동으로 브랜치별 CI가 진행됩니다. 예를 들어 feature 브랜치에 커밋이 반영되면 커밋에 대한 빌드 테스트, 유닛 테스트 등이 수행되고, PR 브랜치에 커밋이 반영되면 빌드 테스트, 유닛 테스트, 정적 테스트 등이 수행되고 그 결과가 GitHub 체크와 연동되서 테스트를 통과해야만 머지 버튼이 활성화되는 CI가 수행됩니다.

GitHub 머지 버튼을 통해 마스터 브랜치에 PR이 머지 되면, 해당 마스터 소스코드로 도커 이미지를 빌드 하여 사내 도커 레지스트리에 푸시하고 이에 대한 배포 과정을 실행합니다. 배포 과정에서는 workflow 파일에 기술된 사용자 설정을 참고하여 Helm 차트에 쓰일 values 파일을 생성하고, 이를 GitOps 리포지토리에 푸시 해줍니다. Argo CD는 이 내용과 쿠버네티스 배포된 내용을 확인하여 변경이 일어난 부분을  동기화하게 되고 배포 과정은 끝이 납니다. 

즉, Prod의 경우, 배포가 Merge Pull Request 버튼으로만 이루어집니다. 배포 과정 중 개발자의 입력이 필요한 경우(배포 승인,  배포 공지글 등록 등)에 대해서는, CI가 연관된 인터페이스 URL과 함께 카카오톡으로 알람을 줍니다.

 

인프라 서비스 설치 및 관리하기

 

저희 파트의 경우 다음과 같이 다양한 오픈소스 인프라를 사용하고 있습니다. 이러한 오픈소스 인프라를 최소한의 인적 리소스로 설치하고 관리하고 운영해야 합니다.

오픈소스 인프라의 경우, 인터넷에 검색만 해도 가져다 쓸 수 있는 Helm 차트나 쿠버네티스 리소스 파일이 방대하게 나옵니다. 하지만, 그대로 가져다 쓰게 되면 손을 쓸 수 없을 경우가 왕왕 발생하게 됩니다. Helm 차트에 적용된 리소스의 코드의 가독성이 너무 떨어져 이해를 할 수 없다거나, 변경하고자 하는 부분이 values에서 변경할 수 없게 짜여 있다거나, 무작정 이것저것 적용하다 보면 롤백 할 방법이 묘연하다거나 할 수 있습니다. 

따라서, 오픈소스 인프라는 Helm 설치를 바로 하지 말고 Kustomize를 이용하여 필요한 부분만 패치해서 쓰기로 결정했습니다.

 

– Kustomize(커스터마이즈)?

기본(base) 폴더를 바탕으로 동일한 애플리케이션의 여러 버전을 정의하는 오버레이 방식을 통해 필요한 부분만 패치하여 사용할 수 있는 템플릿화 도구입니다. Kustomize를 사용하면 여러 오버레이 하위 폴더를 통해 여러 버전의 애플리케이션을 가독성 있게 쉽게 관리할 수 ​​있습니다. 

(base) 예를 들어, 디플로이먼트 YAML 리소스(deployment.yaml)를 커스터마이제이션 YAML(Kustomization.yaml)에 해당 디플로이먼트 리소스를 가져다 쓰겠다고 기입해 놓습니다. 공통적으로 필요한 네임스페이스나 이미지, 이미지 태그 등을 Kustomization.yaml에서 변경할 수 있도록 합니다. 

(overlay) 패치가 필요한 쿠버네티스 리소스에는 변경하고자 하는 부분만 YAML로 만들어서 패치 형태로 쓸 수 있습니다.

 

 

Helm -> Kustomize 전환: Replicated의 쉽(Ship)  사용

이제, Helm을 Kustomize 형태로 변환하는 작업이 필요합니다. 이를 도와주는 도구가 쉽(Ship)입니다.

https://www.replicated.com/ship/oss/
https://github.com/replicatedhq/ship

 

  • 먼저 쉽을 사용해 Helm을 템플릿화된 쿠버네티스 리소스 형태로 변경합니다.
  • 이후 쿠버네티스 리소스에서 필요한 부분만 Kustomize 패치를 사용해 변경하고 변경한 부분은 GitOps 리포지토리에 넣어서 관리합니다.
  • GitOps 리포지토리에 git push origin HEAD:install/ ${cluster}/${namespace}/${app_name}과 같은 Git CLI 명령을 통해 install 브랜치가 생성이 되면 해당 브랜치의 ${app_name} 인프라를 배포하는 CI/CD 파이프라인이 실행됩니다. 배포의 형상은 마찬가지로 Argo CD를 통해 GitOps로 관리합니다.

 

Jenkins도 Stateless(스테이트리스)하게 관리

Stateful(스테이트풀)한 애플리케이션인 Jenkins를 어떻게 Stateless하게 관리해서 운영 리소스를 줄일 것인가에 대해서 고민했습니다.

 

  • 먼저, 설정에 대해서는 Configuration as code라는 플러그인을 설치했습니다. 이렇게 되면 코드로 Jenkins 설정을 관리할 수 있습니다. 
  • 플러그인의 경우 init 컨테이너를 따로 두어서 띄울 때마다 init 컨테이너를 통해서 플러그인을 설치할 수 있도록 하고 설치할 플러그인 목록(이름과 버전)은  다시 코드로 관리했습니다.
  • GitOps 도입으로, 배포 이력 등 유지해야 할 데이터는 이미 GitHub에 있어서 Jenkins 빌드 Job 등은 날아가도 상관이 없는 상태였습니다.
  • Job은 DSL(Domain Specific Language) 화해서 코드로 관리하고 생성 자체도 코드로 하도록 했습니다. 이렇게 해서 Stateful 한 애플리케이션을 Stateless 하게 관리할 수 있게 됐습니다.
  • 확장의 경우도 레플리카스(replicas)를 늘리면 자연스럽게 스케일아웃이 되도록 Jenkins 쿠버네티스 리소스를 구성했습니다. 이는 Jenkins 에이전트와 쿠버네티스 플러그인을 활용하면 됩니다.

 

Jenkins 운영이 매우 간단해졌습니다. Jenkins 플러그인 설치하다가 문제가 발생하여 재시작이 안 되는 경우가 있을 수 있습니다. 이런 경우에는 그냥 PV(Persistent Volume) 지우고 파드 다시 죽였다 살리면 바로 재시작이 됩니다. 최악의 상황에서도 네임스페이스 날리고 다시 git push origin HEAD:install/ ${cluster}/${namespace}/${app_name: jenkins}와 같이 재설치하면 형상 그대로 재설치되도록 해서 운영 리소스를 줄였습니다.

 

개별적인 쿠버네티스 리소스 관리하기

마지막으로 변경하고 관리하는 독립적인 쿠버네티스 리소스(예를 들어, Ingress global config map 등)가 있을 수 있습니다. 이러한 리소스도 마찬가지로 GitOps 방식으로 Git에 커밋하고 코드 리뷰하고 마스터에 머지 한 후 Argo CD를 통해 동기화하여 배포하는 방식을 사용하고 있습니다.

 

GitOps와 Argo CD의 도입: 매우 만족합니다! 

최종적으로 대부분 관리해야 하는 리소스는 GitOps를 통해 Argo CD로 관리하고 있습니다.

Argo CD에서는 모든 쿠버네티스 리소스들이 시각화되어서 표현되어 배포의 형상을 직관적으로 파악할 수 있으며, 애플리케이션의 생성, 수정, 삭제 등이 편리한 인터페이스로 제공되기 때문에 배포를 컨트롤하기도 매우 쉽습니다. 현재까지도 Argo CD, GitOps를 운영에 대한 부담이 거의 없이 매우 만족하면서 사용하고 있기 때문에 독자분들에게도 사용을 적극 추천합니다.

카카오톡 공유 보내기 버튼

Latest Posts

제5회 Kakao Tech Meet에 초대합니다!

Kakao Tech Meet #5 트렌드와 경험 및 노하우를 자주, 지속적으로 공유하며 개발자 여러분과 함께 성장을 도모하고 긴밀한 네트워크를 형성하고자 합니다.  다섯 번째

테크밋 다시 달릴 준비!

(TMI: 이 글의 썸네일 이미지는 ChatGPT와 DALL・E로 제작했습니다. 🙂) 안녕하세요, Kakao Tech Meet(이하 테크밋)을 함께 만들어가는 슈크림입니다. 작년 5월에 테크밋을 처음 시작하고,