본문 바로가기
카테고리 없음

✅ 레거시 코드에서 TDD 적용하기 | 4편: DI 적용과 테스트 코드 구조 정리 전략

by bbongz 2025. 3. 29.

 

앞선 3편까지는 레거시 코드에서 TDD를 적용하기 위한 환경 구성과 실제 통합 테스트까지 진행해보았습니다. 이번 4편에서는 코드의 테스트 용이성을 높이기 위한 핵심 리팩토링 기법인 DI(Dependency Injection)의 개념과 적용 방법을 설명하고, 실제 리팩토링 전/후 코드를 비교해 테스트 가능성을 어떻게 확보하는지를 실례를 통해 보여드립니다.


🔧 1. 왜 DI(의존성 주입)가 필요한가?

❌ 레거시 코드의 전형적인 문제

  • 외부 의존성을 직접 생성하거나 static 호출
  • 테스트 환경에서 분리 불가능
  • 코드 재사용 및 유지보수 어려움

✅ DI(Dependency Injection)란?

의존성 주입(DI)은 객체가 필요로 하는 의존성을 외부에서 주입하는 설계 패턴입니다.
이를 통해 코드의 유연성과 테스트 가능성이 대폭 향상됩니다.


📉 2. 리팩토링 전/후 코드 비교

🔴 리팩토링 전 코드 (테스트 불가)


public class UserService
{
    public bool Register(string email)
    {
        var user = UserRepository.GetUserByEmail(email); // static 호출
        if (user != null) return false;

        EmailSender.Send(email); // static 호출
        return true;
    }
}

🔍 문제점

  • UserRepository, EmailSender는 static → 테스트 시 Mock 불가
  • 외부 시스템 의존 → 테스트 작성 불가
  • 코드 내부에서 의존성을 직접 생성

🟢 리팩토링 후 코드 (DI 적용)


// 인터페이스 정의
public interface IUserRepository
{
    User GetUserByEmail(string email);
}

public interface IEmailSender
{
    void Send(string email);
}

// DI 적용
public class UserService
{
    private readonly IUserRepository _userRepository;
    private readonly IEmailSender _emailSender;

    public UserService(IUserRepository userRepository, IEmailSender emailSender)
    {
        _userRepository = userRepository;
        _emailSender = emailSender;
    }

    public bool Register(string email)
    {
        var user = _userRepository.GetUserByEmail(email);
        if (user != null) return false;

        _emailSender.Send(email);
        return true;
    }
}

✅ 개선 사항

  • 외부 의존성을 인터페이스로 추상화
  • 생성자에서 의존성 주입 가능
  • Moq으로 유닛 테스트 가능

📦 3. 주요 클래스 설명

📘 User 클래스

사용자 정보를 나타내는 단순 POCO 객체입니다.


public class User
{
    public string Email { get; set; }
}

📘 UserRepository 클래스

기존에 DB 혹은 메모리에서 사용자 정보를 조회하는 static 클래스입니다.


public static class UserRepository
{
    public static User GetUserByEmail(string email)
    {
        // DB 조회 또는 저장소 접근
    }
}

📘 EmailSender 클래스

실제 이메일을 전송하는 static 유틸리티 클래스입니다.


public static class EmailSender
{
    public static void Send(string email)
    {
        // SMTP 또는 외부 API 호출
    }
}

👉 이러한 클래스는 직접 호출 방식(static)에서 벗어나, 인터페이스 기반으로 변경되어야 Mocking이 가능해집니다.


🧪 4. 테스트 코드 구조 정리 전략 (MSTest + Moq)

✅ AAA 패턴 적용 (Arrange → Act → Assert)


[TestClass]
public class UserServiceTests
{
    [TestMethod]
    public void Register_ShouldSendEmail_WhenUserDoesNotExist()
    {
        // Arrange
        var mockRepo = new Mock<IUserRepository>();
        mockRepo.Setup(r => r.GetUserByEmail("test@domain.com")).Returns((User)null);

        var mockSender = new Mock<IEmailSender>();
        var service = new UserService(mockRepo.Object, mockSender.Object);

        // Act
        var result = service.Register("test@domain.com");

        // Assert
        Assert.IsTrue(result);
        mockSender.Verify(s => s.Send("test@domain.com"), Times.Once);
    }

    [TestMethod]
    public void Register_ShouldReturnFalse_WhenUserAlreadyExists()
    {
        // Arrange
        var mockRepo = new Mock<IUserRepository>();
        mockRepo.Setup(r => r.GetUserByEmail("exist@domain.com"))
                .Returns(new User { Email = "exist@domain.com" });

        var mockSender = new Mock<IEmailSender>();
        var service = new UserService(mockRepo.Object, mockSender.Object);

        // Act
        var result = service.Register("exist@domain.com");

        // Assert
        Assert.IsFalse(result);
        mockSender.Verify(s => s.Send(It.IsAny<string>()), Times.Never);
    }
}

📂 5. 테스트 프로젝트 폴더 구조 예시


Solution
│
├── MyApp              # 실제 서비스 코드
│   ├── Services
│   ├── Repositories
│   └── Interfaces
│
└── MyApp.Tests        # 테스트 코드
    ├── Services
    └── Repositories
  • 서비스 단위로 테스트 클래스 구분
  • 클래스명 + Tests.cs 규칙 사용 (예: UserServiceTests.cs)

✅ 6. 리팩토링 전후 요약 비교표

항목 리팩토링 전 리팩토링 후 (DI 적용)
의존성 처리 내부에서 직접 생성 / static 호출 외부에서 생성자 주입
테스트 가능성 매우 낮음 높음 (Moq으로 Mock 가능)
유지보수성 변경 시 전체 코드 수정 필요 인터페이스 구현체만 교체
코드 유연성 결합도 높음 결합도 낮음 (추상화)
SOLID 원칙 D 원칙 위반 ✅ D(Dependency Inversion) 원칙 준수

🎯 결론

  • 레거시 코드의 테스트 가능성을 높이기 위해서는 의존성 분리가 핵심입니다.
  • DI(Dependency Injection)를 통해 테스트 가능한 구조로 리팩토링하면 코드의 유연성, 유지보수성, 확장성이 크게 향상됩니다.
  • 인터페이스 도입과 Moq 기반의 테스트를 병행하면 실제 운영 코드 수정 없이도 신뢰성 높은 테스트 환경을 구축할 수 있습니다.
  • DI 적용은 단순한 설계 변경이 아닌, TDD를 실현하기 위한 근본적인 첫걸음입니다.

🚀 다음 편 예고

👉 마지막 5편에서는 테스트 커버리지 도구 활용,
👉 그리고 CI 환경에서의 테스트 자동화 전략을 다룰 예정입니다.