Q. 애플리케이션의 각 계층에서 수행되는 입력값 검증의 범위와 책임을 어떻게 나눌 것인지에 대해 설명해 주세요. 특히 중복 검증을 피하면서도 안정성을 확보하는 방안과, 이와 관련된 트레이드오프에 대해 설명해 주세요.
A. 답변
1. 계층별 입력값 검증의 범위와 책임
| Controller | 외부에서 들어온 데이터가 올바른 형태인지 검증 | 필수 값, 형식(이메일/길이/패턴), 타입 | @Valid + DTO의 @NotBlank, @Email, @Size |
| Service | 비즈니스 규칙, DB 상태에 맞는지 검증 | 이메일·전화번호 중복 가입 불가, 권한, 상태 전이 규칙 | 직접 조회 후 예외, 필요하면 @Validated |
| Domain(Entity) | 객체가 항상 유효한 상태를 유지하도록 불변식 보장 | 가격 음수 불가, 수량 0 이상 등 | 생성자·정적 팩토리·상태 변경 메서드 안에서 검증 |
| DB | 저장 시점의 데이터 무결성을 지키는 최종 방어선 | UNIQUE, NOT NULL, FK, CHECK | 스키마 제약조건 |
DB 제약조건이 필요한 대표 사례는 동시성이 발생하였을때 두 요청이 동시에 Service의 중복 체크를 통과해도 최종적으로 DB의 UNIQUE 제약이 제한한다.
2. 중복 검증을 피하면서 안정성을 확보하는 방안
- 각 계층은 자기 책임만 검증 Controller에서 확인한 검증을 Service에서는 다시 하지 않고 Service는 DB 조회가 필요한 규칙만 보기)
- Domain은 DB에 의존하지 않는 규칙만 가짐 중복 이메일처럼 조회가 필요한 검증은 Service의 영역
- DB 제약 조건은 중복이 아니고 보험임 Service가 뚫렸을때 방어 할 수 있는 최종 방어선으로 이때 발생할 있는 DataIntegrityViolationException를 잡아서 사용자 친화적 메시지를 반환할 수 있음
3. 트레이드 오프
| 방식 | 얻는 것 | 잃는 것 |
| 여러 계층에 검증 | 친절한 오류, 탄탄한 방어 | 중복, 유지보수 비용 |
| DB에만 맡김 | 코드 단순 | 불친절한 오류, 늦은 실패 |
| Domain에 몰아넣음 | 규칙이 한곳에 모임 | 엔티티 비대, 계층 의존성 문제 |
Q. 테스트에서 사용되는 Mockito의 Mock, Stub, Spy 개념을 각각 설명하고, 어떤 상황에서 어떤 방식을 선택해야 하는지 구체적인 예시와 함께 설명하세요.
A. 답변
1. Mockito란?
- DB, 외부 API처럼 테스트에서 그대로 실행하기 어려운 의존성을 가짜로 대체하거나 제어할 수 있게 해주는 테스트 라이브러리
2. Mock
- 개념 : 전부 가짜인 객체 @Mock으로 만들면 껍데기만 있고, 메서드를 호출해도 실제 로직은 돌지 않고 기본값(null, 0, false, 빈컬렉션)을 반환
- 특징 : 호출 기록이 남아서 verify()로 어떤 메서드가 몇 번 호출 됐는지 검증 가능
- 언제 : 외부 시스템을 실제로 호출하면 안되고 호출했다는 사실 자체가 중요할 때 사용
- 예시 : 회원가입 후 이메일 발송 서비스가 호출 됐는지 확인 시 사용
EmailService mockService = mock(EmailService.class);
// when: 별도로 정의하지 않아도 호출 자체는 가능
mockService.send("user@example.com", "Hello");
// then: 해당 메서드가 특정 인자로 호출되었는지를 검증
verify(mockService).send("user@example.com", "Hello");
3. Stub
- 개념 : Mock이나 Spy에 이 입력이 들어오면 이 값을 돌려줘라고 미리 정해두는 행위를 뜻한다. 객체가 아니고 행위라는 점이 중요
- 언제 : 테스트 대상이 의존성의 반환값을 받아서 로직을 진행할 때 사용
- 예시 : 회원가입 서비스를 테스트할 때, 실제 메일을 보내지 않도록 EmailService의 send()가 항상 발송 성공(true)을 반환하도록 설정할 때 사용
EmailService stub = mock(UserService.class);
// when: 특정 입력에 대해 지정된 결과를 미리 설정
when(stub.send(anyString(), anyString())).thenReturn(true);
// then: 실제 외부 호출 없이 테스트 가능
boolean result = stub.findBy("user@example.com", "Welcome");
assertTrue(result); // 항상 true 반환
4. Spy
- 개념 : 실제 객체를 감싼 객체로 Stub 하지 않는 메서드는 실제 로직이 실행, 필요한 메서드만 골라서 가짜 결과로 바꾸기 가능 진짜와 가짜를 섞은 하이브리드
- 언제 : 대부분 실제 동작을 그대로 쓰고 특정 메서드 하나만 통제하고 싶을 때 사용
- 예시 : ArrayList의 실제 기능을 사용하면서, size() 메서드만 조작하고 싶은 경우
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList); // 실제 객체를 '감싸는' Spy 생성
// add()를 두 번 호출했으니 size()는 2를 반환해야 함
spyList.add("Hello");
spyList.add("World");
// when: size()만 의도적으로 조작
when(spyList.size()).thenReturn(100);
// then: add()는 실제로 동작했지만 size()는 가짜 값 반환
assertEquals(100, spyList.size());
5. 선택 기준
- 의존성을 완전히 끊고, 호출 여부를 검증하고 싶을 때 : Mock + verify
- 의존성이 특정 값을 돌려줘야 로직이 진행될 때 : Mock + stub
- 실제 로직은 쓰되 일부 메서드만 바꾸고 싶을 때 : spby ( + stub)
Mockito는 대표적으로 Service를 테스트할 때 실제 DB에 의존하지 않고 Service만 빠르게 검증하고 싶을 때 사용할 수 있을 것 같다.
반응형
'코드잇 스프린트 > 위클리페이퍼' 카테고리의 다른 글
| [위클리페이퍼] 261006_Docker (0) | 2026.10.06 |
|---|---|
| [위클리페이퍼] 26.08.31 데이터베이스 설계와 Spring Data JPA (0) | 2026.08.30 |
| [위클리페이퍼] 26.08.24 RESTful APIs 구현하기 (0) | 2026.08.23 |
| [위클리페이퍼] 26.08.16 Spring과 Spring Boot 2 (0) | 2026.08.17 |
| [위클리페이퍼] 26.08.09 Spring과 Spring Boot 2 (0) | 2026.08.09 |