
들어가며
사이드 프로젝트로 블로그 분석 서비스를 만들고 있습니다. 서비스를 운영한지 일주일 정도 되었습니다. 서비스를 운영하다보니, 기능을 새로 추가하고, 수정해야 할 일이 많이 생깁니다. 이때 어떻게 안정적으로 기능을 추가하고, 리팩터링 할 수 있을까요?
프론트엔드에서는 리팩터링 후 UI 와 기능이 정상 동작하는지 확인하기가 간편했고 명확했습니다. 백엔드는 더 다양한 케이스를 다룰 뿐더러, 하나씩 API 를 호출해보는 과정은 좀 더 까다로웠습니다. 또는 API 에는 다른 많은 로직이 섞여있으므로, API 를 통해 하나의 기능을 테스트하기가 번거롭습니다.
어떻게 리팩터링을 잘 할 수 있을까 고민하다가, 회귀 방지(Regression Protection)가 핵심이라는 생각이 들었습니다. 회귀 방지를 할 수 있는 흐름을 고민하다가, Characterization Test 를 알게 되었고, 이를 적용해 보았습니다.
문제 상황
빠른 기능 개발이 필요했고, Claude Code 가 이런 로직을 짜주었습니다. 정상 동작을 확인하고, 빠르게 배포했죠. 이후에 코드를 수정하려고 하니, 코드 수정이 어려웠습니다.
유지보수 하기 좋은 코드가 되기 위해서, 리팩터링이 필수적이었습니다.
이런 코드였습니다.
function analyzeWritingTime(posts: RSSPostType[]): TimeAnalysisResult {
// 생략...
// 가장 많은 시간대 판별
let timeCategory: "아침형" | "낮형" | "저녁형" | "밤형";
const max = Math.max(
distribution.morning,
distribution.afternoon,
distribution.evening,
distribution.night
);
if (max === distribution.night) {
timeCategory = "밤형";
} else if (max === distribution.evening) {
timeCategory = "저녁형";
} else if (max === distribution.afternoon) {
timeCategory = "낮형";
} else {
timeCategory = "아침형";
}
return {
averageWritingTime,
timeCategory,
distribution,
};
}
전략 수립
어떻게 전략을 짜야, 제가 자신감을 가지고 리팩터링 하면서, 동일한 결과물을 유지할 수 있을까요. “단위 테스트” 책 에서 읽은 회귀 방지(Regression Protection)이 떠올랐습니다.
소프트웨어에서 회귀란, 이전에 잘 되던 기능이 코드 변경 후 깨지는 것을 말합니다. 회귀 방지는, 리팩터링을 해도 기존과 동일한 결과를 유지하는 것이죠.
이를 위해서, 블랙박스, Characterization Test 라는 개념을 적용해보기로 했습니다. Characterize 란 특성을 규명한다는 의미입니다. 현재 코드가 어떤 상태로 동작하는지 규명하는 테스트를 작성하는 것입니다.
즉, 내부 로직은 블랙박스로 두고, 입력에 따른 출력만 정확히 테스트 하는 것이죠.
이렇게 현재의 동작으로 테스트를 작성하고나서, 내부 로직을 리팩터링 합니다. 그리고 마지막으로 다시 이 테스트가 정상 동작하는지 체크합니다. 이것이 정상 동작한다면, 리팩터링 내성이 있는 것입니다.
리팩터링 내성이 높다는건, 내부 구현을 바꾸어도 테스트가 깨지지 않는 것을 말합니다. 블랙 박스 테스트는 리팩터링 내성이 높고, 우리가 지향해야 하는 테스트는 대부분 여기에 해당합니다.
그럼 이제 실제로 이 전략을 적용해보시죠.
Step1. Characterization Test
핵심은 입력과 출력만을 테스트하는 것입니다. 기존 동작을 그대로 테스트해두는 것이지요.
이걸 해두면 이런 같은 장점이 생깁니다. 리팩터링 내성이 생겨, 자신감 있는 리팩터링이 가능해 집니다.
현재 코드를 테스트 해봅시다.
describe('analyzeWritingTime', () => {
it('RSS 에서 글 게시 시간을 확인하여, 평균 글쓰기 시간과 시간대 분포를 계산한다.', () => {
// Given
const blogPosts = RSS_BLOG_POSTS;
// When
const result = analyzeWritingTime(blogPosts);
// Then
expect(result).toEqual({
averageWritingTime: '22:49',
timeCategory: '밤형',
distribution: { morning: 0, afternoon: 0, evening: 3, night: 7 }
});
});
});
테스트를 통과시킵니다.

Step2. 내부 리팩터링 w/ TDD
이제 내부 로직을 리팩터링 해봅니다. 여기에는 TDD 전략을 활용할 것입니다. 내부 로직에 대한 테스트를 먼저 작성하고(Red), 테스트를 가능한 빠르게 통과시키고(Green), 함수/테스트를 리팩터링(Refactor) 합니다.
이를 통해 빠른 피드백을 확보하여, 테스트의 가치를 더욱 키울 수 있습니다.
1. 단위 테스트 코드를 먼저 짠다.
determineTimeCategory 로 분리할 계획입니다. 구현체 없이 테스트만 만들어 Red 를 만듭니다.
describe('determineTimeCategory', () => {
it('시간 분포에 따라 시간 카테고리를 결정한다', () => {
// Given
const distribution = { morning: 0, afternoon: 0, evening: 3, night: 7 };
// When
const result = determineTimeCategory(distribution);
// Then
expect(result).toBe('밤형');
})
})
2. 빠르게 함수를 구현하여 테스트를 통과시킨다.
기존 로직을 그대로 옮겨와서, 최대한 빠르게 Green 을 만듭니다. 그대로 로직을 옮겨왔으니, 실패하지 않습니다.
export function determineTimeCategory(distribution: TimeDistribution): TimeCategory {
let timeCategory: "아침형" | "낮형" | "저녁형" | "밤형";
const max = Math.max(
distribution.morning,
distribution.afternoon,
distribution.evening,
distribution.night
);
if (max === distribution.night) {
timeCategory = "밤형";
} else if (max === distribution.evening) {
timeCategory = "저녁형";
} else if (max === distribution.afternoon) {
timeCategory = "낮형";
} else {
timeCategory = "아침형";
}
return timeCategory;
}
3. 함수/테스트 코드를 리팩터링 한다.
다음에 함수를 리팩터링 합니다. 중복이 많고, 확장성 낮은 코드를 간결하게 바꾸었습니다.
const CATEGORY_MAP: { key: keyof TimeDistribution; label: TimeCategory }[] = [
{ key: "night", label: "밤형" },
{ key: "evening", label: "저녁형" },
{ key: "afternoon", label: "낮형" },
{ key: "morning", label: "아침형" },
];
export function determineTimeCategory(distribution: TimeDistribution): TimeCategory {
return CATEGORY_MAP.reduce((max, curr) =>
distribution[curr.key] > distribution[max.key] ? curr : max
).label;
}
이렇게 determineTimeCategory 로직이 안전하게 완성되었기에, 기존 함수의 내부 로직을 수정할 준비가 되었습니다.
Step3. 기존 코드 교체
바로 기존 코드를 교체해봅니다.
export function analyzeWritingTime(posts: RSSPostType[]): TimeAnalysisResult {
// 생략...
const timeCategory = determineTimeCategory(distribution);
return {
averageWritingTime,
timeCategory,
distribution,
};
}
Step4. 기존 테스트 정상 동작 확인
기존 코드를 바꾸었다면, 처음과 동일한 동작이 나와야겠죠. 블랙박스로 설계한 Characterization Test 를 택했으므로, 내부 로직을 바꾸어도 테스트가 깨지지 않습니다.

이렇게 기존 동작과 달라진 것이 없다는 것을 마지막으로 테스트하였고, 안전한 리팩터링을 완수했습니다.
마치며
현업에서 코드를 수정할 때, 두려움이 생기기 마련입니다. 그럼에도 불구하고, 우리 개발자는 리팩터링을 해야 하죠. 두려움을 줄이는, 용기를 가져다 주는 여러 방법론들이 존재한다는 것을 깨달았습니다. Characterization Test 기반 리팩터링이 그런 예시입니다. 개발 과정에서는 TDD 도 용기를 심어주는 방법이므로, 합쳐서 적용해보았습니다. 자신감이 매우 높은 상태로 리팩터링을 진행했습니다. 이렇게 마음이 편한 리팩터링은 처음인 것 같습니다. 다음에 많이 사용해볼 전략이 될 듯 합니다.
'서버' 카테고리의 다른 글
| DB 테이블 스키마 변경 전략 (0) | 2026.02.22 |
|---|---|
| Node.js 에서 Date 객체 Time Zone 다루기 (0) | 2026.02.09 |
| NestJS 철학 이해하기 1 : Layered Architecture (0) | 2026.02.01 |
| Gemini API Latency 추적 (1) | 2026.01.30 |
| From Cloudflare Workers, To Gemini API 에러 (0) | 2026.01.28 |