ResponseEntity 로 통일화된 응답 형식 만들기
NodeJS 서버 프레임워크 중 express 에서는 응답을 보낼 때 간단하게 res.json() 으로 보낼 수 있다. 간단한 앱에서는 응답을 보내는 목적으로 문제가 없어 보인다.
처음 express 를 사용할 때 아래와 같이 코드를 작성했었다.
app.get('/user', (req, res) => {
res.json({
id: 1,
name: 'John Doe',
email: 'john.doe@example.com'
});
});
app.get('/product', (req, res) => {
res.json({
id: 101,
name: 'Laptop',
price: 999.99
});
});
app.get('/order', (req, res) => {
res.json({
orderId: 5001,
userId: 1,
product: 'Laptop',
quantity: 2,
total: 1999.98
});
});하지만 점점 비즈니스 로직이 커져가면 라우터, 컨트롤러가 많아진다. 여기서 일관된 응답 형식을 강제할 수 없는 문제가 발생한다.
이를 해결하기 위해 다양한 방법이 있지만, JAVA Spring Boot 의 ResponseEntity 를 사용하는 방식이 일관된 응답 형식을 지키는데 좋다 보였다. 인터페이스를 참고해 nodeJS express 에서 사용 가능한 코드로 작성해보았다.
ResponseEntity 클래스
자바 스프링에서 사용되는 ResponseEntity 를 참고했기 때문에 아래와 같이 코드를 작성했다.
/**
* 응답 Body 타입
*/
export type SuccessResponse<T> = {
result: true; data: T
};
export type ErrorResponse = {
result: false;
error: {
message: string,
errorCode: string,
[key: string]: any }
};
export type ResponseContent<T> =
| SuccessResponse<T>
| ErrorResponse;
/**
* 공통 ResponseEntity 클래스
*/
export class ResponseEntity<T> {
private status: number;
private bodyContent: ResponseContent<T> | null = null;
private headers: HttpHeaders;
constructor(
status: HttpStatusKey,
headers = new HttpHeaders()
) {
this.status = HttpStatus[status];
this.headers = headers;
}
static ok<T>(): ResponseEntity<T> {
return new ResponseEntity<T>('OK');
}
static badRequest<T>(): ResponseEntity<T> {
return new ResponseEntity<T>('BAD_REQUEST');
}
static internalServerError<T>(): ResponseEntity<T> {
return new ResponseEntity<T>('INTERNAL_SERVER_ERROR');
}
body(content: T): this {
this.bodyContent = { result: true, data: content };
return this;
}
error(
message: string,
errorCode: string,
additionalInfo: { [key: string]: any } = {}
): this {
this.bodyContent = {
result: false,
error: {
message,
errorCode,
...additionalInfo
}
};
return this;
}
getStatus(): number {
return this.status;
}
getBody(): ResponseContent<T> | null {
return this.bodyContent;
}
getHeaders(): HttpHeaders {
return this.headers;
}
}주로 많이 사용되는 정상 응답인 ok 정적 메소드와 클라이언트의 잘못된 요청을 뜻하는 400 에러를 보내는 badRequest 그리고 서버 에러인 500 을 반환하는 internalServerError 정적 메소드를 추가했다.
정적 메소드를 사용하는 방법과 직접 생성자로 응답 객체를 만드는 2가지를 구현해보았다. 그리고 body 를 추가할 때는 **체인 메소드** 패턴을 사용해서 body를 넣는 코드를 구분하도록 했다.
응답에는 성공과 실패를 나타내는 **result** 와 성공할 때는 **data** 그리고 실패할 때는 **error** 를 보내서 구분할 수 있도록 일관된 방법을 택했다. 클라이언트는 result 의 boolean 값을 보고 data / error 에 대한 타입을 분기로 추론할 수 있도록 할 수 있어진다.
위 코드에서 사용된 나머지 객체인 HttpStatus 와 HttpHeaders 코드는 아래와 같다.
/**
* ----------------------------------------------
* HTTP 상태
* ----------------------------------------------
*/
export const HttpStatus = {
OK: 200,
BAD_REQUEST: 400,
INTERNAL_SERVER_ERROR: 500,
// 나머지 Http Status Code 추가 ...
} as const;
export type HttpStatusKey =
keyof typeof HttpStatus;
/**
* ---------------------------
* HTTP 헤더
* ---------------------------
*/
type WellKnownHeaders =
| 'Content-Type'
| 'Authorization'
| 'Accept'
| 'Cache-Control'
| 'User-Agent'
| 'Referer';
export class HttpHeaders {
private headers: Record<string, string>;
constructor() {
this.headers = {};
}
set(
name: WellKnownHeaders | string,
value: string
): void {
this.headers[name] = value;
}
get(name: WellKnownHeaders | string): string | undefined {
return this.headers[name];
}
getAll(): Record<string, string> {
return this.headers;
}
}HTTP 상태명과 코드를 나타낸 HttpStatus 와 많이 사용되는 Http Header 키를 미리 타입으로 등록하고 그 외 키는 문자열로 추가/조회 할 수 있는 HttpHeader 를 작성했다.
헤더를 추가할 때는 WellKnownHeaders 타입 추론의 도움을 받을 수 있도록 했다. 이제 만든 ResponseEntity 객체를 사용하는 Controller 코드를 살펴보자.
/**
* Controller
*/
class MyController {
// ...
public getMain(req: Request, res: Response): ResponseEntity<object> {
return ResponseEntity.ok()
.body({ message: 'OK' });
}
public postData(req: Request, res: Response): ResponseEntity<object> {
const requestData = req.body;
if (!requestData || Object.keys(requestData).length === 0) {
return ResponseEntity.badRequest()
.error('Invalid data', 'INVALID_DATA', { missingFields: ['data'] });
}
return ResponseEntity.ok()
.body({
message: 'Data received successfully',
data: requestData
});
}
}요청, 응답 타입은 express 타입을 사용했다. 간단히 GET 과 POST 에 대한 코드만 작성했다. 변경된 코드가 매우 깔끔해지고 규칙있는 코드로 바뀌어졌다.
하지만 지금까지 코드를 봤을 때 의문이 들 수 있다. 결국에는 express 에서는 res.json() 으로 요청을 보내야하는데 ResponseEntity 가 직접 호출하지 않고 있다. 어떻게 응답을 보낼 수 있는 걸까?
응답 작업을 담당하는 Middleware 와 Router 로 위임하기
나는 이를 미들웨어와 라우터에 위임했다. ResponseEntity 는 응답에 필요한 데이터를 담는 역할에 충실하도록 했다.
이 객체를 해석해서 express 의 res.json 를 내부적으로 호출하는 res.sendResponseEntity 를 미들웨어에서 주입해주었다. 그리고 라우터에서 이를 통해 응답하도록 분리했다.
이제 ResponseEntity 를 해석할 수 있는 미들웨어를 추가해줘야 한다.
app.use(sendResponseEntityMiddleware);알다시피 app.use 로 미들웨어를 등록할 수 있다. 이제 미들웨어 구현 코드로 이동해보자.
/**
* ResponseEntity 객체를 해석하고 응답 작업을 담당하는 미들웨어
*/
export function sendResponseEntityMiddleware(req: Request, res: Response, next: NextFunction) {
res.sendResponseEntity = <T>(responseEntity: ResponseEntity<T>) => {
const headers = responseEntity.getHeaders().getAll();
for (const key in headers) {
res.setHeader(key, headers[key]);
}
const body = responseEntity.getBody();
if (body !== null) {
res.status(responseEntity.getStatus()).json(body);
} else {
res.sendStatus(responseEntity.getStatus());
}
};
next();
}
/**
* 타입스크립트 컴파일러에게 타입 알려주기
*/
declare module 'express-serve-static-core' {
interface Response {
sendResponseEntity: <T>(responseEntity: ResponseEntity<T>) => void;
}
}직접 만든 ResponseEntity 객체를 resolve 하고 express 의 응답 호출을 하도록 하는 sendResponseEntity 를 미들웨어에서 선언해준다. 이를 라우터에서 이용해서 클라이언트에 응답하면 된다.
여기서 express-serve-static-core는 @types/express 패키지에 포함된 모듈 중 하나로 아까 사용한 Request, Response 타입을 선언하고 있다. 기존 타입을 확장하기 위해 declare module 부분 코드를 추가해주었다.
라우터에서 응답 처리
라우터에서는 미들웨어에서 주입한 res 객체의 메소드를 호출하기만 하면 된다.
class MainRouter {
private handleGetMain(req: Request, res: Response) {
const responseEntity = this.mainController.getMain(req, res);
res.sendResponseEntity(responseEntity);
// 미들웨어에서 주입한 ResponseEntity resolver & respon 함수 호출
}
private handlePostData(req: Request, res: Response) {
const responseEntity = this.mainController.postData(req, res);
res.sendResponseEntity(responseEntity);
}
}
export default new MainRouter().router;위와 같은 코드를 구성하면, GET 요청을 보냈을 때, 클라이언트는 다음과 같은 응답을 받게 된다.
{
"result": true,
"data": {
"message": "OK"
}
}이렇게 응답 보내는 작업을 일관된 방법으로 공통화해서 코드 퀄리티를 향상 시킬 수 있고 클라이언트 단에서 예측 가능한 형식을 얻게 되었다.
마무리
공통 응답 객체를 직접 구현해보면서 프레임워크에서 제공해주는 이점들을 크게 느끼는 것 같다. 응답 뿐만 아니라, 서버에서 공통 작업들을 하나의 인터페이스로 통일화 한다면 예측 가능한 코드에 더욱 가까워지게 된다.