[내배캠/41일차] TIL - Bean 생명주기, 예외처리, 코드 개선 과제

euphony·2025년 2월 24일
0

내일배움캠프

목록 보기
55/66

✅오늘의 한 일

  • Spring 심화 강의 완강
  • 과제 필수 기능 구현

💻오늘의 학습

심화 Spring 3주차

Bean 생명주기

Bean의 생명주기 동안 특정 작업을 실행하기 위해 사용하는 Annotation이다. Bean의 초기화와 소멸 시점에 호출되는 메서드를 지정할 때 사용한다.

  • @PostConstruct : Bean 생성 & 의존성 주입이 완료된 후 호출되는 메서드 지정, 초기화 작업 시 사용
  • @PreDestroy : Bean 소멸 직전 호출되는 메서드 지정

예외처리

  • @ExceptionHandler : 특정 컨트롤러 내에서 발생한 예외 처리 Annotation. 예외 발생 시 설정된 메서드가 호출되어 예외 처리.
  • @RestControllerAdvice : REST API에서 발생한 예외를 JSON 형식으로 처리하는 Annotation. 모든 컨트롤러에 전역으로 적용.

Spring 심화 주차 개인 과제

Lv.1 코드 개선

Early return과 불필요한 if-else 제거를 빠르게 끝내고 Validation까지 완료했다. 아래 코드 부분을 해당 API의 요청 DTO에서 처리할 수 있게 개선하는 문제다.

if (userChangePasswordRequest.getNewPassword().length() < 8 ||
        !userChangePasswordRequest.getNewPassword().matches(".*\\d.*") ||
        !userChangePasswordRequest.getNewPassword().matches(".*[A-Z].*")) {
    throw new InvalidRequestException("새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다.");
}

그동안 과제에서 했던 내용이어서 바로 @size@Pattern을 사용해 다음과 같이 작성했다. 그런데 @Pattern을 하나로 합치는게 나을 것 같았다. 새 비밀번호는 숫자와 대문자를 포함해야 합니다. 라는 메세지도 어디에 넣어야 할지 애매했다.

@NotBlank
@Size(min =  8, message = "새 비밀번호는 8자 이상이어야 합니다.")
@Pattern(regexp = ".*\\d.*")
@Pattern(regexp = ".*[A-Z].*")
private String newPassword;

그래서 정규표현식을 검색해 수정 후 다음과 같이 한 번에 넣고 메세지도 같이 넣었다. 사실 어느 방법이 맞는지 잘 몰라서 한 번 더 확인이 필요할 것 같다. 다른 방향으로 생각하면 오류 메세지를 세세하게 나누면 좋지 않을까? 싶어서 조금 헷갈리는 부분이다.

@NotBlank
@Size(min =  8, message = "새 비밀번호는 8자 이상이어야 합니다.")
@Pattern(regexp = "^(?=.*\\d)(?=.*[A-Z]).*$", message = "새 비밀번호는 숫자와 대문자를 포함해야 합니다.")
private String newPassword;

Lv.2 N+1 문제

다음 getTodos() 메서드에서 발생하고 있는 N+1 문제를 @EntityGraph를 사용해 해결해야 한다.

public Page<TodoResponse> getTodos(int page, int size) {
    Pageable pageable = PageRequest.of(page - 1, size);

    Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);

    return todos.map(todo -> new TodoResponse(
            todo.getId(),
            todo.getTitle(),
            todo.getContents(),
            todo.getWeather(),
            new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()),
            todo.getCreatedAt(),
            todo.getModifiedAt()
    ));
}

위 코드에서 할일을 모두 불러온 후, 그 아래 유저를 또 다시 불러오는 과정에서 N+1 문제가 발생할 가능성이 높아보인다.

Todo 엔티티의 일부를 살펴보면 User@ManyToOne 다대일 연관관계를 맺고 있고, 지연 로딩(FetchType.LAZY)으로 설정되어 있다. 지연 로딩으로 설정하면 Todo를 조회할 때 User를 바로 가져오는 것이 아니라 프록시 객체로 있다가 실제 사용될 때 SELECT 쿼리가 실행되는 것이다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

먼저 findAllByOrderByModifiedAtDesc() 메서드를 통해 다음과 같이 모든 todo를 불러오고 있다.

Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);

이때 다음과 같은 쿼리가 실행될 것이다.

SELECT * FROM todo ORDER BY modified_at DESC LIMIT ?, ?

그리고 다음 코드에서 todo.getUser()를 하면 User에 대한 SELECT 추가 쿼리가 실행된다. 즉 todo가 100개라면 위에서 1개의 쿼리를 실행하고, 아래에서 100개의 쿼리가 또 발생하게 되는 것이다.

todos.map(todo -> new TodoResponse(
        todo.getId(),
        todo.getTitle(),
        todo.getContents(),
        todo.getWeather(),
        new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()), // N+1 문제 발생 가능
        todo.getCreatedAt(),
        todo.getModifiedAt()
));

Repository에서는 이미 다음과 같이 fetch join을 사용해 N+1 문제를 해결했다.

@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

여기서 @EntityGraph라는 어노테이션을 활용해 해결해보자. @EntityGraph(attributePaths = {"user"})와 같이 적어주면 User도 함께 조회하도록 설정할 수 있다.

@EntityGraph(attributePaths = {"user"})
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

💭오늘의 궁금한 점

  • @EntityGraph 로 N+1문제를 해결할 때, 예제와 같이 정렬이 필요한 경우는 따로 쿼리를 작성해야하는지, 아니면 JPA 메서드명을 통해 정렬을 해야 하는지 궁금하다.
    • 쿼리를 따로 적는 것보다 JPA 메서드명을 사용하는 것이 괜찮을 것 같다고 하셨다.

📝오늘의 회고

제일 집중이 안되는 월요일..! 잠이 너무 쏟아져서 미칠 것 같다.😴 정신 차리고 내일 강의랑 과제 다 끝내버리고 질문 많이 모아서 해야지.

📌내일의 할 일

  • 수준별 학습반 강의 듣기
  • 과제 도전 기능 구현
  • 튜터님께 질문📜

0개의 댓글

관련 채용 정보