설계가 바뀌면 이름도 바뀌어야 한다
앞선 글에서 공통 HTTP 요청 처리를 executeRequest라는 함수로 추상화하고, api-executor.js라는 파일에 위치시키기로 했다. 그런데 도메인 서비스 레이어 설계로 방향을 잡으면서 이 함수의 역할이 조금 달라졌다.
처음 executeRequest를 설계할 때의 역할은 이랬다. 요청 실행, 응답 파싱, 예외 처리를 전부 담당하는 구조였다.
// 초기 설계: 요청 실행 + 파싱 + 예외 처리를 모두 담당
export async function executeRequest(requestFn) {
try {
const response = await requestFn();
return await Response.getServerResponseData(response); // 요청 실행 + 파싱
} catch (err) {
handleServerException(err); // 예외 처리
}
}
그런데 도메인 서비스 레이어(user.js)가 응답 파싱까지 책임지기로 하면서, 이 함수가 실제로 하는 일이 달라졌다.
// 변경 후: 파싱은 domain에서 완료, 여기선 예외 처리만
export async function executeRequest(requestFn) {
try {
return await requestFn();
} catch (err) {
handleServerException(err); // 실질적으로 예외 처리만 담당
}
}
요청 실행은 user.js가 하고, 파싱도 user.js가 한다. 이 함수가 실제로 하는 일은 거의 예외 처리뿐이다. 그런데 이름은 여전히 executeRequest, 파일명은 api-executor.js다. 이름과 역할이 어긋나 있었다.
파일명부터 다시
api-executor.js는 "API 실행기"라는 뜻이다. 그런데 실제 API 실행은 도메인 레이어에서 한다. 이 파일이 하는 일을 솔직하게 표현하면 "요청을 처리한다", 즉 요청 흐름에서 예외 처리를 담당하는 핸들러에 가깝다.
request-handler.js가 더 정직한 이름이다.
함수명: 내부 관점 vs 호출부 관점
파일명을 request-handler.js로 정했다면, 함수명은 자연스럽게 handleRequest가 후보가 된다.
// request-handler.js
export async function handleRequest(requestFn) {
try {
return await requestFn();
} catch (err) {
handleServerException(err);
}
}
함수 내부 입장에서는 맞는 이름이다. "요청을 핸들링한다"는 역할을 잘 표현한다.
그런데 호출부에서 읽으면 살짝 어색하다.
// 호출부의 목적은 "결과를 받는다"에 가까운데...
const userInfo = await handleRequest(() => User.getUserInfo());
호출부의 목적은 "요청을 처리한다"가 아니라 "결과를 받는다"에 가깝다. handleRequest는 함수 내부가 하는 일을 설명하는 이름이고, 호출부가 원하는 것을 표현하는 이름이 아니다.
물론 모든 요청이 값을 반환하는 건 아니다. changePassword나 deleteAccount처럼 성공 여부만 중요한 경우도 있다. 그래서 "결과를 가져온다"는 느낌의 이름도 완전히 맞지는 않는다.
invoke
여러 후보를 검토했다.
fetchResult— 조회가 아닌 케이스에서 어색하다request— 너무 범용적이고 이미 다른 맥락에서 쓰이는 단어다call— 범용적이지만 의미가 너무 얕다handleRequest— 내부 관점의 이름, 호출부에서 어색하다
invoke는 "함수를 실행한다"는 의미다. 조회든, 수정이든, 삭제든 어떤 종류의 요청에도 자연스럽게 읽힌다. 그리고 내부에서 예외 처리가 일어난다는 사실을 굳이 드러내지 않아도 된다. 호출부는 "실행한다"는 의도만 표현하면 충분하다.
// 조회, 수정, 삭제 모두 자연스럽게 읽힌다
const userInfo = await invoke(() => User.getUserInfo());
await invoke(() => User.changePassword(data));
await invoke(() => User.deleteAccount(data));
최종 구조
// request-handler.js
export async function invoke(requestFn) {
try {
return await requestFn();
} catch (err) {
handleServerException(err);
}
}
// user.js — 도메인 서비스 레이어
async function getUserInfo() {
const headers = await createAuthHeaders();
const response = await apiRequest(Config.backendServerPath('/user'), 'GET', headers);
return await Response.getServerResponseData(response);
}
// 호출부
const userInfo = await invoke(() => User.getUserInfo());
await invoke(() => User.changePassword(data));
파일명 request-handler.js는 이 파일의 역할을 말하고, 함수명 invoke는 호출부의 의도를 말한다. 내부에서 예외 처리가 일어난다는 사실은 호출부가 알 필요가 없다.
이름을 다시 짓는다는 것
이번 네이밍 고민에서 배운 게 하나 있다.
이름은 설계가 확정된 이후에 한 번만 짓는 게 아니다. 설계가 바뀌면 이름도 바뀌어야 한다. executeRequest라는 이름은 처음 설계에서는 맞는 이름이었다. 그런데 도메인 레이어가 파싱을 가져가면서 이 함수의 역할이 달라졌고, 이름이 역할을 따라가지 못하게 됐다.
이름이 역할과 어긋나기 시작하면 코드를 읽는 사람이 혼란스러워진다. "이름은 이걸 한다고 하는데, 실제로는 저걸 하네"라는 상황이 생기면 코드에 대한 신뢰가 떨어진다.
그리고 함수명을 지을 때 내부 관점과 호출부 관점 중 어느 쪽을 우선할지도 중요한 판단이다. 둘 다 맞을 수 있지만, 그 함수를 주로 누가 읽느냐에 따라 더 적합한 이름이 달라진다. invoke가 선택된 이유는 호출부에서 더 자연스럽게 읽히기 때문이었다.
설계를 고민하는 만큼, 이름도 고민해야 한다.