[큐시즘 29기] 2개월 간의 밋업 프로젝트 회고록 ✍🏻

Sangho Han·2024년 6월 30일
10

약 한 달 전인 지난 5월 25일...
2개월 간 열심히 달려왔던 큐시즘 29기 밋업 프로젝트가 끝이 났다!

밋업을 본격적으로 시작하기 전에는 글을 자주 작성했었는데..
학기중에다가 밋업 + 캡스톤 + 창업 동아리 프로젝트를 동시에 진행하다 보니 너무 정신이 없어서 글을 쓰는 데에는 많은 시간을 쓰지 못했던 것 같다 🥲

그래서 이제부터 하나씩 차근차근 다시 정리를 해보면서,
내가 배운 점, 좋았던 점, 아쉬웠던 점, 앞으로 발전하고 싶은 점 등을 기록해보려고 한다.


🧐 1. 밋업 프로젝트?

큐시즘의 밋업 프로젝트는 큐시즘에서 진행하는 가장 메인이 되는 프로젝트로,
팀 빌딩을 진행한 후 약 2개월 동안 프로젝트를 진행하며 결과물을 만들어 내고
이를 사람들 앞에서 발표하며 마무리를 하는 시간을 가진다.

팀 빌딩 이전에는 학회의 모든 구성원 들 중 원하는 인원이 밋업 프로젝트에서 사용할 아이디어를 발제하게 되고, 여기서 투표를 통해 뽑힌 주제들을 통해서 프로젝트가 진행된다.

가. 큐넥팅 데이

이렇게 선정된 발제자들은 3월 23일 큐넥팅 데이에 한 번 더 앞에 나와서 주제에 대한 발표를 진행하게 되고, 학회원들의 피드백을 받으며 아이디어를 더욱 구체화 해 나간다.

또한 학회원들은 원하는 주제에 다시 한 번 팀 빌딩을 위한 투표를 진행하면서, 최종적으로 2개월 간 함께 달려 나갈 팀원들이 구성되게 된다.

나. 팀 빌딩

팀 빌딩은 생각보다 빠른 시간 안에 이루어졌고, 나는 D조에 속하게 되었다!

우리 팀의 아이디어 발제자 및 PM은 29기 학회장이었고, 워낙 일 자체를 잘 하는 친구라고 생각을 했기에 걱정이 되지 않고 든든했던 것 같다.

다만 이제 이번 기수 자체가 성비가 여자가 많기도 하였는데, 그 때문인지 나는 팀 내에서 혼자 남자라 빠르게 친해질 수 있을까가 조금 걱정이 되기는 했다 😅

그래도 함께 기업 프로젝트를 했던 프론트 누나가 한 명 있어서 좋았던 것 같다.

다. 큐톡데이

4월 6일 큐커톤이 지나고 나서 바로 다음주인 4월 13일, 파트 별로 발표 및 강연을 듣는 큐톡데이가 진행되었다.

개발 파트는 밋업 팀과 한 조를 이루어 발표를 준비했어야 하기 때문에, 나는 같은 백엔드 팀원인 민정이와 준비를 하게 되었다.

주제는 어떤 것으로 할 지 고민을 하다가.. 무중단 배포로 결정을 하였다! 이후 실제로 프로젝트에서 무중단 배포까지 진행을 했기 때문에 좋은 선택이었다는 생각이 들었다.

발표를 준비하며 무중단 배포의 대표적인 종류 3가지 (롤링, 카나리, 블루-그린)에 대해서도 알게 되었고, 블루-그린에서 어쩔 수 없이 일시적인 중단이 일어나는 이유에 대해서도 공부를 해 보며 조금이나마 엔진엑스에 대해 알 수 있었던 것 같다!

그래서 개인적으로는 이러한 세미나 형태의 커리큘럼이 의미가 있었다고 생각을 한다 👍🏻

이후로는 시험기간이 지나고.. 본격적으로 개발을 시작했던 것 같다.


📑 2. 기획

본격적으로 개발에 대한 이야기를 하기 전에, 프로젝트를 통해 어떤 서비스를 만들어 냈는지에 간략하게 말해보고자 한다.
즉, 여기서는 기획에 대한 이야기를 좀 다루려고 한다.

🔮 가. 셀피스

  • 프로젝트명 : 나만의 조각을 찾아 브랜딩하는 공간, 셀피스(SELPIECE)
  • 퍼스널 브랜딩의 초기 여정에서 겪는 명확한 방향 설정이 어려운 문제를 해결하기 위한 솔루션

주로 2030 세대를 통해 설문 및 인터뷰를 진행한 결과, 새로운 가치를 창출하고자하는 집단은 자기이해에 대한 강력한 니즈와 그 과정에서 문제를 겪고 있다는 점을 발견할 수 있었음.
공통적으로 퍼스널 브랜딩을 목적이자 목표로 하고 있었으며, 이에 따라 퍼스널 브랜딩 의 여정을 시작한 집단이 초기 설계 단계에서 겪는 문제를 해결할 수 있는 솔루션에 대해 고민하게 됨.

d23

셀피스에서 해결할 문제

  • 나를 브랜딩하여 세상에 알리고자 하는 집단이, 퍼스널 브랜딩의 초기에 나를 이해하는 과정 에서 겪는 문제

  • 퍼스널 브랜딩은 [인지-탐색-구축-확산-관리]의 과정을 거치는데, 셀피스는 [인지-탐색-구축] 단계에서 겪는 어려움을 해결하고자 함.

  • [인지-탐색-구축]의 과정에 해당되는 자기이해에 대한 강력한 동기와 문제를 가지고 있기에 아래의 문제를 해결하려 함.

1️⃣ 자기이해에 대한 어려움

  • 퍼스널 브랜딩을 시작하는 단계에서, 자신을 정의하고 강점과 적성을 파악하는 데 어려움을 겪음.

  • ex) 나는 어떤 사람인지, 무엇을 잘하고, 무엇에 대한 열정이 있는지 파악하기 어렵다.
    내가 어떤 브랜드를 구축해서 퍼스널 브랜딩을 성공시킬 수 있을지 모르겠다.
    자신의 강점과 약점을 명확하게 알지 못하여 브랜드 방향 설정에 어려움을 겪는다.
    타인과 차별화되는 자신만의 매력을 찾는데에 어려움이 있다.

2️⃣ 자기이해와 퍼스널 브랜딩에 대한 이해 부족

  • 퍼스널 브랜딩의 과정에서, 자기이해와 어떤 방법에 따라 나를 이해하고 퍼스널 브랜딩을 해야 실패하지 않을 수 있는지 잘 알지 못함.

ex) 심층적으로 나의 전문성을 포지셔닝 하고싶은데, 그 방법을 모르겠다.
나와 유사한 방향성을 가진 사람들의 사례를 접하기 어렵다.
퍼스널브랜딩과 관련한 방대한 학습 자원이 산발적으로 존재해 한 곳에서의 탐색이 어렵다.

셀피스에서 제안하는 솔루션

  • 퍼스널 브랜딩에 대한 니즈가 있는 집단이 브랜드 구축의 초기 단계에서 나의 정체성를 파악하고 확립하는 것을 도와줌.

1️⃣ 자기이해를 기반으로 한 정체성 확립

  • 퍼스널 브랜딩 이전에, 나의 적성과 흥미 등 나를 더 잘 이해할 수 있도록 함.
  • 간단한 과정을 통해, 자신의 흥미와 적성을 확인할 수 있다.
  • 내 흥미와 적성을 살린 브랜드 방향을 설정할 수 있다.
  • 타인과 차별화된 나만의 키워드를 확인하고, 정의한다.

1️⃣의 솔루션으로 셀피스에서 제공하는 테스트 3가지를 개발하였다.

2️⃣ 브랜드 방향성 설정 및 정보 제공

  • 퍼스널 브랜딩과 자기이해에 대한 교육 프로그램을 제공하여, 퍼스널브랜딩에 대한 정보 및 지식을 함양할 수 있도록 함.
  • 서비스에서 다 해소하지 못한 자기이해에 대한 니즈를 교육 프로그램 수강을 통해 달성한다.
  • 퍼스널 브랜딩과 관한 강의를 통해, 퍼스널 브랜딩을 더 잘할 수 있는 방법들을 학습한다.

2️⃣의 솔루션으로 셀피스에서 자기이해 & 브랜딩을 도울만한 다양한 콘텐츠들을 제공하였다.

🧑🏻‍💻 위 내용 말고도 훨씬 많은 기획 내용들이 있는데... 여기서 다 적는 것은 무리일 것 같다. 밋업 프로젝트를 하면서 기획에서 해야 할 일이 이렇게나 많고, 또 이렇게까지 해줄 수 있는 거구나 라는 걸 느낄 수 있었다. 더욱 자세한 내용이 궁금하다면 깃허브 링크로 .. 👇🏻

Github : https://github.com/KUSITMS-29th-TEAM-D

나. 팀원

팀원

우리 팀은 8명으로 구성되었고, 내가 테크리드를 맡게 되었다!
그렇게 큰 일을 하는 것은 아니고, 개발 파트의 일정을 관리하여 시간 내에 개발이 완수되도록 조정하고 PM과 주로 소통을 진행하는 중간 다리 역할 느낌이었다.

물론 일을 조~금 더 한 것 같기는 하지만 어떻게 해야 일정 관리를 잘 할 수 있을지에 대해 조금 더 고민해볼 수 있어서 좋았던 것 같다. 이번에는 개발을 조금 빠듯하게 진행한 것 같아 ... 이 부분에서의 나의 역량이 조금 부족했던 것 같다 🥲

어떻게 해야 부담스럽지 않게 재촉(?)할 수 있는지 .. 참 어려운 것 같다! 그래도 다들 별 말 없이 잘 따라주어서 개발 팀원들에게 고마웠다.

다. 서비스 블루프린트

기획 & 디자인과 이렇게 길게 협업을 해 본 것은 처음이었기 때문에, IA & 서비스 블루프린트 등등 다소 생소한 용어들을 처음 알게 되었다.
그리고 서비스 블루 프린트에는 개발과 관련한 내용도 들어가기에 제작에 참여했었는데, 기억에 남아 적어보려고 한다.

블1

블2

위와 같이 사용자가 서비스를 진행하며 행동하게 되는 액션들과 그에 따라 Front & Back에서 동작하는 방식들이 나와있다.
기존에는 기능 명세서 & 플로우차트 정도만 작성을 하고 머릿 속에서 생각을 하며 개발을 했었던 것 같은데, 이번에는 이렇게 서비스 블루프린트를 그려 보며 흐름을 정리하니 훨씬 이해하기가 수월했던 것 같다!

또한 이외에도 유저 페르소나 & BM 등 대부분의 기획 단계를 볼 수 있어서 정말 유익했던 것 같다. 그동안에는 잘 몰랐던 용어들에도 익숙해졌고, 내가 왜 이 기능을 만들어야 하는가가 더욱 명확해져서 개발하는 동기도 더 생겼던 것 같다.

이전에 유튜브에서 본 영상 중, 개발자는 코딩만 잘해서는 안 되고 비즈니스를 이해해야 한다는 말이 기억에 남았는데 왜 그런지 조금씩 알아가는 듯하다.

그래서 나도 딱 개발만! 하기 보다는 조금 더 시야를 넓게 하여 서비스를 전체적으로 파악하며 프로젝트를 진행하려고 노력하는 듯하다.
물론 기본적으로 개발을 잘 해야겠지만.. 😅 장기적으로는 나에게 분명 도움이 될 것이라고 생각한다!

기획을 잘 몰라서 이 정도밖에 적는 것이 아쉬울 정도로 너무 탄탄한 기획을 해 준 팀원들에게 고마움을 느낀다 👍🏻👍🏻


🎨 3. 디자인

디자인도 잘 모르지만 .. 빼 놓을 수가 없기에 간략하게 적어보려고 한다.

가. 디자인 무드보드

74

21

이러한 것들을 디자인 무드보드(?) 라고 하나 보다!
디자인 팀원들이 너무 실력이 좋았어서.. 나는 그저 행복하게 개발을 진행했던 것 같다.
디자인이 처음 나왔을 때 너무 예뻐서 놀랐다 😮

테스트를 진행하고 나면 결과로 위 이미지와 같은 카드를 유저에게 제공하는 방식이기 때문에, 디자인이 공유하고 싶을 만한 퀄리티여야 한다고 생각했는데 걱정이 무색할 정도였다.

그동안 거의 개발자들끼리만 프로젝트를 하다가 이렇게 딱 각자의 역량을 펼칠 수 있는 프로젝트를 하니까 확실히 여러 면에서 퀄리티가 높아져서 정말 만족스러웠던 것 같다.

여전히 디자인에 대해서는 문외한이지만, 그래도 디자이너들이 어떤 용어 및 툴들을 사용하고 어떤 프로세스로 작업을 진행하는 지 조금은 알 수 있어서 큰 도움이 되었다!
고생해 준 디자인 팀원들 너무 고맙다 👍🏻👍🏻


💻 4. 개발

지금부터는 백엔드에서는 어떤 점을 중점을 두어 이번 프로젝트에 임했고, 어떠한 기술로 기능들을 개발했는지 등을 말해보려고 한다.

가. 각자의 목표

민정

우선 민정이는 이전 프로젝트에서 이미 무중단 배포를 해 본 경험이 있었지만, 제대로 이해하고 쓴 것 같지는 않아 이번에는 이해까지 한 상태로 해 보고 싶다고 하였다.
즉, Github Actions, Docker, NginX 를 사용할 블루-그린 무중단 배포를 하고 싶다고 하였다.

실제로 개발을 진행하기 전에 CI/CD를 구축할 수 있었는데, 이러한 점 덕분에 브랜치에서 진행된 개발 사항을 develop 브랜치로 올리기만 하면 배포가 진행되어 편리했던 것 같다.
또한 테스트도 진행해 주어, Merge를 하기 전에 코드에 문제가 있는지도 확인해볼 수 있어서 좋았다.

한 가지 또 언급하고 넘어갈 점은 이번 29기 밋업 프로젝트에서는 네이버 클라우드의 크레딧 지원을 받아 서버, DB, 도커 레지스트리, AI 등 모든 클라우드를 NCP 한 곳에서 이용할 수 있어서 좋았던 것 같다.

성능 상 느린 점도 딱히 느끼지 못했고, 무엇보다도 원하는 기능들을 써볼 수 있었기 때문에 개발자 입장에서는 좋았다 👍🏻

나도 별반 다르지 않았는데, 이전에 Github Actions, Docker, NginX를 통해서 배포를 해 본 적은 있으나 제대로 이해한 것 같은 느낌이 아니었다.
특히나 NginX는 아예 모른 상태로 사용을 했던 것 같아서.. 이전에 문제가 생겼을 때 골머리를 앓았던 적도 있었다.

때문에 나도 CI/CD를 잘 구축을 하는 것이 1차적인 목표였고, 그 다음으로는 DB를 잘 설계해 보고 싶었다. DB 공부라 하면 학교 수업에서 1번 들은 것과, SQLD를 딴 것이 전부인데... 이걸로는 절대 좋은 DB 설계를 할 수 없을 것 같았다.

또한 아직 개발을 많이 해본 것은 아니지만, 할수록 코드를 짜는 것보다 설계를 하는 것과 DB에 대해 잘 아는 것이 더 중요하다는 생각이 들어 이러한 목표를 세웠었다.

결론적으로 DB에 대해 깊게 공부를 하지는 못했던 것 같다. 다만 전보다는 조금? 더 이해가 된 상태로 설계를 하고 JPA를 사용했던 것 같기는 하다. 이 부분은 아직도 많이 부족하다고 생각 하여 꾸준히 공부를 해 나가야 할 부분인 것 같다!

나. 설계

개발을 진행하기 전에 다양한 설계를 진행해야 한다.
대표적으로 ERD, 프로젝트 아키텍처 등인데 이에 대해 말해보려고 한다.
참고로 이 때 당시에 한 달에 ERD 설계만 3번을 하니까 너무 질렸다.. 🤮

1) 프로젝트 아키텍처

selpiece_아키텍처

프론트엔드

a. Organization 레퍼지토리로Vercel 배포를 진행할 경우 유료 서비스를 이용해야 하기에, Fork Repository 배포용으로 활용함

b. Git Flow 전략을 사용하여 각 로컬에서 개발을 진행한 후, Organization Main Brach로 Merge 진행

c. Github Actions가 동작하여 Fork 레퍼지토리로 자동 동기화 진행

d. Vercel 동작하며 배포 완료

e. 추가적으로, Organization 레퍼지토리와 Slack을 연동하여 PR 또는 Merge가 완료되면 알림을 받을 수 있도록 함

백엔드

a. Git Flow 전략을 사용하여 각 로컬에서 개발을 진행한 후, Github Develop Branch에 Merge 진행

b. Github Actions가 동작하여 build된 jar파일을 이미지화하여 NCP Container Registry에 Push

c. NCP 서버에 접속한 후 이미지를 Pull 받아 컨테이너 생성 및 실행

d. Nginx의 리버스 프록시를 새롭게 생성된 컨테이너로 변경하여 새로운 버전으로 배포 완료

e. 기존 컨테이너 실행 종료

🧑🏻‍💻 위와 같은 프로세스로 개발 및 배포가 진행되었다. NCP를 사용하였고, 무중단 배포를 구축하였으며, Redis를 사용한 점 등이 새로웠다.

2) ERD

image

🧑🏻‍💻 ERD는 항상 쓰던 ERD cloud에서 제작했고, 테이블 개수가 조금 많은 것 같기는 하지만 그래도 최대한 깔끔하게 짜려고 노력했다! ERD는 어떻게 해야 잘 짜는 것인지 아직은 잘 모르겠다 ...

다. CI/CD

위에서도 여러 번 언급을 했지만, 자세한 동작 과정에 대해서도 한 번 적어보려고 한다.

Github Actions

name: Coolpiece CI/CD with Gradle

on:
  push:
    branches: [ "main", "develop" ]
  pull_request:
    branches: [ "main", "develop" ]

permissions:
  contents: read

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: 🧁 Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: 🧁 Gradle Caching - 빌드 시간 향상
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: 🧁 gradle build를 위한 권한을 부여합니다.
        run: chmod +x gradlew

      - name: 🧁 gradle build 중입니다.
        run: ./gradlew build -x test #여기까지가 CI

      - name: 🧁 NCP Container Registry에 로그인 후, docker image build 후 NCP Container Registry에 push합니다.
        run: |
          docker login -u ${{ secrets.NCP_API_ACCESS_KEY }} -p ${{ secrets.NCP_API_SECRET_KEY }} ${{secrets.NCP_CONTAINER_REGISTRY_PUBLIC_ENDPOINT}}
          docker build -f Dockerfile -t ${{ secrets.NCP_CONTAINER_REGISTRY_PUBLIC_ENDPOINT }}/${{ secrets.NCP_CONTAINER_REGISTRY_IMAGE }} .
          docker push ${{ secrets.NCP_CONTAINER_REGISTRY_PUBLIC_ENDPOINT }}/${{ secrets.NCP_CONTAINER_REGISTRY_IMAGE }}

      - name: 🧁 NCP Container Registry에서 pull 후 deploy합니다.
        uses: appleboy/ssh-action@master
        with:
          username: ${{ secrets.NCP_SERVER_USERNAME }}
          password: ${{ secrets.NCP_SERVER_PASSWORD }}
          host: ${{ secrets.NCP_SERVER_HOST }}
          port: ${{ secrets.NCP_SERVER_PORT }}
          script: |
            chmod 777 ./deploy.sh
            ./deploy.sh
            docker image prune -f

Github Actions 파이프라인 스크립트로, .github/workflows.yml 형태로 존재하게 된다.
메인 혹은 디벨롭 브랜치에 Push or PR이 오게 되면, 해당 스크립트가 작동하게 된다.
환경변수 또한 Github Actions의 시크릿으로 설정할 수가 있다.

우선은 빌드를 진행한 후에 Docker Image로 만들어 NCP 컨테이너 레지스트리에 해당 이미지를 Push한다.

그리고 서버에 ssh 접속을 진행한다.

docker-compose.yml

version: "3.9"
services:
  blue:
    image: coolpiece-registry.kr.ncr.ntruss.com/coolpiece2
    container_name: blue
    restart: always
    env_file:
      - .env
    ports:
      - 8081:8080
    environment:
      - TZ=Asia/Seoul
  green:
    image: coolpiece-registry.kr.ncr.ntruss.com/coolpiece2
    container_name: green
    restart: always
    env_file:
      - .env
    ports:
      - 8082:8080
    environment:
      - TZ=Asia/Seoul
  redis:
    image: redis:alpine
    restart: always
    container_name: redis
    hostname: redis
    ports:
      - "6379:6379"
    command: redis-server --bind 0.0.0.0

서버 내부에는 위와 같은 도커 컴포즈 파일이 존재한다.
이는 블루 or 그린 컨테이너를 편리하게 생성 및 실행할 수 있게 해준다. 또한 레디스도 사용했기 때문에 설정해두었다.

여기서 주의해야 할 점이, 가능하면 버전을 최신으로 사용하는 것이 좋다.
처음에는 그러지 않았더니 종종 컨테이너를 생성하는 부분에서 문제가 생겨 CI/CD가 제대로 되지 않았었다.
그래서 최신 버전으로 사용을 하고, 버전 또한 큰 따옴표 " 를 사용해서 명시해 주었더니 문제가 해결 되었었다.

deploy.sh

#!/bin/bash


# 현재 실행중인 App이 blue인지 확인합니다.
IS_GREEN=$(docker ps | grep green) 
DEFAULT_CONF=" /etc/nginx/nginx.conf"

#blue가 실행중이라면 green을 up합니다.
if [ -z $IS_GREEN  ];then

  echo "### BLUE => GREEN ###"

  echo ">>> 1.green image를 pull합니다."
  docker compose pull green

  echo ">>> 2.green container를 up합니다."
  docker compose up -d green 
  
  while [ 1 = 1 ]; do
  echo ">>> 3.green health check 중..."
  sleep 3

  REQUEST=$(curl http://127.0.0.1:8082) # green으로 request
    if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지
            echo "🧁health check success!!!"
            break ;
            fi
  done;

  echo ">>> 4. nginx를 다시 실행합니다."
  sudo cp /etc/nginx/green-nginx.conf /etc/nginx/nginx.conf
  sudo nginx -s reload

  echo ">>>v5. blue container를down합니다."
  docker compose stop blue

# green이 실행중이면 blue를 up합니다.  
else
  echo "### GREEN => BLUE ###"

  echo ">>> 1. blue image를 pull합니다."
  docker compose pull blue

  echo ">>> 2. blue container up합니다."
  docker compose up -d blue

  while [ 1 = 1 ]; do
    echo ">>> 3. blue health check 중..."
    sleep 3
    REQUEST=$(curl http://127.0.0.1:8081) # blue로 request

    if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지
      echo "🧁health check success!!!"
      break ;
    fi
  done;

  echo ">>> 4. nginx를 다시 실행합니다." 
  sudo cp /etc/nginx/blue-nginx.conf /etc/nginx/nginx.conf
  sudo nginx -s reload

  echo ">>> 5. green container를 down합니다."
  docker-compose stop green
fi

도커 컴포즈 파일과 같은 위치에는 위 스크립트가 있어야 한다.

이는 현재 실행 중인 컨테이너가 블루 or 그린인지를 확인한 후에,
1) 블루라면 그린 실행, 블루 종료
2) 그린이라면 블루 실행, 그린 종료
와 같은 동작으로 새로운 버전을 배포하게 된다.

블루는 8081 포트를 사용을 하고, 그린은 8082 포트를 사용하게 되는데, 이는 NginX를 리로드 하며 라우팅을 변경하여 문제 없이 진행이 된다.

다만 이제 조금 의문이었던 점은,
블루 = 기존에 있던 것 (구 버전)
그린 = 새로운 것 (신 버전)
으로 알고 있었는데 지금과 같이 진행을 하게 되면 블루와 그린이 1번씩 왔다갔다 하기 때문에 의미가 조금 안 맞지 않나..? 라는 생각도 들기는 하였다.

하지만 어떻게 보면 이름만 다른 것이고 신 버전을 띄우고 구 버전을 종료한다는 프로세스는 동일하기 때문에 우선 사용을 했던 것 같다.
블루-그린 무중단 배포 방식에 대해서 조금 더 알아 보며 의문점을 풀어 보면 좋을 것 같다!

라. 소셜 로그인 구현

큐시즘 기업 프로젝트, 큐커톤에 이어서 밋업 프로젝트에서도 내가 소셜 로그인을 담당해서 구현을 하였다.
Spring Security를 사용해서 소셜 로그인을 구현 하는 것이 벌써 5번째였는데... 그래서 조금 더 빠르게 구현이 가능할 것 같아 내가 하기로 하였다.
하지만 그동안에는 많이 모르고 진행했던 점도 많았고, 버전이 변경된 점도 많아서 조금 더 공부를 진행했었다.

https://velog.io/@hsh111366/Spring-Security-백엔드에서-소셜-로그인-구현하기-프론트에서-해야-할-일-총정리-feat.-OAuth2.0

그렇게 구현을 한 후에 위처럼 글로 정리를 했었는데, 반응도 좋았고 실제로 같은 백엔드 파트 친구들이 도움을 받았다고 말해줘서 많이 뿌듯했었다.
딱 시작 부분까지만 정리가 되어 있고, 이후 리프레쉬 토큰이나 쿠키 등 보안적인 부분은 작성하지 못한 글이라 추후 2탄을 작성하고자 한다.

위처럼 네이버, 카카오, 구글 3종류로 진행을 했다.
소셜 로그인 형태는 거의 비슷하기 때문에, 한 번 잘 구현해 두면 여러 개를 만드는 것은 어렵지 않은 것 같다.

1) Redis

이번에는 처음으로 Redis를 사용해서 리프레쉬 토큰을 관리해보았다.
생각해 보면 조금 부끄럽지만 애초에 리프레쉬 토큰을 사용한 것이 이번이 처음인 것 같다 😂
그동안에는 소셜 로그인이 동작하게 하는 것만에도 급급해서 보안 및 UX 적으로는 신경을 거의 못 썼던 것 같은데, 이번에는 많이 해볼 수 있었던 것 같다!

어찌되었든 유저의 리프레쉬 토큰을 발행한 후에 Redis에 담아 두었고, 비교가 필요할 시 꺼내서 비교하는 식으로 진행했다.
Redis를 사용하는 것이 크게 어렵지는 않아서 다행히 금방 할 수 있었다.

하지만 리프레쉬 토큰 하나만을 위해서 Redis를 사용하는 것이 효율적인가?에 대한 논의도 학회원들과 잠깐 나누게 되었는데, 사실 그렇다고 답하지는 못했던 것 같다.

그래서 다음에 만약 사용을 하게 된다면 조금 더 논리적으로 왜 이 기술을 사용해야 하는가? 프로젝트에 정말 필요한가?를 더욱 생각해봐야겠다고 느꼈다!

액세스 토큰은 로그인 시에 쿼리 파라미터로 보낸 후에 프론트에서 관리하기 때문에 백엔드에서 더 이상 관리할 수는 없었지만, 리프레쉬 토큰은 백엔드에서 관리하는 것이 보안성 면에서도 더 좋겠다고 생각했다.

때문에 쿠키를 사용해서 리프레쉬 토큰을 관리했고, 백엔드 측에서만 접근할 수 있도록 보안 처리를 하여 아무나 꺼낼 수 없도록 하였다.

또한 리프레쉬 토큰이 탈취되는 상황을 우려하여, 유저 당 1개의 리프레쉬 토큰만 발급되도록 하였고, 액세스 토큰을 재발행 할 때는 리프레쉬 토큰도 함께 재발행하는 방식으로 구현을 진행했다. 여기서는 아래의 글을 많이 참고하였다.

https://mgyo.tistory.com/832

3) Register Token

말 그대로 유저를 등록하기 위한 토큰인데, 온보딩 과정에서만 사용되는 토큰이었다.
소셜 로그인을 진행하고 나면 보통 유저에게 닉네임, 성별, 나이, 직업 등 더욱 다양한 정보들을 요구하게 된다.

유저가 소셜 로그인을 통해서 처음 들어 오면 자동으로 DB에 저장이 되어 회원가입 처리가 되고, 온보딩을 진행하게 되는데, 만약 온보딩을 진행하던 중에 종료를 하거나 오류가 생긴다면 문제가 생기게 된다. 유저는 회원 가입이 되었는데, 필요한 정보들은 DB에 없는 것이다.

그래서 이러한 상황을 방지하기 위해서 레지스터 토큰을 추가로 도입하였다.
과정은 아래와 같다.

1️⃣ 신규 유저라면 바로 DB에 저장하는 것이 아니라 우선 레지스터 토큰을 발급하여 프론트로 보낸다. 레지스터 토큰에는 유저의 기본 정보가 담겨 있다.
2️⃣ 프론트에서는 레지스터 토큰이 오면 유저 온보딩을 진행한다.
3️⃣ 유저가 올바르게 온보딩을 진행하며 입력하면, 프론트에서는 해당 입력 값들과 레지스터 토큰을 백엔드로 보낸다.
4️⃣ 백엔드에서는 이를 확인하고 문제가 없다면 DB에 저장하여 회원가입을 마친다.
5️⃣ 유저의 UUID가 담긴 액세스 토큰을 프론트로 보내준다.

이러한 방식은 세미 밋업 데이 때 타 팀 백엔드 친구들과 대화를 하며 알게 되었는데 정말 유익했던 것 같다 👍🏻 이래서 많은 사람들을 만나 대화를 해 보아야 좋은 것 같다!

🧑🏻‍💻 이처럼 로그인 & 회원 가입 부분에서도 생각 보다 많은 것들을 배우고 적용해볼 수 있었다. 그래서 시간이 조금 걸리기는 했지만.. 보안성까지 조금 더 신경쓸 수 있어서 뿌듯했다.

마. BaseEntity 사용

package kusitms.jangkku.global.common.dao;

import jakarta.persistence.*;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public abstract class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @CreatedDate
    @Column(updatable = false)
    @Convert(converter = Jsr310JpaConverters.LocalDateTimeConverter.class)
    private LocalDateTime createdDate;

    @LastModifiedDate
    @Column
    @Convert(converter = Jsr310JpaConverters.LocalDateTimeConverter.class)
    private LocalDateTime updatedDate;
}

이런 식으로 BaseEntity라는 추상 클래스를 만들어서, 엔터티에서 이를 상속 받게 했다.
그렇게 하면 생성일자, 수정일자 컬럼을 해당 엔터티에서 굳이 만들지 않아도 되어 편리했다.

다만 여기서는 id까지 상속 받도록 하였는데, 컬럼명이 너무 통일되어 버려서 다음부터는 id는 넣지 않는 게 좋을 것 같다!

사. DTO of & to 메서드 사용

public static DiscoverPersonaDto.QuestionResponse of(Long chattingId, String question, Boolean isComplete) {
            return DiscoverPersonaDto.QuestionResponse.builder()
                    .chattingId(chattingId)
                    .question(question)
                    .isComplete(isComplete)
                    .build();
        }

DTO -> 엔터티 객체 or 엔터티 객체 -> DTO 로의 변환을 편리하게 해주기 위해 이러한 메서드를 DTO에서 사용하는 것으로 알고 있다. 그래서 이번에 처음 사용을 해보았는데 위 코드는 좀 잘못 짠 것 같다..

of 메서드의 인자로 엔터티 객체를 받은 후에, getter로 값을 불러와 DTO를 만드는 것이 옳은 방식인 것 같다. 다행히 다른 프로젝트에서는 이를 깨달아서 개선을 진행하기는 했었다.

어쨌든 예전에는 service 단에서 되게 많은 작업들을 수행했었는데, 조금씩 더 분리를 하고 있는 것 같다는 느낌이 들었다.

아. 테스트 알고리즘 구현

셀피스에는 3가지의 테스트가 존재하는데, 그 중에서도 현재의 자기자신을 파악하는 정의하기 테스트 (Define) 에서 필요한 알고리즘이었다.

테스트 로직은 아래와 같다.

사용자는 위와 같이 5개의 키워드를 선택하게 되고, 이를 총 3번 반복한다.
이러한 키워드들은 홀랜드 검사에서 착안하였고, 각 챕터마다 선택한 키워드를 기반으로 1개의 유형이 나오게 된다.

예를 들어, 첫 번째가 현실형(R) vs 사회형(S)이고 유저가 현실형 키워드 3개, 사회형 키워드 2개를 선택했다면, 현실형(R)이 되는 것이다.

이렇게 총 3개가 선택되고 최종적인 유형이 나오며, 그에 따른 조각 카드를 발급해준다.

그래서 사실 로직 자체가 어렵지는 않았다. 단순하게 개수를 세서 3가지 유형을 파악한 후에 최종적인 결과만 보내주면 되기 때문이다.

이를 위해서는 우선 백엔드에서 모든 키워드를 저장하고 있어야했는데, DB에 넣어두면 매번 꺼내야 하기 때문에 성능이 별로일 것 같았다. 그래서 아래와 같이 Enum을 사용했다.

package kusitms.jangkku.domain.persona.constant;

import java.util.Arrays;
import java.util.List;

public enum Keyword {
    REALISTIC(Arrays.asList(
            "거침 없는",
            "솔직한",
            "욕심이 없는",
            "지구력 있는",
            "활동적인",
            "소소함을 즐기는",
            "낯가리는",
            "단순한")),
    SOCIAL(Arrays.asList(
            "사람들을 좋아하는",
            "어울리기 좋아하는",
            "친절한",
            "이해심 많은",
            "남을 잘 도와주는",
            "봉사적인",
            "감정적인",
            "이상주의적인")),
    INVESTIGATIVE(Arrays.asList(
            "탐구적인",
            "논리적인",
            "분석적인",
            "합리적인",
            "정확한",
            "지적 호기심이 풍부한",
            "비판적인",
            "내성적인",
            "수줍은",
            "신중한")),
    ENTERPRISING(Arrays.asList(
            "리더십 있는",
            "통솔력 있는",
            "지도력 있는",
            "말을 잘 하는",
            "설득력 있는",
            "경제적인",
            "야망 있는",
            "외향적인",
            "낙관적인",
            "열정적인")),
    ARTISTIC(Arrays.asList(
            "상상력이 풍부한",
            "감수성 강한",
            "자유분방한",
            "개방적인",
            "독창적인",
            "개성적인",
            "개인중심의")),
    CONVENTION(Arrays.asList(
            "정확한",
            "완벽주의인",
            "조심성 많은",
            "세밀한",
            "계획적인",
            "안정적인",
            "완고한",
            "책임감 있는"));

    private final List<String> keywords;

    Keyword(List<String> keywords) {
        this.keywords = keywords;
    }

    public List<String> getKeywords() {
        return keywords;
    }
}

Enum을 제대로 써 본 것도 이번이 처음이라.. 이렇게 쓰는 게 맞는지는 모르겠지만 우선은 사용했다.
그래서 위와 같이 키워드들을 관리했다.

	// 첫번째 유형 도출 메서드
    private String judgeStepOneType(List<String> stepOneKeywords, List<String> definePersonaKeywords) {
        List<String> realisticKeywords = Keyword.REALISTIC.getKeywords();
        List<String> socialKeywords = Keyword.SOCIAL.getKeywords();

        List<String> moreCountKeywords = judgeMoreCountKeywords(stepOneKeywords, realisticKeywords, socialKeywords);
        definePersonaKeywords.add(moreCountKeywords.get(0));
        definePersonaKeywords.add(moreCountKeywords.get(1));

        return realisticKeywords.contains(moreCountKeywords.get(0)) ? "R" : "S";
    }

    // 두번째 유형 도출 메서드
    private String judgeStepTwoType(List<String> stepTwoKeywords, List<String> definePersonaKeywords) {
        List<String> investigativeKeywords = Keyword.INVESTIGATIVE.getKeywords();
        List<String> enterprisingKeywords = Keyword.ENTERPRISING.getKeywords();

        List<String> moreCountKeywords = judgeMoreCountKeywords(stepTwoKeywords, investigativeKeywords, enterprisingKeywords);
        definePersonaKeywords.add(moreCountKeywords.get(0));
        definePersonaKeywords.add(moreCountKeywords.get(1));

        return investigativeKeywords.contains(moreCountKeywords.get(0)) ? "I" : "E";
    }

    // 세번째 유형 도출 메서드
    private String judgeStepThreeType(List<String> stepThreeKeywords, List<String> definePersonaKeywords) {
        List<String> artisticKeywords = Keyword.ARTISTIC.getKeywords();
        List<String> conventionKeywords = Keyword.CONVENTION.getKeywords();

        List<String> moreCountKeywords = judgeMoreCountKeywords(stepThreeKeywords, artisticKeywords, conventionKeywords);
        definePersonaKeywords.add(moreCountKeywords.get(0));

        return artisticKeywords.contains(moreCountKeywords.get(0)) ? "A" : "C";
    }

그리고 이러한 메서드를 만들어 유저가 선택한 키워드들을 기반으로 3가지 유형을 뽑아냈다.
만약에 RIA라는 유형이 나왔다고 해보자. 이는 백엔드에서만 사용하는 유형 코드이기 때문에 프론트에게는 서비스에서 사용하는 이름으로 보내주어야 했다.

때문에 아래와 같은 과정이 필요했다.

package kusitms.jangkku.domain.persona.constant;

public enum Type {
    CREATOR("RIA", "크리에이터"),
    INSIGHTER("RIC", "인사이터"),
    INNOVATOR("REA", "이노베이터"),
    INVENTOR("REC", "인벤터"),
    PROJECTOR("SIA", "프로젝터"),
    CONNECTOR("SIC", "커넥터"),
    ENCOURAGER("SEA", "인커리져"),
    ORGANIZER("SEC", "오거나이져");

    private final String code;
    private final String name;


    Type(String code, String name) {
        this.code = code;
        this.name = name;
    }

    public String getCode() {
        return code;
    }

    public String getName() {
        return name;
    }
}

이런식으로 서비스에서 사용하는 조각의 이름들을 Enum으로 만들어 두었고,

	// 3가지 유형으로 페르소나를 판단하는 메서드
    private String judgeDefinePersonaName(String definePersonaCode) {

        for (Type type : Type.values()) {
            if (type.getCode().equals(definePersonaCode)) {
                return type.getName();
            }
        }

        throw new PersonaException(PersonaErrorResult.NOT_FOUND_PERSONA_TYPE);
    }

이 중에서 일치하는 유형을 찾아서 반환하도록 하였다.

🧑🏻‍💻 MBTI 유형 테스트를 만드는 것처럼, 크게 어려운 작업은 아니었다. 하지만 어떻게 해야 좀 더 직관적으로 이해가 되고 효율적인 코드를 짤 지 고민해볼 수 있었던 시간이었던 것 같다!

자. 네이버 클로바 Studio 사용

그리고 AI도 나름 사용해볼 수 있었는데, 네이버 클라우드 크레딧이 꽤나 많았기 때문에 편한 마음으로 이것저것 테스트 해 볼 수 있었다!

1) 이해하기 페르소나 테스트(Discover)

위에서 말한 정의하기 테스트 (Define)이 현재라면, 이해하기 테스트는 유저의 과거를 알아 볼 수 있는 기능이다.

이는 위와 같은 셀퍼 라는 챗봇과의 대화를 통해서 이루어진다. 챕터는 건강, 커리어, 사랑, 여가 총 4가지로 이루어져 있으며, 각각 3개의 질문을 진행하게 된다

유저가 질문에 대해 답을 하면, 셀퍼는 반응 및 공감을 해준 후에 다음 질문을 묻는다.
또한 유저의 답변은 실시간으로 오른쪽에 요약되어 나타나게 되어, 본인이 어떻게 대답했는지 다시 확인해볼 수 있다. 이는 다른 카테고리로 넘어가더라도 동일하게 유지된다.

한 챕터를 끝낸다면, 즉 3개의 질문에 대해 모두 대답을 했다면 위처럼 유저의 대답에서 키워드를 6개 뽑아 볼 수 있도록 해준다.

테스트 로직은 위와 같고, 이를 위해서는 아래와 같은 기능들이 필요했다.

1) 유저에게 카테고리별로 질문

질문은 팀에서 제작한 것이기 때문에 이 또한 Enum으로 관리했다.
또한 유저에게 어느 질문까지 했는지를 알아야 하기 때문에, 질문 리스트를 DB에 저장하여 하지 않은 질문부터 다시 하도록 구현했다.

2) 유저의 답변에 대한 공감 & 요약

따지고 보면 챗봇은 아닌데, 챗봇처럼 구현한 것이기 때문에 답변에 대한 공감이 필요했다. 여기서 네이버 클로바 스튜디오를 활용했다.

	// 공감과 요약을 생성해 응답하는 메서드
    @Override
    public DiscoverPersonaDto.AnswerResponse getReactionAndSummary(String authorizationHeader, DiscoverPersonaDto.AnswerRequest answerRequest) {
        User user = jwtUtil.getUserFromHeader(authorizationHeader);

        DiscoverPersonaChatting discoverPersonaChatting = discoverPersonaChattingRepository.findById(answerRequest.getChattingId())
                .orElseThrow(() -> new PersonaException(PersonaErrorResult.NOT_FOUND_CHATTING));

        DiscoverPersona discoverPersona = discoverPersonaChatting.getDiscoverPersona();

        String reaction = clovaService.createDiscoverPersonaReaction(answerRequest.getAnswer());
        // 마지막 대화인 경우 마무리 멘트 추가
        if (discoverPersona.getIsComplete()) {
            String category = discoverPersona.getCategory();
            String finalComment = getConversation(category, 0);
            reaction += finalComment;
        }
        String summary = clovaService.createDiscoverPersonaSummary(answerRequest.getAnswer());

        discoverPersonaChatting.updateAnswer(answerRequest.getAnswer());
        discoverPersonaChatting.updateReaction(reaction);
        discoverPersonaChatting.updateSummary(summary);
        discoverPersonaChattingRepository.save(discoverPersonaChatting);

        // 대화가 완료된 경우 키워드 생성
        if (discoverPersona.getIsComplete()) {
            createPersonaKeywords(discoverPersona);
        }

        return DiscoverPersonaDto.AnswerResponse.of(discoverPersonaChatting.getQuestion(), reaction, summary);
    }

서비스 코드는 위와 같았고, 클로바 API와 통신하는 로직은 아래의 clovaService에서 진행했다.

package kusitms.jangkku.domain.clova.application;

import jakarta.transaction.Transactional;
import kusitms.jangkku.domain.clova.dto.ClovaDto;
import kusitms.jangkku.domain.clova.dto.Message;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
@Transactional
@RequiredArgsConstructor
public class ClovaServiceImpl implements ClovaService {

    @Value("${clova.api.url}")
    public String apiUrl;
    @Value("${clova.api.api-key}")
    private String apiKey;
    @Value("${clova.api.api-gateway-key}")
    private String apiGatewayKey;
    private final WebClient webClient;

    // 설계하기 페르소나를 CLOVA로 생성하는 메서드
    @Override
    public String createDesignPersona(String message) {
        ClovaDto.ChatBotRequestDto request = ClovaDto.ChatBotRequestDto.DesignPersonaRequestOf();
        request.getMessages().add(Message.creatUserOf(message));

        return requestWebClient(request);
    }

    // 돌아보기 페르소나 공감을 생성하는 메서드
    @Override
    public String createDiscoverPersonaReaction(String message) {
        ClovaDto.ChatBotRequestDto request = ClovaDto.ChatBotRequestDto.DiscoverPersonaReactionRequestOf();
        request.getMessages().add(Message.creatUserOf(message));

        return requestWebClient(request);
    }

    // 돌아보기 페르소나 요약을 생성하는 메서드
    @Override
    public String createDiscoverPersonaSummary(String message) {
        ClovaDto.ChatBotRequestDto request = ClovaDto.ChatBotRequestDto.DiscoverPersonaSummaryRequestOf();
        request.getMessages().add(Message.creatUserOf(message));

        return requestWebClient(request);
    }

    // 돌아보기 페르소나 키워드를 생성하는 메서드
    @Override
    public String createDiscoverPersonaKeywords(String message) {
        ClovaDto.ChatBotRequestDto request = ClovaDto.ChatBotRequestDto.DiscoverPersonaKeywordRequestOf();
        request.getMessages().add(Message.creatUserOf(message));

        return requestWebClient(request);
    }

    // CLOVA와 통신하여 답변을 가져오는 메서드
    public String requestWebClient(ClovaDto.ChatBotRequestDto request) {
        ClovaDto.ChatBotResponse message = webClient.post()
                .uri(apiUrl)
                .header("X-NCP-CLOVASTUDIO-API-KEY", apiKey)
                .header("X-NCP-APIGW-API-KEY", apiGatewayKey)
                .header("Content-Type", "application/json")
                .body(Mono.just(request), request.getClass())
                .retrieve()
                .bodyToMono(ClovaDto.ChatBotResponse.class)
                .block();

        return message.getResult().getMessage().getContent();
    }
}

클로바 스튜디오 API 레퍼런스를 보며 DTO 등을 구성했는데, 여기에는 프롬프팅 내용 등이 들어갔다. 자세한 코드는 아래와 같다.

package kusitms.jangkku.domain.clova.dto;

import jakarta.annotation.PostConstruct;
import lombok.Builder;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Base64;

@Data
@Builder
public class Message {
    private static String designPersonaPrompt;
    private static String reactionPrompt;
    private static String summaryPrompt;
    private static String keywordPrompt;
    private ROLE role;
    private String content;

    public enum ROLE {
        system, user, assistant
    }

    public static Message creatUserOf(String content) {
        return Message.builder()
                .role(ROLE.user)
                .content(content)
                .build();
    }

    public static Message creatSystemOf(String content) {
        return Message.builder()
                .role(ROLE.system)
                .content(content)
                .build();
    }

    public static Message createDesignPersonaSystemOf() {
        return Message.builder()
                .role(Message.ROLE.system)
                .content(new String(Base64.getDecoder().decode(designPersonaPrompt)))
                .build();
    }

    public static Message createReactionOf() {
        return Message.builder()
                .role(Message.ROLE.system)
                .content(new String(Base64.getDecoder().decode(reactionPrompt)))
                .build();
    }

    public static Message createSummaryOf() {
        return Message.builder()
                .role(Message.ROLE.system)
                .content(new String(Base64.getDecoder().decode(summaryPrompt)))
                .build();
    }

    public static Message createKeywordOf() {
        return Message.builder()
                .role(Message.ROLE.system)
                .content(new String(Base64.getDecoder().decode(keywordPrompt)))
                .build();
    }

    @Component
    public static class Config {
        @Value("${clova.prompt.design}")
        private String design;
        @Value("${clova.prompt.discover.reaction}")
        private String reaction;
        @Value("${clova.prompt.discover.summary}")
        private String summary;
        @Value("${clova.prompt.discover.keyword}")
        private String keyword;

        @PostConstruct
        public void init() {
            designPersonaPrompt = design;
            reactionPrompt = reaction;
            summaryPrompt = summary;
            keywordPrompt = keyword;
        }
    }
}

이러한 형태로 DTO를 만드는 메서드들을 만들어 놓았고, 필요시에 호출하여 만들어 사용했다.
또한 프롬프팅을 원래는 텍스트로 그냥 적어놓았었는데, 깔끔하지도 않고 밖에 노출하고 싶지 않아서 인코딩하여 환경 변수로 관리했다.

유저의 답변을 받으면 이러한 과정을 통해 공감과 요약을 생성했고, DB에도 저장한 후에 프론트 측으로 보내주었다.

3) 카테고리별 키워드 6개 도출

가장 마지막으로 진행되는 과정으로, 유저의 답변 3개를 기반으로 6개의 키워드를 뽑아내는 과정이다. 이 또한 프롬프팅을 통해서 키워드를 뽑아내도록 유도해서 얻을 수 있었다.

다만 조금 아쉬웠던 점은 AI가 요약 서비스에 특화되어서 답변을 키워드처럼 아주 짧게 요약할 뿐 새로운 형용사를 만들도록 유도하기는 어려웠다.

그리고 외부 API에다가 AI라서 그런지 호출을 하면 약 3~5초 정도의 시간이 소모되기는 하였다.

🧑🏻‍💻 위 기능을 만들기에 더 적합한 AI 기술들이 있었겠지만 개발 시간 상 동일한 AI를 프롬프팅으로 유도하여 사용하는 것이 한계였다. 그래도 생각보다 원하는 결과물과 유사하게 나오는 점이 뿌듯하고 신기했다!

🧑🏻‍💻 사실 해당 기능을 만드는 것은 불가능에 가까웠는데... 개발 마감 거의 일주일 전?쯤 부터 급하게 준비를 한 것 같다.
개발 자체는 한 1~2일 안에 끝낸 것 같고... 함께 해준 민하 누나가 정말 고생해서 잘 만들어 주어서 완성할 수 있었던 것 같다.
로직이 많이 복잡하기도 하고, 시간이 정말 부족해서 급하게 만들어 오류도 많았지만 그래도 해냈음에 큰 뿌듯함을 느꼈다..! 🙂

2) 설계하기 페르소나 테스트 (Design)

그리고 마지막으로 본인의 미래, 즉 본인을 브랜딩하기 위한 테스트인 설계하기 테스트에서도 네이버 클로바 스튜디오를 활용하였다.

여기서도 5개의 챕터로 나뉘어 유저가 정해진 개수의 키워드를 선택하게 된다.

정의하기 테스트에서는 꼭 5개의 키워드를 선택해야 만 진행이 되었었지만, 여기서는 그렇지는 않았다. 왜냐하면 개수를 세어야 하는 테스트는 아니기 때문이다.

이런 식으로 마지막까지 선택을 하고 난 후에, 결과를 보게 되면 아래와 같이 본인을 1줄로 요약해 준다.

🧑🏻‍💻 해당 기능은 5개의 챕터에서 유저에게 입력 받은 값들을 한 번에 프론트 측으로 받은 후에, 클로바 스튜디오로 보내 한 줄 요약을 받은 뒤에 다시 반환하는 식으로 구현했다.
클로바 API에 대해 어느정도 익숙해졌기 때문에 크게 어려운 작업은 아니었다.
다만 문장의 퀄리티를 더 높이고 싶었는데 그 점이 잘 안되어 아쉬웠다 🥲

대표적으로 새롭게 느끼고 고생해서 만든 기능은 이정도로 정리할 수 있을 것 같다!


🏆 5. 프로젝트 결과

그리고 5월 25일, 밋업데이가 진행되었고 발표 및 시연까지 완료를 하였다.

(시연영상 링크 : https://www.youtube.com/watch?v=jgDSXNzTFo4)

결론적으로 수상은 하지 못했다! 물론 아쉬웠지만 그래도 최선을 다했기에 후회는 남지 않았다.
역량이 부족했을 뿐이지, 열심히 하지 못해서 후회하는 것은 아니라 다행이었다.

오히려 더욱 많은 것을 배울 수 있던 것 같다. 이에 대해서는 아래에서 정리해보려 한다.


💡 6. 배운 점 & 느낀 점

가. 기획 & 디자인과의 협업

아마 가장 크게 배운 점이 아닐까 싶다.
이전에는 해커톤에서만 한 번 협업을 해 보고, 항상 개발자들끼리만 프로젝트를 했었는데 확실히 한계가 많이 느껴졌었다.
이러한 갈증을 해소하고자 큐시즘에 들어왔던 것이고, 그리고 이를 많이 충족할 수 있어서 행복한 프로젝트였던 것 같다.

새로운 분야에 대해서 알게 되는 것도 재미있었고, 나의 시야도 조금 더 넓어지는 듯한 느낌이 들어 좋았다!

나. 보안성

위에서도 말했던 것처럼 보안성을 높이고자 조금 더 신경을 쓴 부분이 있었다.
이러한 부분들은 사실 프로젝트 수준에서는 넘어갈 수도 있지만, 사소한 디테일 하나하나가 차이를 만드는 것이라고 생각하기 때문에 가능한 한 더 찾아보면서 하려 했던 것 같다.

앞으로 이러한 부분에 더 신경을 써서 개발을 진행하며, 공부 또한 제대로 해봐야겠다는 생각이 들었다!

다. 아키텍처 & 테스트 코드

밋업 때 심사를 받으면서 느낀 점이, 기능도 물론 중요하지만 그보다 아키텍처와 테스트 코드를 더 중요시한다는 것이었다.

심사위원 분께서 우리 팀이 기능을 많이 만든 것은 인정해주셨지만, 테스트 코드가 없는 것에 대해서 많이 언급하셨다. 또한 아키텍처 설계에 분명한 이유가 존재하고, 테스트 코드를 꼼꼼히 작성한 팀들이 좋은 점수를 받았기 때문에 이러한 생각이 들었다.

그래서 여기서 나의 역량이 많이 부족함을 느꼈고, 앞으로 어떤 부분을 더 공부해야 할 지 파악할 수 있었다.

아키텍처에서는 무중단 배포를 처음 해보았기 때문에, 사실상 더 어렵고 새로운 것을 하기는 어려웠던 것 같다. 그래도 다음에는 디자인 패턴을 더 공부해 본 후에 적용해 보고 싶다는 생각이 들었다!

또한 테스트 코드는 앞으로 꼭 작성해야겠다는 생각이 들었다. 아직도 제대로 테스트 코드를 짜보지 않았음에 부끄러움도 느껴졌다 😞

라. 소통

이번 프로젝트를 하면서 슬랙을 정말 많이 사용했는데, 이 점이 좋았던 것 같다.

PM 민선이가 슬랙을 활용을 잘 한다고 느꼈고, 나도 여기서 많이 배우게 된 것 같다.

슬랙에서 주로 중요한 내용 및 업무와 관련된 내용들을 얘기를 하고, 카톡에서는 그 외의 사담 등을 나누어서 분리가 잘 되었다.

특히 업무를 시작할 때, 체크인을 하며 어떤 업무를 할 것인지를 공유하고,
종료할 때는 체크아웃으로 어떤 업무까지 완료했는지까지 다시 공유하니
모든 팀원들의 진행상황을 파악할 수 있어 좋았던 것 같다.

진행상황 공유가 프로젝트의 성공에 정말 중요한 키라는 것을 여러 번 깨닫는 요즘이다!


이 외의 느낀 점은 위에서 중간중간 작성하였기 때문에 다시 언급하지는 않아도 될 것 같다.

막바지에는 거의 매일 밤을 새며 했던 것 같은데... 정말 힘들고 정신 없었던 건 사실이지만 또 동시에 즐거움도 느꼈다. 새로운 기능을 만들어보는 게 재미 있었고, 내가 성장하고 있음이 느껴져서 행복했다.

끝까지 포기하지 않고 함께 달린 팀원들에게 정말 고생했다고 고맙다고 말해주고 싶다 🙃

profile
안녕하세요. 비즈니스를 이해하는 백엔드 개발자, 한상호입니다.

4개의 댓글

comment-user-thumbnail
2024년 6월 30일

멋있고 멋있고 멋있다. 큐시즘을 잘부탁해 상호🔥

1개의 답글
comment-user-thumbnail
2024년 7월 1일

그저 감탄뿐. .
또한번 자극받구 갑니다~

1개의 답글

관련 채용 정보