Outside-in vs Inside-out 테스트 (Java/Spring)

Yono·2026년 2월 12일
post-thumbnail

Outside-in vs Inside-out 테스트 작성법 (Java/Spring 예제)

테스트를 어디서부터 작성하느냐에 따라 접근 방식은 크게 두 가지로
나뉩니다.

  • Outside-in: 사용자/API 관점에서 시작해 내부로 들어가는 방식
  • Inside-out: 핵심 로직(도메인/서비스)부터 시작해 바깥으로
    확장하는 방식

📌 테스트 피라미드로 이해하기

테스트 전략을 설명할 때 자주 사용하는 개념이 Test Pyramid 입니다.

Test
Pyramid

  • Unit Test는 많고 빠르게
  • Integration Test는 적절히
  • E2E Test는 최소한으로

예제 시나리오: 로그인 API

요구사항:

  • POST /api/login
  • 성공 → 200 OK + token
  • 실패 → 401 Unauthorized

구조:

Controller → Service → Repository

1️⃣ Outside-in 방식

사용자 관점(API)부터 테스트를 작성합니다.

Controller

@RestController
@RequestMapping("/api")
public class AuthController {

    private final AuthService authService;

    public AuthController(AuthService authService) {
        this.authService = authService;
    }

    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest req) {
        String token = authService.login(req.email(), req.password());
        return ResponseEntity.ok(new LoginResponse(token));
    }
}

public record LoginRequest(String email, String password) {}
public record LoginResponse(String token) {}

Controller 테스트 (Outside-in)

@WebMvcTest(AuthController.class)
class AuthControllerTest {

    @Autowired MockMvc mockMvc;
    @MockBean AuthService authService;

    @Test
    void login_success_returnsToken() throws Exception {
        given(authService.login("a@b.com", "pw")).willReturn("token-123");

        mockMvc.perform(post("/api/login")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"email\":\"a@b.com\",\"password\":\"pw\"}"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.token").value("token-123"));
    }
}

✔ API 계약을 먼저 고정\
✔ 내부 구현은 Mock 처리


2️⃣ Inside-out 방식

핵심 비즈니스 로직부터 테스트합니다.

Service

@Service
public class AuthService {

    private final UserRepository userRepository;
    private final PasswordHasher passwordHasher;
    private final TokenIssuer tokenIssuer;

    public AuthService(UserRepository userRepository,
                       PasswordHasher passwordHasher,
                       TokenIssuer tokenIssuer) {
        this.userRepository = userRepository;
        this.passwordHasher = passwordHasher;
        this.tokenIssuer = tokenIssuer;
    }

    public String login(String email, String password) {
        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new BadCredentialsException("bad"));

        if (!passwordHasher.matches(password, user.passwordHash())) {
            throw new BadCredentialsException("bad");
        }
        return tokenIssuer.issue(user.id());
    }
}

Service 테스트 (Inside-out)

@ExtendWith(MockitoExtension.class)
class AuthServiceTest {

    @Mock UserRepository userRepository;
    @Mock PasswordHasher passwordHasher;
    @Mock TokenIssuer tokenIssuer;

    @InjectMocks AuthService authService;

    @Test
    void login_success_issuesToken() {
        User user = new User(1L, "a@b.com", "HASH");

        given(userRepository.findByEmail("a@b.com"))
            .willReturn(Optional.of(user));
        given(passwordHasher.matches("pw", "HASH"))
            .willReturn(true);
        given(tokenIssuer.issue(1L))
            .willReturn("token-123");

        String token = authService.login("a@b.com", "pw");

        assertThat(token).isEqualTo("token-123");
    }
}

✔ 도메인 규칙을 촘촘히 검증\
✔ 복잡한 정책이 늘어날수록 강력함


🧱 헥사고날 아키텍처 관점

Outside-in / Inside-out은 헥사고날 구조와도 잘 맞습니다.

  • 바깥(Controller, DB, 외부 API)
  • 안쪽(도메인, 유스케이스)

🔥 실무 추천 전략 (혼합 방식)

  1. 핵심 사용자 플로우는 Outside-in
  2. 복잡한 도메인 규칙은 Inside-out
  3. Repository는 통합 테스트로 검증

👉 결론: 둘 중 하나만 고집하지 말고, 상황에 맞게 섞어 쓰는 것이 가장
현실적입니다.

profile
Java,Spring,JavaScript

0개의 댓글