코드의 테스트 가능성과 유지보수성을 높이기 위해 의존성 관리가 핵심 과제가 됩니다. 오늘은 "Extract and Override Getter" 기법을 살펴보며, 클래스 내부의 복잡한 의존성을 어떻게 효과적으로 분리하고 테스트 가능한 구조로 바꿀 수 있는지에 대해 이야기하겠습니다.

문제 정의: 내부 의존성의 문제점
클래스 내부에서 객체를 직접 생성하거나 관리하면, 그 클래스는 해당 의존성에 강하게 결합됩니다. 이로 인해 다양한 문제가 발생합니다. 첫 번째로, 테스트 작성이 복잡해집니다. 외부에서 의존성을 대체하거나 모의(Mock) 객체를 주입하기 어려운 구조 때문에 코드 테스트가 제한적이 됩니다. 두 번째로, 요구사항 변화에 따른 코드 변경이 어렵습니다. 의존성이 코드에 하드코딩되어 있으면 수정 범위가 넓어지고, 이로 인해 오류가 발생할 가능성이 커집니다.
Extract and Override Getter 기법은 객체 생성 로직을 별도의 메소드로 분리하여 관리하는 방식을 말합니다. 이를 통해 필요한 경우 하위 클래스에서 이 메소드를 재정의함으로써 의존성을 대체하거나 확장할 수 있는 유연성을 제공합니다. 결과적으로 클래스의 결합도를 낮추고, 테스트와 유지보수를 용이하게 만듭니다.
적용 사례
초기 코드: 문제점이 드러난 코드
아래는 WorkflowEngine 클래스의 초기 코드입니다. 이 코드는 TransactionManager 객체를 생성하는 복잡한 로직을 내부에 포함하고 있습니다.
// WorkflowEngine.h
class WorkflowEngine
{
private:
TransactionManager *tm;
public:
WorkflowEngine();
...
};
// WorkflowEngine.cpp
WorkflowEngine::WorkflowEngine()
{
Reader *reader = new ModelReader(AppConfig::getDryConfiguration());
Persister *persister = new XMLStore(AppConfiguration::getDryConfiguration());
tm = new TransactionManager(reader, persister);
...
}
위 코드에서 WorkflowEngine은 TransactionManager 객체 생성 로직에 강하게 결합되어 있습니다. 따라서, 테스트 환경에서 이를 대체하거나 확장하는 것이 매우 어렵습니다.
개선된 코드: Getter 메소드 추출
이제 TransactionManager 객체 생성 로직을 별도의 getTransactionManager 메소드로 분리하여 유연성을 확보한 개선된 코드를 살펴보겠습니다.
// WorkflowEngine.h
class WorkflowEngine
{
private:
TransactionManager *tm;
protected:
TransactionManager *getTransactionManager() const;
public:
WorkflowEngine();
...
};
// WorkflowEngine.cpp
WorkflowEngine::WorkflowEngine()
: tm(nullptr)
{
...
}
TransactionManager* getTransactionManager() const
{
if(tm == nullptr)
{
Reader *reader = new ModelReader(AppConfig::getDryConfiguration());
Persister *persister = new XMLStore(AppConfiguration::getDryConfiguration());
tm = new TransactionManager(reader, persister);
}
return tm;
}
위 코드에서는 객체 생성 로직이 getTransactionManager라는 메소드로 분리되었습니다. 하위 클래스에서 이 메소드를 재정의하면, TransactionManager 객체를 다른 방식으로 생성하거나 대체할 수 있습니다.
WorkflowEngine의 하위 클래스인 TestWorkflowEngine을 작성하여 테스트 환경에서 TransactionManager를 대체합니다. 이를 통해 FakeTransactionManager를 활용한 테스트를 실행할 수 있습니다.
class TestWorkflowEngine : public WorkflowEngine
{
public:
TransactionManager *getTransactionManager() override { return &transactionManager; }
FakeTransactionManager transactionManager;
};
TEST(transactionCount, WorkflowEngine)
{
auto_ptr engine(new TestWorkflowEngine);
engine->run();
LONGS_EQUAL(0, engine->transactionManager.getTransactionCount());
}
이 테스트는 FakeTransactionManager를 통해 복잡한 의존성을 대체하여 간단하고 명확하게 동작을 검증합니다.
결과 및 장점
Getter 메소드 추출과 재정의를 통해 얻을 수 있는 주요 장점은 다음과 같습니다.
첫째, 테스트 가능성이 크게 향상됩니다. 복잡한 의존성을 테스트 더블(Test Double)로 대체함으로써 테스트 환경을 더 유연하게 설정할 수 있습니다.
둘째, 코드 유지보수성이 증가합니다. 객체 생성 로직을 메소드로 분리하면 변경 사항이 발생하더라도 영향을 최소화할 수 있습니다.
셋째, 확장성이 강화됩니다. 새로운 요구사항이나 기능 추가가 필요할 때, 간단히 하위 클래스에서 메소드를 재정의하여 유연하게 대처할 수 있습니다.
이 기법은 대규모 시스템에서 테스트 가능한 코드를 작성하거나 기존 코드를 리팩토링할 때 특히 유용합니다. 의존성 문제를 해결하는 데 어려움을 겪고 있다면, Getter 메소드 추출과 재정의 기법을 적극 검토해 보세요. 이는 코드 품질과 유지보수성을 동시에 높일 수 있는 실용적인 접근법입니다.
'SW 개발 일반 > 레거시코드와 놀기' 카테고리의 다른 글
레거시 코드와 놀기: 인스턴스 위임 도입 (Introduce Instance Delegate) (0) | 2025.02.03 |
---|---|
레거시 코드와 놀기: 인터페이스 추출 (Extract Interface) (0) | 2025.01.31 |
레거시 코드와 놀기 백서: Working Effectively with Legacy Code (0) | 2025.01.25 |
레거시 코드와 놀기: 호출 추출과 재정의 (Extract and Override Call) (0) | 2025.01.24 |
레거시 코드와 놀기: 정적 메소드 드러내기 (Expose Static Method) (0) | 2025.01.22 |