TEST

테스트 코드를 작성하며 설계의 문제를 발견한 이야기 - 데미터의 법칙

류큐큐 2025. 1. 14. 10:18

 

 

시작하며: 책 한 권이 바꾼 시각

얼마 전 "Junit in Action 3" 이라는 책을 읽었습니다. 테스트 코드의 중요성과 TDD(Test-Driven Development)가 설계에 어떤 영향을 미치는지 다룬 내용이 인상 깊었지만, 당시에는 "테스트 코드가 설계를 개선한다? 그런가 보다" 정도로만 생각했습니다.

그러다 실제로 코드를 작성하는 도중, 책에서 본 내용이 떠올랐습니다. 테스트 코드 작성이 설계의 문제를 드러내고, 더 나은 코드를 만드는 계기가 될 줄은 그때는 몰랐습니다.

 

 

문제 상황: 무심코 작성한 코드

토이 프로젝트로 진행하며 Git Merge Request 이벤트를 처리하는 코드를 작성하던 중, 아래와 같은 구조를 만들었습니다.

 

별문제 없어 보이는 코드였지만, 어딘가 불편해지는 느낌이 들었습니다.

Optional<Project> projectOpt = projectFinder.findByGitProjectId(
    gitMergeRequestEvent.project().getGitProjectId()
);

 

이 코드는 gitMergeRequestEvent에서 project()를 호출하고, 다시 getGitProjectId()를 호출하는 방식으로 Git의 프로젝트 ID를 가져옵니다. 처음에는 별문제 없어 보였지만, 테스트 코드를 작성하는 과정에서 다음과 같은 문제를 발견했습니다.

 

문제 발견: 테스트 코드가 알려준 설계 결함

 

1. 과도한 모킹(Mock):

@Test
void shouldRegisterGitMergeRequestEvent() {
    GitMergeRequestEvent event = Mockito.mock(GitMergeRequestEvent.class);
    Project project = Mockito.mock(Project.class);

    when(event.project()).thenReturn(project);
    when(project.getGitProjectId()).thenReturn(123L);

    // 테스트 로직 ...
}

 

  • GitMergeRequestEvent 내부의 Project 객체를 모킹해야 했습니다.
  • 테스트 코드가 객체의 내부 구현에 지나치게 의존하게 되었습니다.
  • 테스트 코드가 내부 구조(project(), getGitProjectId())에 강하게 의존했습니다.
  • 내부 객체(Project)를 모킹해야 하는 복잡성이 추가되었습니다.

 

2. 캡슐화 부족:

GitMergeRequestEvent는 내부의 Project 객체를 외부로 노출하고 있습니다

gitMergeRequestEvent.project().getGitProjectId();

 

  • Project의 구조가 변경되면, 이 코드를 사용하는 모든 곳에서 영향을 받습니다.
  • GitMergeRequestEvent의 역할이 불분명해집니다. 이벤트는 gitProjectId라는 정보를 제공해야 할 뿐, 내부 구조를 공개할 필요는 없습니다.

3. 유지보수와 테스트 어려움 

이 설계는 아래와 같은 문제를 야기합니다:

  • 유지보수성 저하:
    Project 객체가 바뀔 때마다, 이를 사용하는 모든 테스트와 코드에 영향을 미칩니다.
  • 테스트 코드의 복잡성 증가:
    테스트 과정에서 불필요한 모킹 작업이 추가되고, 테스트의 의도를 파악하기 어려워집니다.

4. 데미터의 법칙 위반

 

이 코드는 **데미터의 법칙(Law of Demeter)**을 위반하고 있습니다.
데미터의 법칙은 객체는 자신이 직접적으로 알고 있는 객체와만 상호작용해야 하며, 간접적으로 연결된 객체의 내부 상태에 의존하지 말아야 한다는 원칙입니다.

 

// 위반된 예: GitMergeRequestEvent의 내부 구조(Project)에 접근
gitMergeRequestEvent.project().getGitProjectId();

 

이런 구조는:

  1. 객체 간 강한 결합을 초래합니다.
  2. 내부 구현이 외부로 노출되어, 객체의 변경 가능성이 높아집니다.

 

테스트가 없었다면 몰랐을 것

테스트를 작성하지 않았다면, 이 문제가 설계 결함이라는 것을 깨닫지 못했을 것입니다. 테스트는 단순히 동작을 확인하는 수단이 아니라, 코드가 제대로 설계되었는지 검증하는 도구라는 점을 체감했습니다.

 

해결: 설계 개선의 첫걸음

문제를 해결하기 위해 GitMergeRequestEvent의 설계를 수정했습니다.

기존 코드의 문제

  • 내부 객체인 Project를 직접 노출하여 메시지 체인을 만들었습니다.
  • 이는 **데미터의 법칙(Law of Demeter)**을 위반하며, 테스트와 유지보수에 불리한 구조였습니다.

개선된 설계

GitMergeRequestEvent에서 Project를 노출하지 않고, gitProjectId를 직접 제공하도록 변경했습니다.

public class GitMergeRequestEvent {
    private final long gitProjectId;
    private final Branch branch;
    private final List<Commit> commits;

    public GitMergeRequestEvent(long gitProjectId, Branch branch, List<Commit> commits) {
        this.gitProjectId = gitProjectId;
        this.branch = branch;
        this.commits = commits;
    }

    public long getGitProjectId() {
        return gitProjectId;
    }
}

 

 

결과

  • 캡슐화를 유지하며, 내부 구조를 숨길 수 있었습니다.
  • 테스트 작성이 간단해지고, 모킹 의존성이 줄어들었습니다.

 

회고: 테스트 코드가 설계를 이끌다

이번 경험을 통해 다음과 같은 교훈을 얻었습니다:

  1. 테스트 코드는 설계의 결함을 드러낸다:
    • 메시지 체인과 같은 설계 문제는 테스트 코드를 작성할 때 더 명확히 드러납니다.
  2. 캡슐화와 추상화는 협업의 핵심:
    • 내부 구현을 숨기고 필요한 데이터만 노출하는 것이 유지보수성과 협업 효율성을 높입니다.
  3. TDD는 단순한 테스트 도구가 아니다:
    • 테스트를 먼저 작성하면 더 나은 설계를 자연스럽게 유도할 수 있습니다.

 

마무리하며: 작은 변화가 만드는 큰 차이

무심코 지나칠 뻔한 설계 결함도, 테스트 코드 작성이라는 작은 변화 덕분에 발견할 수 있었습니다. TDD가 설계를 어떻게 바꾸는지 경험하며, "테스트는 단순히 버그를 방지하는 도구가 아니다"라는 말이 왜 중요한지 알게 되었습니다.

 

앞으로도 테스트를 먼저 작성하며, 클린코드와 유지보수성을 위해 끊임없이 고민하려 합니다.

작은 변화가 큰 차이를 만든다는 것을 잊지 말아야겠습니다.