코드잇 BE스프린트

위클리 페이퍼 - 10주차

beginner-development 2026. 4. 27. 21:57

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

- Controller

  • 주로 컨트롤러에서는 도메인의 형식 및 필수값 검증을 담당
  • 데이터 타입, 필수 필드 존재 여부 검증
  • DTO에서 Bean Validation (ex: @NotNull, @NotBlank, @Size 등)

- Service

  • 주로 비즈니스 로직과 연관된 도메인 규칙 검증을 담당
  • 비즈니스 규칙 위반 여부 검증(ex: 중복 가입 여부, 주문 가능 여부 등)

- Repository

  • 주로 데이터 무결성 검증 및 제약조건 관리를 담당
  • 데이터베이스 제약조건(Unique 제약, FK제약 등)
  • 실제 DB 제약조건으로 강제

중복 검증을 피하면서 안정성 확보 방안

 - 계층별 책임 명확화

  • Controller : 요청 데이터의 형식만 검증
    • 형식 오류는 Controller 단계에서 빠르게 실패 처리(ex: @Valid 및 Bean Validation 활용)
  • Service : 비즈니스 로직을 기반으로 한 검증 수행
    • 형식이 올바른 데이터에 대해서만 중복 여부나 상태 전이 등의 검증(ex: 이메일 중복 가입 여부 확인)
  • Repository : 데이터베이스 무결성을 위한 제약조건 설정
    • DB 자체에서 중복 방지(ex: Unique 인덱스 생성)

 - 적극적인 데이터베이스 제약조건 활용

  • 최종 데이터 무결성은 반드시 데이터베이스 차원에서 강제
  • Repository에서 DB 예외 처리 후, Service로 다시 비즈니스 예외로 변환하여 전달
    • 중복된 요청이 동시에 들어왔을 때 경쟁 상태를 방지함

중복 검증을 피하기 위한 트레이드오프

 - Controller ↔ Service 검증 중복 문제

  • 중복 발생 원인 : Controller에서 비즈니스 검증까지 수행하면 로직이 중복됨
  • 해결방안 : 
    • Controller는 형식 검증만 명확히 한정
    • 비즈니스 로직은 Service 계층에서만 처리하여 중복 제거

 - Service ↔ Repository 검증 중복 문제

  • 중복 발생 원인 : Service에서 미리 중복 체크 후 저장을 시도했지만, DB 레벨에서 제약조건을 다시 체크하여 중복 발생
  • 해결 방안 : 
    • 서비스는 중복 여부의 선제적 확인, Repository는 최종 방어
    • 예외 발생 시점은 Repository로 통일, 서비스는 DB 예외를 처리하여 일관된 예외로 반환

 - 트레이드 오프

계층 무엇을 장점 단점 도움말
Controller/DTO 형식·타입·범위·크기, 기본 정규화(trim 등) 입구에서 빨리 차단, 리소스 절약, 에러 응답 통일 비즈니스 규칙·동시성은 못 막음, UI와 규칙 중복 우려 @Valid + @ControllerAdvice로 포맷만 검증
Service/도메인 비즈니스 규칙, 상태 전이, 교차 필드/권한 체크 도메인 중심으로 의미 검증, 친절한 메시지 사전조회는 경합 취약, 트랜잭션 관리 필요 사전검사 + DB 예외 매핑(둘 다)로 안전하게
Repository/DB 절대 무결성: NOT NULL, UNIQUE, FK, CHECK 최종 안전망, 동시성에 강함 에러 메시지 기술적, 스키마 변경 비용 유니크·참조 무결성은 반드시 DB로 보장

 

나만의 언어로 정리해보기

각 계층에서 수행되는 입력값 검증의 범위와 책임
1. Controller
-
도메인의 형식 및 필수값 검증을 담당한다.
데이터 타입 및 필수 필드 존재 여부를 검증한다.
- DTO에서 Bean Validation을 통해 검증한다.

2. Service
- 비즈니스 로직과 연관된 도메인 규칙 검증을 담당
- 비즈니스 규칙 위반 여부 검증

3.Repository
 -
데이터 무결성 검증 및 제약조건 관리를 담당
 - 
데이터베이스 제약조건
 - 실제 DB 제약 조건으로 강제

중복 검증을 피하면서 안정성 확보 방안
 -
계층별 책임 명확화
 -
적극적인 데이터 베이스 제약조건 활용

※ 중복 검증을 피하기 위한 트레이드 오프는 위의 표 참조

 

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

Mock

  • 개념 : 메서드 호출 여부 및 특정 행위를 확인하기 위해 사용하는 가짜 객체
  • 특징 : 
    • 실제 로직을 사용하지 않는다.
    • 모든 메서드가 기본적으로 빈 값이나 null을 반환한다.
    • 모든 행동은 when() 또는 given()과 같은 메서드로 미리 정의해야한다.

Stub

  • 개념 : 테스트에서 호출될 때 미리 준비된 값을 반환함으로써, 테스트 대상 코드가 의도한 특정 상태에서 동작하도록 만드는 가짜 객체
  • 특징 : 
    • 메서드 호출 시 원하는 값을 반환하도록 정의한다.
    • 특정 메서드 호출에만 초점을 맞춘다.
    • 메서드 호출 여부나 횟수는 검증 X
    • 테스트에 필요한 데이터를 제공하여 특정 상태를 흉내 낸다.

Spy

  • 개념 : 실제 객체를 기반으로 동작하면서, 테스트에 필요한 특정 메서드의 행동만 선택적으로 바꿀 수 있는 부분적인 가짜 객체
  • 특징 : 
    • 기본적으로 실제 로직 실행
    • 필요한 메서드만 Stub으로 재정의 가능
    • 특정 메서드의 행동만 선택적으로 변경 가능

각 상황 별 구체적인 활용 예시

예) 공통 시나리오

주문(Order)을 처리하는 OrderService를 테스트.

ItemRepository(상품 재고를 조회하고 차감하는 역할)와 NotificationClient(주문이 완료되면 사용자에게 알림(이메일, SMS 등)을 보내는 역할)의 두가지 외부 의존성을 가짐.

 

예시 코드)

// 테스트 대상 클래스 (System Under Test, SUT)
public class OrderService {
    private ItemRepository itemRepository;
    private NotificationClient notificationClient;

    // 생성자 주입
    public OrderService(ItemRepository itemRepository, NotificationClient notificationClient) {
        this.itemRepository = itemRepository;
        this.notificationClient = notificationClient;
    }

    // 주문 처리 메서드
    public void placeOrder(String itemName, int quantity) {
        int stock = itemRepository.getStock(itemName);
        if (stock < quantity) {
            throw new RuntimeException("재고가 부족합니다.");
        }
        itemRepository.deductStock(itemName, quantity); // 재고 차감

        // 주문 완료 알림 발송
        notificationClient.send(itemName + " 주문이 완료되었습니다.");
    }
}

 

Stub : 상태 제어

  • 테스트에 필요한 특정 상황이나 데이터를 만들어주기 위해 사용
  • placeOrder 메서드를 테스트하려면, 먼저 상품의 재고가 충분한 '상태'를 만들어야 함. 실제 데이터베이스에 연결하는 대신, ItemRepository의 getStock() 메서드가 항상 특정 재고량을 반환하도록 Stub을 생성.
  • 상황
    • placeOrder가 재고가 충분할 때 정상적으로 실행되는지 확인
  • 선택
    • ItemRepository를 가짜로 만들고(mock), getStock("테스트상품")이 호출되면 100을 반환하도록 행동을 정의(stubbing).(itemRepository는 Stub의 역할을 수행)

예시 코드)

@Test
void 재고가_충분하면_주문이_성공해야_한다() {
    // 1. Stub 설정 (ItemRepository가 Stub 역할)
    // ItemRepository의 가짜 객체를 만들고,
    ItemRepository itemRepository = Mockito.mock(ItemRepository.class);
    // getStock("사과")가 호출되면 무조건 100을 반환하도록 정의 (stubbing)
    Mockito.when(itemRepository.getStock("사과")).thenReturn(100);

    // NotificationClient는 이 테스트의 관심사가 아니므로 가짜로 만듦
    NotificationClient notificationClient = Mockito.mock(NotificationClient.class);

    // 2. 테스트 실행
    OrderService orderService = new OrderService(itemRepository, notificationClient);
    orderService.placeOrder("사과", 10); // 재고(100) > 주문(10) 이므로 성공해야 함

    // 3. (선택적) 검증
    // 이 테스트의 핵심은 아니지만, 재고 차감 메서드가 호출되었는지 확인할 수 있음
    Mockito.verify(itemRepository).deductStock("사과", 10);
}

 

Mock : 행위 검증

  • 특정 객체의 메서드가 예상대로 호출되었는지, 몇 번 호출되었는지 확인하기 위해 사용
  • 주문이 성공적으로 완료되면, NotificationClient의 send() 메서드가 정확히 한 번 호출되어야 함. 이 "호출되었는가?" 라는 행위 자체가 검증의 핵심
  • 상황
    • placeOrder가 성공했을 때, 사용자에게 알림을 보내는 send() 메서드가 정말로 호출되었는지 확인
  • 선택
    • NotificationClient를 Mock 객체로 생성. 이 Mock 객체는 send() 메서드가 호출되었는지 여부를 기억하고 있다가, 나중에 verify()를 통해 우리가 확인할 수 있게 함.

예시 코드)

@Test
void 주문이_성공하면_알림이_전송되어야_한다() {
    // 1. Stub 설정 (테스트 환경 구성)
    ItemRepository itemRepository = Mockito.mock(ItemRepository.class);
    Mockito.when(itemRepository.getStock("사과")).thenReturn(100);

    // 2. Mock 객체 생성 (검증 대상)
    // NotificationClient의 가짜 객체를 만들어서 행위를 검증할 준비
    NotificationClient notificationClient = Mockito.mock(NotificationClient.class);

    // 3. 테스트 실행
    OrderService orderService = new OrderService(itemRepository, notificationClient);
    orderService.placeOrder("사과", 10);

    // 4. 행위 검증 (Verification)
    // notificationClient의 send() 메서드가 "사과 주문이 완료되었습니다." 라는 메시지와 함께
    // '정확히 1번' 호출되었는지 검증한다.
    Mockito.verify(notificationClient, Mockito.times(1)).send("사과 주문이 완료되었습니다.");
}

 

Spy : 실제 객체의 일부만 변경

  • 대부분 실제 객체의 로직을 그대로 사용하면서, 특정 메서드만 우리의 의도대로 동작하게 만들고 싶을 때 사용
  • OrderService에 주문 이력을 저장하는 saveHistory라는 내부 메서드가 있고, 이 메서드는 실제 파일 시스템에 접근해서 테스트가 느려진다고 가정했을 때, placeOrder의 전체적인 로직은 실제 코드로 테스트하되, saveHistory만 아무것도 하지 않도록 만들고 싶음.
  • 상황
    • OrderService의 전체적인 흐름은 실제 코드로 테스트하고 싶은데, 외부 시스템과 연동되거나 무거운 saveHistory 메서드만 동작하지 않도록 하고 싶음.
  • 선택
    • Spy를 사용해 실제 OrderService 객체를 감싼다. 이렇게 하면 모든 메서드 호출은 기본적으로 실제 객체로 전달되지만, saveHistory 메서드만 우리가 원하는 대로 가짜 행동을 정의할 수 있다.

예시 코드)

// Spy를 적용하기 위해 OrderService에 가상의 메서드 추가
public void saveHistory(String message) {
    // 실제로는 파일에 주문 이력을 기록하는 복잡하고 느린 로직
    System.out.println("이력 저장: " + message);
}

public void placeOrder(String itemName, int quantity) {
    // ... (기존 로직과 동일) ...
    itemRepository.deductStock(itemName, quantity);
    notificationClient.send(itemName + " 주문이 완료되었습니다.");

    // 이력 저장 메서드 호출
    saveHistory(itemName + " " + quantity + "개 주문됨");
}

// --- 테스트 코드 ---
@Test
void 주문시_파일저장_로직은_실행하지_않고_테스트하고_싶다() {
    // 1. 의존성 객체 준비 (Stub/Mock)
    ItemRepository itemRepository = Mockito.mock(ItemRepository.class);
    Mockito.when(itemRepository.getStock("사과")).thenReturn(100);
    NotificationClient notificationClient = Mockito.mock(NotificationClient.class);

    // 2. Spy 객체 생성
    // 실제 OrderService 객체를 생성하고 Spy로 감싼다.
    OrderService realService = new OrderService(itemRepository, notificationClient);
    OrderService spyOrderService = Mockito.spy(realService);

    // 3. Spy의 일부 메서드 행동 정의
    // spyOrderService의 saveHistory() 메서드가 호출되어도 실제 로직을 타지 않도록 설정
    // when(spy.method())는 실제 메서드를 호출하므로 doNothing()을 사용해야 안전하다.
    Mockito.doNothing().when(spyOrderService).saveHistory(Mockito.anyString());

    // 4. 테스트 실행
    // Spy 객체의 메서드를 호출한다.
    spyOrderService.placeOrder("사과", 10);

    // 5. 검증
    // placeOrder의 다른 로직들은 실제 코드대로 실행되었는지 확인
    Mockito.verify(notificationClient).send("사과 주문이 완료되었습니다.");
    // saveHistory가 호출은 되었지만, 실제 내용은 실행되지 않았음을 검증
    Mockito.verify(spyOrderService).saveHistory("사과 10개 주문됨");
}

 

나만의 언어로 정리해보기

Mockito의 Mock, Stub, Spy 개념
1. Mock
-
메서드 호출 여부 및 특정 행위를 확인하기 위해 사용하는 가짜 객체
특정 객체의 메서드가 예상대로 호출되었는지, 몇 번 호출되었는지 확인하기 위해 사용 (행위 검증)
- 실제 로직을 사용하지 않고 모든 메서드가 기본적으로 빈 값이나 nul 값을 반환

2. Stub
- 테스트에서 호출될 때, 미리 준비된 값을 반환함으로써, 테스트 대상 코드가 의도한 특정 상태에서 동작하도록 만드는 가짜 객체
- 테스트에 필요한 특정 상황이나 데이터를 만들어 주기 위해 사용 (상태 제어)
- 메서드 호출 여부나 횟수는 검증 X

3.Spy
 -
실제 객체를 기반으로 동작하면서, 테스트에 필요한 특정 메서드의 행동만 선택적으로 바꿀 수 있는 부분적인 가짜 객체
 - 
특정 메서드만 우리의 의도대로 동작하게 만들고 싶을 때 사용 (실제 객체의 일부만 변경)
 - 기본적으로 실제 로직을 실행하지만 필요한 메서드만 stub으로 재정의 가능

'코드잇 BE스프린트' 카테고리의 다른 글

위클리 페이퍼 - 12주차  (0) 2026.04.29
위클리 페이퍼 - 11주차  (0) 2026.04.28
위클리 페이퍼 - 9주차  (0) 2026.04.16
위클리 페이퍼 - 8주차  (1) 2026.04.15
위클리 페이퍼 - 7주차  (1) 2026.04.14