앞선 글에서는 “어디서부터 타입 리팩터링을 시작해야 하는가”를 순서 중심으로 살펴봤습니다. 이번 글에서는 한 단계 더 나아가, 실제 API 코드가 어떻게 바뀌는지를 처음 상태부터 개선된 상태까지 그대로 보여줍니다.
실무에서 가장 도움이 되는 순간은 원칙 설명이 아니라 “이 코드가 이렇게 바뀌는구나”를 보는 순간입니다. 그래서 이번 글은 설명보다 예제를 중심으로 구성합니다.
초기 상태: 흔한 NestJS API 코드
다음은 실제 프로젝트에서 자주 볼 수 있는 컨트롤러와 서비스 코드입니다.
// user.controller.ts
@Get(':id')
async getUser(@Param('id') id: string) {
return this.userService.getUser(id);
}
// user.service.ts
async getUser(id: string): Promise<any> {
const user = await this.repo.findById(id);
if (!user) {
throw new NotFoundException();
}
return user;
}
이 코드는 문제없이 동작하지만, 타입 관점에서는 다음 한계를 가지고 있습니다.
- any로 인해 반환 구조가 숨겨져 있다
- 실패 흐름이 예외에 묻혀 있다
- 컨트롤러는 성공/실패를 구분할 수 없다
1단계: 서비스 반환 타입부터 명확히 한다
가장 먼저 서비스 레이어의 반환 타입을 고정합니다. 이 단계에서는 컨트롤러를 건드리지 않습니다.
type User = {
id: string;
email: string;
};
type UserError =
| { type: 'NOT_FOUND' };
type Result<T, E> =
| { ok: true; data: T }
| { ok: false; error: E };
// user.service.ts
async getUser(
id: string,
): Promise<Result<User, UserError>> {
const user = await this.repo.findById(id);
if (!user) {
return { ok: false, error: { type: 'NOT_FOUND' } };
}
return { ok: true, data: user };
}
이 변경만으로도 서비스의 책임과 결과가 타입에 드러나기 시작합니다.
2단계: 컨트롤러에서 결과를 해석한다
이제 컨트롤러가 “무조건 성공한다”는 가정을 버립니다.
// user.controller.ts
@Get(':id')
async getUser(@Param('id') id: string, @Res() res: Response) {
const result = await this.userService.getUser(id);
if (!result.ok) {
if (result.error.type === 'NOT_FOUND') {
return res.status(404).end();
}
}
return res.json(result.data);
}
여기서 중요한 변화는 다음입니다.
- 컨트롤러가 실패 가능성을 인지한다
- HTTP 응답 코드 결정 로직이 명확해진다
- 새 에러 타입이 추가되면 컴파일 에러로 드러난다
3단계: 예외 필터에 타입 정보를 연결한다
이제 Result 기반 구조를 유지한 채, NestJS 예외 처리와 연결합니다.
function toHttpException(error: UserError): HttpException {
switch (error.type) {
case 'NOT_FOUND':
return new NotFoundException();
}
}
이렇게 하면
- 도메인 에러와 HTTP 예외가 분리되고
- 매핑 규칙이 한 곳에 모이며
- 에러 확장이 쉬워집니다
운영 관점에서 실제로 달라지는 점
이 구조를 적용하면 운영 단계에서 체감되는 변화가 분명합니다.
- 로그에서 error.type 기준으로 집계 가능
- 장애 발생 시 어떤 실패인지 바로 구분 가능
- 컨트롤러 단에서 응답 누락 가능성 감소
실무에서 자주 하는 실수
- Result 타입을 모든 내부 함수에 강제하는 경우
- 도메인 타입에 HTTP 개념을 섞는 경우
- 초반부터 공통 유틸로 일반화하는 경우
이 글의 예제에서처럼, Result는 레이어 경계에서만 쓰는 것이 핵심입니다.
실무 적용 체크리스트
- 서비스 반환 타입에 성공/실패가 드러나는가
- 컨트롤러가 결과 해석 책임을 가지는가
- 도메인 에러와 HTTP 예외가 분리되어 있는가
- 타입 변경 시 영향 범위를 예측할 수 있는가
실무에서 유효한 타입 리팩터링은 “대단한 타입”이 아니라 코드의 책임을 드러내는 타입에서 시작됩니다.
컨트롤러, 서비스, 예외 처리 흐름을 예제처럼 한 번만 정리해두면, 이후 기능 추가와 운영 대응이 눈에 띄게 수월해집니다.
'개발 > Typescript' 카테고리의 다른 글
| [TYPESCRIPT] Variadic Tuple Types를 실무에서 쓰는 경우 (0) | 2026.02.04 |
|---|---|
| [TYPESCRIPT] 실제 서비스 코드로 끝까지 따라가는 타입 리팩터링: 로그인 API 한 개를 제대로 고쳐보기 (0) | 2026.02.03 |
| [TYPESCRIPT] 리팩터링 우선순위 예제로 보는 타입 개선 전략: 어디서부터 손대야 효과가 나는가 (0) | 2026.02.01 |
| [TYPESCRIPT] 리팩터링 예제로 보는 타입 개선 과정: 나쁜 TypeScript 타입을 좋은 타입으로 바꾸는 방법 (0) | 2026.01.31 |
| [TYPESCRIPT] 타입 자동화 적용 한계: 권한·피처 플래그·환경 변수에서 멈춰야 할 지점 (0) | 2026.01.30 |
