Releases: boostcampwm-2024/refactor-web07-Ask-It
1.3.0 (2025.02.20)
Release 1.3.0 (2025.02.20)
Note
- AI 기능 성능을 개선했습니다.
- LLM 응답 생성시 스트림 방식을 통한 사용자 경험 개선을 진행했습니다.
- 토큰 기반의 악의적인 요청 차단과 함께 IP 기반 정책을 추가했습니다.
- 비속어 탐지 기능을 추가했습니다.
개선 사항
스트림 방식 전환 (PR #98, #103)
클라우드 서비스에서 제공되는 LLM 모델을 이용하여 응답을 생성시 완성된 응답을 받기까지 지연시간이 오래 걸려, 사용자 경험이 저하되는 문제가 있습니다.
클라우드 서비스에서 토큰 단위로 데이터를 받을 수 있기에, 해당 방법으로 전환하여 사용자가 응답을 받아보는 첫 시점을 이전보다 개선했습니다.
2025-02-18.20.26.19.mov
질문/답변 개선 모델 튜닝
튜닝을 했음에도 불구하고 개선된 질문과 답변이 만족스럽지 않은 경우가 있어 성능을 더 높이고자 학습 데이터셋을 추가하고 프롬프트 문장을 개선하였습니다.
질문과 답변을 개선하는 과정에서 발생한 문제는 다음과 같았습니다.
1번 문제의 경우 프롬프트 문장에 다음과 같은 문장을 추가하여 해결하였습니다.
[사용자 질문 입력]
최종적으로 재작성된 질문만 출력해 주세요.
정중하고 공손한 어투와 존댓말을 사용하여 재작성해주세요.
2번 문제의 경우 데이터 셋을 400개에서 1000개로 늘려 성능을 높이는 작업을 하였습니다.
IP 기반 악의적인 요청 차단 (PR #107)
AI 기능에 소모되는 토큰을 줄이기 위해 사용자가 AI 기능 요청을 악의적으로 계속해서 보내는 경우를 막았습니다.
기존에는 서비스에서 제공하는 세션토큰을 기반으로 사용자의 무분별한 요청을 막았습니다. (PR #76)
그러나 Ask-It 서비스는 토큰의 무제한 발급이 가능하기 때문에 토큰을 바꿔가며 요청을 보낼 경우 제대로 차단하기 어려울 수 있어 사용자의 IP 를 기반으로도 요청을 차단할 수 있게 하였습니다.
따라서 사용자의 토큰과 IP 를 캐싱한 뒤, 3초 이내에 같은 토큰이나 IP 가 포함된 요청이 발생할 경우 해당 요청을 반환할 수 있도록 하였습니다.
추가된 기능
비속어 탐지 기능
여러 실험적인 도전을 진행하다가 BERT 모델이 GPU 없는 인스턴스 서버에서 동작하는데 큰 무리가 없음을 확인하였습니다.
비속어 탐지 기능 자체는 모델의 성능이 뛰어나지 않더라도 실험적으로 도전해볼 수 있을 것 같다고 판단하여 해당 기능을 추가했습니다.
classifier server (PR #92, #94)
BERT 모델을 fine tuning 을 진행하여 공격적인 문장과 그렇지 않은 문장들을 분류할 수 있는 모델로 학습하였습니다.
해당 모델을 사용할 수 있는 API 서버를 추가하여, 문장들을 입력으로 주면, 문장을 분류하여 확률과 함께 응답으로 제공하도록 개발했습니다.
입력 | 출력 |
---|---|
![]() |
![]() |
배치 작업을 통한 비속어 필터링 (PR #101)
classifier 서버를 활용하여 채팅의 비속어를 필터링 기능을 만들었습니다. 다만, 채팅 50개에 30초 가량의 검사 시간이 걸리기 때문에 매 채팅이 생성될 때마다 classifier 서버로 검사를 진행하게 되면 서버 전체의 블로킹 우려가 있었습니다. 이러한 특징을 고려하여 비속어 필터 작업을 배치 작업으로 구현했습니다.
매 분 당 50개의 채팅 검사를 진행할 수 있도록 하였습니다. classifier 서버를 토대로 비속어 여부를 검사하고 그 결과를 DB에 저장한 뒤, 비속어 채팅 리스트들을 브로드캐스트하여 사용자들의 화면에 실시간으로 반영될 수 있도록 하였습니다.
필터링 작업에 대한 프론트엔드 작업 (PR #96)
클라이언트에서는 부적절한 표현이 담겨 있는 채팅이라면 해당 채팅을 다른 문장으로 변경하여 표시하도록 변경했습니다.
또한, 필터링 기능을 토글로 제공하여 사용자가 가시여부를 결정할 수 있도록 제공합니다.

What's Changed
- feat(be): add a classifier that determines whether question or reply have been improved by @cjeongmin in #92
- feat(be): add slang detection by @cjeongmin in #94
- feat(be): stream response by @shl0501 in #98
- fix(be): fix swagger by @shl0501 in #99
- feat(be): chatting filter by @wlgh1553 in #101
- fix(be): update socket server prisma schema by @wlgh1553 in #104
- feat(fe): add abuse chatting message filtering feature by @cjeongmin in #96
- feat(fe): add animated text component for AI features by @cjeongmin in #103
- fix(fe): adjust z-index of abuse message toggle button by @cjeongmin in #105
- feat(be): restrict request by using token and ip by @shl0501 in #107
- feat(be): improve abuse classifier model performance, implement input batch processing by @cjeongmin in #111
- fix(be): add history api in ai controller by @shl0501 in #112
- fix(be): fix ai controller code by @shl0501 in #113
- refactor(be): move chatting filter batch jobs from API server to Socket server by @wlgh1553 in #114
- style(fe): update cleanbot alert message by @wlgh1553 in #115
Full Changelog: v1.2.0...v1.3.0
1.2.0 (2025. 02. 13)
Release 1.2.0 (2025.02.13)
Note
- AI 기능의 성능을 개선하여 응답을 개선했습니다.
- 사용자가 보다 원하는 결과가 나올 수 있도록 유도할 수 있는 기능을 추가했습니다.
- 질문 개선 기능에 이어, 답변 개선 기능을 추가하였습니다.
- 추가로, 사용자가 악의적인 행동으로 AI 기능을 연속하여 사용하는 것을 방지하는 기능이 추가되었습니다.
- 마지막으로 질문/답변 축약 기능을 삭제하는 것으로 결정되어 해당 기능을 제거하는 작업이 진행되었습니다.
개선 사항
AI 기능 성능 개선
질문 개선하기 기능의 응답을 개선할 필요가 있었습니다. 또한, 사용자가 악의적인 입력을 하는 것에 대해서 추가적인 대책이 필요했습니다.
프롬프트 엔지니어링만을 이용해서는 눈에 띄게 개선되지 않아, 다른 방법을 추가하기로 결정했습니다.
튜닝을 이용한다면 기존보다 응답이 개선될 것이라고 판단하였고 악의적인 입력에 대해서 완전한 해결을 할 순 없겠지만, 비슷한 입력에 대해서 응답 결과를 데이터로 추가하여 튜닝한다면, 기존보다 악의적인 입력에 대해서 강인해질 수 있다고 판단하였습니다.
튜닝을 위해 데이터셋이 필요해졌고, 이를 위해 데이터셋을 만들었습니다. 학습 데이터를 직접 생성하여 추가하고 이를 GPT를 이용하여 데이터셋을 확장하고, 클로바 스튜디오의 데이터 확장 기능을 이용하여 학습 데이터를 생성했습니다.
Important
현재 모델은 인위적으로 생성된 데이터를 통해 학습하였지만, 현재는 사용자의 AI 기능을 사용하고 해당 응답을 채택한 결과를 데이터베이스에 저장하고 있습니다. 이후 모델을 개선할 때 사용할 수 있을 것입니다.
이후, 모델을 데이터셋을 이용해서 튜닝을 진행하고 해당 모델을 이용하여 응답을 생성하도록 수정 작업을 거쳤습니다.
아래 문서에서 확인할 수 있는 것처럼 원치 않은 응답을 내놓던 질문에서도 정상적으로 질문을 개선하여 응답을 생성하는 것을 확인할 수 있습니다. (기존엔 많은 응답이 잘못되었으나 이후 모든 응답이 개선되었습니다)
추가적으로 더 좋은 응답을 받을 수 있도록, 질문에 대한 맥락을 이해하도록 수정되었습니다.
Important
세션 전체에 대해 맥락을 이해하는 것은 아닙니다.
프롬프트 데이터베이스 추가 (PR #72)
기존엔 모델에게 넘겨지는 프롬프트 내용을 서버에서 상수로 저장하고 있었습니다.
프롬프트의 변경이 필요할 때 서버가 다시 배포될 필요가 있었고, 이는 개발하는 환경에서도, 실제로 배포된 환경에서도 변경이 필요할 때 유연하게 대응할 수 없었고, 부정적인 개발자 경험을 제공했습니다.
따라서, 프롬프트 내용을 데이터베이스에 추가하여 서버에서 해당 내용을 불러와서 사용하는 방식으로 개선하여, 변경이 필요할 땐 데이터베이스를 수정하는 방식으로 서버의 변경 없이 프롬프트 내용을 변경할 수 있게 개선되었습니다.
현재는 매번 데이터베이스에서 불러오고 있지만, 프롬프트를 불러오는 행위가 자주 일어날 것이 분명하기에 캐싱을 추가하는 작업이 필요합니다.
AI 기능 응답 결과 저장 (PR #72, #77)
AI가 제공한 개선된 질문과 답변을 데이터베이스에 저장할 수 있도록 하였습니다. 이때 사용자의 accept와 reject 여부도 함께 저장하여 차후 ai를 개선하는데 사용할 수 있도록 하였습니다.
사용자의 피드백을 추가하여 재요청할 수 있는 기능이 추가됨에 따라 데이터베이스에 어떤 정보까지 저장해야하는지에 대한 고민을 하였습니다.
기본적으로 튜닝의 목적은 사용자의 경험을 높이고, 토큰 발생 비용을 줄이는 것이라고 생각하여, 사용자의 피드백이 반영되지 않은 AI의 최초 답변만을 저장하는 것으로 결정하였습니다.
추가된 기능
재요청시 사용자 피드백 추가 (PR #81, #83)
질문을 개선하거나 답변을 개선할 때, 응답이 원치 않는 수준의 응답을 받을 수 있습니다.
이를 해결하기 위해 응답 결과에 대해 피드백을 전달하는 기능을 추가했습니다.
2025-02-13.15.26.05.mov
해당 기능을 통해서, 사용자는 응답 결과에 대해 피드백을 통해서 보다 원하는 개선 결과를 받을 수 있습니다.
답변 개선 기능 추가 (PR #86, #89)
기존의 질문 개선 기능에 이어 답변 개선 기능을 만들었습니다. 질문 개선은 사용자의 질문만을 입력으로 받고, 해당 질문을 명확하고, 다채롭게 하는 것에 초점이 맞춰져 있었습니다. 반면 답변 개선은 사용자의 질문과 답변을 모두 받고, 질문을 통해 답변의 맥락을 파악한 뒤, 이를 통해 답변을 개선합니다. 아래와 같은 상황에서 답변 개선 기능은 큰 효과를 볼 수 있습니다.
예시)
사용자 질문 : 코딩테스트에서 Go와 파이썬 중 무엇을 사용하는 것이 좋을까요?
호스트 답변 : 파이썬 → 자료구조 많음
개선된 답변 : 파이썬이 많은 자료구조를 지원하기 때문에 코딩테스트에 더 적합합니다.
위와 같이 질문을 통해 맥락을 이용하고, 이해한 맥락을 통해 호스트의 답변을 보다 더 명확하게 작성할 수 있도록 합니다.
2025-02-13.17.52.40.mov
사용자의 악의적인 요청 방지 (PR #76)
AI의 토큰 비용을 줄이고자 사용자로부터의 악의적인 요청을 방지하도록 하였습니다. 악의적인 요청을 2가지로 정의하였습니다.
- 매우 많은 입력을 보내는 경우
- 짧은 시간안에 여러 요청을 보내는 경우
이를 위해 프론트에서 뿐만 아니라 백엔드에서도 사용자의 입력이 500자 이상일 경우 에러를 던질 수 있도록 하였습니다.
또한 저희 서비스는 세션 기반 인증을 사용하므로 각 사용자마다 고유한 세션토큰을 가지게 됩니다. 이를 활용하여 3초안에 같은 토큰을 가진 사용자가 AI 기능을 요청하면 에러를 던질 수 있도록 하였습니다.
삭제된 기능
질문/답변 축약 기능 (PR #83)
개선된 답변과 질문을 사용자의 요청을 담아 재요청할 수 있는 기능이 추가됨에 따라 기존에 기획했던 질문과 답변의 축약 기능을 삭제하게 되었습니다.
사용자가 메시지를 담아 재요청을 보낼 경우, 챗 gpt 처럼 기존 맥락을 유지하는 것이 개선에 도움이 될 것이라고 생각하였습니다. 그러나 사용자가 재요청을 계속할 경우 맥락이 너무 길어지는 문제가 있을 것이라고 생각하였습니다.
따라서 사용자의 원본 글 + 사용자의 요청 메시지 + 이전 AI의 답변 총 3가지를 전달하는 것으로 하였습니다.
What's Changed
- feat(be): create prompt result insertion API by @wlgh1553 in #72
- feat(be): implement ai request-restriction guard by @shl0501 in #76
- feat(fe): resend with requirements (UI), integrate the history API, and add a Popover component by @cjeongmin in #77
- feat: add question retry API by @wlgh1553 in #81
- feat(fe): integrate question improvement retry API, update Popover component, and remove question shortening feature by @cjeongmin in #83
- feat(be): content length validation by @wlgh1553 in #84
- feat(be): add reply improve and retry API by @wlgh1553 in #86
- fix(be): update history prompt type by @wlgh1553 in #87
- feat(fe): integrate improve reply api by @cjeongmin in #89
Full Changelog: v1.1.0...v1.2.0
1.1.0 (2025. 02. 06)
Release 1.1.0 (2025. 02. 06)
LLM을 활용해 사용자의 질문을 개선하고 축약하는 기능을 추가하는 것을 중점적으로 진행했습니다. 그와 함께 일부 이슈를 해결하고, UI/UX를 개선했습니다.
추가된 기능
질문 개선
사용자가 작성한 질문이 호스트에게 의도대로 전달되지 않는 경우가 종종 생기기도 합니다.
질문과 답변으로 이루어지는 Ask-It 서비스는 질문을 보다 작성하기 쉽게 지원하기 위해서 AI를 활용한 질문 개선 기능을 추가하기로 결정했습니다.
- 사용자의 경험을 개선하기 위해서 질문 작성시, AI를 활용하여 현재 질문을 개선할 수 있는 기능을 추가했습니다.
- 질문 작성 중, 질문 개선하기 버튼을 통해서 질문을 보다 명확하고 이해하기 쉬운 질문으로 변환할 수 있습니다.
2025-02-06.16.45.09.mov
글자 수 제한
질문과 답글에 글자 수를 500자로 제한하여 정책상 변화가 생겼습니다.
AI 서비스의 도입에 따라 토큰 비용이 발생하기 때문에 글자수 제한을 도입하였습니다.
- 참고 서비스인 slido의 150자 제한
- 현실적인 질문의 길이
- 마크다운 지원을 위해, 500자 제한
질문 요약
질문이 너무나도 길어지면, 읽기도 이해하기도 어려울 수 있습니다. 또한 AI 질문 개선 기능을 사용시에 500자를 넘는 응답을 받을 수도 있습니다.
이를 위해 질문 요약 기능을 추가했습니다. 사용자가 직접 요약하기보단 AI를 통해 요약한다면 사용자 경험을 높일 수 있을 것이라고 판단하였습니다.
- 사용자의 질문이 500자 제한 정책에 위배되지 않도록 AI를 활용해 500자 이하로 줄입니다.
- 또한 너무 길어서 호스트가 읽기 어려운 질문을 AI를 활용해 핵심만 요약하도록 합니다.
2025-02-06.17.30.36.mov
BE
질문 개선 기능 (PR #58)
프롬프트
프롬프트를 하는 과정에서 다음과 같은 문제가 발생하였고, 아래와 같은 방법을 통해 해결하였습니다.
- 마크다운 요소와 사용자가 첨부한 링크를 누락
- 입출력 예시를 제공하여 AI가 이를 참고해 개선된 질문을 할 수 있도록 하였습니다.
- 사용자의 질문에 마크 다운 요소가 있다면 꼭 포함할 것을 0번 규칙으로 지정하였습니다.
- AI가 사용자의 질문에 대한 답변을 추가함
- 재작성한 질문만을 답변으로 내놓고, 자신의 분석, 정보 등을 포함하지 말 것을 직접적으로 명시함으로써 문제를 해결하였습니다.
- 500자 이상 작성된 글이 답변으로 반환됨
- 질문 요약 기능을 추가하여 500자 이상 작성되었을 경우 질문요약기능을 통해 다시 글을 받아볼 수 있도록 합니다.
질문 요약 기능 (PR #67)
프롬프트
프롬프트를 하는 과정에세 다음과 같은 문제가 발생하였고, 아래와 같은 방법을 통해 해결하였습니다.
- 마크다운 요소가 누락되는 문제 발생
- 질문 개선 기능과 마찬가지로 마크다운 요소가 누락되는 문제가 발생하였습니다. 해당 문제를 질문 개선 기능때와 같은 방법으로 해결하였습니다.
- 상황마다 글자수 제한 정책(500자)을 넘는 글이 작성되는 문제 발생
- 질문과 상황에 따라 요약 기능을 통해 받아온 글이 글자수 제한 정책인 500자보다 더 많은 경우가 발생하였습니다.
- 이 문제는 차후 AI를 통해 요약한 글이 500자가 넘을 경우 자동으로 재요청을 보낼 수 있도록 처리할 예정입니다.
- 질문과 상황에 따라 요약 기능을 통해 받아온 글이 글자수 제한 정책인 500자보다 더 많은 경우가 발생하였습니다.
도커 컨테이너 bcrypt 문제 (PR #61)
bcryptjs
의 도입
docker container 실행 시에 bcrypt
의존성 문제가 발생했습니다.
2025-02-05 15:44:58 Error: Cannot find module '/app/node_modules/.pnpm/bcrypt@5.1.1/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node'
2025-02-05 15:44:58 Require stack:
2025-02-05 15:44:58 - /app/node_modules/.pnpm/bcrypt@5.1.1/node_modules/bcrypt/bcrypt.js
- 다양한 해결방안을 시도했지만 대부분의 알려진 방법들이 잘 작동하지 않아
bcrypt
라이브러리를bcryptjs
라는 js전용 라이브러리로 바꾸기로 결정을 했습니다. - 다음은
bcryptjs
설명의 일부입니다.- Optimized bcrypt in JavaScript with zero dependencies. Compatible to the C++ bcrypt binding on node.js and also working in the browser.
bcryptjs
를 사용하면bcrypt
보다 30% 가량의 성능 저하가 발생할 수 있습니다.- 하지만 저희 서비스에서는 access/refresh token을 활용하기 때문에 로그인 주기가 일주일 단위로 길고, 회원 가입도 자주 일어나지 않기 때문에 성능 저하가 발생하더라도 서비스 품질에 큰 영향을 주지 않을 것이라고 판단했습니다.
출력 토큰 제한으로 인한 문제
현재 AI의 출력 토큰이 최대 1024개로 제한되어 있어, 다음과 같은 문제가 발생할 수 있습니다.
- AI가 생성한 답변이 1024 토큰을 초과할 경우, 글이 중간에 잘려 불완전한 상태로 반환됨
- 사용자가 긴 URL을 포함한 경우, AI가 이를 온전히 포함하지 못하거나 URL이 누락됨
이러한 문제를 해결할 수 있는 방안을 검토할 예정입니다.
FE
질문/답변 UI/UX 개선 (PR #57, PR #64, PR #66)
질문과 답변 지원 기능이 추가됨으로써, 기존 UI의 변경이 필요하다고 생각이 되는 지점이었습니다.
따라서, 질문/답변 UI/UX를 개선하는 작업을 진행했습니다.
개선 전
질문 | 답변 |
---|---|
![]() |
![]() |
개선 후
질문 | 답변 |
---|---|
![]() |
![]() |
- 모달 하단 AI 기능을 사용하는 버튼 추가
- 마크다운 미리보기, 답변 작성시 질문 보기와 같은 버튼은 입력 영역 우측으로 위치 변경
또한, 추가적으로 질문과 답변을 작성한 후에 실수로 삭제되는 경우를 제거하기 위해서 일부 수정을 진행했습니다.
- 드래그 상태로 모달 밖으로 나갈 경우, 모달이 닫히는 경우 수정
- 질문, 답변 작성시 모달 외부를 클릭해도 모달이 닫히지 않도록 수정
질문 개선/축약 기능 추가 (PR #64, PR #69)
질문 개선/축약 기능을 사용할 시 기존의 경우와 다르게 추가적인 플로우가 필요했습니다.
- 응답 적용
- 재요청
- 응답 거절
따라서, 개선/축약 기능을 사용하고 질문/답글 입력 화면이 응답 이후에 사용자가 선택할 수 있도록 일부 UI가 수정되었습니다.
또한, 정책상 질문/답변의 글자수를 500자 이하로 제한하여 글자수를 카운트하는 기능에서 링크 또는 이미지의 경우에 링크 텍스트 자체는 제외하도록 추가적인 작업이 진행되었습니다.
What's Changed
- refactor(fe): split QuestionItem, ReplyItem components into multiple files by @cjeongmin in #48
- feat(fe): add skeleton UI by @cjeongmin in #50
- refactor(fe): improve code quality using SonarLint, SonarQube by @cjeongmin in #52
- docs: update system architecture by @wlgh1553 in #53
- feat(be): ai question enhancement by @wlgh1553 in #58
- fix(fe): modify modal UI that create question and reply by @cjeongmin in #57
- refactor(be): update markdown prompt by @wlgh1553 in #61
- refactor:(be) update prompt content by @wlgh1553 in #63
- feat(fe): Integrate question improvement API and enhance question/reply writing UI by @cjeongmin in #64
- feat(be): implement question shortening feature by @shl0501 in #67
- fix(fe): enhance UX by adding new features during question/reply creation by @cjeongmin in #66
- feat(fe): integrate question shortening API by @cjeongmin in #69
Full Changelog: v1.0.1...v1.1.0
1.0.1 (2025. 01. 23)
Release 1.0.1 (2025. 01. 23)
프로젝트 전반적으로 성능 개선과 유지보수성, 그리고 확장 가능한 설계를 목표로 Docker 기반 CI/CD, Redis 캐싱, 좋아요 동시성 문제 해결, 프론트엔드 렌더링 버그 수정, 코드 품질 및 프로젝트 구조 개선 등을 중점적으로 진행했습니다.
BE
Docker CI/CD (PR #28)
[배경]
- 기존에는 PM2를 사용해 애플리케이션을 관리했으나, 컨테이너 기반의 배포 방식으로 전환하여 일관된 환경에서 애플리케이션을 실행하고 관리하고자 했습니다.
- Docker 기반으로 CI/CD를 구성하여 개발, 테스트, 배포의 효율성을 높이고자 했습니다.
[문제]
- 환경 일관성 부족
- PM2는 서버 환경에 의존적이며, 배포 과정에서 환경별 차이에 따른 문제가 종종 발생했습니다.
- 복잡한 배포 프로세스
- 서버 설정과 배포 스크립트의 관리가 점점 복잡해지고, 유지 보수 부담이 증가했습니다.
[해결]
- Docker 컨테이너 기반으로 전환
- 애플리케이션과 모든 의존성을 컨테이너로 패키징.
- Dockerfile을 작성해서, 개발 및 배포 환경 간의 차이를 제거.
- CI/CD 파이프라인 개선
- GitHub Actions를 사용하여 자동화된 빌드, 테스트, 배포 프로세스를 구현.
- PR 머지 시 자동으로 컨테이너 이미지를 빌드하고, 서버에 배포.
[결과]
- 일관된 실행 환경
- 모든 서버에서 동일한 컨테이너 환경을 사용해 실행 오류가 감소.
- 배포 프로세스 간소화
- GitHub Actions와 Docker를 사용한 CI/CD로, 배포 시간을 단축하고 효율성을 향상.
- 확장성 및 유지 보수성 증가
- 컨테이너화된 애플리케이션을 통해 서버 스케일링과 업데이트가 용이.
- 개발자의 로컬 운영체제와 무관한 실행 테스트
- Docker는 애플리케이션을 컨테이너화하므로, 로컬 개발 환경(OS에 상관없이)에서도 프로덕션과 동일한 환경에서 테스트가 가능합니다.
- 예: 개발자가 Windows, macOS, Linux 등 다른 OS를 사용하더라도 동일한 컨테이너를 실행할 수 있습니다.
Redis 캐싱을 통한 API 성능 개선 (PR #34)
[배경]
- 서비스의 핵심기능 중 '질문 목록 조회', '질문 작성', '답변 작성', '좋아요 토글'에 대해 부하테스트를 진행하였습니다.
- 테스트는 200명의 가상 사용자를 대상으로 5분간 진행되었으며, 질문 목록 조회(40%), 질문 작성(30%), 답변 작성(20%), 좋아요 토글(50%)의 비중으로 구성되었습니다.
[문제]
- 특정 요청의 경우, 사용자의 요청 속 세션의 정보, 사용자의 토큰 등을 GUARD에서 검사하며, 이 작업은 많은 DB의 접근과 처리시간을 필요로 합니다.
- 부하테스트 결과, 응답 시간이 중간값 기준 118.6ms, 95퍼센타일 기준 204.2ms로 나타났습니다.
- 특히 읽기 작업이 많은 서비스 특성상(질문 목록 조회 40%), 매 요청마다 DB에 접근하는 것이 성능 병목의 주요 원인이 되었으며, 이로 인해 DB Connection Pool이 고갈되어 Timeout으로 인한 500 응답이 발생하기도 하였습니다.
[ERROR] https://ask-it.site/api/questions - Status: 500, Duration: 27ms Error response: <html> <head><title>500 Internal Server Error</title></head> <body> <center><h1>500 Internal Server Error</h1></center> <hr><center>nginx/1.18.0 (Ubuntu)</center> </body> </html>
[해결]
- Redis를 통해 GUARD 부분에 캐시를 도입하여 자주 사용되는 사용자의 정보, 세션의 토큰 등을 저장합니다.
- Cache hit가 발생할 경우 DB에 접근하지 않고 바로 반환해주는 방법을 통해 성능을 개선하였습니다.
- 특히 읽기 작업이 많은 서비스 특성을 고려하여, 캐시 적용 대상과 전략을 최적화하였습니다.
[결과]
- 전반적인 응답 시간이 평균 80% 이상 감소했습니다:
- 중간값 기준: 118.6ms → 22.9ms (80.7% 감소)
- 95퍼센타일 기준: 204.2ms → 37.0ms (81.9% 감소)
- 시스템 처리량이 18.3% 향상되었습니다 (캐싱 시나리오 기준)으로 1.69 RPS → 2.00 RPS)
좋아요 동시성 문제 해결 (PR #39)
[배경]
- Ask-It 서비스에서는 유저가 질문에 좋아요를 남길 수 있는 기능을 제공합니다.
- 유저는 질문당 하나의 좋아요를 남길 수 있으며, 재클릭 시 좋아요 취소가 가능합니다. (토글 방식)
[문제]
-
부하 테스트 결과 좋아요 토글 기능에서 동일 유저가 여러 번 Like를 누르는 경합(Race Condition) 발생했습니다.
-
문제의 원인을 다음 코드에서 찾았습니다.
// apps/server/src/questions/questions.service.ts async toggleLike(questionId: number, createUserToken: string) { const exist = await this.questionRepository.findLike(questionId, createUserToken); if (exist) await this.questionRepository.deleteLike(exist.questionLikeId); else await this.questionRepository.createLike(questionId, createUserToken); return { liked: !exist }; }
[해결]
model QuestionLike {
questionLikeId Int @id @default(autoincrement())
questionId Int
createUserToken String
question Question @relation("QuestionLikes", fields: [questionId], references: [questionId], onDelete: Cascade)
createUserTokenEntity UserSessionToken @relation("TokenQuestionLikes", fields: [createUserToken], references: [token])
@@unique([questionId, createUserToken])
}
- DB 스키마 수정
@@unique([questionId, createUserToken])
를 추가해 중복 삽입을 DB 레벨에서 강제로 막았습니다.
async toggleLike(questionId: number, createUserToken: string) {
try {
// 1) 무조건 create를 시도
await this.questionRepository.createLike(questionId, createUserToken);
return { liked: true };
} catch (error) {
// 2) UNIQUE_CONSTRAINT_VIOLATION 에러면 이미 존재하므로 delete 수행
if (
error instanceof PrismaClientKnownRequestError &&
error.code === PRISMA_ERROR_CODE.UNIQUE_CONSTRAINT_VIOLATION
) {
await this.questionRepository.deleteLike(questionId, createUserToken);
return { liked: false };
}
throw error;
}
}
- 애플리케이션 로직 수정
- 유니크 제약이 있는 상태에서
create
를 시도하고, 에러가 나면(이미 존재) 그때delete
수행하도록 하였습니다.
- 유니크 제약이 있는 상태에서
[결과]
- 이전 부하테스트에서 발생하던 500에러 4건이 0건으로 완전히 개선되었습니다. 즉, Race Condition으로 인한 데이터 불일치 문제가 해결되었습니다.
리프레시 토큰 (PR #44)
[배경]
- 레디스의 다양한 기능을 활용해 리프레시 토큰을 관리해 보았습니다.
[문제]
- 인메모리 방식의 refresh token 관리는 다음과 같은 한계가 있었습니다:
- 서버 재시작 시 모든 토큰 정보가 손실됨
- 주기적인 cleanup 로직을 직접 구현하고 관리해야하는 부담
[해결]
- Redis를 도입하여 refresh token 저장소로 활용하였습니다:
- Redis의 TTL(Time To Live) 기능을 활용하여 토큰의 자동 만료 처리
- 토큰 저장 및 관리를 Redis에 위임하여 애플리케이션 로직 단순화
[결과]
- 서버가 재시작되더라도 사용자는 불편함없이 그대로 유지할 수 있습니다.
FE
렌더링 버그 수정 (PR #32)
사용자가 로그인 이후, 화면이 렌더링이 하지만 되지 않는 문제를 발견했습니다.
프로젝트에서 로그인 여부를 판단하는 isLogin
이라는 함수를 사용하고 있습니다. 해당 함수는 AuthStore
라는 스토어에서 액션으로 제공되고 있습니다.
사용자가 로그인 이후에 액세스 토큰 값이 변경되지만, isLogin
함수는 재생성되지 않아 해당 함수를 사용하는 코드는 리렌더링이 발생되지 않았습니다.
따라서, isLogin
함수를 사용하지 않고, 스토어에서 액세스 토큰 값을 이용한 파생 상태를 만들어 액세스 토큰 값의 변경에 렌더링이 일어나도록 수정했습니다.
const { isLogin, clearAuthInformation: clearAccessToken } = useAuthStore(
useShallow((state) => ({
- isLogin: state.isLogin,
+ isLogin: state.accessToken != null,
clearAuthInformation: state.clearAuthInformation,
})),
);
최종적으로 사용자가 로그인 이후, 로그인 여부에 따른 UI들이 전부 정상적으로 렌더링 되도록 수정되었습니다.
코드 품질 개선 (PR #36)
프로젝트가 확장되면서 코드 베이스가 복잡해지고 유지보수 비용이 증가했습니다.
특히, 깊게 중첩된 컴포넌트는 로직을 파악하기 어려울 뿐 아니라 재사용성도 떨어졌고, 복잡도가 높은 함수는 디버깅 및 기능 추가 과정에서 오류 발생 확률을 높였습니다.
- 이에 따라 개발 생산성과 품질을 동시에 개선하기 위해, 코드 전반에 걸쳐 구조적·도구적 리팩토링을 진행했습니다.
- 우선, 중복 구현되거나 불필요하게 복잡했던 모달 컴포넌트의 구조를 간소화하고, 4단계를 초과하는 컴포넌트 중첩을 모두 정리하여 가독성과 유지보수성을 높였습니다.
- 또한, 복잡도가 10을 넘거나 지나치게 긴 함수를 분리·재설계해 로직 이해도를 높이고, 수정 시 발생하는 리스크를 줄였습니다.
- ESLint 설정 파일은 기존
json
형식에서config.js
형식으로 전환하며 규칙을 수정하였고, 이 과정을 통해 약 30건의 린트 에러도 정정하여 코드 품질과 일관성을 확보했습니다.
결과적으로, 모달 컴포넌트를 비롯한 전반적인 컴포넌트 계층 구조가 단순해져 유지보수가 한결 수월해졌고, 복잡도가 높은 로직을 분리함으로써 오류 발생 가능성을 효과적으로 줄였습니다.
프로젝트 구조 개선 (PR #40)
프로젝트가 확장되는 과정에서 파일 구조가 지나치게 복잡해지고, 역할이 중복되거나 모호해지는 문제가 드러났습니다. 이를 해결하기 위해 폴더 구조를 일관된 기준으로 재정비하고, 책임이 지나치게 크거나 복잡도가 높은 폴더들을 분리·재설계함으로써 개발 효율을 높였습니다.
- 일관성 있는 구조 확립: 역할별 폴더를 재조정하여 프로젝트 전반의 구조적 일관성을 확보
- 혼동 방지: 중복 또는 모호한 폴더를 정리해 구조 파악을 용이하게 개선
- 향후 확장 대비: 기능 추가 시 폴더 구조를 명확히 유지하면서도 유연하게 수정 가능
프로젝트 내부의 파일 구조에서 다음과 같은 문제가 관찰되었습니다
- 일관성 부족: 유사한 기능임에도 서로 다른 위치에 존재하거나, 책임이 명확치 않은 폴더 구조
- 복잡도 증가: 특정 폴더나 경로에 지나치게 많은 기능이 몰려 있어 수정 시 충돌이 잦고 유지보수가 어려움
- 역할 구분 모호: 동일한 기능이나 역할이 여러 폴더에 걸쳐 분산·중복돼, 구조 파악이 어려워짐
.
├── public
├── src
│ ├── components
│ │ ├── modal
│ │ ├── my
│ │ └── qna
│ │ └── hooks
│ ├── features
│ │ ├── auth
│ │ │ └── validation
│ │ ├── modal
│ │ ├── session
│ │ │ ├── chatting
│ │ │ └── qna
│ │ ├── socket
│ │ ├── toast
│ │ └── user
│ ├── pages
│ ├── routes
│ │ └── session
│ │ └── $sessionId
│ │ └── $questionId
│ └── shared
├── test-results
└── tests
기존에 FSD(Feature-Sliced Design) 구조를 어느 정도 차용한 상태였으나, 이번 개편을 통해 FSD 구조를 더욱 엄격하고 일관되게 적용했습니다. 데이터와 행위(기능)를 분리하는 레이어를 두고, 이를 필요에 따라 상위에서 조합해 사용하는 방식을 채택했습니다. 개선된 구조는 다음과 같습니다
apps/client/src/
├── app
│ └── config
├── entities
│ └── session
│ ├── model
│ └── ui
├── features
│ ├── auth
│ │ ├── api
│ │ ├── model
│ │ └── ui
│ ├── close-question
│ │ └── api
- 생략 -
│ ├── terminate-session
│ │ ├── api
│ │ └── ui
│ └── update-session-host
│ └── api
├── pages
│ ├── home
│ │ └── ui
│ ├── my
│ │ └── ui
│ └── session
│ ├── model
│ └── ui
├── routes
│ └── session
│ └── $sessionId
│ └── $questionId
├── shared
│ ├── model
│ │ └── validation-status
│ └── ui
│ ├── InputField
│ ├── button
│ ├── modal
│ └── toast
│ ├── model
│ └── ui
└── widgets
├── chatting-list
├── header
├── question-list
│ └── ui
└── reply-list
└── ui
- 유지보수성 향상
- 기능을 기준으로 코드를 분리함으로써, 해당 기능에 필요한 로직이 한 곳에 모이도록 구조화되었습니다.
- 따라서, 문제가 발생했을 때 어디서 해결해야 하는지가 명확해져, 디버깅과 수정 작업이 훨씬 수월해질 것으로 기대합니다.
- 높은 응집도와 낮은 결합도
- 각 기능 내에서 관련된 로직만 모아두어 응집도가 높아졌으며, 다른 기능과의 의존성은 필요한 최소 수준으로 관리됩니다.
- 기능 변경이 다른 부분에 미치는 영향을 최소화하여 유지보수 부담을 줄였습니다.
- 확장성 및 규모 확장 용이
- 새 기능을 추가할 때 독립된 슬라이스를 생성하면 되므로, 기존 코드에 대한 대규모 수정 없이 기능 확장이 가능합니다.
- 기능을 나누어 개발하기에도 용이해 충돌이 줄어들고, 확장 요구 사항에 유연하게 대응할 수 있습니다.
- 예컨대, AI 기능을 새롭게 추가해야 할 때도 기존 코드를 대폭 수정할 필요 없이 손쉽게 확장할 수 있습니다.
- 코드 가독성 및 구조화 향상
- 기능 별로 코드가 명확히 분리되어 있어 설계 의도가 직관적으로 드러납니다.
- 재사용...
1.0.0 (2025. 01. 16)
Release 1.0.0 (2025. 01. 16)
프로젝트 전반적으로 성능 개선과 유지보수성과 확장 가능한 설계를 위한 작업을 주로 이뤘습니다.
BE
RBAC 도입 (PR #9) (Wiki)
[배경]
Ask-It 각 사용자에게 아래와 같은 역할을 부여합니다
-
슈퍼호스트
- Q&A 세션 페이지를 생성한 사용자
-
서브호스트
- SuperHost에 의해 host가 되어 Q&A 세션페이지의 관리(댓글 삭제 등)를 맡는 사용자
-
참가자
- 세션 페이지에 참여하여 질문과 채팅을 남기는 일반 참여자
각 참가자는 아래와 같은 권한을 행사할 수 있습니다.
권한 | 슈퍼 호스트 | 서브 호스트 | 참가자 |
---|---|---|---|
1. 세션 종료 | O | X | X |
2. 타인에게 호스트 권한 부여 | O | X | X |
3. 타인의 호스트 권한 해제 | O | X | X |
4. 질문 삭제 | O | O | X |
5. 답변 삭제 | O | O | X |
6. 질문 고정 | O | O | X |
7. 질문에 대한 답변 완료 기능 | O | O | X |
[문제]
기존의 권한 처리 로직은 IF 분기문을 통해 처리하였습니다.
예를 들어 사용자가 질문 삭제를 요청하였다면 아래와 같이 처리합니다.
async deleteQuestion(questionId: number, question: Question, { token }: BaseDto) {
const { isHost } = await this.sessionAuthRepository.findByToken(token);
if(!isHost)
throw new ForbiddenException('권한이 없습니다.');
// 이하 생략...
}
그러나 서비스의 권한정책이 다음과 같이 변한다고 가정해봅니다.
- 서브호스트가 서브서브호스트를 생성합니다.
- 질문의 삭제는 서브서브호스트만이 가능합니다.
그렇다면 다음과 같은 추가 처리가 필요합니다.
- DB 수정
- DB 수정에 따른 DB 접근 로직 변경(어떤 테이블에서 어떤 데이터를 가져올 것인가)
- 서비스 로직 변경(사용자가 서브서브호스트일때만 질문 삭제가 가능하도록)
즉, 서비스의 정책이 변경되어 역할과 권한이 수정 및 확대된다면 많은 코드가 변경되어야 하며, 이는 유지보수 및 확장에 장애가 됩니다.
[해결]
- 기존에 퍼져있는 권한 조건을 DB 스키마 변경을 통해 RBAC기반 권한 관리로 수정했습니다.
- 역할과 권한 테이블을 만든 후에 다대다 관계를 맺어줬습니다.
- 이를 통해 다음과 같은 권한 관리를 구현했습니다.
async deleteQuestion(questionId: number, question: Question, { token }: BaseDto) {
const { role } = await this.sessionAuthRepository.findByTokenWithPermissions(token);
const granted = role.permissions.some(
({ permissionId }) => permissionId === Permissions.DELETE_QUESTION,
);
if (!granted)
throw new ForbiddenException('권한이 없습니다.');
// 이하 생략...
}
[결과]
- 단순 분기문을 통한 기존의 권한 관리를 RBAC 기반의 권한 관리로 변경하였습니다.
- 차후 서비스 정책상 권한이 수정되어도 코드의 변경 없이 DB 데이터만을 변경하여 해결할 수 있습니다.
- 이를 통해 권한의 수정과 역할의 확장이 용이해지는 결과를 얻을 수 있었습니다.
FE
모달 애니메이션 추가 (PR #11)
모달을 이용한 사용자 경험이 많은 Ask-It 서비스 특성상 모달의 사용자 경험은 중요합니다.
모달이 열리고 닫히는 부분에 자연스러운 애니메이션을 추가하여 사용자 경험을 향상시켰습니다.
Lighthouse 성능 개선 (PR #19)
개선 전 | 개선 후 |
---|---|
![]() |
![]() |
랜딩페이지에서 초기 렌더링 부분에서 아쉬운 성능을 보이는 것을 확인했습니다.
폰트 로딩 최적화와 코드 스플리팅을 통한 초기에 다운로드 해야하는 번들 크기가 감소되어 초기 렌더링 부분에서 성능을 향상시킬 수 있었습니다.
렌더링 성능 최적화 (PR #22, PR #24)
개선 전 | 개선 후 |
---|---|
![]() |
![]() |
부하테스트 과정 중, 채팅창의 변화가 다른 컴포넌트의 렌더링에 영향을 미치는 것을 확인했습니다.
질문이 많아 질문 목록의 컴포넌트가 굉장히 커져있는 상태에서 새로운 채팅이 올라와 새롭게 렌더링되는 것은 사용자 경험에도 부정적인 영향을 미치는 것을 확인했습니다.
zustand
이용하여 전역 상태를 관리하고 있는 상황에서, 많은 부분에서 스토어의 특정 상태, 액션만을 구독하는 것이 아닌 객체 자체를 구독하고 있어 객체의 변경에 쉽게 영향을 받는 것을 확인할 수 있었습니다.
이를 스토어의 특정 상태를 가져오는 selector
함수를 메모이제이션하는 useShallow
라는 훅을 이용하여 렌더링을 최적화하였습니다.
- const { sessionId, sessionToken, expired, addQuestion, updateQuestion } = useSessionStore();
+ const { sessionId, sessionToken, expired, addQuestion, updateQuestion } = useSessionStore(
+ useShallow((state) => ({
+ sessionId: state.sessionId,
+ sessionToken: state.sessionToken,
+ expired: state.expired,
+ addQuestion: state.addQuestion,
+ updateQuestion: state.updateQuestion,
+ })),
+ );
현재, 질문 목록, 답변 목록, 채팅창 부분에서 굉장히 많은 요소들이 렌더링 되는 것에 대해서는 현재 아무런 처리가 되어 있지 않아 가상 스크롤과 같은 기법을 추가하여 성능 최적화를 추가적으로 진행하고자 하고 있습니다.
What's Changed
- fix(fe): modify tailwind config file by @cjeongmin in #4
- refactor: refactor test code by @yu-yj215 in #2
- fix: modify deploy.yml by @yu-yj215 in #3
- fix: Update deploy.yml by @cjeongmin in #5
- refactor(fe): add loadSessionData utility for session management by @cjeongmin in #7
- refactor(be): rbac by @wlgh1553 in #9
- feat(fe): add animation effect to modal by @cjeongmin in #11
- refactor(be): separate client, server, and socket-server apps by @yu-yj215 in #14
- fix: modify deploy.yml by @yu-yj215 in #16
- fix: modify deploy.yml by @yu-yj215 in #17
- refactor(fe): improve lighthouse performance by @cjeongmin in #19
- fix(fe): fix build error by @cjeongmin in #20
- refactor(fe): optimize state access by @cjeongmin in #22
- refactor(fe): use useShallow access for improved performance by @cjeongmin in #24
New Contributors
Full Changelog: https://github.com/boostcampwm-2024/refactor-web07-Ask-It/commits/v1.0.0