TypeScript를 활용한 서비스개발

KyoungDuck Kim
당근 테크 블로그
9 min readSep 18, 2020

당근마켓은 중고거래 서비스를 넘어선 동네 기반의 하이퍼 로컬 플랫폼으로 도약하기 위해 다양한 서비스를 준비하고 있어요. 그리고 다양한 서비스들을 TypeScript를 활용하여 만들고 있어요. TypeScript는 JavaScript의 동적 타입(Dynamic Typing)정적 타입(Static Typing)으로 활용할 수 있는 superset이에요. TypeScript는 정적 타입을 통해 컴파일 타임에 타입 검사를 해서 코드의 안정성을 향상시켜요.

PHP, Python, JavaScript와 같은 동적 타입의 언어들은 코드를 빠르게 작성할 수 있게 도와주지만 규모가 크거나 기능이 많은 서비스를 개발하기 시작하면 항상 버그의 위험에 노출되어 있어요. 예를 들어 동료가 개발해서 넘겨준 함수의 return값이 string이라고 생각해서 split 메서드를 사용했으나 number타입인 경우 서비스가 동작하는 런타임에 에러가 발생하고 만약 프로덕션에 배포된 서비스라면 서비스 장애로 이어질 가능성이 높아져요.

타입스크립트 코드(컴파일에러)

//index.ts 
const getMessageByKey = (userKey: string) => {
const [userId, tag] = userKey.split(':');
const message = messageRepo.findOne({ where: {userId, tag} });
//....
};
//컴파일 에러
getMessageByKey(3927);

자바스크립트 코드(런타임 에러)

//index.js
const getMessageByKey = (userKey) => {
const [userId, tag] = userKey.split(':');
const message = messageRepo.findOne({ where: {userId, tag} });
//....
};
//런타임 에러
getMessageByKey(3927);

TypeScript의 정적 타입을 사용하게 되는 경우 컴파일 단계에서 타입체크가 되어서 바로 버그가 있는 것을 인지하고 코드를 수정하여 코드의 안정성이 높아지게 되어요.

당근마켓에서는 많은 신규 프로젝트들이 생겨나고 서비스의 규모도 트래픽에 맞게 커지고 있는 만큼 많은 코드를 좀 더 안정적으로 관리할 필요가 있어서 TypeScript를 적극적으로 활용하고 있어요.

NestJS

NestJS

당근마켓은 TypeScript와 함께 NestJS 프레임워크를 사용해요. NestJS는 TypeScript를 완벽하게 지원하는 서버-사이드 프레임워크에요. NestJS를 이용하면 확장 가능하며 유지 관리가 쉬운 서버 애플리케이션을 쉽게 개발할 수 있어요.

NestJS와 다른 프레임워크와의 큰 차이점은 아키텍처 구조를 프레임워크에서 제공해요. 그렇다면 왜 아키텍처를 프레임워크에서 정의를 할까요? 쉽게 예를 들어볼게요. NestJS를 사용하지 않고 순수 express를 사용하는 프로젝트에 여러명의 팀원들이 같이 개발을 하고 있을 때 한 팀원 A가 B에게 이렇게 말을 해요.

A: “B, 이 코드는 언제 호출하는 코드에요?”

B: “그 코드는 제가 개발하지 않아서 C한테 물어보셔야 해요”

A는 다시 C에게 가서 이렇게 말해요.

B: “C, 이 코드는 언제 호출하는 코드에요?”

C: “아 그 코드는 사용자를 인증할 때 호출되는 코드에요. 저는 이런 코드는 미들웨어에 넣고 비즈니스 로직 관련 코드는 클래스를 따로 정의하고 컨트룰러에서 그걸 호출하고…”

A가 B에게 물어봤지만 B도 다른 사람이 작성한 코드가 쉽게 눈에 들어오지 않아요. 그리고 C의 아키텍처 디자인 스타일을 한참을 설명 받고 나서야 C가 개발한 코드가 이해가 되요. 사람마다 아키텍처 디자인 스타일이 다르거나 팀마다 스타일이 다르면 이처럼 커뮤니케이션 하는데 비용이 증가해요.

당근마켓 팀 또한 규모도 점점 커지고 많은 신규 프로젝트들을 개발하고 있기 때문에 개발자마다, 프로젝트마다 구조가 달라질 가능성이 있어요. NestJS는 이런 아키텍처의 정의도 프레임워크에서 제공하기 때문에 각 개발자들의 아키텍처가 통일되고 개발자들이 서로가 작성한 코드의 구조를 쉽게 파악할 수 있어요.

그럼 NestJS가 어떻게 아키텍처를 정의를 제공하는지 NestJS 기능들을 살펴볼게요.

Module

모듈은 비슷한 기능을 하는 코드들의 모음이에요. NestJS은 비슷한 기능들을 모아서 모듈로 캡슐화해요. 일반적으로 하나의 모듈은 하나의 비즈니스 로직과 관련된 기능들을 담고 있어요. 예를 들어 유저 정보에 대한 CRUD(쓰기, 읽기, 수정, 삭제)에 대한 로직이 있는 service와 이런 API를 제공하는 controller가 하나의 모듈이 될 수 있어요.

유저 모듈

또한 하나의 모듈은 다른 모듈의 기능이 필요할 때 import하여서 다른 모듈의 기능을 이용할 수 있어요. 게시글 정보를 불러오는 모듈에서 유저 정보를 갖고 오기 위해 유저 모듈을 import해서 그 기능을 이용할 수 있죠.

모듈을 정의할 때 @Module 데코레이터를 이용해서 쉽게 정의가 가능해요.

이처럼 비슷한 기능들을 하나의 모듈로 모아서 응집력을 높이고 다른 모듈끼리 import하고 인스턴스의 생성을 NestJS에 맡겨서 느슨한 결합이 되어 쉽게 확장 가능한 아키텍처 설계가 가능해요.

Controller

컨트룰러는 해당 모듈을 호출할 수 있는 경로를 정의하고 비즈니스 로직을 호출 할 수 있는 곳이에요. REST방식으로 API를 정의할 수 있고 각 EndPoint의 요청을 받아들여 해당 로직을 수행한 후 응답을 반환시켜요. @Controller 데코레이터를 이용하여 쉽게 정의가 가능해요.

@Get , @Post 와 같은 데코레이터로 HTTP 메서드를 지정하고 해당 엔드포인트의 로직을 함수로 정의 할수 있어요.

Provider

Class에 @Injectable 데코레이터가 달리면 해당 객체는 런타임에 NestJS에 의해 다른 모듈로 주입될 수 있어요. 이것이 바로 Provider객체에요. 모듈안에 Provider객체를 정의하고 등록하면 그 Provider객체는 모듈 안에서 사용될 수 있어요. 뿐만 아니라 서로 다른 모듈을 import해서 그 안에 필요한 Provider객체를 사용할 수도 있어요.

이렇게 손쉽게 Provider객체를 이용할 수 있는 비결은 NestJS에서 Provider객체를 인스턴스화 하고 필요할 때 인스턴스를 주입시켜주어요. 따라서 개발자가 런타임에 객체를 관리하는 일이 없이 필요한 객체를 주입받아서 사용하면 되요.

성능

NestJS의 성능 또한 중요해요. NestJS가 좋은 아키텍처를 갖고 있고 유용한 기능들이 많지만 이런 기능들을 사용하는데 NestJS를 사용하지 않고 서비스를 구현 했을 때와 퍼포먼스가 큰 차이가 나면 당근마켓의 서비스들을 만드는데 사용하기 적절하지 않을 수 있어요. 당근마켓에서 NestJS로 구현한 프로젝트중 초당 요청이 1500(RPS)까지 증가하는 서비스들도 있어요. 이런 서비스들은 성능에 정말 민감해서 많은 트래픽을 염두에 두고 개발을 해야해요.

NestJS는 Express를 기본적으로 사용하고 있어요. 그리고 NestJS의 Github Repository에서는 PR마다 순수한 Express와 NestJS를 사용하는 경우에 대한 RPS(초당 요청 수)를 비교하는 벤치마크를 측정해요.

이 벤치마크에는 Express가 아닌 fastify를 사용했을 때의 벤치마크도 포함되어 있어요.

https://github.com/nestjs/nest/pull/5207/checks?check_run_id=946935870

최근에 merge된 PR에 대한 벤치마크에요. NestJS를 사용하는 경우가 순수한 Express를 사용하는 경우보다 조금더 느렸어요. 약 7% 정도 차이가 났어요. NestJS가 Express와 속도 차이가 나는 이유는 프레임워크 내부적으로 Express를 사용하지만 거기에 더하여 여러가지 프레임워크 기능을 사용하기 때문이에요.

만약 정말 간단한 Node서버를 만든다면 Express가 성능은 더 좋을 수 있어요. 하지만 서비스에 여러기능을 개발하게 되고 Express에서 미들웨어나 서비스 레이어의 크기가 점점 커지면 결국 NestJS를 사용했을 경우와 큰 성능차이가 나지 않아요.

Etc.

NestJS 프레임워크에서는 흔히 웹 서비스를 개발하는데 자주 사용하는 기능들이 구현되어 있어요. validation을 위해 Pipe를 사용할 수 있고 인가(Authorization)을 위해 Guard를 사용할 수도 있어요.

프레임워크에서 제공하는 기능 뿐만 아니라 NPM 생태계에서 많이 쓰이는 다양한 라이브러리를 NestJS 모듈로 추상화해서 제공하기도 해요. ORM를 사용하고 싶으면 TypeORM을 사용하고 NestJS에서 원하는 Entity의 Repository를 주입 받을 수 있어요. TypeORM의 Repository에 대한 자세한 내용은 TypeORM의 공식문서를 참조해주세요(https://typeorm.io/#/working-with-repository).

TypeORM 외에도 Node.js에서 유명한 패키지나 라이브러리들을 NestJS에서 쉽게 쓸 수 있게 제공하고 있어요. 자세한 내용은 NestJS 공식문서에서 확인할 수 있어요. https://docs.nestjs.com/

마무리하며

당근마켓은 MAU를 1000만 달성하며 가파른 성장을 하고 있어요. 그 만큼 당근마켓 유저들에게 다양한 서비스들을 제공하려고 해요. 그래서 생산성 있는 개발과 많은 트래픽을 감당 할 수 있는 기술을 찾았어요. 뿐만 아니라 점점 서비스와 팀이 많아졌기 때문에 개발자들이 협업하기 좋은 해결책을 찾았어요. 그 결과 현재는 TypeScript와 NestJS를 사용하고 있어요. 하지만 지금도 더 좋은 방법을 찾기 위해 고민하고 있어요.

만약 당신 근처의 개발자들과 함께 고민하고 좋은 서비스를 만들고 싶은 기회를 찾고 계신다면 당근마켓 팀에 알려주세요. 👉 https://dngn.kr/join-us-dev

--

--