Round 1 회고

2025. 10. 31. 16:23·Loopers 2기

Loopers 2기 참여하여 첫주 과제진행 후 배운 점 및 느낌을 정리하고자 합니다.

 

 

종합적으로 전반적으로 새로운 아키텍처 구조에서 과제를 구현하며 새로운 테스트 스타일, 예외 처리 등을 배울 수 있는 과제였습니다.

 


1. 통합테스트, E2E 테스트 (Ent-to-End Test)

  • 인프런 강의를 통해 도메인, 서비스, 레포지토리, 컨트롤러 레이어로 레이어 테스트를 학습해봤고, 도메인 레이어의 단위테스트는 타 교육에서 많이 진행해왔기에 익숙했습니다
  • 하지만 통합테스트, E2E 테스트라는 이름으로 테스트를 진행한 것은 처음이라 통합테스트, E2E 테스트 작성에 대해서 학습할 수 있었습니다.
  • 통합테스트는 Facade 에서 진행하였고, 기존에 하던 Service 테스트와 유사하였습니다.
  • E2E 테스트는 기존의 컨트롤러 API 테스트와 비슷한데 API 마다 @Nested 어노테이션과 함께 inner class 로 테스트를 작성하는 방식을 배울 수 있었습니다.
    • 이렇게 하니 1개의 API 테스트에 대한 테스트들만을 작성하니 가독성이나 테스트 작성하는데에도 집중하기 좋았습니다.

통합테스트란?

 

통합 테스트는 애플리케이션의 여러 계층(Layer)이 함께 올바르게 작동하는지 검증하는 테스트입니다. 주로 Service Layer와 Repository Layer 간의 상호작용을 테스트합니다.

 

E2E 테스트란? 

 

E2E 테스트는 사용자 관점에서 HTTP API를 직접 호출하여 전체 플로우를 검증하는 테스트입니다. 실제 사용자가 API를 호출하는 것과 동일한 환경에서 테스트합니다.

 

✅ 통합테스트 예시

@DisplayName("회원 가입 시 Member 저장이 수행된다")
@Test
fun saveMemberOnJoin() {
    // Given
    val command = JoinMemberCommand("user1", "test@email.com", "1990-05-15", "MALE")

    // When
    memberFacade.joinMember(command)

    // Then
    verify(memberFacade).joinMember(any())
}

 

✅ E2E 테스트 예시

@DisplayName("POST /api/v1/users/join - 회원 가입")
@Nested
inner class Join {
    @DisplayName("회원 가입이 성공할 경우, 생성된 유저 정보를 응답으로 반환한다")
    @Test
    fun successfulJoin() {
        // Given: 회원 가입 요청 데이터
        val request = JoinMemberRequest(
        "testUser1",
        "test@gmail.com",
        "1990-05-15",
        "MALE"
        )
        // When: POST /api/v1/users/join 호출
        val responseType = object : ParameterizedTypeReference<ApiResponse<MemberV1Dto.MemberResponse>>() {}
        val response = testRestTemplate.exchange(
            "/api/v1/users/join",
            HttpMethod.POST,
            HttpEntity(request),
            responseType
        )

        // Then: 응답 검증
        assertAll(
            { assertThat(response.statusCode).isEqualTo(HttpStatus.OK) },
            { assertThat(response.body?.data?.memberId).isEqualTo("testUser1") },
            { assertThat(response.body?.data?.email).isEqualTo("test@gmail.com") },
            { assertThat(response.body?.data?.gender).isEqualTo("MALE") },
        )
    }
}

 

 

🧪 고민한 부분

 

1.  제공된 예제 코드에는 Service를 통합테스트의 대상으로 삼고 있었으나, Facade에서 통합테스트 하는 것이 다수의 Service를 활용하고 트랜잭션 제어에도 용이, 그리고 전체 로직을 테스트하기 위해 Facade에서 테스트 하기로 결정하였습니다. 

 

2.  대체적으로E2E 테스트가 API 호출을 다루다보니 코드양이 많아 자칫 한 클래스에 몰려서 테스트 작성 및 실행이 불편할 거 같아 

MemberController 하나에 들어갈 API중에 Point 관련한 Api 들을 분리해서 PointController를 추가하였습니다.

 


2. Spy 검증

Mock과 Spy의 차이를 정확히 몰라 비슷한건가 보다 생각했는데 정확하게 이해할 수 있었습니다. 

 

  1. Spy란?

     Spy는 실제 객체를 감싸서(wrapping) 실제 동작은 그대로 수행하면서, 메서드 호출을 추적할 수 있는 객체입니다.

 

   2. Mock vs Spy

  // Mock: 가짜 객체 (모든 동작이 stubbing 필요)
  @MockBean
  private lateinit var memberFacade: MemberFacade
  // memberFacade.joinMember()는 아무 동작도 하지 않음 (null 반환)

  // Spy: 실제 객체를 감싼 것 (실제 동작 수행 + 호출 추적)
  @MockitoSpyBean
  private lateinit var memberFacade: MemberFacade
  // memberFacade.joinMember()는 실제로 동작함 (DB 저장 등)

 

3. 코드 예시 

@MockitoSpyBean
private lateinit var memberFacade: MemberFacade

@DisplayName("회원 가입 시 Member 저장이 수행된다")
@Test
fun saveMemberOnJoin() {
    val command = JoinMemberCommand("user1", "test@email.com", "1990-05-15", "MALE")

    memberFacade.joinMember(command)

    verify(memberFacade).joinMember(command)
}

 

 


3. 예외 처리

 

 

 

위 2개의 과제 퀘스트를 처리하기 위해서는 

먼저 발생하는 예외를 확인해서 이를 처리하는 예외핸들러 메서드를 구현해주어야 한다. 

 

    @ExceptionHandler
    fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException): ResponseEntity<ApiResponse<*>> {
        val fieldErrors = e.bindingResult.fieldErrors
        val errorMessage = fieldErrors.joinToString(", ") { error ->
            "${error.field}: ${error.defaultMessage}"
        }

        log.warn("Validation failed: {}", errorMessage)
        return failureResponse(
            errorType = ErrorType.BAD_REQUEST,
            errorMessage = errorMessage
        )
    }

    @ExceptionHandler
    fun handleMissingRequestHeader(e: MissingRequestHeaderException): ResponseEntity<ApiResponse<*>> {
        val errorMessage = e.message
        log.warn("MissingRequestHeaderException: {}", errorMessage)
        return failureResponse(
            errorType = ErrorType.BAD_REQUEST,
            errorMessage = errorMessage
        )
    }
  • 성별 값이 없는 경우에는 MethodArgumentNotValidException 이 발생
  • RequestHeader 어노테이션이 달린 값이 헤더값이 부재한 경우에는 MissingRequestHeaderException 발생

MethodArgumentNotValidException, MissingRequestHeaderException 를 처리하는 메서드 구현하면 이를 잡아 처리함 

 

 

'Loopers 2기' 카테고리의 다른 글

쿠폰 중복 사용 버그, 비관적 락 한 줄로 해결하기  (0) 2025.11.21
재고 차감, 검증은 몇 번 해야 할까?  (0) 2025.11.14
Round 2 회고  (0) 2025.11.07
'Loopers 2기' 카테고리의 다른 글
  • 쿠폰 중복 사용 버그, 비관적 락 한 줄로 해결하기
  • 재고 차감, 검증은 몇 번 해야 할까?
  • Round 2 회고
고구마와 감자
고구마와 감자
Amor DevFati는 김연자-Amor Fati에 Development(개발)의 Dev 를 첨가하여 만든 이름
  • 고구마와 감자
    Amor DevFati(아모르 개발파티)
    고구마와 감자
  • 전체
    오늘
    어제
    • 분류 전체보기 (156)
      • Loopers 2기 (4)
      • 스프링 (5)
      • 알고리즘 (113)
        • 백준 (70)
        • 프로그래머스 (7)
        • 인프런_자바코테강의 (20)
        • 리트코드 (5)
        • 해커랭크 (0)
        • 코드업 (3)
        • 이것저것 (7)
      • 자바 (7)
      • GIT (0)
      • 파이썬 (1)
      • 개발이론 (4)
      • JPA (0)
      • 김영한 강의 (13)
        • 모든 개발자를 위한 HTTP 웹 기본 지식 (2)
        • 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (6)
        • 스프링 핵심 원리 - 기본편 (5)
      • 일기 및 아무말 적기 (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    2의 제곱인가
    스프링 핵심 원리
    홀수일까 짝수일까
    1247
    더하기 3
    3059
    전투 드로이드 가격
    16673
    등장하지 않는 문자의 합
    2921
    10409
    4458
    할로윈의 사탕
    2857
    카이사르 암호
    1598
    11966
    10178
    5361
    11023
    백준
    5988
    꼬리를 무는 숫자 나열
    14656
    고려대학교에는 공식 와인이 있다
    남욱이의 닭장
    Mini Fantasy War
    그대로출력하기2
    조교는 새디스트야!!
    첫 글자를 대문자로
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
고구마와 감자
Round 1 회고
상단으로

티스토리툴바