소프트웨어 유지보수와 테스트 자동화를 어렵게 만드는 주요 요인 중 하나는 의존성(Dependency)입니다. Working Effectively with Legacy Code에서 소개된 "인터페이스 추출(Extract Interface)" 기법은 이러한 의존성을 줄이고, 테스트 가능성을 높이는 데 중요한 역할을 합니다. 이번 글에서는 인터페이스 추출 기법의 개념과 이를 활용하여 코드의 테스트 가능성을 높이는 방법을 살펴보겠습니다.
1. 인터페이스 추출이란?
인터페이스 추출(Extract Interface)은 기존 클래스에서 특정한 역할을 수행하는 메서드들을 추출하여 새로운 인터페이스를 정의하는 기법입니다. 이를 통해 기존 코드의 직접적인 의존성을 줄이고, 테스트를 위한 대체 구현(Fake, Mock 등)을 쉽게 만들 수 있습니다.
적용 전 코드 예시
public class PaydayTransaction extends Transaction {
public PaydayTransaction(PayrollDatabase db, TransactionLog log) {
super(db, log);
}
public void run() {
for(Iterator it = db.getEmployees(); it.hasNext(); ) {
Employee e = (Employee)it.next();
if (e.isPayday(date)) {
e.pay();
}
}
log.saveTransaction(this);
}
}
위 코드에서 TransactionLog 클래스는 saveTransaction 메서드를 제공하는데, 이는 데이터베이스 기록을 남기는 기능을 담당합니다. 하지만 테스트할 때 실제 데이터베이스에 접근하는 것은 피해야 합니다.
2. 인터페이스 추출 적용
테스트 가능성을 높이기 위해 TransactionLog의 역할을 인터페이스로 분리하면 다음과 같이 변경할 수 있습니다.
인터페이스 정의
interface TransactionRecorder {
void saveTransaction(Transaction transaction);
void recordError(int code);
}
기존 클래스 인터페이스 적용
class TransactionLog implements TransactionRecorder {
@Override
public void saveTransaction(Transaction transaction) {
// 기존 로직 유지
}
@Override
public void recordError(int code) {
// 에러 기록 로직
}
}
이제 TransactionLog가 TransactionRecorder 인터페이스를 구현하므로, PaydayTransaction에서 직접적인 TransactionLog 의존성을 제거하고, 대신 TransactionRecorder를 사용하도록 변경할 수 있습니다.
변경된 PaydayTransaction 코드
public class PaydayTransaction extends Transaction {
private final TransactionRecorder recorder;
public PaydayTransaction(PayrollDatabase db, TransactionRecorder recorder) {
super(db, recorder);
this.recorder = recorder;
}
public void run() {
for(Iterator it = db.getEmployees(); it.hasNext(); ) {
Employee e = (Employee)it.next();
if (e.isPayday(date)) {
e.pay();
}
}
recorder.saveTransaction(this);
}
}
3. 테스트를 위한 대체 구현(Fake) 작성
위와 같이 변경하면, 실제 TransactionLog를 대체할 수 있는 가짜(Fake) 구현체를 만들어 테스트를 용이하게 할 수 있습니다.
FakeTransactionLog 구현
public class FakeTransactionLog implements TransactionRecorder {
private List<Transaction> transactions = new ArrayList<>();
private List<Integer> errors = new ArrayList<>();
@Override
public void saveTransaction(Transaction transaction) {
transactions.add(transaction);
}
@Override
public void recordError(int code) {
errors.add(code);
}
public boolean containsTransaction(Transaction transaction) {
return transactions.contains(transaction);
}
}
테스트 코드 작성
void testPayday() {
FakeTransactionLog fakeLog = new FakeTransactionLog();
Transaction t = new PaydayTransaction(getTestingDatabase(), fakeLog);
t.run();
assertTrue(fakeLog.containsTransaction(t));
}
위와 같이 FakeTransactionLog를 활용하면 실제 데이터베이스에 접근하지 않고도 PaydayTransaction의 동작을 검증할 수 있습니다.
4. 인터페이스 추출 기법의 장점
의존성 제거 및 결합도 감소
기존 코드에서는 PaydayTransaction이 TransactionLog에 직접 의존하였지만, 인터페이스 추출을 통해 이를 추상화하여 유연한 설계를 만들 수 있습니다.
테스트 가능성 향상
FakeTransactionLog 같은 대체 구현을 쉽게 만들 수 있어, 외부 시스템과의 결합 없이 단위 테스트가 가능합니다.
유지보수성 증가
새로운 로깅 방식이 필요할 경우 TransactionRecorder 인터페이스만 구현하면 되므로 코드 변경이 최소화됩니다.
결론
인터페이스 추출(Extract Interface) 기법은 기존 클래스의 책임을 명확히 하고, 의존성을 제거하여 코드의 테스트 가능성을 높이는 효과적인 방법입니다. 이번 예제처럼 의존성이 강한 부분을 인터페이스로 분리하고, 테스트를 위한 대체 구현을 활용하면 유지보수성과 확장성이 뛰어난 소프트웨어를 개발할 수 있습니다.
레거시 코드를 테스트 가능한 상태로 만들고 싶다면, 인터페이스 추출을 적극적으로 활용해보시기 바랍니다.
'SW 개발 일반 > 레거시코드와 놀기' 카테고리의 다른 글
소프트웨어 엔트로피 (Software Entropy) (0) | 2025.03.06 |
---|---|
레거시 코드와 놀기: 인스턴스 위임 도입 (Introduce Instance Delegate) (0) | 2025.02.03 |
레거시 코드와 놀기: Getter 메소드 추출과 재정의 (Extract and Override Getter) (0) | 2025.01.27 |
레거시 코드와 놀기 백서: Working Effectively with Legacy Code (0) | 2025.01.25 |
레거시 코드와 놀기: 호출 추출과 재정의 (Extract and Override Call) (0) | 2025.01.24 |