서비스 링크: https://www.resumarble.site
main: 배포용 서비스 코드가 있는 메인 브랜치로, 항상 현재 서비스 상태를 반영해야 합니다.
develop: 다음 릴리스를 위해 최신 개발이 진행되는 개발 브랜치입니다.
feature 브랜치: 새로운 기능 개발에 사용되는 브랜치입니다. develop 브랜치에서 생성되며, 기능 개발이 완료되면 develop 브랜치에 병합됩니다.
fix 브랜치: 빠르게 코드를 수정하는 데 사용되는 브랜치입니다. develop 브랜치에서 생성되며, 수정이 완료되면 develop -> main 브랜치에 병합됩니다.
이러한 브랜치 전략을 통해 개발 과정을 체계적으로 관리하고, 기능별로 병렬적인 작업을 진행할 수 있습니다. 코드 변경은 각자의 기능 브랜치에서 이루어지며, 최종적인 안정 버전은 main 브랜치에 반영됩니다.
- Kotlin 1.9.20
- Spring Boot 3.1.6 ~ 3.2.0
- Spring MVC
- Spring Webflux
- Spring Data R2DBC
- Spring Security + JWT
- JPA(+ Spring Data JPA)
- kotlin jdsl 3.0
- Spring Cloud Gateway
- MySQL 8.0.33
- Redis(+ Spring Data Redis)
- JASYPT Encrypt
- Kotest
- Kotlin Coroutine
- Docker
- AWS EC2, GCP Compute Engine
- Jenkins
블로그 링크: [Link]
- 문제점 발생 및 이슈
- 질문 생성 요청 -> 외부 API로 질문 생성 -> 부가적인 로직(질문 저장, 사용자 이력 저장) -> 결과 응답의 흐름에서 문제 발생
- 면접 예상 질문 생성시 외부 API 및 부가적인 로직의 예외 전파로 메인 로직이 실패하는 현상 발생
- 해결 방안
- DB 저장이 실패하더라도 사용자가 결과를 확인할 수 있는 것이 서비스에서 더 적합하다고 판단 -> 부가적인 로직을 비동기로 처리
- Facade 패턴 적용
- 네트워크를 이용하는 외부 API를 트랜잭션 밖으로 이동
- DB 트랜잭션 레벨을 다운그레이드하여 트랜잭션의 범위를 최소한으로 설정
- 예상 질문과 사용자 로그를 코루틴으로 비동기적으로 저장하여 예외 전파 해결 시도
- 결과
- 트랜잭션이 외부 서비스 및 부가적인 로직과 묶여서 예외 전파로 인해 롤백되는 강결합 문제 해결
- 코루틴 Scope 내에서 발생하는 부가적인 로직의 예외 발생시 CoroutineExceptionHandler로 핸들링 및 별도로 로깅 처리
- 문제점 발생 및 이슈
- 예상 질문 생성시, 더블 클릭을 할 경우 서버로 같은 두 개의 요청이 오는 문제 발생
- 사용자는 하나의 결과만을 확인하게 되지만, 서버에서는 응답을 DB에 모두 저장하게 되므로, 중복 데이터가 DB에 저장
- 이는 마이페이지에서 의도하지 않는 중복 데이터를 확인할 수 있고, Open Ai의 limit 토큰을 불필요하게 사용하는 문제로 이어질 수 있다.
- 해결 방안
- RateLimiter를 활용해서 일정 시간 내에는 동일한 요청시 응답으로 429를 반환하도록 변경해야 함
- Redis를 사용하고 있기 때문에, Counter 형식으로 구현
- 로직마다, 회원마다 uniqueKey를 구분해야함(캐시 충돌 방지)
- 결과
- Redis를 활용하여 해당 로직의 Key에 대한 count값을 통해, 지정한 값 이상일 경우 예외를 반환
- AOP의 장점을 활용하여, 핵심 로직에서 RateLimit 로직을 분리하여 코드의 가독성과 재사용성을 향상
- 핵심 로직 내에서 RateLimit 처리를 직접 포함시키는 것은 로직의 복잡성을 증가시킬 수 있다.
- 다른 로직에도 RateLimit을 사용하고자 할 경우, 코드가 중복되고 쉽게 확장할 수 없다.
- count값의 경우 함수명 + userId를 통해 uniqueKey를 생성하므로, 캐시 충돌 방지 시도
- 문제점 발생 및 이슈
- 한번의 요청으로 면접 예상 질문 생성을 최대 3개까지 할 수 있도록 확장했지만, 블로킹 방식의 네트워크 요청은 개수에 비례해서 응답 시간이 늘어나는 문제가 발생 → 코루틴을 활용한 병렬 처리를 적용하였음
- 코루틴 사용시, 자식 코루틴(네트워크 요청)이 실패하면서 다른 코루틴까지 실패하는 문제가 발생
- 해결 방안
- 부모 코루틴과 자식 코루틴의 Structured Concurrency 특성으로, 자식 코루틴의 예외가 부모로 전파되고, 부모가 종료를 위해 남은 자식 코루틴이 종료시키는 문제
- 코루틴 빌더 중 SupervisorScope를 이용할 경우 자식 코루틴에서 발생한 예외는 전파되지 않을 것이라고 판단
- 결과
- 자식 코루틴(async)에서 예외가 발생하더라도 예외가 부모 코루틴까지 전파되지 않도록 격리할 수 있었음
- 또한 요청이 병렬적으로 3번 요청되는 상황에서 각 요청은 독립적으로 처리되어 서로의 실패에 영향을 받지 않게 됨(3개 중 1개 실패, 2개 성공시 모두 실패하던 로직 → 사용자는 2개에 대한 결과를 확인 가능)
- 문제점 발생 및 이슈
- AOP가 적용된 비즈니스 로직을 내부 함수로 호출했을 때 AOP가 적용되지 않는 문제가 발생한다.
- A함수에서 B함수를 호출할 때(B함수는 AOP 적용) A에서 호출하는 B 함수는 프록시가 아닌 인스턴스 자신의 함수이기 때문에 프록시가 사용될 수 없다.
- 해결 방안
- 관심사를 분리할 때 AOP가 아닌 코루틴의 후행 람다를 사용해서 분리한다.
- 결과
- 로그 처리와 RateLimiter를 AOP를 사용하지 않고 내부 함수 호출시에도 적용할 수 있게 되었지만, Continuation 객체가 넘어가야하기 때문에 suspend로 함수식을 선언했다.
- 아직 완벽하게 이해하지 못했기 때문에 검증이 더 필요하다.
- 문제점 발생 및 이슈
- 레주마블 서버는 MVC 톰캣에서 실행되기 때문에, 사용자가 요청부터 결과를 받아볼 때까지 20초 내외가 소요된다.
- 최대 쓰레드 수가 디폴트로 200개인 톰캣 옵션을 변경하여 다양하게 부하 테스트를 진행해본 결과, 사용자 요청이 크게 늘어나지 않았다.
- 사용자 요청당 1 쓰레드가 배정되기 때문에, 동시에 200개(디폴트)의 요청만 수행할 수 있고, 이후에는 대기 큐에 쌓인다.
- 외부 API 호출에 코루틴을 활용하더라도, 사용자 요청에 배정된 쓰레드는 코루틴이 종료될 때까지 기다리게된다.
- 요청을 많이 처리하기 위해 쓰레드 개수를 늘리는 것은 리소스를 더 많이 사용하기 때문에, 최적의 방법이라곤 할 수 없다.(스케일 업과 유사)
- 해결 방안
- 스레드 점유 시간이 긴 서비스를 별도의 서버로 분리하여 운영한다.
- Webflux를 활용하여 스레드를 블로킹 시키지 않는다.(더 확인이 필요하다.)
- 이슈
- 면접 예상 질문을 생성하는 것을 병렬로 처리하더라도, 응답을 내려줄 때에는 모든 외부 API 요청을 취합해서 반환한다.
- 만약 특정 외부 API 요청이 길어질 경우 그에 따라 응답 시간이 길어지게 된다.
- 해결 방안
- SSE를 활용해 병렬적인 요청에 대해 결과가 생성될 때마다 실시간으로 응답을 내려주도록 변경한다.
- 코루틴의 Flow를 활용한다.
- WebSocket으로도 실시간 소통이 가능하지만, 서버에서 클라이언트로의 비동기 통신만 하면 되므로 SSE를 선택했다.
- Polling 방법의 경우 TCP 연결을 주기적으로 해주어야하기 때문에 비효율적이라고 판단했다.
- 결과
- 3개의 요청을 병렬적으로 처리하더라도 각 요청이 결과를 생성하는 동시에 Flow로 방출하고, 사용자는 응답을 실시간으로 받아볼 수 있다.
- 면접 예상 질문 생성이 1000회 기준 평균 22초, 최장 32초가 소요되므로 만료 시간을 35초로 설정한다.
- 사용자가 복수의 요청을 했을때 예상 질문이 생성된 순서대로 확인할 수 있다.
Backend. | Frontend. |
---|---|
전상준 | 이유 |