인프런 커뮤니티 질문&답변

InJun Choi님의 프로필 이미지

작성한 질문수

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발

유효성 검증 위치 관련 질문

22.12.08 22:29 작성

·

465

·

수정됨

1

안녕하세요. 로드맵 수강하고 토이 프로젝트 진행 중인데 궁금한 점이 계속 나오네요..

일반적으로 사용하는 표현 - 비즈니스 - 도메인 계층 구조에서 비즈니스 정책을 검증하는 위치와 관련해서 질문 드립니다.

예를 들어 사용자용 게시글 삭제 API는 현재 요청 사용자가 게시글의 작성자가 맞는지 검사합니다. 이 검증 작업을 서비스 계층에서 처리했습니다.

public class PostService {
  private final PostRepository postRepository;

  // 게시글 삭제 기능
  public void deletePost(Long postId, Long currentMemberId) {
    // 엔티티 조회 후 게시글 작성자가 현재 요청 사용자( currentMemberId )와 일치하는지 검사
    Post post = postRepository.findById(postId);
    if (!post.getWriter().getId().equals(currentMemberId)) {
      // 작성자가 아니면 예외 발생
    }
    post.delete();
  }
}

이 메서드를 사용자 API 컨트롤러에서는 잘 사용했는데, 관리자 API는 게시글 작성자 검증이 필요 없다는게 문제였습니다. 사용자 및 관리자용 API 컨트롤러에서 호출하는 것은 결국 '게시글 삭제'라는 동일한 기능이므로 하나의 서비스 클래스에서 제공하는 게 맞다 판단하여 deletePost() 메서드에서 수행하던 유효성 검증 로직을 사용자 API 컨트롤러로 이관했습니다.

public class PostController {
  private final PostService postService;

  // 실제 코드는 아니며 설명용 코드입니다.
  // 예를 들어 currentMemberId의 경우 실제론 @AuthenticationPrincipal 등을 통해 얻습니다.
  @DeleteMapping("/{postId}")
  public void delete(Long currentMemberId, @PathVariable Long postId) {
    Post post = postService.findPost(postId);
    if (!post.getWriter().getId().equals(currentMemberId)) {
      // 작성자가 아니면 예외 발생
    }
    postService.deletePost(postId);
  }
}

이렇게 구성하니 PostService를 사용자 API 컨트롤러에서도, 관리자 API 컨트롤러에서도 사용 가능했지만.. 표현 계층에서 비즈니스 정책을 검증하니 구조가 이상한 것 같아서 질문 드립니다.

  1. '게시글을 삭제하려는 사용자가 게시글의 작성자인가?'를 확인하는 작업은 비즈니스 정책에 의한 작업이라 생각합니다. 그러므로 서비스 계층에서 검사하는 게 맞다 생각되는데, 관리자 API에서 사용하기 불편하더라구요. 이런 경우 구조를 어떻게 가져가시나요?

    PostService는 비즈니스 로직만 처리하고, PostUserService 같은 래퍼 서비스를 만들어 검증 부분을 처리할까 했는데, 썩.. 맘에 들진 않았습니다.

  2. 예시처럼 정책 검증을 컨트롤러에서 수행한다 가정하면, 제 코드의 경우 PostService.findPost() 메서드에 @Transactional(readOnly=true)를 적용한 상태라 deletePost() 메서드에서 SELECT 쿼리를 또 실행합니다.

    @Transactional(readOnly=true)은 정말 필요한 곳에만 선택적으로 적용하시나요? 아니면 읽기 전용 메서드는 별도로 구분하시나요?

  3. '서비스는 최대한 비즈니스 로직만 수행하고, 정책 유효성 검증은 다른 위치에서 진행하는 것이 좋다'라는 분들도 있던데, 보통 어떤 식으로 처리하시나요?

답변 1

2

David님의 프로필 이미지

2022. 12. 09. 10:46

안녕하세요. InJun Choi님, 공식 서포터즈 David입니다.

  1. "게시글을 삭제하려는 사용자가 게시글의 작성자인가?"가 정책이라면 해당 정책이 관리자를 뺀 사용자에 대한 검증만 포함하고 있고 그 정책을 그대로 구현하다보니 관리자에 대한 부분을 처리하기 어려운 것 같습니다. 애초에 정책 단계에 관리자에 대한 것도 포함 시켜야 할 것 같습니다. 그러면 자연스럽게 관리자 식별에 대한 부분도 고민하게 되고 하다못해 관리자를 식별할 수 있는 플래그를 넘겨 받는 파라미터라도 메서드 시그니처에 추가하게 될 수 있을 것 같습니다. 사용자, 관리자 구분의 경우 3번 내용을 참고해주세요.

  2. 선택적으로 사용하다가 읽기 전용 메서드가 늘어나면 CQS 패턴을 적용해서 읽기 전용 메서드들을 모아둔 xxxQueryService를 별도로 구성할 수 있습니다.

  3. 여러가지 방법이 있겠지만 단순 사용자 검증의 경우 필터나 인터셉터 또는 HandlerMethodArgumentResolver를 사용해서 처리하는 경우가 많고 검증 책임을 별도의 서비스로 분리하여 처리할 수도 있습니다.

감사합니다.

InJun Choi님의 프로필 이미지
InJun Choi
질문자

2022. 12. 09. 16:11

David님 답변 감사합니다.

현재 스프링 시큐리티를 이용해 관리자 권한을 가진 경우에만 관리자 API( /api/admin/** ) 호출을 허용 중입니다. 즉, '관리자 핸들러 메서드 호출 성공 = 관리자 권한이 있다'라고 이미 확인된 상태이다 보니 서비스 클래스에서 다시 검증하는 게 꺼려졌던 것 같습니다.

  1. 아무래도 권한에 따른 정책이니 표현 계층에서 검증하지 말고, 권한 정보를 비즈니스 계층으로 넘기는 게 맞을까요?

  2. 제 예시 말고도 권한에 따라 동작이 바뀌는 경우가 꽤 있을 것 같습니다. 매번 서비스에서 관련 정보를 메서드 파라미터로 받아야 할까요? 아니면 다른 방법이 있을까요? 일반적으로 권한에 따른 처리를 어떤 식으로 구성하는지 알고 싶습니다.

David님의 프로필 이미지

2022. 12. 09. 17:46

  1. 네, 저는 서비스에서 진행할 것 같습니다.

  2. 일반적으로는 어떤지는 잘 모르겠습니다. 시큐리티를 사용중이시라면 @PreAuthorize를 사용할 것 같고, 아니어도 비슷하게 구현하여 처리할 것 같습니다.