객체지향원칙을 고려한 Spring 테스트 설계 전략 (JUnit)
Spring 애플리케이션에서 객체지향 원칙을 준수하는 테스트 설계는 유지보수성과 확장성을 보장하는 데 매우 중요합니다.
특히, DIP(의존 역전 원칙)와 IoC(제어의 역전)을 활용한 설계를 기반으로 테스트를 작성하면, 테스트 코드가 변경 없이 다양한 구현체를 검증할 수 있습니다.
이번 글에서는 객체지향 원칙을 고려한 테스트 설계 전략을 스프링 컨테이너와 JUnit을 활용해 설명합니다.
🔹 객체지향 원칙과 테스트 설계의 관계
Spring은 기본적으로 객체지향 프로그래밍(OOP) 원칙을 따르며, 이를 바탕으로 DI(의존성 주입)를 활용하여 객체 간 결합도를 낮춥니다.
테스트 코드에서도 객체의 책임을 분리하고, 유연한 구조를 유지하기 위해 객체지향 원칙을 적용해야 합니다.
1️⃣ 객체지향 원칙 (SOLID)과 테스트 설계
원칙 | 설명 | 테스트 적용 예시 |
SRP (단일 책임 원칙) | 하나의 클래스는 하나의 책임만 가져야 한다. | 단위 테스트는 하나의 클래스 또는 메서드만 테스트해야 함. |
OCP (개방-폐쇄 원칙) | 확장에는 열려 있고, 변경에는 닫혀 있어야 한다. | 기존 테스트 코드를 변경하지 않고도 새로운 기능을 추가할 수 있어야 함. |
LSP (리스코프 치환 원칙) | 상위 타입을 하위 타입으로 대체할 수 있어야 한다. | Mock 객체나 가짜 구현체(Fake)를 활용하여 테스트할 수 있어야 함. |
ISP (인터페이스 분리 원칙) | 클라이언트가 자신이 사용하지 않는 메서드에 의존하면 안 된다. | 테스트를 위해 단순한 인터페이스를 만들어 사용해야 함. |
DIP (의존 역전 원칙) | 상위 모듈이 하위 모듈에 의존하면 안 되고, 추상화에 의존해야 한다. | 구현 클래스가 아닌 인터페이스를 활용하여 테스트를 진행해야 함. |
이제 이러한 객체지향 원칙을 실제 Spring 테스트 코드에 적용해 봅시다.
✅ 객체지향 원칙을 반영한 Spring 테스트 설계
1️⃣ DIP(의존성 역전 원칙) 준수를 위한 인터페이스 기반 설계
의존성 역전 원칙을 지키려면, 테스트 대상이 직접 객체를 생성하는 것이 아니라 인터페이스를 통해 외부에서 주입받아야 합니다.
즉, OrderServiceImpl은 DiscountPolicy 인터페이스만 의존하고, 구체적인 할인 정책(FixDiscountPolicy, RateDiscountPolicy)을 직접 참조하지 않아야 합니다.
📌 올바른 설계 (DIP 원칙 준수)
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy; // 인터페이스만 의존
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
📌 올바른 테스트 전략
- DiscountPolicy 인터페이스만 사용하여 구체 클래스와의 결합을 방지
- 다양한 할인 정책(FixDiscountPolicy, RateDiscountPolicy)을 테스트할 때 유연하게 변경 가능
- Mock 객체 또는 가짜 객체(Fake)를 사용하여 독립적인 테스트 가능
2️⃣ 객체 생성을 관리하는 IoC 컨테이너 활용 (제어의 역전, IoC)
Spring에서는 @Configuration을 사용하여 객체 생성과 의존성을 관리할 수 있습니다.
이렇게 하면 테스트 시 의존성 주입을 활용하여 변경 없이 테스트 가능합니다.
✅ IoC를 활용한 객체 관리 (AppConfig)
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy(); // 여기만 변경하면 할인 정책이 바뀜
}
}
📌 IoC를 활용한 테스트 전략
- OrderServiceImpl 내부에서 new 키워드로 객체를 생성하지 않음 → 유지보수성 향상
- AppConfig를 통해 테스트 환경을 쉽게 변경 가능 (할인 정책 변경)
- 테스트에서는 @TestConfiguration을 활용하여 다른 설정을 사용할 수도 있음
🔹 객체지향 원칙을 고려한 Spring 테스트 방법 (JUnit)
1️⃣ 단위 테스트 - Mock 객체 활용 (Mockito 사용)
- Mock을 활용하여 테스트 대상을 격리
- 외부 의존성이 없어 빠르게 테스트 가능
✅ OrderService 단위 테스트 (Mockito 활용)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private MemberRepository memberRepository;
@Mock
private DiscountPolicy discountPolicy;
@InjectMocks
private OrderServiceImpl orderService;
@Test
void 주문_생성_테스트() {
// given
Member member = new Member(1L, "VIP 고객", Grade.VIP);
when(memberRepository.findById(1L)).thenReturn(Optional.of(member));
when(discountPolicy.discount(member, 10000)).thenReturn(1000);
// when
Order order = orderService.createOrder(1L, "상품A", 10000);
// then
assertEquals(1000, order.getDiscountPrice());
verify(discountPolicy, times(1)).discount(member, 10000);
}
}
📌 단위 테스트의 객체지향 설계 적용
- @Mock을 사용하여 구체 클래스 대신 인터페이스를 활용 (DIP 적용)
- Mock 객체를 활용하여 외부 의존성을 제거 (SRP 준수)
- 특정 메서드가 호출되었는지 verify()를 통해 확인 (테스트 검증 강화)
2️⃣ 통합 테스트 - 스프링 컨테이너 활용 (@SpringBootTest)
통합 테스트에서는 Spring IoC 컨테이너를 활용하여 실제 빈을 주입해야 합니다.
✅ OrderController 통합 테스트 (MockMvc 활용)
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
void 주문_API_테스트() throws Exception {
// given
Order order = new Order(1L, "상품A", 10000, 1000);
when(orderService.createOrder(1L, "상품A", 10000)).thenReturn(order);
// when & then
mockMvc.perform(get("/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.discountPrice").value(1000));
}
}
📌 통합 테스트의 객체지향 설계 적용
- @MockBean을 활용하여 컨트롤러 테스트 시 Service만 Mocking → 느린 DB 접근을 피함
- @SpringBootTest를 활용하여 Spring IoC 컨테이너에서 실제 빈 주입
- MockMvc를 활용하여 API 테스트를 실제 HTTP 요청처럼 수행
🚀 결론: 객체지향 원칙을 고려한 Spring 테스트 전략
테스트 유형 | 적용 객체 지향 원칙 | 주요 기술 |
단위 테스트 | DIP, SRP | @Mock, Mockito, @InjectMocks |
통합 테스트 | IoC, DIP | @SpringBootTest, MockMvc, @MockBean |
설정 테스트 | OCP, DIP | @Configuration, @TestConfiguration |