본문 바로가기
내일배움캠프/내일배움캠프

[내일배움캠프] 심화 과제 진행해보기 Lv. 1

by TIP__ 2024. 11. 13.

플러스 주차 개인 과제를 진행하며 기록을 남겨보기 위해 글을 작성합니다.

이번 과제는 레거시 코드를 리펙토링해보는 과제입니다.


레거시 코드

-. 레거시 코드는 개발된 지 오래되어서 최신 기술이나 표준과는 다소 맞지 않는 코드를 뜻합니다.

레거시 코드를 리펙토링해보면

-. 레거시 코드를 리펙토링 하기 위해서는 먼저 코드를 확실히 이해하고있어야 하기 때문에 리펙토링을 해보며 코드를 이해하고 분석하는 능력을 키울 수 있습니다.


Lv 1

1. 코드 개선 퀴즈 - @Transaction의 이해

할 일 저장 기능을 구현한 API(/todos)를 호출할 때, 아래와 같은 에러가 발생하고 있어요.

jakarta.servlet.ServletException: Request processing failed: org.springframework.orm.jpa.JpaSystemException: could not execute statement [Connection is read-only. Queries leading to data modification are not allowed] [insert into todos (contents,created_at,modified_at,title,user_id,weather) values (?,?,?,?,?,?)]

에러가 발생하지 않고 정상적으로 할 일을 저장할 수 있도록 코드를 수정해주세요.

* /todos를 호출할 때 에러가 발생한다고 했는데 저는 해당 에러가 발생하지 않고 정상적으로 저장이 되긴 했습니다;;

에러 고찰

"Connection is read-only. Queries leading to data modification are not allowed"
: 현재 데이터베이스 연결이 읽기전용(read-only) 설정되어있기 때문에 데이터 변경이 허용되지 않습니다.
: 즉, 읽기 전용 설정이 되어있기 때문에 일정을 작성했을 때 저장하는 부분에서 오류가 발생하는 것으로 추정됩니다.

문제 해결 과정

데이터를 입력하는 코드에 @Transactional을 붙여 데이터를 수정할 수 있도록 변경하였습니다.


2. 코드 추가 퀴즈 - JWT의 이해

기획자의 긴급 요청이 왔어요!
아래의 요구사항에 맞춰 기획 요건에 대응할 수 있는 코드를 작성해주세요.

  • User의 정보에 nickname이 필요해졌어요.
    • User 테이블에 nickname 컬럼을 추가해주세요.
    • nickname은 중복 가능합니다.
  • 프론트엔드 개발자가 JWT에서 유저의 닉네임을 꺼내 화면에 보여주길 원하고 있어요.

문제 해결 과정

1. User 테이블에 nickname 컬럼 추가

private String nickname;

 

2. JwtUtil 에서 토큰에 닉네임 정보 추가

Jwts.builder()
    .setSubject(String.valueOf(userId))
    .claim("email", email)
    .claim("userRole", userRole)
    // 닉네임 정보 추가
    .claim("nickname", nickname)
    .setExpiration(new Date(date.getTime() + TOKEN_TIME))
    .setIssuedAt(date) // 발급일
    .signWith(key, signatureAlgorithm) // 암호화 알고리즘
    .compact();

 

3. JwtFilter 에서 닉네임 정보를 가져올 수 있도록 수정

UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class));
// 닉네임 가져오기
String nickname = claims.get("nickname", String.class);

httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
httpRequest.setAttribute("email", claims.get("email"));
httpRequest.setAttribute("userRole", claims.get("userRole"));
// 닉네임 추가
httpRequest.setAttribute("nickname", nickname);

 

4. AuthUserArgumentResolver 에서 JwtFilter에서 set한 정보를 가져올 때 nickname 정보 추가

// JwtFilter 에서 set 한 userId, email, userRole, nickname 값을 가져옴
Long userId = (Long) request.getAttribute("userId");
String email = (String) request.getAttribute("email");
UserRole userRole = UserRole.of((String) request.getAttribute("userRole"));
String nickname = (String) request.getAttribute("nickname");

return new AuthUser(userId, email, userRole, nickname);

 

5. 그 외 데이터를 반환할 때 nickname 정보를 추가


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

AOP가 잘못 동작하고 있어요!
-. UserAdminController 클래스의 changeUserRole() 메소드가 실행 전 동작해야 해요.
-. AdminAccessLoggingAspect 클래스에 있는 AOP가 개발 의도에 맞도록 코드를 수정해주세요.

문제 해결 과정

@After Annotation을 @Before Annotation으로 변경

메서드명 또한 After를 Before로 수정하였습니다.

더불어 pointcutUserAdminController.changeUserRole으로 변경하였습니다.

@Before("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))")
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());
    }

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

테스트 패키지 org.example.expert.domain.todo.controllertodo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다() 테스트가 실패하고 있어요.
테스트가 정상적으로 수행되어 통과할 수 있도록 테스트 코드를 수정해주세요.

문제

400 BadRequest를 반환해야 하는데 200 ok를 반환하고 있었습니다.

문제 해결 과정

isOk() > isBadRequest() 변경
.OK > .BED_REQUEST 변경

mockMvc.perform(get("/todos/{todoId}", todoId))
    // 테스트 코드 퀴즈 - 컨트롤러 테스트의 이해
    // 400 BadRequest 를 반환해야 하는데 200 ok를 반환
    // ok > BadRequest 변경
    .andExpect(status().isBadRequest())
    .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.name()))
    .andExpect(jsonPath("$.code").value(HttpStatus.BAD_REQUEST.value()))
    .andExpect(jsonPath("$.message").value("Todo not found"));

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

기획자의 긴급 요청이 왔어요!
아래의 요구사항에 맞춰 기획 요건에 대응할 수 있는 코드를 작성해주세요.

  • 할 일 검색 시 weather 조건으로도 검색할 수 있어야해요.
    • weather 조건은 있을 수도 있고, 없을 수도 있어요!
  • 할 일 검색 시 수정일 기준으로 기간 검색이 가능해야해요.
    • 기간의 시작과 끝 조건은 있을 수도 있고, 없을 수도 있어요!
  • JPQL을 사용하고, 쿼리 메소드명은 자유롭게 지정하되 너무 길지 않게 해주세요.

-. 필요할 시, 서비스 단에서 if문을 사용해 여러 개의 쿼리(JPQL)를 사용하셔도 좋습니다.

문제 해결 과정

1. TodoController의 getTodosRequestParam(required = false)으로 weather, startDate, endDate를 받아오기
-. startDateendDateDATE형식인지 확인하고 형식이 잘못된 경우 에러 메세지반환

TodoController

@GetMapping("/todos")
public ResponseEntity<Page<TodoResponse>> getTodos(
        @RequestParam(required = false) String weather,
        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
        @RequestParam(defaultValue = "1") int page,
        @RequestParam(defaultValue = "10") int size
) {
    return ResponseEntity.ok(todoService.getTodos(weather, startDate, endDate, page, size));
}

 

GlobalExceptionHandler

// 입력된 날짜 형식이 옳바르지 않은 경우 예외처리
@ExceptionHandler(DateTimeParseException.class)
public ResponseEntity<Map<String, Object>> handleDateTimeParseException(DateTimeParseException ex) {
    HttpStatus status = HttpStatus.BAD_REQUEST;
    return getErrorResponse(status, "입력된 날짜의 형식이 잘못되었습니다. 2000-01-01 형식으로 입력해주세요.");
}

 

2. TodoService의 getTodos에 해당 조건의 로직을 작성
-. 유효성 검사를 통한 예외처리

TodoService

public Page<TodoResponse> getTodos(String weather, LocalDate startDate, LocalDate endDate, int page, int size) {

    // 예외처리
    // 기간조건 중 시작일이나 종료일 둘 중 하나라도 입력된 경우 예외처리 로직 실행
    if(startDate != null || endDate != null) {
        validateGetTodo(startDate, endDate);
    }

    Pageable pageable = PageRequest.of(page - 1, size);

    Page<Todo> todos;

    // weather 조건과 기간 조건이 있는 경우
    if(weather != null && startDate != null && endDate != null) {
        LocalDateTime startDateTime = convertToStartDateTime(startDate);
        LocalDateTime endDateTime = convertToEndDateTime(endDate);
        todos = todoRepository.findByWeatherAndDate(weather, startDateTime, endDateTime, pageable);
    // weather 조건만 있는 경우
    } else if (weather != null) {
        todos = todoRepository.findByWeatherOrOrderByModifiedAt(weather, pageable);
    // 기간 조건만 있는 경우
    } else if (startDate != null && endDate != null) {
        LocalDateTime startDateTime = convertToStartDateTime(startDate);
        LocalDateTime endDateTime = convertToEndDateTime(endDate);
        todos = todoRepository.findByDateOrderByModifiedAt(startDateTime, endDateTime, pageable);
    // 아무 조건도 없는 경우 전체 조회
    } else {
        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.getUser().getNickname()),
            todo.getCreatedAt(),
            todo.getModifiedAt()
    ));
}

// 예외처리
public void validateGetTodo( LocalDate startDate, LocalDate endDate) {
    // weather 입력값이 옳지 않은 경우 > weather 입력값을 전부 알 수가 없으므로 제외
    // startDate, endDate 가 LocalDate 형식이 아닌 경우 -> 컨트롤러에서 처리
    // startDate 또는 endDate 가 없는 경우
    if (startDate == null || endDate == null) {
        throw new InvalidRequestException("입력되지 않은 날짜가 존재합니다.");
    }
    // startDate 보다 endDate 가 빠른 날짜인 경우
    // ex) startDate : 2024-11-15, endDate : 2024-11-14
    if (startDate.isAfter(endDate)) {
        throw new InvalidRequestException("종료날짜가 시작날짜보다 빠를 수 없습니다.");
    }
}

// LocalDate 형식의 날짜 데이터를 LocalDateTime 형식으로 변환
public LocalDateTime convertToStartDateTime (LocalDate startDate) {
    LocalDateTime startDateTime = startDate.atStartOfDay();
    return startDateTime;
}
public LocalDateTime convertToEndDateTime (LocalDate endDate) {
    LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX);
    return endDateTime;
} 

 

3. TodoRepository에 해당 조건의 로직을 작성

TodoRepository

// weather 조건과 기간 조건이 모두 있는 경우
@Query("select t from Todo t left join fetch t.user " +
        "where t.weather = :weather " +
        "and t.modifiedAt between :startDate and :endDate " +
        "order by t.modifiedAt desc")
Page<Todo> findByWeatherAndDate(@Param("weather") String weather,
                                @Param("startDate") LocalDateTime startDate,
                                @Param("endDate") LocalDateTime endDate,
                                Pageable pageable);
// weather  조건만 있는 경우
@Query("select t from Todo t left join fetch t.user " +
        "where t.weather = :weather " +
        "order by t.modifiedAt desc")
Page<Todo> findByWeatherOrOrderByModifiedAt(@Param("weather") String weather,
                                            Pageable pageable);
// 기간 조건만 있는 경우
@Query("select t from Todo t left join fetch t.user " +
        "where t.modifiedAt between :startDate and :endDate " +
        "order by t.modifiedAt desc")
Page<Todo> findByDateOrderByModifiedAt(@Param("startDate") LocalDateTime startDate,
                                       @Param("endDate") LocalDateTime endDate,
                                       Pageable pageable);
// 아무 조건도 없는 경우 전체 조회
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

댓글