| 계층 | 역할 / 책임 |
|---|---|
| Controller | 기본적인 형식/구조 검증 → @Valid, @NotNull, @Pattern 등→ 빠르게 잘못된 요청 차단 |
| Service | 비즈니스 규칙 검증 → 예: 유저가 이미 탈퇴했는지, 채널 수정 권한 있는지 등 |
| Domain | 도메인 규칙 검증 및 상태 변경 책임 → 도메인 객체 스스로 불변성 유지 → 예: channel.rename(name) 시 유효성 내부 체크 |
| Repository (DB) | DB 무결성 보장 → 유니크 제약조건, FK 등 → 모든 검증을 통과해도 최후 보루로 작동 |
계층에 따른 검증 책임 철저히 분리
ex) @Email은 Controller, "이미 존재하는 이메일" 여부는 Service.
비즈니스 검증은 반드시 Service 계층에서만
계층에 따른 예외 처리
→ Controller: MethodArgumentNotValidException
→ Service: UserAlreadyExistsException, ChannelPermissionException 등
controller는 주로 요청 자체가 잘못된 경우를 처리한다.
형식적인 오류 (@Valid, @NotBlank, @Email 등) 에 대해서는 스프링 기본 예외가 발생하지만,
사용자에게 더 명확한 메시지를 주기 위해 커스텀 예외로 변환하거나 직접 던지는 방식도 활용된다.
service는 도메인 상의 로직을 위반한 경우를 처리한다.
해당 도메인에 맞는 의미 있는 커스텀 예외를 직접 던진다.
역할이 명확해져서 유지보수가 쉬움
검증 로직 중복 없이 재사용 가능
단위 테스트 작성이 쉬워짐
신중한 구조 파악 필요
검증이 누락되거나 중복될 수 있음
간단한 기능엔 오버엔지니어링처럼 느껴질 수 있음
UserRepository userRepository = mock(UserRepository.class);
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
사용하는 경우
→ 의존성만 필요하고 내부 동작은 중요하지 않을 때
→ verify()로 호출 확인하고 싶을 때
when(calculator.add(2, 3)).thenReturn(5);
사용하는 경우
→ 특정 입력값에 대한 결과만 중요할 때 - 지금 테스트하는 대상 외에 다른 의존 기능은
그냥 원하는 결과만 나오게 하고 넘어가고 싶을 때
→ 복잡한 검증 없이 결과만 받고 싶을 때
List<String> list = new ArrayList<>();
List<String> spyList = spy(list);
// 진짜 동작
spyList.add("hello");
System.out.println(spyList.get(0)); // "hello"
// 일부만 가짜로 바꾸기
doReturn(100).when(spyList).size();
System.out.println(spyList.size()); // 100 (원래 size는 1이어야 함)
사용하는 경우
→ 일부 메서드만 가짜로 바꾸고, 나머지는 실제 동작해야 할 때
→ 기존 동작 보존하면서 추적하고 싶을 때
| 상황 | 설명 |
|---|---|
| 1. 호출 여부는 추적하고, 실제 동작은 막고 싶을 때 | 예: 이메일 전송 메서드 호출은 확인하지만 실제로 보내지 않음 |
| 2. 일부 메서드만 가짜로, 나머지는 진짜처럼 동작하게 하고 싶을 때 | 예: get()은 가짜 값 반환, put()은 진짜 저장 |
| 3. 특정 동작만 실패하도록 시뮬레이션하고 싶을 때 | 예: 외부 API만 실패 처리하고 전체 흐름 테스트 |
| 4. 상태 변화나 부작용이 많은 객체의 일부 동작만 통제하고 싶을 때 | 예: 캐시, 파일 저장, 알림 전송 등 |
| 상황 | 선택 |
|---|---|
| 전체 기능 대체 | Mock |
| 반환값만 지정해서 빠르게 테스트 | Stub |
| 진짜 객체 쓰고 일부만 조작 | Spy |
정리가 깔끔하네요! 잘 읽고 갑니다!!👍