[SB 3기] 코드잇 스프린트 위클리페이퍼 10주차

JHLee·2025년 6월 22일
6
post-thumbnail

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

✅ 각 계층의 입력값 검증 범위와 책임

1. Presentation Layer (Controller)

  • 사용자의 입력값이 기본적인 형식 요건을 충족하는지 검증하는 1차 방어선이다.

  • ex. 문자열 길이 제한, 이메일 형식, null 허용 여부 등

  • 일반적으로 @Valid, @NotNull, @Size, @EmailBean Validation 을 사용하여 DTO 필드의 형식을 검증한다.

    @PatchMapping(path = "/{messageId}")
    @Override
    public ResponseEntity<MessageDto> update(
            @PathVariable UUID messageId,
            @Valid @RequestBody MessageUpdateRequest messageUpdateRequest
    ) {
    	...
    }

💡 Bean Validation이란?
어노테이션을 통해 객체의 필드에 제약 조건을 선언적으로 정의할 수 있는 검증 API이다.
이를 사용하면 검증 로직이 명확해지고, 코드의 가독성과 유지보수성을 높일 수 있다.
또한, 표준화된 방식으로 검증을 수행하여 일관된 검증을 구현할 수 있다.

2. Business Layer (Service)

  • 비즈니스 규칙에 따른 유효성을 검증한다.

  • ex. 이메일 중복 여부, 사용자 권한 검증, 상태값 검증 등

  • 검증 실패 시 적절한 예외를 발생시켜 도메인 계층에 잘못된 요청이 전달되지 않도록 한다.

if (userRepository.existsByEmail(email)) {
	log.warn("유저 생성 실패: 이미 존재하는 이메일");
	throw UserAlreadyExistsException.byEmail(email);
}

3. Domain Layer (Entity)

  • 객체 자체가 지켜야 할 불변 조건을 검증한다.

  • 잘못된 상태의 객체 생성을 방지하기 위해, 주로 생성자나 정적 팩토리 메서드 내부에서 검증을 수행한다.

  • ex. 이메일은 null일 수 없음, 상태는 enum 범위 내여야 함 등

public User(String email, String name) {
    if (email == null || email.isBlank()) {
        throw new IllegalArgumentException("이메일은 필수입니다.");
    }
    this.email = email;
    this.name = name;
}

📌 왜 도메인(Entity)에서도 유효성 검증이 필요한가?
@Valid를 활용한 DTO 검증과 도메인 내부의 검증이 유사하게 느껴져 '중복 검사 아닌가?' 하는 의문이 들 수 있다.
하지만 이는 컨트롤러를 거치지 않고 객체가 직접 생성되거나 조작되는 경우에도 유효성을 보장하기 위한 안전 장치다. 즉, 생성 시점의 불변 조건 검증은 도메인의 책임으로, 이는 중복이 아닌 계층적 책임 분리로 이해해야 한다.

4. Persistence Layer (Repository, DB)

  • 데이터베이스 제약 조건을 통해 데이터 무결성을 보장하는 최종 방어선 역할을 한다.

  • 앞 계층에서 모든 유효성 검증을 수행했더라도, 동시성 이슈나 예외 상황 등으로 인해 잘못된 데이터가 저장될 수 있다.

  • 애플리케이션 코드 외부에서 발생하는 비정상 입력도 막을 수 있어야 한다.

  • 주요 제약 조건:

    • UNIQUE: 중복 방지
    • FOREIGN KEY: 참조 무결성 보장
    • NOT NULL: 필수 컬럼 누락 방지
    • CHECK: 값의 범위나 조건 제약

📌 계층별 검증 역할 요약

계층검증 목적예시 및 기술 방식
Presentation (Controller, DTO)사용자 입력 형식 검증@Valid, @NotBlank, @Email
Business (Service)비지니스 규칙 검증 (중복, 상태 등)이메일 중복, 권한 체크
Domain (Entity)객체의 불변 조건 보장생성자/팩토리 메서드에서 null, enum 등 검증
Persistence (DB)데이터 무결성 보장UNIQUE, NOT NULL, FOREIGN KEY, CHECK

✅ 중복 검증이란?

  • 동일한 검증 로직(ex. 이메일 형식 체크, 중복 여부 등)이 여러 계층에서 반복되는 현상이다.

  • 이는 코드 중복, 유지보수 비용 증가, 버그 유발 가능성으로 인해 지양해야 한다.

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

  • 각 계층의 책임에 따라 검증 로직을 명확히 분리한다.

  • 단, 예외 상황이나 동시성 이슈에 대비해 일부 검증은 의도적인 중첩 방어 전략으로 허용되어야 한다.

💡 의도적인 중첩 방어가 필요한 경우
ex. 이메일 중복
-> Service 단에서 중복 체크 + DB에서 UNIQUE 제약으로 최종 무결성 보장
-> 이는 단순한 중복이 아니라, 시스템 신뢰성과 무결성을 확보하기 위한 방어적 설계로 볼 수 있다.

🔄 트레이드오프

👍 장점

  • 계층 간 책임 분리로 인해 유지보수가 쉬워지고 테스트가 용이하다.

  • 검증 로직의 일관성, 재사용성, 가독성이 높아진다.

  • 중복을 줄여 코드 품질이 향상된다.

👎 단점

  • 계층간 책임이 명확하지 않으면 책임 누락/중복 가능성이 생길 수 있다.

  • 예외 상황에 대한 방어력이 낮아질 수 있다. (ex. 동시성, 외부 API 등)

➡️ 중복 검증이 불가피한 경우, "의도적인 중첩 방어"임을 명시하고, 주석이나 문서를 통해 그 목적을 기록해두어야 한다.

📌 트레이드 오프란?
어떤 것을 얻기 위해 다른 것을 포기해야 하는 상황을 말하며, 현실적인 선택을 위해서는 선택 간의 상충 관계를 명확히 파악하고 우선순위를 고려해야 한다.


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

✅ Mockito란?

  • 의존 객체를 가짜(Mock)로 대체하여 격리된 환경에서 테스트할 수 있도록 지원하는 Java 기반 테스트 프레임워크이다.

1. Mock

  • 실제 객체를 완전히 대체하는 모의 객체로, 원하는 행위를 사전에 설정할 수 있다.
  • 내부 로직은 실행되지 않으며 메서드 호출 여부, 횟수 등의 행위 자체를 검증하는 데 중점을 둔다.
@Test
void 파일업로드_호출확인() {
    // given (Mock 객체 생성)
    FileUploader mockUploader = Mockito.mock(FileUploader.class);

    // when
    mockUploader.upload("simple.txt");

    // then
    verify(mockUploader).upload("simple.txt"); // 호출 검증
}

💡 언제 사용하면 좋을까?

  • 내부 로직 대신 행위 기반 테스트가 필요한 경우
  • 외부 시스템 호출 여부 확인 (ex.이메일 전송)

2. Stub

  • 특정 메서드 호출에 대하여 예측 가능한 결과값을 미리 설정하는 것이다.
  • 주로 Mock + when().thenReturn() 형태로 사용된다.

@Test
void 파일업로드_성공() {
    // given (Mock 객체의 동작 정의)
    FileUploader stubUploader = Mockito.mock(FileUploader.class);
    when(stubUploader.upload(anyString())).thenReturn(true);

    // when
    boolean result = stubUploader.upload("simple.txt");

    // then
    assertTrue(result);
}

💡 언제 사용하면 좋을까?

  • 테스트 대상 로직 외의 의존 객체 동작을 단순화 하고 싶을 때 (ex. 외부 API)
  • 테스트 결과 제어가 필요할 때 (예외 상황 유도 등)

3. Spy

  • 실제 객체를 감싸서 사용하는 부분 모의 객체다.
  • 대부분의 메서드는 실제 동작을 유지하고, 일부 메서드만 내가 원하는 대로 조작할 수 있다.
  • 상태 기반 테스트행위 검증을 동시에 할 수 있다.

@Test
void 파일업로드_일부상황만_조작() {
    // given (실제/Spy 객체 생성)
    FileUploader realUploader = new FileUploader();
    FileUploader spyUploader = Mockito.spy(realUploader);

    // 메서드 동작 정의 (특정 상황만 업로드 실패)
    doReturn(false).when(spyUploader).upload("bad/file.txt");

    // when
    boolean normal = spyUploader.upload("good/file.txt"); // 실제 동작
    boolean blocked = spyUploader.upload("bad/file.txt"); // 가짜 동작

    // then
    assertTrue(normal);   // 실제 업로드
    assertFalse(blocked); // 강제로 실패 처리
    verify(spyUploader).upload("bad/file.txt");
}

💡 언제 사용하면 좋을까?

  • 실제 객체 기반 테스트를 하되, 특정 상황만 제어하고 싶은 경우
  • 상태 검증과 행위 검증을 동시에 하고 싶을 때

⚠️ 단, Spy는 실제 객체의 상태에 의존하므로, 복잡한 로직 테스트에서는 주의가 필요하다.


📌 Mock, Stub, Spy 차이점

  • Mock : 호출 유무와 횟수 등 행위 자체를 검증
    - ex. upload() 메서드를 누가 호출했는지 알고 싶어

  • Stub : 특정 입력에 대해 고정된 응답만 반환
    - ex. upload() 메서드를 호출하면 항상 "true"를 반환해줘

  • Spy : 실제 객체처럼 동작하지만, 일부 동작만 조작 가능
    - ex. bad.txt 파일을 업로드 할때만 "false"를 반환해줘

구분객체 생성 방식실제 동작 여부주로 사용하는 목적
Mockmock()❌ 실행 안 됨호출 여부 검증, 외부 API 대체
Stubmock() + when().thenReturn()❌ 실행 안 됨특정 입력 → 고정된 반환값 지정
Spyspy()⭕ 실제 동작 (일부 ❌)실제 동작 유지 + 일부 동작만 조작 가능

📄 참고 문서

profile
개발자로 성장하기

4개의 댓글

comment-user-thumbnail
2025년 6월 23일

알찬 내용 잘 보고 갑니다!👍🏻

답글 달기
comment-user-thumbnail
2025년 6월 23일

잘 읽었습니다! 도움이 많이 되었습니당

답글 달기
comment-user-thumbnail
2025년 6월 23일

잘 읽었쑵니당~~~!!!!

답글 달기
comment-user-thumbnail
2025년 6월 24일

좋은 글 잘 읽고 갑니다~

답글 달기