앞선 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 환경에서의 테스트 자동화 전략을 다룰 예정입니다.