TIL

플러스 주차 개인과제 TIL 3.

phonebee 2025. 3. 12. 20:33

▶ LEVEL 1.

▷ 4. 테스트 코드 퀴즈 - 컨트롤러 테스트의 이해

@Test
void todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다() throws Exception {
    // given
    long todoId = 1L;

    // when
    when(todoService.getTodo(todoId))
            .thenThrow(new InvalidRequestException("Todo not found"));

    // then
    mockMvc.perform(get("/todos/{todoId}", todoId))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value(HttpStatus.OK.name()))
            .andExpect(jsonPath("$.code").value(HttpStatus.OK.value()))
            .andExpect(jsonPath("$.message").value("Todo not found"));
}

해당 테스트 코드를 실행했을 때 실패하고 있다.

문제는 테스트 코드가 정상적으로 수행되어 통과할 수 있도록 테스트 코드를 수정하는 것이다.

 

먼저, 문제의 테스트 코드를 실행보자

<오류 로그>

java.lang.AssertionError: Status expected:<200> but was:<400>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:59)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:122)
at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:637)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:214)
at org.example.expert.domain.todo.controller.TodoControllerTest.todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다(TodoControllerTest.java:72)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

 

로그를 살펴보면 예상한 값은 200인데 400이 나와 에러가 발생하고 있다.

즉, 400에러가 나타나야 했는데 200으로 정상 실행된 것이다.

오류가 나타나는 코드는

.andExpect(status().isOk())

해당 코드는 상태값 예상을 200으로 한 것이다.

즉, 테스트 코드가 의도에 맞게 정상작동하려면

.andExpect(status().isBadRequest())

로 고쳐야한다.

다만, 해당 부분을 고치고 실행을 시키면 다시 오류가 나타나는데

.andExpect(jsonPath("$.status").value(HttpStatus.OK.name()))
.andExpect(jsonPath("$.code").value(HttpStatus.OK.value()))
.andExpect(jsonPath("$.message").value("Todo not found"));

이는 밑에 있는 코드도 상태값을 OK(200)으로 받기 때문에

.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.name()))
.andExpect(jsonPath("$.code").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(jsonPath("$.message").value("Todo not found"));

이런 식으로 변경해야한다.

 

※ 왜 404(NOT FOUND)가 아닌 400(BAD REQUEST)가 나타나는 걸까?

코드를 보면 InvalidRequestException에 예외를 던지는데, GlobalExceptionHandler코드에 가면 커스텀한 InvalidRequestException이 있으며 해당 코드는 BAD REQUEST를 리턴하기 때문이다.

@ExceptionHandler(InvalidRequestException.class)
public ResponseEntity<Map<String, Object>> invalidRequestExceptionException(InvalidRequestException ex) {
    HttpStatus status = HttpStatus.BAD_REQUEST;
    return getErrorResponse(status, ex.getMessage());
}

 

▷ 5. 코드 개선 퀴즈 - AOP의 이해

● 문제의 요구사항

1. UserAdminController 클래스의 changeUserRole() 메소드가 실행 전 동작해야한다.

2. AdminAccessLoggingAspect 클래스에 있는 AOP가 개발 의도에 맞도록 코드를 수정해라

 

위의 두 요구사항을 해결하기 위해 우선 AdminAccessLoggingAspect 클래스를 살펴보자

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AdminAccessLoggingAspect {

    private final HttpServletRequest request;

    @After("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))")
    public void logAfterChangeUserRole(JoinPoint joinPoint) {
        String userId = String.valueOf(request.getAttribute("userId"));
        String requestUrl = request.getRequestURI();
        LocalDateTime requestTime = LocalDateTime.now();

        log.info("Admin Access Log - User ID: {}, Request Time: {}, Request URL: {}, Method: {}",
                userId, requestTime, requestUrl, joinPoint.getSignature().getName());
    }
}

먼저 첫번쨰 요구사항을 해결해보도록 하자

첫번째 요구사항은 changeUserRole()이 실행 전에 동작해야한다는 것이다.

코드를 살펴보면 위의 코드는 실행 후에 동작한다는 것을 알 수 있는데 이는 After어노테이션 때문이다.

After 어노테이션은 메서드 실행이 완료된 후 실행할 때 사용하며, 실행 후이기 때문에 예외가 발생하든 정상적으로 끝나든 무조건 실행하게 된다.

After 어노테이션과 반대되는 것이 Before 어노테이션인데

Before 어노테이션은 메서드 실행 전에 실행할 때 사용되며, 타겟 메서드의 실행 여부와 관계없이 동작한다.

즉, changeUserRole()이 실행되기 전에 동작해야하기 때문에 After 어노테이션을 Before 어노테이션으로 변경하면된다.

@Before("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))")
public void logBeforeChangeUserRole(JoinPoint joinPoint) {
    String userId = String.valueOf(request.getAttribute("userId"));
    String requestUrl = request.getRequestURI();
    LocalDateTime requestTime = LocalDateTime.now();

    log.info("Admin Access Log - User ID: {}, Request Time: {}, Request URL: {}, Method: {}",
            userId, requestTime, requestUrl, joinPoint.getSignature().getName());
}

 

이제 두번째 요구사항을 해결해보도록 하자

두번쨰 요구사항은 해당 클래스에 있는 AOP가 개발의도에 맞도록 코드를 수정하는 것이다.

해당 AOP는 관리자(Admin) 사용자의 특정 API 접근을 로깅한다.

그래서 API가 실행하기 전에 접근 로그를 남겨서 어떤 관리자가, 언제, 어떤 API를 호출했는지 기록한다.

그래서 해당 AOP를 어떻게 수정해야할까?

총 3가지 수정해야할 부분이 존재한다.

1. try-catch문 추가

: 예외가 발생하더라도 로그가 깨지지 않도록 try-catch문을 추가한다.

 

2. RequestContextHolder를 활용

: HttpServletRequest는 @Aspect에서 직접 주입할 때 스레드 세이프하지 않을 수 있기에 RequestContextHolder를 활용하여 HttpServletRequest를 가져오도록 한다.

 

3. Optional 사용

: 가져온 userId가 null일 가능성이 있으므로 Optional을 이용하여 처리한다.

 

수정해야할 부분을 추가하면

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AdminAccessLoggingAspect {

    @Before("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))")
    public void logBeforeChangeUserRole(JoinPoint joinPoint) {
        try{
            HttpServletRequest request=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            
            String userId= Optional.ofNullable(request.getAttribute("userId"))
                    .map(String::valueOf)
                    .orElse(null);

            String requestUrl = request.getRequestURI();
            LocalDateTime requestTime = LocalDateTime.now();

            log.info("Admin Access Log - User ID: {}, Request Time: {}, Request URL: {}, Method: {}",
                    userId, requestTime, requestUrl, joinPoint.getSignature().getName());
        }catch (Exception e){
            log.error("Failed to log admin access logging", e);
        }
    }
}

이런 식으로 수정하게 된다.

 

▶ LEVEL 2

▷ 6. JPA Cascade

● 문제의 요구사항

1. Cascade 기능을 활용하여 할 일을 생성한 유저가 담당자로 등록될 수 있게 제작해라

문제의 코드를 살펴보면

@OneToMany(mappedBy = "todo")
private List<Manager> managers = new ArrayList<>();

이런 식으로 작성되어 있다.

이를 Cascade를 활용하여 할 일을 생성한 유저가 담당자로 등록될 수 있게 제작해야한다.

먼저 Cascade가 뭔지 알아보자

Cascade는 부모 엔티티가 특정 작업을 수행할 때, 관련된 자식 엔티티에도 동일한 작업을 자동으로 적용하는 기능이다.

Cascade의 종류에는

CascadeType 설명
ALL 모든 작업(persist, merge, remove, refresh, detach)을 전이
PERSIST persist() 시 자식 엔티티도 함께 저장
MERGE merge() 시 자식 엔티티도 병합
REMOVE remove() 시 자식 엔티티도 삭제
REFRESH refresh() 시 자식 엔티티도 새로고침
DETACH detach() 시 자식 엔티티도 영속성 컨텍스트에서 분리

가 있다.

 

그럼 위의 조건을 충족하기 위해서 어떤 Cascade타입을 써야할까?

답은 Persist이다.

할 일을 생성하면 유저가 담당자로 등록하기 위해서는 같이 저장되어햐하기 때문이다.

@OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST)
private List<Manager> managers = new ArrayList<>();

 

※ 왜 All은 사용하지 않는가?

All도 사용을 해도 정상작동이 되지만, 모든 작업이 전이되기 때문에 원치 않는 상황에도 적용될 가능성이 있기 때문이다.

'TIL' 카테고리의 다른 글

플러스 주차 개인 과제 TIL 5.  (0) 2025.03.14
플러스 주차 개인과제 TIL 4.  (0) 2025.03.13
플러스 주차 개인과제 TIL 2.  (0) 2025.03.11
플러스 주차 개인과제 TIL 1.  (0) 2025.03.10
Spring 심화주차 개인 과제 TIL  (0) 2025.02.27