우리가 만든 서비스
올려올려 라디오
언제 어디서나 DJ가 여러분에게 따뜻한 위로를 전해드립니다.
upup-radio.site
글을 쓰고 있는 현재, 우리 팀이 만든 서비스가 1Pick을 받았습니다!
이 서비스는 위로를 받고싶은 누구나, 언제 어디서든 따뜻한 위로를 받을 수 있는 서비스입니다.
포텐데이에 참여하게 된 계기
예약 대기 시스템 프로젝트를 진행하면서 중간중간에 힘이 빠지는 날들이 있었습니다.
지금 다시 생각해보면 핵심은 `예약 대기 시스템`인데, `어드민 시스템`을 먼저 구현해서 힘이 빠졌던 것 같습니다.
욕심이 화를 부른다고 했나요? '`어드민 시스템`과 함께 동작하는 `예약 대기 시스템`을 만드는 게 나에게 더 도전적인 일이야.'라는 욕심이 문제였다고 생각해요.
그러던 와중 핵심 기능에 집중할 수 있는 환경을 만들 수 있는 포텐데이를 알게 되었습니다.
여기에 참가해서 MVP를 만들고 유지보수하며 원래 하던 예약 대기 시스템도 다시 핵심 기능에 집중해보려고 참여 신청을 했어요.
기획자, 디자이너, 프론트엔드 개발자들과 협업할 수 있는 기회도 있어 일석이조라고 생각했습니다.
주어진 시간은 10일, 뭐부터 해야 하지?
포텐데이 콘셉트는 딱 10일 동안 모든 것을 팀에서 스스로 산출물을 만들어내는 활동이에요.
각 팀에서 주제를 정하고, 기획부터 디자인, 개발까지 (밤 새지 않고) 10일 안에 MVP를 만드는데, 포텐데이 사이트 메인 화면에서도 '밤을 새지 않고'라는 문구가 있었어요.
10일 안에 MVP를 '밤을 새지 않고' 만들 수 있을까?라는 생각을 했습니다. 이전 기수들의 산출물들을 봐왔는데, 이런 느낌이었습니다.
이걸 10일 안에 만들었다고?
물론 10일 안에 만들고 나서 고도화된 서비스들도 있었습니다. 그런데도 여러 서비스들이 10일 안에 만들었다기엔 너무 멋져 보였어요.
빠른 팀 결성, 처음엔 아이디어 선정부터
적극적으로 팀을 구하는 사람이 적극적인 팀원들을 만날 것이기 때문에 저도 적극적으로 팀을 구하다 보니, 기획자 분께서 먼저 연락을 주셨어요. 그러다 보니 우리 팀은 하루 만에 결성이 됐고, 구성은 기획자 1명, 디자이너 2명, 프론트엔드 1명, 백엔드 1명(저)이 됐습니다.
우리 팀은 포텐데이가 시작하기 전부터 열정적이었습니다. 아이디어 선정을 위해 온라인 회의도 하고 오프라인으로 회의도 했죠.
아이디어 회의에서 걸림돌 제거하기
아이디어 회의를 할 때 비개발 포지션들이 고민하셨던 부분은 '이게 정말 가능할까?'였습니다.
저를 포함한 개발자들은 주어진 시간 안에 자신들의 스킬과 숙련도에 따라 가능한지 불가능한지 판단이 어느 정도 가능하지만,
비개발 직군들은 정말 가능한 아이디어인지, 허무맹랑한 아이디어인지 판단하기 어렵다는 것을 알았습니다.
그래서 기술적인 이야기는 나중에 하고, 괜찮은 아이디어 먼저 뽑자고 제안했습니다.
결과적으로 포텐데이가 시작되고 나서도 여러 개의 괜찮은 아이디어가 많이 나왔고, 우린 투표를 통해 아이디어를 선정하게 됐죠.
선정한 아이디어도 엎어질 수 있다
드디어 팀원 모두가 공감하는 아이디어가 투표를 통해 선정됐고, 아이디어로부터 기획, 디자인, 개발을 시작했습니다.
백엔드에선 네이버 클라우드 플랫폼(NCP) 구축과 Clova X 사용법 등을 익히고, 예전에 했던 프로젝트에서 쓸만한 코드들을 모으고 있었습니다.
그러다 2일 동안 진행한 작업을 엎기로 결정됐습니다.
이유는 여러 가지가 있었지만 최소 기능 제품에서 무엇을 핵심으로 가져가야 하는지 명확하게 정의하기가 어려웠다고 생각합니다.
결국 남은 시간이 8일도 안 남은 상황이었지만 모두가 열정 넘치게 밤늦게까지 회의를 했죠.
신속하게 어떤 제품을 만들 것인지 최소 기능들에 대해 명확하게 정의하면서 의사 결정을 했고, 본격적으로 시작했습니다.
데이터 모델링부터 기능 구현까지
올려 올려 라디오의 핵심 기능은 라디오에 `편지를 쓰는 기능`과, 사연에 대한 `답장을 받는 기능`입니다.
선호하는 MBTI의 `F` 또는 `T` 유형에 따라 원하는 답변을 먼저 받을 수 있습니다. (F, T 모든 유형에 맞도록 답변합니다.)
데이터 모델링
유저(`User`)가 편지(`Letter`)를 쓰면, 답장(`Reply`)을 받는 단순한 구조 덕분에 모델링 자체는 쉽게 했습니다.
유저가 OAuth2 로그인을 했을 때 프로필 이미지를 OAuth2 (카카오)에서 설정한 이미지로 동기화할 것인지 여부를 추가한 것 외에는 변경 사항은 없었습니다.
Clova X 학습시키기
AI 프롬프트는 네이버 클라우드 플랫폼 크레딧 지원으로 부담 없이 사용할 수 있는 네이버의 Clova X를 이용하기로 결정했습니다.
서비스의 목적에 맞는 답변을 얻기 위해서는 사전에 만든 템플릿으로 올바르게 답변하는지 확인이 필요했는데요,
`F 유형`은 유저에게 먼저 공감하는 말과 함께 이모지를 섞은 따뜻한 멘트를 하도록 지시했고, `T 유형`에겐 원인을 분석하고 `현실적인 조언`을 하도록 지시했습니다.
네이버 Clova Studio 가 사용하기 굉장히 편리하게 되어있어
유저의 편지를 Clova X 에게 전달하기
유저가 편지를 쓰면 Clova X 에게 전달해서 F, T 두 가지 유형의 답변을 받아야 합니다.
이를 위해서 서버가 클라이언트가 되어 유저의 편지를 전달해야 하죠.
클라이언트 기술은 Non-blocking으로 작동하는 `WebClient`, blocking으로 작동하는 `OpenFeign`, `RestTemplate` 등이 있었습니다.
무슨 기술을 써야 하지?
제가 선택한 기술은 `OpenFeign`인데요, blocking 방식이지만 몇 번 사용해 본 경험이 있고, 최소한의 설정과 애너테이션만으로 바로 적용할 수 있기 때문입니다.
`RestTemplate`는 여러 프로젝트에서 많이 사용되지만, deprecated 될 예정이라 공식 문서에서도 `WebClient`를 사용하는 것을 권장하고 있기 때문에 고려 대상에서 제외했습니다.
`WebClient`는 새로 학습하고 적용해야 하기 때문에 시간이 촉박한 상황에서 바로 적용하기 어렵다는 판단을 했습니다.
그래도 출시 전까지 시간이 남으면(남을 리 없겠지만) `WebClient`로 바꿔서 응답 속도를 조금이라도 개선할 수 있도록 해야겠죠.
어떻게 사용했지?
`OpenFeign`은 Netflix가 개발한 `선언적인 REST 클라이언트`입니다. 인터페이스를 선언하고, 애너테이션(Annotation)을 사용해서 사용할 수 있죠. Spring MVC 애너테이션을 지원하기 때문에 해당 기술을 사용한다면 정말 쉽게 사용할 수 있습니다.
Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it.
...
Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web.
- Spring Cloud OpenFeign 공식문서
Clova X 에게 메시지를 전송하려면 필요한 요청 헤더(Request Header) 들이 있는데요, 아래 Naver Cloud Platform API 가이드에 따르면 3개의 헤더가 필수로 필요할 것을 알 수 있습니다.
여기에 콘텐츠 협상에 필요한 `Accept` 를 지정하지 않으면 `text/event-stream` 형식으로 응답을 받게 되는데요,
이 응답 형식은 우리가 GPT와 대화할 때 글자를 타이핑하듯이 응답을 받는 것처럼 스트림 형식으로 응답을 받게 됩니다.
우리 서비스는 사연에 대한 DJ의 답장을 받는 콘셉트이기 때문에 스트림으로 데이터를 받는 것이 아닌 JSON 형식으로 전달해야 합니다.
따라서 Accept 헤더에도 `application/json`으로 설정해야 하죠.
결과적으로 아래와 같이 `OpenFeign` 인터페이스를 선언해서 클라이언트를 만듭니다.
이제 직접 만든 `ClovaRequestDto`를 통해서 메시지를 보내기만 하면 됩니다.
그리고, 요청 메시지를 보낼 때 `생성할 토큰의 다양성`, `최대 토큰 개수`, `결괏값의 일관성` 등을 설정하는 부분을 함께 보내면 되는데요,
자세한 내용은 클로바 공식 API 문서에 너무너무 잘 설명되어 있습니다.
다음으로 고민해야 할 부분은, 생성한 답변(토큰)이 요구 사항 형식대로 답변하도록 설정하는 것입니다.
먼저 적절한 요청 메시지 예시를 보면 messages 배열에 `role`과 `content`를 하나의 답변 예시 객체로 묶고, 여러 답변 예시를 설정할 수 있습니다.
"messages" : [ {
"role" : "system",
"content" : "너는 유저의 사연을 듣고 F 유형과 T 유형에 각각 답장을 보내는 라디오 DJ야. (생략)"
}, {
"role" : "user",
"content" : "친구가 다이어트 때문에 힘들어해요. 힘든 모습을 보니 저도 힘들어서 어떻게.. (생략)"
}, {
"role" : "assistant",
"content" : "친구가 힘들어하는 모습 때문에 사연자도 많이 힘드셨겠군요. 많이 힘드셨겠어요 (생략)"
} ],
이렇게 프롬프트에게 역할을 주어주고 답변을 받는 프롬프트 방법론을 응용하면 답변 포맷대로 답변할 확률 매우 높습니다.
이제 요청 메시지 DTO를 생성할 때마다 답변 포맷을 지정하면 됩니다.
Clova X 로부터 받은 답변 저장하기
이제 클로바에게 받은 답변을 DB에 차곡차곡 저장을 해야 합니다.
답변을 저장할 때는 `F 유형` 답변과 `T 유형` 답변을 하나의 레코드로 저장해야 하는데, 답변을 어떻게 저장하면 좋을까요?
우선, 클로바가 정해진 답변 포맷으로 답변을 해야 `F 유형`과 `T 유형`을 명확히 나눠 파싱 할 수 있습니다.
그래서 답변 포맷을 아래처럼 응답하도록 했습니다.
"F 유형 답변: 블라블라블라\n---\nT 유형 답변: 블라블라블라"
위의 답변 형식에서 구분자가 보이시나요?
`F 유형 답변`과 `T 유형 답변` 사이에 구분자를 `\n---\n` 로 지정했습니다. 답변 포맷에서 반드시 이 구분자를 포함하라고 설정했기 때문에 클로바가 생성한 답변은 항상 순서대로 `F 유형 답변`, 구분자 `\n---\n`, `T 유형 답변` 입니다.
이제 두 유형의 답변을 파싱 하기 위해 저에게 익숙한 `정규표현식`을 사용한 유틸리티 클래스를 작성했습니다.
테스트 코드를 통해 정확한 답변 형식은 두 유형으로 파싱 되는 것을 확인했습니다. 만약 파싱에 실패했다면 정해진 답변 형식이 아니기 때문에 `IllegalArgumentException` 예외를 던지게 되고, 나중에 이 예외는 `RestControllerAdvice`에 의해 적절한 HttpResponse 응답을 만들게 되죠.
이로써 클로바에게 메시지를 전달하고, 응답을 받아 데이터베이스에 저장할 수 있게 됐습니다.
글이 길어져 다음 포스팅에 이어서 작성하겠습니다.
긴 글 읽어주셔서 감사합니다.
'Project' 카테고리의 다른 글
[올려올려 라디오] 분석 서비스 개발 중 만난 데드락과 동시성 문제 해결기 (1) (0) | 2024.11.25 |
---|---|
[올려올려 라디오] 올려올려 라디오 서비스 개발기 - 2 (2) | 2024.10.08 |
[예약 대기 시스템] 4. 컨테이너 환경에서 테스트하기 (Testcontainers) (4) | 2024.09.16 |
[예약 대기 시스템] 3. 프로젝트 설정 (어드민 시스템) (1) | 2024.09.13 |
[예약 대기 시스템] 2. 어드민 시스템 데이터 모델링 (개체-관계 모델, ERD) (2) | 2024.09.12 |