Notice
Recent Posts
Recent Comments
Link
«   2025/09   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

코딩 이래요래

위클리 페이퍼 10주차 본문

위클리 페이퍼

위클리 페이퍼 10주차

강범호 2025. 6. 23. 01:52

Q. 애플리케이션의 각 계층에서 수행되는 입력값 검증의 범위와 책임을 어떻게 나눌 것인지에 대해 설명해주세요. 특히 중복 검증을 피하면서도 안정성을 확보하는 방안과, 이와 관련된 트레이드오프에 대해 설명해주세요.

 

1. Controller

  • 형식적(Validational) 검증
  • ex: @Valid, @Pattern, @NotNull, @Min, @Size 등
  • 클라이언트의 입력이 형식적으로 유효한지 확인 (JSON Body, Query Param 등)

2. Service

  • 비즈니스 규칙 검증
  • ex: 부서 인원 수가 50명을 넘을 수 없음, 입사일이 오늘 이후일 수 없음 등
  • 도메인 제약이나 로직 상의 규칙을 보장함

3. Persistence (Repository / DB)

  • 데이터 정합성을 위한 최종 방어선
  • ex: DB 제약조건 (UNIQUE, FOREIGN KEY, NOT NULL, CHECK 등)
  • 동시성 이슈나 외부 요인으로부터 데이터 무결성을 보호

중복 검증을 피하면서 안정성을 확보할 수 있는 전략

  • 입력 형식 검증은 Controller에서 단일 책임화 하고, Service는 반드시 신뢰 가능한 데이터만 처리하도록 구성한다.
  • 단, 외부 API 요청, DB 조회 결과 등 신뢰할 수 없는 외부 입력은 Service 계층에서도 별도 방어 로직을 추가한다.
  • DB 제약 조건은 최후의 보루로, 어플리케이션 레벨에서 놓칠 수 있는 예외 상황을 막아준다.

트레이드오프 및 고려사항

 

구분 이점 단점
중복 검증 보안·안정성 향상 (Fail-safe) 코드 중복, 유지보수 비용 증가
단일 계층 검증 구현 간결, 테스트 쉬움 예외 상황 누락 시 위험 증가
다단계 최소 검증 책임 분리 + 안정성 확보 각 계층 책임 구분이 명확해야 함

검증을 필요 이상으로 중복하지 않으면서도, 문제가 있는 값이 중요한 비즈니스 로직 안으로 들어오지 못하게 계층마다 적절한 방어선을 세우는 것이 중요하다. 즉,

핵심은 불필요한 중복은 줄이되, 불안정한 입력이 도메인 로직을 오염시키지 않도록 계층적 방어선을 세우는 것이다.

Q. 테스트에서 사용되는 Mockito의 Mock, Stub, Spy 개념을 각각 설명하고, 어떤 상황에서 어떤 방식을 선택해야 하는지 구체적인 예시와 함께 설명하세요.

 

Mockito에서의 Mock, Stub, Spy 개념

1. Mock

  • 실제 객체의 대체품으로, 행위를 설정하거나 검증할 수 있는 객체.
  • 상호작용(verify)을 중점적으로 테스트할 때 사용.
  • 예시
// UserRepository를 Mock으로 정의
@MockitoBean private UserRepositroy userRepository;

// userRepository.findById()를 호출 하면 Optional.of(new User())을 반환한다.
when(userRepository.findById(1L)).thenReturn(Optional.of(new User()));
  • Repositroy, 외부 API 호출처럼 테스트 대상과 직접 관련 없는 의존 객체가 있는 경우.
  • 행위 검증(호출 횟수, 순서 등)이 필요한 경우.

2. Stub

  • 특정 상황에서 미리 정해진 응답을 반환하는 객체.
  • Mockito 공식 API에서는 Stub이라는 용어를 직접 정의하지 않지만, When(...).thenReturn(...) 구조를 stubbing이라고 칭함.
  • 예시
// orderService.getOrderAmount()를 호출하면 10000을 return한다.
when(orderService.getOrderAmount()).thenReturn(10000);
  • 테스트 중 반환값을 고정시키고, 나머지 로직의 흐름을 확인하고 싶은 경우.
  • 외부 시스템이나 랜덤/시간 기반 결과를 예층 가능한 값으로 고정하고자 할 때.

3. Spy

  • 실제 객체를 감싸되, 특정 메서드만 mocking 할 수 있는 부분 mock 객체
  • 예시
List<String> spyList = spy(new ArrayList<>());
// List.size() 메서드만 mocking한다.
when(spyList.size()).thenReturn(100);
  • 일부는 진짜 동작을 수행하고, 일부만 mocking하고 싶은 경우
  • 상태 기반 검증(State Varification)과 행위 기반 검증을 함께 수행할 때.

실제 사용 예시 (MessageRepositroy 슬라이스 테스트)

// 외부 의존성 mock객체 등록
@Mock UserRepository userRepository;
@Mock ChannelRepository channelRepository;
@Mock BinaryContentRepository binaryContentRepository;
@Mock BinaryContentStorage binaryContentStorage;
@Mock MessageRepository messageRepository;
@Mock MessageMapper messageMapper;
@Mock PageResponseMapper pageResponseMapper;

@InjectMocks private BasicMessageService messageService;

private User user;
private Channel channel;

@BeforeEach // 각 테스트가 동작하기 전 실행되는 메서드
void setUp() {
    user = new User("테스트유저", "test@gmail.com", "00000000", null);
    channel = new Channel(ChannelType.PRIVATE, "테스트 채널", "테스트 채널");
}

@Test
@DisplayName("정상적인 요청을 통해 채널의 메세지 목록을 조회할 수 있다")
void shouldFindMessageListByChannelIdWhenRequestValid() {
    // given
    UUID channelId = UUID.fromString("00000000-0000-0000-0000-000000000001");
    Instant cursor = null;
    Pageable pageable = PageRequest.of(0, 10, Sort.by("createdAt").descending());

    Message message1 = new Message("msg1", channel, user);
    Message message2 = new Message("msg2", channel, user);

    List<Message> messages = List.of(message1, message2);
    Slice<Message> slice = new SliceImpl<>(messages, pageable, false);

    List<MessageDto> messageDtos = messages.stream()
            .map(msg -> new MessageDto(UUID.randomUUID(), Instant.now(), Instant.now(), msg.getContent(), channelId, null, null))
            .toList();
	
    // stub 각 메서드에서 항상 특정한 값을 반환하게 설정
    given(channelRepository.findById(channelId)).willReturn(Optional.of(channel));
    given(messageRepository.findByChannelIdOrderByCreatedAtDesc(channelId, pageable))
            .willReturn(slice);
    given(messageMapper.toDto(any())).willAnswer(invocation -> {
        Message msg = invocation.getArgument(0);
        return new MessageDto(UUID.randomUUID(), Instant.now(), Instant.now(), msg.getContent(), channelId, null, null);
    });

    given(pageResponseMapper.fromSlice(any(Slice.class)))
            .willReturn(new PageResponse<>(messageDtos, null, messageDtos.size(), false, (long) messageDtos.size()));

    // when
    PageResponse<?> result = messageService.findAllByChannelId(channelId, cursor, pageable);

    // then
    assertThat(result).isNotNull();
    assertThat(result.content()).hasSize(2);
    assertThat(result.hasNext()).isFalse();
    assertThat(result.content())
            .extracting("content")
            .containsExactly("msg1", "msg2");
}
  • 의존 객체들은 모두 실제 구현 없이 Mock으로 대체.
  • 테스트 대상인 MessageService는 @InjectMocks로 이 Mock 객체들을 주입 받음.
  • 각각의 의존 메서드 호출에 대해 미리 지정된 결과를 반환하게 함.
  • 실제 DB나 외부 API 없이도, 원하는 흐름과 데이터를 테스트 가능하게 함.

'위클리 페이퍼' 카테고리의 다른 글

위클리 페이퍼 12주차  (2) 2025.07.07
위클리 페이퍼 11주차  (3) 2025.06.30
위클리 페이퍼 - 9주차  (2) 2025.06.02
위클리 페이퍼 8주차  (3) 2025.05.30
위클리 페이퍼 - 7주차  (3) 2025.05.25