플러스 주차 개인 과제를 진행하며 기록을 남겨보기 위해 글을 작성합니다.
이번 과제는 레거시 코드를 리펙토링해보는 과제입니다.
레거시 코드
-. 레거시 코드는 개발된 지 오래되어서 최신 기술이나 표준과는 다소 맞지 않는 코드를 뜻합니다.
레거시 코드를 리펙토링해보면
-. 레거시 코드를 리펙토링 하기 위해서는 먼저 코드를 확실히 이해하고있어야 하기 때문에 리펙토링을 해보며 코드를 이해하고 분석하는 능력을 키울 수 있습니다.
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로 수정하였습니다.
더불어 pointcut
을 UserAdminController.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.controller
의 todo_단건_조회_시_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의 getTodos
에 RequestParam(required = false)
으로 weather
, startDate
, endDate
를 받아오기
-. startDate
와 endDate
가 DATE
형식인지 확인하고 형식이 잘못된 경우 에러 메세지반환
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);
'내일배움캠프 > 내일배움캠프' 카테고리의 다른 글
[내일배움캠프] TIL : Transaction (0) | 2024.11.13 |
---|---|
[내일배움캠프] TIL : Projection (0) | 2024.11.13 |
[내일배움캠프] H2 DB (0) | 2024.11.12 |
[내일배움캠프] 트러블 슈팅 TIL (0) | 2024.11.06 |
[내일배움캠프] 2024. 11. 01. TIL (0) | 2024.11.01 |
댓글