개인 프로젝트를 진행하며 고민한 문제가 있다.
현재 프로젝트에서 게시판에 작성된 글이나 댓글을 update하거나 delete할 때, 작성한 본인 혹은 관리자만 삭제할 수 있게 만들고 싶었다.
1. Service Layer
// PostService.class
@Service
@RequiredArgsConstructor
public class PostService {
private final UserRepository userRepository;
private final PostRepository postRepository;
// Service code...
@Transactional
public Long update(String email, Long id, PostUpdateRequestDto updateDto) {
User user = userRepository.findByEmail(email)
.orElseThrow(EntityNotFoundException::new);
Post post = postRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
if (isPostWriter(user, post) || isAdmin(user)) {
post.update(updateDto.getTitle(), updateDto.getContent());
return id;
}
return -1L;
}
@Transactional
public Long delete(String email, Long id) {
User user = userRepository.findByEmail(email)
.orElseThrow(EntityNotFoundException::new);
Post post = postRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
if (isPostWriter(user, post) || isAdmin(user)) {
post.updateStatusToDisable();
return id;
}
return -1L;
}
// Service code...
private Boolean isPostWriter(User user, Post post) {
return user.getEmail().equals(post.getUser().getEmail());
}
}
// UserRoleChecker.class
public class UserRoleChecker {
public static boolean isAdmin(User user) {
return user.getRole().equals(User.Role.ADMIN);
}
}
맨 처음 본인 확인을 Service Layer에 구현하였다. 위 코드와 같이 직관적이고 구현하기도 쉬웠다.
Service Layer에 구현했던 이유는 간단했다. 작성자가 본인인지 확인하려면 user와 post 모두 Controller Layer에서 전달받은 값에 기반하여 DB 조회를 해봐야 알 수 있다. 따라서 본인 확인은 Persistence Layer에서 넘어온 데이터를 가공하는 Service Layer에서 DB 조회 이후 로직으로 작성되어서 처리해야 한다고 생각했다.
하지만 로직을 완성한 후 고민이 생겼다. 본인 확인은 Authorization의 영역이기 떄문에 Controller Layer에서 처리하는게 조금 더 좋은 코드가 아닌가 하는 생각이 들었다.
2. Controller Layer
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/post")
public class PostController {
private final PostService postService;
// Controller code...
@PutMapping("/{id}")
@PreAuthorize("isAuthenticated() and ((#PUReqDto.writer == principal.username) " +
"or hasRole('ROLE_ADMIN'))")
public Long update(@PathVariable Long id, @RequestBody PostUpdateRequestDto PUReqDto) {
return postService.update(id, PUReqDto);
}
@DeleteMapping("/{id}")
@PreAuthorize("isAuthenticated() and ((#PDReqDto.writer == principal.username) " +
"or hasRole('ROLE_ADMIN'))")
public Long delete(@PathVariable Long id, @RequestBody PostDeleteRequestDto PDReqDto) {
return postService.delete(id);
}
// Controller code...
}
Controller Layer에서 본인 확인을 할 수 있게 수정하였다.
@PreAuthorize은 Controller 코드가 실행되기 이전에 권한 검증을 하는 어노테이션이다. 검증 내용은 "로그인 되어있고, 그리고 requestDto의 writer가 로그인한 계정의 username과 같은지, 또는 관리자 권한을 가지고 있는지" 이다.
컨트롤러에서 본인 확인을 하는 것은 뚜렷한 장단점이 있다.
장점은 불필요한 쿼리들이 발생하지 않는다는 점이다. Service Layer에서 본인 확인을 처리하는 경우 본인이 아닌 경우에도 user와 post를 DB에서 조회하기 위해 쿼리가 발생한다. Controller Layer에서 처리하는 경우 Controller 코드가 실행되기 전에 권한을 검증하기 때문에 본인이 아닌 경우 쿼리가 발생하지 않는다.
단점은 데이터 전송이 추가로 발생할 수 있다. 나의 경우 #reqDto.writer와 같이 DTO의 필드에서 비교할 정보를 가져오기 때문에 post와 comment와 관련된 일부 DTO에 writer필드가 추가되었다.
이전 Service Layer에서 본인 확인을 처리할 때 보다 Controller Layer에서 처리하는 것이 조금 더 역할에 맞고 관심사 분리가 잘 된 코드로 생각된다.
3. Domain Layer
@Entity
@Table(name = "post_table")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class Post extends BaseEntity {
// Domain code...
public void update(String title, String content, User user) {
if (!this.user.equals(user) || user.getRole().equals(Role.ADMIN)) {
throw new CannotUpdatePostException();
}
this.title = title;
this.content = content;
}
public void delete(User user) {
if (!this.user.equals(user) || user.getRole().equals(Role.ADMIN)) {
throw new CannotDeletePostException();
}
this.status = Status.DISABLE;
}
// Domain code...
}
블로그 글을 작성하기 위해 자료를 찾던 중 발견한 토론이다. https://softwareengineering.stackexchange.com/questions/262424/where-does-authorization-fit-in-a-layered-architecture
Where does authorization fit in a layered architecture?
Typically, I place authorization decisions in my server side controllers. These have been RESTful endpoints recently, but I think the same stands for MVC type architectures. For the sake of argum...
softwareengineering.stackexchange.com
위 링크를 읽다보면 Authorization를 비즈니스 로직의 일부로 보는 견해도 있었다. 그런 경우 위와 같이 Domain Layer에서 처리를 할 수 있다.
'BE > Spring' 카테고리의 다른 글
Spring에서 AWS RDS MySQL Replication 적용하기 (0) | 2022.11.29 |
---|---|
스프링 S3 연동 오류: No valid instance id defined (0) | 2022.07.27 |
yaml 파일을 그룹으로 관리하기 (0) | 2022.07.09 |
Springboot에서 Redis Cache 적용하기 (0) | 2022.06.27 |
스프링 스케줄러(@Scheduled) 사용하는 법 (0) | 2022.06.23 |
댓글