객체 지향 설계 과정에서 구현 로직과 제어 로직이 동일한 클래스 내에 혼재되는 경우가 빈번합니다. 특히 메서드 체이닝 (Fluent Interface) 패턴을 적용할 때는 각 메서드의 마지막에 현재 인스턴스를 반환하는 문구가 필수적으로 등장해야 하는데, 이로 인해 비즈니스 기능 코드와 프레임워크 유지보수 코드가 얽히게 됩니다. 이러한 혼합은 가독성을 저하시키므로, 상속 계층을 활용해 두 영역을 명확히 구분하는 것이 바람직합니다.
기존 방식의 문제점
일반적인 구현 예시로는 데이터 리포트를 생성하는 클래스가 있습니다. 아래 코드에서는 데이터 설정과 연쇄 호출 기능을 동시에 포함하고 있습니다.
class ReportGenerator {
setFormat(type) {
this.formatType = type;
return this;
}
addLayout(style) {
this.layoutStyle = style;
return this;
}
publish() {
// actual publishing logic
return this;
}
}
const report = new ReportGenerator();
report.setFormat('PDF').addLayout('compact').publish();
// 결과물 확인: { formatType: 'PDF', layoutStyle: 'compact' }
각 메서드마다 `return this` 문구를 작성해야만 체이닝이 가능하므로, 비즈니스 관점에서 중요한 데이터 조작 로직이 단순한 제어 흐름 코드에 의해 가려집니다. 이를 해결하기 위해 핵심 로직만을 담당하는 베이스 클래스를 먼저 정의합니다.
로직과 구조의 분리
먼저 순수한 비즈니스 기능만 처리하는 기본 클래스를 작성합니다. 여기서는 연쇄 호출을 위한 반환 처리 없이 상태 변경에만 집중합니다.
class CoreReport {
setFormat(type) {
this.formatType = type;
}
addLayout(style) {
this.layoutStyle = style;
}
publish() {
// actual publishing logic
}
}
이후 메서드 체이닝 기능을 부여하려면 해당 기본 클래스를 확장하여 상속받아 구현할 수 있습니다. 자식의 메서드는 부모의 로직을 실행한 뒤 본인을 반환하도록 재정의합니다.
class ChainableReport extends CoreReport {
setFormat(type) {
super.setFormat(type);
return this;
}
addLayout(style) {
super.addLayout(style);
return this;
}
publish() {
super.publish();
return this;
}
}
상속된 클래스를 사용하여도 호출 방법은 동일하게 유지되며, 코드의 의도가 훨씬 명확해집니다.
const report = new ChainableReport();
report.setFormat('PDF').addLayout('compact').publish();
// 상태 검증 가능: report.formatType === 'PDF'
고차 함수를 활용한 자동화
매번 새로운 클래스를 수동으로 정의하여 상속받는 것은 번거롭습니다. 따라서 고차 함수를 통해 특정 클래스를 입력받으면 자동으로 체이닝 지원 기능을 갖춘 새로운 클래스를 생성해 주는 팩토리를 정의할 수 있습니다.
const enableChaining = (SuperClass) => {
return class AutoChainedClass extends SuperClass {
setFormat(type) {
super.setFormat(type);
return this;
}
addLayout(style) {
super.addLayout(style);
return this;
}
publish() {
super.publish();
return this;
}
};
};
이 패턴을 적용하면 원하는 어떤 클래스에도 유연하게 체이닝 기능을 부여할 수 있으며, 중복된 코드 양을 획기적으로 줄일 수 있습니다.
const FlexibleReport = enableChaining(CoreReport);
const doc = new FlexibleReport();
doc.setFormat('HTML').addLayout('grid').publish();
// 결과 상태 확인: doc.formatType === 'HTML'