-
Notifications
You must be signed in to change notification settings - Fork 3
Trouble Shooting
Open API Specification(OAS) 파일을 생성하기 위해 사용하는 패키지 [tspec](<https://ts-spec.github.io/tspec/introduction>)
(버전 0.1.105)에서 다음과 같은 문제가 발생했습니다.
- API 스펙에 요청 본문(request body)이 없는 경우, OAS 생성에 실패하는 문제
- 요청 본문 또는 응답 본문 타입 선언부에
@mediaType
태그를 명시한 경우, 유효하지 않은 OAS가 생성되는 문제
- API 스펙에 요청 본문이 없는 경우, 해당 속성이
undefined
로 처리되지 않아 OAS 생성 과정에서 오류가 발생했습니다. -
@mediaType
태그를 통해 명시한 미디어 타입이 OAS의schema
속성의 하위 필드로 잘못 포함됨에 따라 유효하지 않은 OAS가 생성되었습니다.
- 요청 본문이 없는 경우를 고려하여, 본문 데이터가
undefined
일 때 적절히 처리하도록 수정했습니다. - OAS의
schema
타입에서mediaType
속성을 제외하도록 변경했습니다.-
bodyParams
에서mediaType
을 분리하여 별도의 변수에 할당하고,bodyParams
에는 포함하지 않도록 처리 -
bodyParams
가 비어있는 경우, 빈 객체를undefined
로 변환
-
https://github.com/ts-spec/tspec/pull/34
https://github.com/ts-spec/tspec/pull/35
https://github.com/ts-spec/tspec/pull/38
서버로 전송된 요청의 검증을 위해 사용 중인 express-openapi-validator
패키지에서 쿼리 파라미터의 공백 문자가 +
로 인코딩되어 있을 때 검증에 실패하는 문제가 발생했습니다. 해당 패키지는 +
문자를 예약 문자(reserved character)로 간주하여 요청을 거부하고 있었습니다.
express-openapi-validator
패키지의 소스코드를 확인한 결과, RFC3986 명세에 따라 +
문자를 예약 문자로 정의하고 있음을 확인했습니다. 이는 패키지 개발자가 의도적으로 설계한 동작이었으며, 표준에 따른 올바른 구현이라는 입장을 고수하고 있었습니다.
해당 패키지의 이슈 트래커를 통해 많은 사용자들이 유사한 문제를 제기한 사실을 확인했습니다. 비록 개발자가 현재의 동작이 표준에 부합한다는 입장이었지만, 사용자 편의성을 고려하여 문제를 해결할 수 있는 방법을 모색했습니다.
패키지 개발자의 제안에 따라, OpenAPI 스키마 정의 시 해당 쿼리 파라미터에 대해 allowReserved: true
옵션을 명시하는 방법으로 문제를 해결하였습니다. 이를 통해 +
문자를 포함한 예약 문자의 사용을 허용할 수 있게 되었습니다.
express-openapi-validator
https://github.com/sct/overseerr/issues/2010
https://github.com/cdimascio/express-openapi-validator/issues/241
Mr.C
https://github.com/MovieReviewComment/Mr.C/pull/110
개발 환경과 프로덕션 환경에서 API 문서 페이지를 효과적으로 제공하기 위해, 서버 부팅 시점에 환경에 따라 OpenAPI Specification(OAS) 생성 방식을 분기할 필요가 있었습니다.
- 개발 환경: OAS를 매번 갱신한 후 Swagger 페이지를 구동하여 최신 API 문서를 제공
- 프로덕션 환경: 이미 생성된 OAS 파일을 기반으로 Swagger 페이지를 구동하여 안정적으로 운영
초기 구현에서는 tspec
패키지를 사용하여 OAS를 생성하고 Swagger 페이지를 구동하였습니다. 그러나 이 방식은 프로덕션 환경에서 다음과 같은 문제를 야기했습니다.
- 포트 충돌 문제
-
initTspecServer
함수가 HTTP 서버 포트와 동일한 포트를 사용하여 Swagger 페이지를 구동하려 했기 때문에 포트 충돌이 발생했습니다. -
tspec
미들웨어를 사용하면 이 문제를 해결할 수 있었지만, 서버 시작 시마다 OAS를 생성하고 Swagger 페이지를 구동하는 데 오랜 시간이 소요되는 단점이 있었습니다.
-
- 불필요한 OAS 생성
- 프로덕션 환경에서는 이미 생성된 OAS 파일을 기반으로 Swagger를 구동하므로,
tspec
을 사용하여 매번 OAS를 생성할 필요가 없었습니다.
- 프로덕션 환경에서는 이미 생성된 OAS 파일을 기반으로 Swagger를 구동하므로,
이러한 문제를 해결하기 위해 다음과 같이 개선하였습니다.
-
swagger-ui-express
패키지 도입- 이미 생성된 OAS 파일을 기반으로 Swagger UI를 구성할 수 있는
swagger-ui-express
패키지를 도입하였습니다. - 이를 통해 프로덕션 환경에서는
tspec
을 사용하지 않고도 Swagger 페이지를 효과적으로 제공할 수 있게 되었습니다.
- 이미 생성된 OAS 파일을 기반으로 Swagger UI를 구성할 수 있는
- 미들웨어를 통한 포트 충돌 해결
-
swagger-ui-express
미들웨어를 사용하여 Swagger 페이지를 구동함으로써 포트 충돌 문제를 해결하였습니다. - 이제 서버의 포트를 그대로 사용하면서 Swagger 페이지를 제공할 수 있게 되었습니다.
-
서버 부팅 시 환경에 따라 OAS 생성 방식을 분기하는 과정에서 발생한 포트 충돌 문제와 불필요한 OAS 생성 문제를 해결하기 위해 swagger-ui-express
패키지를 도입하고 미들웨어를 활용하였습니다. 이를 통해 개발 환경과 프로덕션 환경에 맞는 효율적인 API 문서 제공 방식을 구현할 수 있었습니다.
https://github.com/MovieReviewComment/Mr.C/pull/45
winston
라이브러리를 사용하여 로깅 시스템을 구현하던 중, Error
객체를 로그로 출력할 때 에러 정보가 누락되는 문제가 발생했습니다. 구체적으로는 다음과 같은 상황에서 에러 정보가 제대로 출력되지 않았습니다.
- 메시지가
Error
객체인 경우 - 메타 정보가
Error
객체를 포함하고 있는 경우 - 메타 정보의 특정 속성이
Error
객체를 포함하고 있는 경우
winston
라이브러리의 이슈 트래커에서 유사한 문제가 보고된 것을 확인했으나, 해당 이슈가 오랜 기간 해결되지 않고 있었습니다.
winston
라이브러리 내부에서 Error
객체를 적절히 처리하지 않아 발생한 문제로 파악했습니다. Error
객체를 로깅할 때, 해당 객체의 중요한 정보(메시지, 스택 트레이스 등)가 누락된 채로 빈 객체로 출력되고 있었습니다.
winston
라이브러리의 포맷터를 커스터마이징하여 Error
객체를 적절히 변환하는 방식으로 문제를 해결하기로 했습니다.
-
enumerateErrorFormat
이라는 커스텀 포맷터를 작성했습니다. -
convertErrorPropertiesToObject
함수를 구현하여 로그 객체의 속성을 순회하며Error
객체를 찾아냈습니다. -
Error
객체를 찾으면, 해당 객체의 중요한 정보(메시지, 스택 트레이스 등)를 추출하여 일반 객체로 변환했습니다. - 변환된 객체를 원래의
Error
객체 위치에 할당하여 로그 출력 시 에러 정보가 포함되도록 했습니다. - 중첩된
Error
객체가 있는 경우도 고려하여, 재귀적으로 중첩된Error
객체를 변환하도록 처리했습니다.
구현한 커스텀 포맷터를 winston
로거 설정에 추가하여 적용함으로써, Error
객체를 포함한 로그가 올바르게 출력되도록 수정할 수 있었습니다.
winston
라이브러리의 Error
객체 처리 버그를 우회하기 위해, 커스텀 포맷터를 작성하여 Error
객체를 수동으로 변환하는 방식으로 문제를 해결했습니다. 이를 통해 에러 정보가 누락되지 않고 로그에 정상적으로 출력될 수 있도록 개선했습니다.
다만 이는 근본적인 해결책이라기보다는 임시방편에 가까운 것이므로, 추후 winston
라이브러리의 업데이트를 통해 해당 버그가 공식적으로 수정되기를 기대하고 있습니다.
winston
https://github.com/winstonjs/winston/issues/1338
Mr.C
https://github.com/MovieReviewComment/Mr.C/issues/85
https://github.com/MovieReviewComment/Mr.C/pull/87
기존의 에러 처리 방식은 각 레이어(controller/service/repository)마다 별도의 에러를 정의하고, 에러를 캐치하는 곳에서 내용을 보고 자신의 레이어에 맞는 형태로 재가공하거나 그대로 던지는 방식이었습니다. 이러한 방식은 과정이 필요 이상으로 복잡하고, 그로 인해 얻어지는 이득은 크지 않다는 문제점이 있었습니다.
에러 처리 과정을 보다 간단하고 명료하게 만들기 위해 다음과 같이 개선했습니다.
- 모든 레이어(controller/service/repository 등)에서 분류할 수 있는 모든 에러를
CustomError
로 통일했습니다. -
CustomError
에 에러 코드를 기준으로 분기하여 알맞은 상태 코드를 반환하는 기능을 추가했습니다.
개선 과정에서 Request/Response를 검증하는 패키지에서 던지는 에러는 이미 상태 코드가 설정되어 있다는 점이 문제로 대두되었습니다. 이 에러를 CustomError
로 변환하려면 상태 코드에 대한 내용이 추가되어야 했습니다.
이를 해결하기 위해 두 가지 방안을 고려했습니다.
-
CustomError
에 HTTP 에러에 대한 내용을 추가하는 방안- 장점: 새로운 통신 프로토콜 레이어가 생겨도
CustomError
에 프로토콜의 에러 코드와 변환 기능만 추가하면 되므로 확장성이 좋습니다. - 단점:
CustomError
가 HTTP의 에러 코드까지 알고 있게 됩니다.
- 장점: 새로운 통신 프로토콜 레이어가 생겨도
- HTTP 응답을 위한 별도의 에러(
HttpError
)를 정의하고,CustomError
를HttpError
로 변환하는 과정을 추가하는 방안- 장점:
CustomError
에서 HTTP의 에러 코드 내용이 필요 없어집니다. - 단점: 새로운 통신 프로토콜 레이어가 생길 때마다 해당 프로토콜용 에러를 정의하고, 모든 에러를 그에 맞게 변환하는 과정이 필요해집니다.
- 장점:
두 방안을 비교한 결과, CustomError
가 HTTP의 에러 코드를 알고 있는 것이 큰 문제가 되지 않는다고 판단했습니다. 반면에 HttpError
를 정의하여 변환하는 과정은 개발 공수만 늘어날 뿐 큰 이득이 없다고 보았습니다. 따라서 CustomError
에 HTTP의 에러 코드를 추가하는 방향으로 결정했습니다.
에러 핸들링을 고도화하는 과정에서 에러 처리 방식을 통일하고 간소화하였습니다. 이 과정에서 CustomError
에 HTTP 에러 코드를 추가하는 방향으로 설계를 개선했으며, 에러 로그 기록 시 context
필드를 활용하되 클라이언트 응답에는 포함하지 않기로 했습니다. 또한 에러 메시지 필드와 로그 형식에 대한 기준을 정립하여 일관성과 가독성을 높였습니다.
https://github.com/MovieReviewComment/Mr.C/issues/84
https://github.com/MovieReviewComment/Mr.C/pull/86
Prisma
ORM의 확장된 클라이언트에서 트랜잭션을 별도의 함수로 분리하여 실행할 때, 트랜잭션 클라이언트의 타입을 명시하는 데 어려움이 있었습니다.
-
Prisma
라이브러리의 이슈 트래커에서 유사한 문제가 보고된 것을 확인했습니다. -
이슈에 제시된 해결 방안을 참고하여,
TransactionClient
타입이Prisma.DefaultPrismaClient
에서runtime.ITXClientDenyList
를 제외한 타입으로 정의되어 있다는 점에 착안했습니다. -
이를 바탕으로 확장된 클라이언트에서도
ITXClientDenyList
를 사용하여 트랜잭션 타입을 정의할 수 있었습니다.typescriptCopy codeasync function foo(tx: Omit<ExtendedPrismaService, ITXClientDenyList>) { ... }
-
이 방식을 통해 코드 전반에 걸쳐 일관된 트랜잭션 타입을 사용할 수 있게 되었습니다.
Prisma
ORM의 확장된 클라이언트에서 트랜잭션 타입 문제를 해결하기 위해 라이브러리의 이슈 트래커를 참고하여 ITXClientDenyList
를 활용하는 방안을 모색했습니다. 이를 통해 확장된 클라이언트에서도 트랜잭션 타입을 명확히 정의할 수 있게 되었습니다.
prisma
https://github.com/prisma/prisma/issues/20738
Mr.C
https://github.com/MovieReviewComment/Mr.C/pull/114
레포지토리 계층의 테스트를 위해 실제 사용 중인 PostgreSQL이 아닌 별도의 SQLite 데이터베이스를 사용하려고 했습니다. 이는 레포지토리 구현체는 동일하게 유지하면서 DB 클라이언트만 교체하여 테스트를 간소화하고 병렬적으로 실행하기 위한 목적이었습니다.
그러나 Prisma
는 DBMS에 대한 의존성이 스키마뿐 아니라 구현체에도 깊게 내재되어 있어, 구현 로직만 별도로 DBMS에 독립적으로 테스트하는 것이 불가능했습니다. Prisma
클라이언트가 생성될 때 DBMS에 따라 클라이언트의 타입과 동작이 달라지기 때문에, SQLite로 대체하여 테스트하는 것이 현실적으로 어려웠습니다.
이 문제를 해결하기 위해 Prisma
커뮤니티에서 제안된 멀티 DBMS 지원 방안을 검토해 보았습니다. 제안된 내용은 schema.prisma
파일에서 여러 개의 datasource
를 정의하고, 각 datasource
마다 서로 다른 DBMS 연결 정보와 관련 모델을 지정하는 것이었습니다.
prismaCopy codedatasource proj01 {
provider = "postgresql"
url = env("DATABASE_URL_PROJ01")
models = [Post, User]
}
datasource proj01-test {
provider = "sqlite"
url = "file:./test.db"
models = [Post, User]
}
그리고 Prisma
클라이언트 인스턴스 생성 시, 사용할 datasource
를 명시하여 각 DBMS에 맞는 클라이언트를 생성할 수 있게 하는 방안이 제시되었습니다.
typescriptCopy codeimport { PrismaClient } from '@prisma/client'
const db = process.env.NODE_ENV === 'test'
? new PrismaClient('proj01-test')
: new PrismaClient('proj01')
그러나 이 방안도 근본적으로는 구현 로직을 DBMS에 독립적으로 테스트할 수 있게 해주지는 않았습니다. Prisma
클라이언트의 타입과 동작이 여전히 DBMS에 의존적이기 때문입니다.
결국 Prisma
가 현재로서는 DBMS와 강하게 결합되어 있어, SQLite를 사용하여 PostgreSQL 기반 레포지토리를 독립적으로 테스트하는 것은 현실적으로 어렵다는 결론을 내렸습니다.
따라서 실제 PostgreSQL 인스턴스를 Docker를 활용하여 구동하고, 이를 통해 레포지토리 테스트를 진행하기로 결정했습니다.
비록 초기에 의도했던 SQLite를 통한 간소화된 테스트는 실현하지 못했지만, 실제 운영 환경과 동일한 DBMS를 사용하여 테스트를 진행함으로써 보다 신뢰할 수 있는 테스트 결과를 얻을 수 있을 것으로 기대하고 있습니다.
Prisma
https://github.com/prisma/prisma/issues/2443
Mr.C
https://github.com/MovieReviewComment/Mr.C/pull/61
https://github.com/MovieReviewComment/Mr.C/pull/119
서버 애플리케이션의 Graceful shutdown을 구현할 때, Node.js의 프로세스 레벨 시그널 처리를 통해 각 컴포넌트의 정상 종료를 관리하고자 했습니다. 이 과정에서 시그널 핸들러 내부의 비동기 작업 완료를 보장하는 것이 중요한 이슈로 대두되었습니다.
Graceful shutdown 구현 과정에서 시그널 핸들러 내부의 비동기 작업 완료를 보장하기 위해 EventEmitter
를 사용하였으나, 다음과 같은 문제 상황을 마주했습니다.
-
EventEmitter.once
를 사용하여 이벤트 처리 후 바로 해제할 경우, 이벤트 큐가 비어있는 상태가 되는 순간 Node.js 프로세스가 예기치 않게 종료되는 문제가 발생했습니다. - 이로 인해 Graceful shutdown 로직이 완료되기 전에 프로세스가 종료되어, 의도한 대로 정상적인 자원 해제와 클린업이 이루어지지 않는 상황이 발생했습니다.
EventEmitter.once
대신 EventEmitter.on
을 사용하여 문제를 해결하였습니다.
-
EventEmitter.on
을 사용하여 이벤트 핸들러를 등록함으로써, 해당 이벤트에 대해 지속적으로 이벤트 큐에 콜백 함수를 등록하도록 처리했습니다. - 이렇게 함으로써 비동기 작업이 완료될 때까지 이벤트 루프가 유지되도록 하여, Graceful shutdown 로직이 완료된 후에야 프로세스가 종료되도록 보장할 수 있었습니다.
EventEmitter.once
를 사용할 경우 발생할 수 있는 문제점을 인지하고, EventEmitter.on
을 사용하여 해결할 수 있다는 점을 경험하였습니다. 이를 통해 Node.js 이벤트 루프의 동작 방식과 비동기 작업 처리에 대한 이해도를 높일 수 있었습니다.