티스토리 뷰

위클리페이퍼

위클리 페이퍼10

noobdev25 2026. 1. 31. 21:13

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

 

1. 각 계층별 검증 범위와 책임 분리

 

컨트롤러 레이어(프레젠테이션 계층)

입력값 검증 범위 : 구문적(Syntactic) 검증. 데이터의 형식, 타입, 필수 값 존재 여부

더보기

구문적(Syntactic) 검증: "형태가 맞는가?"

데이터의 '생김새'나 '형식'이 문법적으로 올바른지 확인하는 단계

  • 비유: 은행 가입 신청서에 '전화번호' 칸을 채울 때, 숫자 대신 한글(예: '공일공...')을 적었는지 확인하는 것
  • 검사 내용:
    • 값이 비어있는가? (Null 체크)
    • 글자 수가 너무 짧거나 길지는 않은가?
    • 이메일 형식(@ 포함)이 맞는가?

책임 : 시스템 내부로 잘못된 데이터가 인입되지 않도록 입구에서 차단(Fail-Fast)

이유 : 데이터가 비어있는지, 이메일 형식이 맞는지 같은 '기본 자격'을 확인. 비즈니스 로직(서비스 계층)으로 넘어가기 전에 잘못된 데이터를 걸러내어, 서버가 불필요한 일을 하지 않도록 보호하는 역할

 

서비스 레이어(비즈니스 로직 계층)

입력값 검증 범위 : 의미적(Semantic) 검증. 도메인 규칙 준수 여부 및 비즈니스 상태 확인

더보기

의미적(Semantic) 검증: "내용(논리)이 맞는가?"

데이터의 형식은 완벽하지만, '실제로 가능한 상황'인지 확인하는 단계

  • 비유: 전화번호 형식(010-1234-5678)은 완벽하게 적었지만, 이미 다른 사람이 쓰고 있는 번호인지를 확인하는 것
  • 검사 내용:
    • 중복된 아이디인가?
    • 잔액보다 더 많은 돈을 출금하려 하는가?
    • 존재하지 않는 상품 번호로 주문을 요청했는가?

책임 : 애플리케이션의 핵심 비즈니스 규칙(Business Rule) 보존 및 데이터 정합성 보장

이유 : "아이디 형식이 맞는가?"가 아니라, "이미 존재하는 아이디인가?"처럼 DB를 확인하거나 계산이 필요한 진짜 규칙을 검사 프로그램의 핵심 로직이 올바르게 돌아가는지 최종 판단하는 단계

 

레포지토리 레이어(데이터베이스 접근 계층)

입력값 검증 범위 : 물리적(Physical) 검증. 데이터베이스 스키마 제약 조건 준수 여부

더보기

물리적(Physical) 검증: "실제로 담을 수 있는가?"

최종적으로 데이터가 저장되는 저장소(DB)의 규칙에 어긋나지 않는지 확인하는 단계

  • 비유: 은행 금고 칸의 크기는 10cm인데, 50cm짜리 물건을 넣으려고 하면 물리적으로 들어가지 않음
  • 검사 내용:
    • DB 테이블 컬럼 크기보다 더 긴 문자열을 넣으려 할 때.
    • 중복을 허용하지 않는 컬럼(Unique)에 강제로 같은 값을 넣으려 할 때.
    • 반드시 값이 있어야 하는 컬럼(Not Null)에 빈 데이터를 넣으려 할 때.

책임 : 영속성 계층의 데이터 무결성(Integrity) 확보

이유 : 애플리케이션 코드에서 실수로 놓친 오류가 있더라도, 데이터베이스 자체 규칙(Unique, Not Null 등)을 통해 잘못된 데이터가 저장되는 최후의 사고를 막음

 

2. 중복 검증을 피하면서 안정성을 확보하는 방법

- Bean Validation (@Valid) 적극 활용: DTO에 @NotBlank, @Size 같은 어노테이션을 붙여 컨트롤러에서 자동으로 검증하게 함. 이렇게 하면 서비스 계층에서는 기본적인 형식 검사를 생략하고 비즈니스 로직에만 집중할 수 있어 코드가 깔끔해짐

 

- 객체 스스로 검증하게 만들기: 데이터를 담는 객체(Entity)가 생성될 때 스스로 유효한지 검사하게 설계하면, 이 객체를 사용하는 모든 곳에서 매번 검증 코드를 짤 필요가 없어 중복이 획기적으로 줄어듦

 

- 공통 예외 처리기 사용: 검증 에러가 났을 때 처리하는 코드를 한곳에 모아 관리(Exception Handler)하면, 개별 컨트롤러나 서비스 코드가 훨씬 간결해짐

 

- 검증 로직의 공통화: 여러 서비스에서 공통으로 쓰이는 검증(예: 비밀번호 복잡도 체크)은 별도의 유틸리티 클래스나 Custom Validator로 분리해서 한 곳에서 관리

3. 트레이드 오프

트레이드 오프란?

모든 것을 다 가질 수 없는 선택의 상황에서 발생하는 '저울질' 개발의 세계에는 100% 정답인 기술도, 100% 오답인 기술도 없음 어떤 선택이든 장점과 단점이 공존하기 때문임

 

안정성 vs 유지보수성: 모든 레이어에서 겹겹이 검증하면 데이터는 안전해지지만, 규칙이 바뀔 때마다 모든 코드를 고쳐야 하므로 유지보수가 힘들어짐

검증 로직의 위치: 검증을 한곳에 몰아넣으면 찾기는 쉽지만 코드가 비대해지고, 여러 곳으로 쪼개면 코드는 예쁘지만 전체 흐름을 한눈에 파악하기 어려울 수 있음

성능 vs 보안: 클라이언트에서만 검증하면 빠르지만 보안에 취약하고, 서버에서만 하면 안전하지만 사용자 피드백이 늦어질 수 있음

 


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

 

Mock(가짜 객체)

정의 : 실제 로직이 전혀 없는 빈 껍데기 객체

특징 : 메서드를 호출해도 기본값(null, 0, false)만 반환하며 아무 일도 하지 않음

용도 : "메서드가 호출됐는가?"와 같은 행위 검증(Behavior Verification)에 집중

// 알림 서비스가 실제로 호출되었는지 확인하고 싶을 때
NotificationService mockSms = mock(NotificationService.class);

orderService.completeOrder(order, mockSms);

// "sendSms 메서드가 정확히 1번, '결제 완료'라는 문구와 함께 실행되었는가?"
verify(mockSms, times(1)).sendSms(anyString(), eq("결제 완료"));

 

언제 사용할까?

- 결과값(Return)이 없는 void 메서드를 테스트해야 할 때

(ex : 로그 기록, 이벤트 알림 등)

- 객체 간의 협력 관계가 올바른지 확인하고 싶을 때

(ex : "주문 완료 시 결제 모듈이 제대로 호출되었는가?")

- 메서드가 호출된 횟수나 순서가 비즈니스 로직에서 중요한 경우

 

Stub(상태 주입)

정의 : 특정 입력에 대해 특정 값을 반환하도록 미리 설정된 가짜 동작

특징 : Mockito에서는 별도의 타입이 아니라, when(...).thenReturn(...)을 통해 동작을 정의하는 것(Stubbing) 자체를 의미

용도 : 테스트에 필요한 데이터를 강제로 공급하는 상태 검증(State Verification)에 집중

// 특정 조건(VIP 유저)에 따른 할인을 테스트하고 싶을 때
UserRepository stubRepo = mock(UserRepository.class);

// "ID 1번 유저를 조회하면 VIP 등급인 유저 객체를 반환해라"
when(stubRepo.findById(1L)).thenReturn(new User("Alice", Grade.VIP));

double discount = discountService.getDiscountRate(1L);
assertEquals(0.2, discount); // 20% 할인이 적용되었는지 확인

언제 사용할까?

- 외부 API나 DB에서 데이터를 가져와 그 데이터를 가공하는 로직을 테스트할 때

- 네트워크 장애, 데이터베이스 오류 등 예외 상황(Exception)을 강제로 발생시켜야 할 때

- 특정 조건(예: 유저 등급, 잔액 유무)에 따라 서로 다른 비즈니스 로직 분기를 타야 할 때

Spy(부분 가짜)

정의: 실제 객체를 기반으로 생성된 '감시자'

특징: 기본적으로는 실제 로직을 수행하지만, 특정 메서드만 선택적으로 가짜 결과를 내놓게(Stubbing) 할 수 있음

용도: 실제 로직을 활용하면서 특정 지점만 관찰하거나 수정할 때 사용

// 실제 계산기 로직을 쓰되, 특정 연산만 조작하고 싶을 때
Calculator realCalc = new Calculator();
Calculator spyCalc = spy(realCalc);

// 다른 사칙연산은 실제 로직을 타지만, 더하기 결과만 100으로 고정
doReturn(100).when(spyCalc).add(1, 1);

System.out.println(spyCalc.add(1, 1)); // 출력: 100 (조작됨)
System.out.println(spyCalc.minus(10, 5)); // 출력: 5 (실제 로직 실행)

언제 사용할까?

- 레거시 코드를 테스트하는데, 실제 로직 대부분은 유지하고 특정 부분(예: 외부 시스템 연결)만 교체하고 싶을 때

- 자기 자신의 메서드를 호출하는 로직을 테스트하면서, 특정 메서드의 결과만 조작하고 싶을 때

- 실제 객체의 상태 변화를 추적하면서 특정 메서드의 리턴값만 제어해야 할 때

 

한눈에 보는 요약

구분 검증 대상 실제 로직 실행 여부 주요 Mockito 문법
Mock 호출 횟수, 순서 등 행위 X (전부 가짜) verify()
Stub 메서드의 리턴값 (상태) X (전부 가짜) when().thenReturn()
Spy 실제 로직 + 일부 상태/행위 O (부분 진짜) spy(), doReturn()

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

위클리 페이퍼12  (0) 2026.02.09
위클리 페이퍼11  (0) 2026.02.07
위클리 페이퍼9  (0) 2026.01.12
위클리 페이퍼8  (0) 2026.01.05
위클리 페이퍼7  (0) 2025.12.29
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/08   »
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 31
글 보관함