카테고리 없음

[Nbcam] Level6 정의하고 해결한 문제

limitation01 2025. 12. 5. 00:14

1. ManagerController delete 메소드의 인증된유저 처리 문제

문제

기존 deleteManager 메서드 에서는

  • 컨트롤러가 JWT 구조를 알고있음
  • Authorization Header를 직접 읽고 jwtUtil을 직접 사용 
@DeleteMapping
public void deleteManager(
    @RequestHeader("Authorization") String bearerToken,
    @PathVariable long todoId,
    @PathVariable long managerId
) {
    Claims claims = jwtUtil.extractClaims(...);
    long userId = Long.parseLong(claims.getSubject());
}

Controller가 JWT를 알아야 해서 인증 책임이 컨트롤러단 까지 내려왔다고 생각했다.

해결

인증된 사용자가 Todo의 담당자를 삭제한다는 의미에 맞게 인증된 유저(AuthUser)만 전달 받는 구조가 맞다고 판단했다.

결과

@DeleteMapping("/todos/{todoId}/managers/{managerId}")
    public void deleteManager(
            @Auth AuthUser authUser,
            @PathVariable long todoId,
            @PathVariable long managerId
    ) {
        managerService.deleteManager(authUser, todoId, managerId);
    }

 

  • 컨트롤러에서 JWT 의존 제거
  • 인증 책임을 인증 계층으로 위임

 

2. CommentAdminService delete 메소드 검증 누락 문제

문제

deleteComment 메서드에서 댓글 존재 여부나 삭제 가능 여부에 대한 검증 없이 바로 deleteById를 호출했다. 존재하지 않는 ID 삭제할경우 삭제가 실패했는데, 실패했다는 사실을 아무도 모르는 상태가 된다.

해결

서비스단에서 도메인 행위의 유효성을 검증하고 실패 케이스에 대한 예외처리를 해주는것이 서비스 레이어의 책임이라고 생각했다.

@Transactional
public void deleteComment(long commentId) {
   commentRepository.deleteById(commentId);
}

결과

@Transactional
public void deleteComment(long commentId) {
   Comment comment = commentRepository.findById(commentId)
          .orElseThrow(() -> new InvalidRequestException("댓글이 존재하지 않습니다."));
   commentRepository.delete(comment);
}

 

 


3. 페이지네이션 책임 위치 개선

문제

기존코드는 디폴트 페이지 값이 사용자 관점에서 1로 지정되어있고 서비스단에서 page - 1 보정 로직을 수행했다. 그럼으로써 서비스가 사용자 요청 포맷을 알고 있고 page 보정에 대한 책임이 하위 레이어까지 내려왔다고 판단했다.

    // controller
    @GetMapping("/todos")
    public ResponseEntity<Page<TodoResponse>> getTodos(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size
    ) {
        return ResponseEntity.ok(todoService.getTodos(page, size));
    }
    
    //service
    @Transactional(readOnly = true)
    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()
        ));
    }

해결

  • spring은 pageable 항상 0-base로 처리, 우리가 사용자 관점으로 맞출 필요 없다
  • 페이징 디폴트 값 처리는 컨트롤러 책임

결과

	//controller
    @GetMapping("/todos")
    public ResponseEntity<Page<TodoResponse>> getTodos(
            @PageableDefault(page = 0, size = 10, sort = "modifiedAt", direction = Sort.Direction.DESC)
            Pageable pageable
    ) {
        return ResponseEntity.ok(todoService.getTodos(pageable));
    }
    
    //service
    @Transactional(readOnly = true)
    public Page<TodoResponse> getTodos(Pageable pageable) {
        Page<Todo> todos = todoRepository.findAll(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()
        ));
    }

 


4. Signup Response DTO 설계 문제

문제

회원가입,로그인 응답이 다음과 같이 토큰만 반환하고 있었다. 

{
    "bearerToken": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyIiwiZW1haWwiOiJ6aXkwaW5nMUBuYW1lci5jb20iLCJ1c2VyUm9sZSI6IlVTRVIiLCJleHAiOjE3NjQ3NDYyNjIsImlhdCI6MTc2NDc0MjY2Mn0.cbI5KQxPWg9oAB1PTff5EkM8evwPFNEofeK9BGzBfuY"
}

해결

signup은 계정 생성, signin은 인증 및 토큰 발급의 책임으로 각각 응답의 구조가 달라야 한다고 생각했다. 바뀌어야 할 곳은 책임에 따라 signup이라고 판단했다.

토큰 발급은 signin 역할로 분리했다.

결과

{
    "id": 1,
    "email": "abab@example.com"
}