소프트웨어 개발에서 "의존성"은 피할 수 없는 개념이다. 의존성을 적절히 관리하지 않으면 코드가 복잡해지고, 유지보수와 테스트가 어려워진다. 이번 포스팅에서는 의존성, 의존성 주입(DI), 의존성 역전(DIP) 개념과 이들이 테스트 가능성과 어떤 연관이 있는지 정리해보려고 한다.
의존성이란?
컴퓨터공학에서 말하는 의존성은 결합과 같은 개념으로, 한 객체가 다른 객체의 동작이나 데이터를 필요로 하는 관계를 의미한다.
- A가 B를 사용하면 A는 B에 의존한다.
- 예: A 객체가 B 객체의 메서드를 호출하거나 데이터를 참조하면, A는 B에 의존성을 가진다.
의존성은 소프트웨어에서 자연스럽게 발생하지만, 이 의존성이 과도하거나 명확히 드러나지 않으면 설계와 유지보수에 문제가 생길 수 있다.
의존성 주입(DI, Dependency Injection)
의존성 주입은 객체가 필요한 의존성을 **직접 생성(new)**하는 대신, 외부에서 주입받는 방식을 말한다.
- 직접 생성 (Bad):
- SystemUUIDHolder를 직접 생성하면 AccountService가 구체적인 구현체에 강하게 결합된다.
- 테스트 환경에서 다른 구현체로 대체하기 어렵다.
- class AccountService { private final UUIDHolder uuidHolder = new SystemUUIDHolder(); }
- 의존성 주입 (Good):
- UUIDHolder는 인터페이스로 정의되고, 생성자 주입을 통해 외부에서 구현체를 주입받는다.
- 이를 통해 AccountService는 UUIDHolder의 구체적인 구현을 몰라도 동작할 수 있다.
- class AccountService { private final UUIDHolder uuidHolder; public AccountService(UUIDHolder uuidHolder) { this.uuidHolder = uuidHolder; } }
의존성 주입의 이점
- 유연성 증가: 테스트 환경에서는 Mock 객체를 주입하고, 프로덕션 환경에서는 실제 구현체를 주입할 수 있다.
- 결합도 감소: 클래스 간의 강한 결합을 줄이고, 유지보수를 용이하게 만든다.
- 테스트 가능성 증가: 숨겨진 의존성을 외부로 노출시켜, 입력과 출력을 쉽게 제어할 수 있다.
하지만, DI는 만능이 아니다
의존성 주입을 통해 결합도를 줄일 수 있지만, 의존성을 완전히 제거할 수는 없다.
결국 어딘가에서는 고정된 값이나 구체적인 구현체를 주입해야 한다.
이를 해결하기 위해, 의존성 주입과 **의존성 역전(DIP)**을 함께 사용하는 것이 중요하다.
의존성 역전(DIP, Dependency Inversion Principle)
의존성 역전은 SOLID 원칙 중 하나로, 다음을 의미한다.
- 상위 모듈(고수준 정책)은 하위 모듈(구현체)에 의존하지 않고, **추상화(인터페이스)**에 의존해야 한다.
- 하위 모듈도 추상화에 의존해야 한다.
// 추상화
interface UUIDHolder {
String getUUID();
}
// 구현체 (프로덕션)
@Component
class SystemUUIDHolder implements UUIDHolder {
@Override
public String getUUID() {
return UUID.randomUUID().toString();
}
}
// 구현체 (테스트)
@AllArgsConstructor
class TestUUIDHolder implements UUIDHolder {
private final String uuid;
@Override
public String getUUID() {
return uuid;
}
}
Test Code에서 Product 환경과 Test 환경을 DIP를 통해 분리한 모습
package study.learningtestcode.domain;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;
import java.util.UUID;
import org.junit.jupiter.api.Test;
class AccountServiceTest {
@Test
void create() {
// given
String username = "foobar";
String uuid = UUID.randomUUID().toString();
UUIDHolder uuidHolder = new TestUUIDHolder(uuid);
// when
// 제어의 역전으로 uuidHolder를 TestUUIDHolder로 선언
Account account = Account.create(username, uuidHolder);
//then
assertThat(account.getUsername()).isEqualTo(username);
assertThat(account.getAuthToken()).isEqualTo(uuidHolder.getUUID());
}
}
DIP와 DI의 관계
- **의존성 역전(DIP)**은 설계 원칙이다.
- 상위 모듈과 하위 모듈 모두 인터페이스에 의존하도록 설계한다.
- **의존성 주입(DI)**은 DIP를 실현하기 위한 방법이다.
- 외부에서 구현체를 주입하여, 상위 모듈이 구체적인 구현체에 의존하지 않도록 만든다.
의존성과 테스트 가능성(Testability)
Testability란, 얼마나 쉽게 테스트할 수 있는 코드인가를 의미한다.
구체적으로는 다음 두 가지 질문에 답할 수 있어야 한다.
- 입력(Input)을 쉽게 변경할 수 있는가?
- 외부 의존성을 쉽게 주입하거나, Mock 객체를 사용해 테스트 데이터를 설정할 수 있는가?
- 출력(Output)을 쉽게 검증할 수 있는가?
- 코드가 반환하거나 생성하는 값을 쉽게 확인할 수 있는가?
예: RestTemplate와 같은 외부 의존성
class ExternalService {
private final RestTemplate restTemplate;
public ExternalService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String fetchData(String url) {
return restTemplate.getForObject(url, String.class);
}
}
문제점:
- 외부 API와 통신해야 하므로, 테스트에서 이 동작을 재현하기 어렵다.
- 입력(URL)과 출력(응답 데이터)이 외부 환경에 의존하므로 Testability가 낮다.
해결책:
- Mock 객체를 사용해 외부 API 호출을 대체한다.
- 예를 들어, RestTemplate 대신 MockRestTemplate을 주입하여 테스트 환경을 분리할 수 있다.
테스트 신호와 숨겨진 의존성
테스트가 작성하기 어렵다면, 이는 설계 문제가 있다는 신호다.
- 숨겨진 의존성:
- 의존성이 코드 내부에 하드코딩되어 있다면, 테스트 환경에서 이를 제어할 수 없다.
- 해결책: 의존성을 주입하여 외부에서 제어 가능하게 만든다.
- Mock 프레임워크 의존:
- 테스트 작성이 Mock 프레임워크 없이는 불가능하다면, 설계 개선이 필요하다.
- Mock 없이도 테스트 가능한 구조를 만들어야 한다.
결론
- 의존성은 소프트웨어 설계에서 피할 수 없지만, 의존성 주입(DI)과 의존성 역전(DIP)을 통해 이를 적절히 관리할 수 있다.
- Testability는 DI와 DIP를 얼마나 잘 활용하느냐에 따라 크게 좌우된다.
- 의존성 주입은 결합도를 줄이고, 의존성 역전은 설계를 유연하게 만들어준다.
- 숨겨진 의존성을 외부로 노출시켜, 테스트 가능성을 높이고 유지보수성을 향상시키는 설계를 지향하자.
지금 나도 감을 잡아가는 단계라서 틀린 부분이 있을 수 있다.
만약 잘못된 부분을 발견한다면 언제든지 지적해주세용
'코딩딩 > Spring' 카테고리의 다른 글
JPA에서 createdAt과 updatedAt 관리: @PreUpdate vs @UpdateTimestamp (DDD 관점) (1) | 2024.12.27 |
---|---|
@Builder 사용에 대한 고찰 (0) | 2024.12.15 |
TDD와 BDD, 그리고 테스트 작성의 기본 개념들 (3) | 2024.12.11 |
테스트 코드, 어디까지 작성해야 할까? (5) | 2024.12.11 |
Mockito를 활용한 단위 테스트 정리 (3) | 2024.12.11 |