ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Framework 시리즈 11화 – 테스트 가능한 구조 만들기와 DI 테스트 전략
    기술과 산업/언어 및 프레임워크 2025. 6. 2. 13:16
    728x90

    스프링의 의존성 주입 구조를 테스트에 적합하게 리팩토링하는 방법과 함께, 단위 테스트와 통합 테스트에서 @TestConfiguration, @MockBean, ApplicationContext 활용 전략을 소개합니다.

     

     

    왜 테스트 가능한 구조가 중요한가?

     

    실무에서 스프링 애플리케이션의 성공 여부는 테스트 전략에 크게 좌우됩니다.

    하지만 많은 개발자가 기능은 되는데 테스트는 어려운 구조를 만들어냅니다.

     

    그 이유는 다음과 같습니다:

     

    • new 키워드 남용 → DI 불가
    • 내부 의존성 연결이 강함 → Mock 불가
    • Bean 설정이 하드코딩 → 프로파일 분기 실패

     

    이제, 우리가 앞서 만든 DI 기반 계산기 시스템을 테스트 가능한 구조로 바꿔보고, 실제 테스트 코드까지 작성해보겠습니다.

     


     

    1. 설계 리팩토링 – 인터페이스 도입

     

    테스트를 위해선 핵심 로직을 분리하고, 역할 기반 설계를 해야 합니다.

    public interface Operator {
        double apply(double a, double b);
        String getOperator(); // "+", "-", "*", "/" 등
    }
    @Component
    public class AddOperator implements Operator {
        public double apply(double a, double b) { return a + b; }
        public String getOperator() { return "+"; }
    }

    이런 식으로 Operator 구현체를 여러 개 만들고, 다음과 같은 라우팅 서비스를 구성합니다.

    @Component
    public class OperatorRouter {
    
        private final Map<String, Operator> operatorMap = new HashMap<>();
    
        @Autowired
        public OperatorRouter(List<Operator> operators) {
            for (Operator op : operators) {
                operatorMap.put(op.getOperator(), op);
            }
        }
    
        public double calculate(double a, double b, String op) {
            Operator operator = operatorMap.get(op);
            if (operator == null) throw new IllegalArgumentException("지원하지 않는 연산자: " + op);
            return operator.apply(a, b);
        }
    }
    이렇게 하면 각 연산자별 구현체를 테스트에서 대체(Mock)하거나 제거하거나 분리할 수 있는 구조가 됩니다.

     


     

    2. 단위 테스트 – OperatorRouter 테스트

    @SpringBootTest
    class OperatorRouterTest {
    
        @Autowired
        private OperatorRouter router;
    
        @Test
        void testAddition() {
            double result = router.calculate(3, 5, "+");
            assertEquals(8.0, result);
        }
    
        @Test
        void testUnsupportedOperator() {
            assertThrows(IllegalArgumentException.class,
                () -> router.calculate(4, 2, "%"));
        }
    }

     


     

    3. @MockBean을 활용한 단위 테스트 분리

     

    테스트 대상 외의 Bean을 Mock으로 대체하고 싶을 때 사용합니다.

    @SpringBootTest
    class CalculatorTest {
    
        @Autowired
        private Calculator calculator;
    
        @MockBean
        private OperatorRouter mockRouter;
    
        @Test
        void calculatorShouldCallOperatorRouter() {
            when(mockRouter.calculate(2, 3, "+")).thenReturn(5.0);
    
            calculator.calculate(2, 3, "+");
    
            verify(mockRouter).calculate(2, 3, "+");
        }
    }

     

    • 실제 Bean이 아닌 가짜(Mock)로 대체해서 Calculator의 행동만 테스트
    • Spring Boot 1.4 이상부터 기본 지원
    • 의존성이 많고 무거운 외부 API, DB 연결 등 분리 테스트가 필수인 경우 매우 유용

     


     

    4. 테스트 전용 Bean 구성 – @TestConfiguration

    @TestConfiguration
    public class TestOperatorConfig {
    
        @Bean
        public Operator dummyOperator() {
            return new Operator() {
                public double apply(double a, double b) { return 0.0; }
                public String getOperator() { return "dummy"; }
            };
        }
    }
    @SpringBootTest
    @Import(TestOperatorConfig.class)
    class CustomConfigTest {
    
        @Autowired
        private Operator dummy;
    
        @Test
        void testDummyOperator() {
            assertEquals(0.0, dummy.apply(10, 20));
        }
    }

     

    • 특정 테스트에서만 사용하는 Bean을 정의
    • 운영 설정과 완전히 분리된 테스트용 의존성 주입 가능
    • 예: 테스트용 H2 데이터소스, 테스트 전용 토큰 인증

     


     

    5. 통합 테스트 – 전체 흐름 검증

    @SpringBootTest
    class IntegrationTest {
    
        @Autowired
        private Calculator calculator;
    
        @Test
        void calculateAndPrint() {
            calculator.calculate(10, 2, "*");
            // System.out 출력 테스트는 로그 확인 또는 Mock 활용 가능
        }
    }

     

    • 전체 컨텍스트를 띄우고 흐름을 통합적으로 테스트
    • 실제 ApplicationContext에서 주입된 모든 Bean이 참여

     


     

    마무리 – 테스트 가능한 구조란?

     

    스프링에서 테스트 가능한 구조란 곧 다음을 의미합니다:

     

    • 모든 객체는 외부에서 주입될 수 있어야 한다.
    • 주입 대상은 인터페이스(역할)로 정의되어야 한다.
    • 테스트 환경에서는 운영 설정을 쉽게 대체할 수 있어야 한다.

     

    이 구조를 제대로 만드는 순간, 단위 테스트, 통합 테스트, QA 자동화까지 신뢰 가능한 소프트웨어 개발 기반이 구축됩니다.

     


    다음 12화에서는 본격적으로 Spring MVC의 흐름으로 넘어가, DispatcherServlet의 동작 원리와 요청 처리 체계를 설명합니다. 프론트 컨트롤러 구조가 어떻게 동작하고, HandlerMapping, ViewResolver가 어떤 역할을 하는지 상세히 분석할 예정입니다.

    728x90
Designed by Tistory.