개요
이전 회사에서 수학 교육 플랫폼을 개발하면서 마주쳤던 코드 개선 경험을 공유하고자 합니다. 특히 반복적이고 변경이 잦았던 코드들을 디자인 패턴을 활용해 어떻게 개선했는지, 그 과정에서 얻은 인사이트를 다루려고 합니다.
당시 개발하던 수학 교육 플랫폼은 학습 진도 관리를 위한 다양한 화면들로 구성되어 있었습니다. 특히 아래 화면들은 학습 콘텐츠의 계층 구조를 보여주는 핵심 인터페이스였습니다.
각 화면의 주요 기능은 다음과 같았습니다:
- 좌측 화면: 수학 교육과정의 중단원(chapter) 구조를 계층적으로 보여주는 화면
- 우측 화면: 각 중단원에 속한 소단원(lesson)들의 상세 내용을 표현하는 화면
이러한 UI를 구현하면서 크게 세 가지 도전 과제를 마주했습니다:
- 계층 구조의 표현: 중단원과 소단원이라는 계층적 구조를 어떻게 효율적으로 표현할 것인가?
- 동적 상태 관리: 학습 진행 상태, 문제 수, 잠금 상태 등 다양한 조건에 따라 변화하는 정보를 어떻게 유연하게 표현할 것인가?
- 파생 정보 계산: 기존 데이터를 기반으로 새로운 정보(예: 마지막 학습 항목 활성화)를 어떻게 효과적으로 도출할 것인가?
초기에는 단순히 중첩된 반복문과 조건문으로 이 문제들을 해결했습니다. 하지만 요구사항이 늘어나고 변경될 때마다 코드는 점점 더 복잡해졌고, 유지보수가 어려워졌습니다. 이런 문제를 해결하기 위해 객체지향 설계 원칙과 디자인 패턴을 적용하여 코드를 개선하기로 했습니다.
이 글에서는 당시 마주했던 문제들을 디자인 패턴으로 어떻게 해결했는지, 그리고 그 과정에서 배운 점들을 공유하려고 합니다. 실제 프로덕션 코드를 간소화하여 재구성했지만, 핵심 아이디어와 해결 방법은 그대로 담아보았습니다.
초기 구현의 한계
처음에는 아래와 같이 단순한 중첩 반복문과 조건문으로 구현을 했었습니다.
const learningLogs = ...;
chapters.forEach((chapter) => {
chapter.lessons.forEach(lesson => {
const learningLog = learningLogs.find(learningLog => learningLog.lessonId === lesson.id);
if (learningLog) {
if (learningLog.completed) {
lesson.status = DONE;
lesson.starCount = getStarCount(learningLog.score);
} else {
lesson.status = OPEN;
}
}
});
});
이런 접근 방식으로도 요구사항을 충족할 수는 있었지만, 새로운 요구사항이 들어올 때마다 코드를 수정해야 했고, 점점 더 복잡해져갔습니다. 여러 역할과 책임이 뒤섞인 이 코드를 유지보수하는 것은 갈수록 어려워졌습니다.
디자인 패턴을 활용한 개선
이러한 문제를 해결하기 위해 세 가지 핵심적인 설계 개선을 진행했습니다.
1. 계층 구조의 효율적인 표현
중단원과 소단원은 별개의 개념이지만, ‘단원’이라는 공통점을 가지고 있었습니다. 이 점에 착안하여 공통 인터페이스를 정의하고, 재귀적 합성 기법을 도입했습니다.
const elementGenerator = new ElementGenerator(new LessonElement());
elementGenerator.generate()
2. 동적 상태 관리
각 단원의 상태 정보(문제 수, 완료 여부 등)를 표현하는 방식도 개선했습니다. 상태 정보의 정의(선언부)와 실행을 분리하고, 투명한 포함 방식을 도입하여 확장성을 확보했습니다.
elementGenerator.addCondition(
(ConditionFactory.create(ConditionHelper.IN_PROGRESS))
.addDefiner(new OpenStatusDefiner(new ProblemCountDefiner()))
);
elementGenerator.addCondition(
(ConditionFactory.create(ConditionHelper.COMPLETED))
.addDefiner(
new DoneStatusDefiner(
new ScoreDefiner(),
new ProblemCountDefiner()))
);
3. 파생 정보의 유연한 계산
기존 정보를 기반으로 새로운 정보를 도출하는 로직도 분리했습니다. 분석 작업의 캡슐화를 통해 코드의 응집도를 높이고 유지보수성을 개선했습니다.
elementGenerator.check(new ActiveStatusChecker())
elementGenerator.generate()
실제 적용 사례: 학습 이력 화면
이러한 개선의 효과는 새로운 요구사항이 들어왔을 때 분명하게 드러났습니다. 학생들의 학습 이력을 한눈에 보여주는 새로운 화면을 개발해야 했는데, 기존에 작성한 구조를 거의 그대로 활용할 수 있었습니다.
화면 구성 요소와 패턴 적용
새로운 화면의 각 요소들은 이전에 정의한 패턴들과 자연스럽게 매칭되었습니다:
-
재귀적 합성 패턴의 활용 (빨간색 박스)
- 소단원과 그에 속한 학습 기록들이 트리 형태의 계층 구조를 이루고 있었습니다
- 예를 들어 “여러가지 모양” 단원 아래에 여러 개의 학습 기록이 있는 형태입니다
- 이전에 만들어둔 ElementGenerator를 통해 이 계층 구조를 쉽게 표현할 수 있었습니다
-
투명한 포함(데코레이터) 패턴의 확장 (보라색 박스)
- 각 학습 기록에는 점수, 별점, 결과 보기 버튼 등 다양한 정보가 표시되어야 했습니다
- 이러한 정보들은 모두 독립적으로 추가/제거가 가능해야 했습니다
- Definer 객체들을 조합하여 유연하게 표시 정보를 구성할 수 있었습니다
- 예를 들어, 새로운 정보(학습 시간, 오답 노트 등)를 추가할 때도 기존 코드를 수정하지 않고 새로운 Definer만 추가하면 되었습니다
-
분석작업 캡슐화(비지터) 패턴의 활용 (파란색 박스)
- 학습 기록들을 종합하여 평균 점수를 계산해야 했습니다
- 이는 기존 데이터를 순회하면서 새로운 정보를 도출하는 전형적인 분석 작업이었습니다
- AverageScoreChecker를 비지터로 구현하여 기존 구조를 전혀 수정하지 않고도 새로운 분석 기능을 추가할 수 있었습니다
실제 구현 코드
const elementGenerator = new ElementGenerator(new LearningElement());
// 완료된 학습에 대한 표시 정보 정의
elementGenerator.addCondition(
(LearningConditionFactory.create(ConditionHelper.COMPLETED))
.addDefiner(
new LessonTypeDefiner( // 학습 타입 정보
new ChangedAtDefiner(), // 학습 완료 시간
new ScoreDefiner(), // 획득 점수
new StarCountDefiner())) // 별점 표시
);
// 전체 학습 기록에 대한 평균 점수 계산
elementGenerator.check(new AverageScoreChecker());
elementGenerator.generate();
확장성의 실제 사례
이후 추가된 요구사항들도 기존 구조를 그대로 활용하여 쉽게 구현할 수 있었습니다:
- 오답 노트 기능 추가:
new WrongAnswerDefiner()
를 추가 - 학습 시간 표시:
new StudyTimeDefiner()
를 추가 - 전체 진도율 계산:
new ProgressRateChecker()
를 추가
이처럼 기존 코드의 수정 없이도 새로운 기능을 계속해서 추가할 수 있었고, 이는 처음 선택했던 디자인 패턴들의 효과를 입증하는 좋은 사례가 되었습니다.
회고와 배움
이러한 리팩토링 경험을 통해 발견한 가장 큰 통찰은, 우리가 흔히 말하는 디자인 패턴들이 사실은 보편적인 문제 해결 패턴의 특별한 사례라는 점이었습니다. 제가 적용한 기술적 해결책들은 결과적으로 아래의 디자인 패턴들과 자연스럽게 매칭되었습니다:
- 재귀적 합성 → Composite Pattern
- 투명한 포함 → Decorator Pattern
- 분석작업의 캡슐화 → Visitor Pattern
특히 이러한 패턴들은 단순히 객체지향 프로그래밍의 도구가 아닌, 더 근본적인 문제 해결 방식의 일부임을 깨달았습니다. 예를 들어, 재귀적 합성은 트리나 그래프 자료구조에서도 자주 볼 수 있는 패턴이고, 투명한 포함은 추상구문트리나 유한 상태 오토마타와 같은 컴퓨터 과학의 기본 개념에서도 발견됩니다.
이번 경험을 통해, 디자인 패턴이 단순히 코드 구조화를 위한 도구가 아니라, 문제 해결을 위한 사고방식의 확장이라는 것을 배웠습니다. 앞으로도 이러한 패턴들을 단순 암기가 아닌, 문제 해결의 도구로 활용하며 계속 성장해나가고 싶습니다.