최종적으로 수정된 아키텍처는 다음과 같다.
지난글
지난 글에서 의존하는 방향을 통일하고, 코드의 중복을 줄이고 재활용할 수 있도록 코드 아키텍처를 수정했었다.
지난 글까지의 아키텍처는 대략적으로 아래의 사진과 같다.
문제는 컨트롤러, 서비스, 리포지토리 모두가 엔티티에 의존하고 있다는 사실이다.
안될건 없지만 컨트롤러 레이어가 엔티티를 의존하지 않았으면 하는 생각이 있었다.
이러한 의존성을 완화하는 동시에 새로운 문제가 발생하지 않는 방식으로 아키텍처를 개선하고 싶었다.
위 포스팅에서 코드의 중복을 해결하기 위해 어댑터를 적용했고, 이 방법이 자금의 주제에도 해결법이 될 수 있을것 같았다.
@Service
public class ArticleService {
public Article articleRetrieve(Long id) {
Article article = articleRepository.findById(id).orElseThrow(()-> new NoSuchElementException(String.format("article[%s] 게시글을 찾을 수 없습니다", id)));
return article;
}
}
위 메소드는 서비스 레이어의 메소드이다. Article 하나를 조회하기 위한 메소드인데, 다음과 같이 컨트롤러 , 타 도메인 서비스레이어 등에서 사용하고 있다.
ArticleController
@RestController
@RequestMapping("article")
public class ArticleController {
@ApiOperation(value = "게시글 id로 단건 조회")
@GetMapping("")
public ResponseEntity<ArticleDTO.DetailResponse> retrieveArticle(@AuthenticationPrincipal Member member, @RequestParam Long id) {
//articleRetrieve를 사용하는 부분
Article response = articleService.articleRetrieve(id);
viewCountService.viewCountIncrease(id);
return ResponseEntity.ok(toResponseDto(response));
}
CommentService
@Service
public class CommentService {
public Long commentParentCreate(Member member, CommentDTO.ParentRequest request) {
//articleRetrieve를 사용하는 부분
Article article = articleService.articleRetrieve(request.getPostId());
Comment makeComment = request.toEntity();
makeComment.setArticle(article);
makeComment.setMember(member);
commentRepository.save(makeComment);
return makeComment.getId();
}
코드의 재사용을 위해 ArticleController와 CommentService에서 사용하는 메소드를 통일시키다보니 반환값도 통일시켜야 해서 컨트롤러에서 엔티티를 의존하게 되는 문제가 발생했던 것이다.
어댑터를 통해 이 문제를 해결하였다.
@Adaptor
public class ArticleAdaptor {
@Autowired
private ArticleRepository articleRepository;
public Article retrieveArticle(Long id) {
Article article = articleRepository.findById(id).orElseThrow(()-> new NoSuchElementException(String.format("article[%s] 게시글을 찾을 수 없습니다", id)));
return article;
}
}
어댑터 코드는 리포지토리 코드를 감싸고 있는 형태이다. 기존에 서비스 레이어에서 리포지토리 메소드를 호출하는 것을 대리하며, 예외처리도 이곳에서 하게 된다.
ArticleService
public ArticleDTO.DetailResponse articleRetrieve(Long id) {
ArticleDTO.DetailResponse response = ArticleDTO.DetailResponse.toResponseDto(articleAdaptor.retrieveArticle(id));
return response;
}
변경된 코드에서 ArticleService는 더이상 엔티티를 반환하지 않고 DTO를 반환하게 된다.
또한 entity->dto를 진행하는 메소드가 컨트롤러 내부에 있었는데, dto 내부로 옮기게 되었다. dto가 엔티티를 의존하는 것은 문제가 되지 않는다고 생각했기 때문이다.
ArticleController
@ApiOperation(value = "게시글 id로 단건 조회")
@GetMapping("")
public ResponseEntity<ArticleDTO.DetailResponse> retrieveArticle(@RequestParam Long id) {
ArticleDTO.DetailResponse response = articleService.articleRetrieve(id) ;
viewCountService.viewCountIncrease(id);
return ResponseEntity.ok(response);
}
따라서 컨트롤러 메소드는 서비스 레이어의 메소드를 호출하고 반환하게 된다.
CommentService
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
//articleService 의존성 삭제
@Autowired
private ArticleAdaptor articleAdaptor;
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
public Long commentParentCreate(Member member, CommentDTO.ParentRequest request) {
Article article = articleAdaptor.retrieveArticle(request.getPostId());
Comment makeComment = request.toEntity();
makeComment.setArticle(article);
makeComment.setMember(member);
commentRepository.save(makeComment);
return makeComment.getId();
}
다른 도메인의 서비스 메소드에서 Article 엔티티가 필요할 때에는 ArticleService가 아닌 ArticleAdaptor의 메소드를 호출하여 엔티티를 가져오게 된다.
수정된 아키텍처는 다음과 같다.
다음은 기존 서비스 레이어의 코드이다.
@Service
public class CommentService {
... 생략
public Comment commentRetrieveById(Long id) {
//중복된 예외처리
Comment comment = commentRepository.findById(id).orElseThrow(()-> new NoSuchElementException(String.format("comment[%s] 댓글을 찾을 수 없습니다",id)));
return comment;
}
public Long commentEdit(Member member, CommentDTO.Edit request) {
//중복된 예외처리
Comment editComment = commentRepository.findById(request.getId()).orElseThrow(() -> new NoSuchElementException(String.format("comment[%s] 댓글을 찾을 수 없습니다",request.getId())));
if (!editComment.getMember().equals(member)) {
throw new NoAuthorityExceoption("수정 권한이 없습니다. 본인 소유의 글만 수정 가능합니다.");
}
editComment.edit(request);
commentRepository.save(editComment);
return editComment.getId();
}
public void commentDelete(Member member, Long id) {
//중복된 예외처리
Comment deleteComment = commentRepository.findById(id).orElseThrow(() -> new NoSuchElementException(String.format("comment[%s] 댓글을 찾을 수 없습니다",id)));
if (!deleteComment.getMember().equals(member)) {
throw new NoAuthorityExceoption("삭제 권한이 없습니다. 본인 소유의 글만 삭제 가능합니다.");
}
commentRepository.delete(deleteComment);
}
}
주석으로 표시된 세 부분은 모두 같은 메소드를 예외처리한다. 따라서 코드의 중복이 발생하는데 어댑터를 이용하면 문제가 해결된다.
CommentAdaptor
@Adaptor
public class CommentAdaptor {
... 생략
public Comment commentRetrieveById(Long id) {
Comment comment = commentRepository.findById(id).orElseThrow(()-> new NoSuchElementException(String.format("comment[%s] 댓글을 찾을 수 없습니다",id)));
return comment;
}
}
CommentService
@Service
public class CommentService {
... 생략
public Comment commentRetrieveById(Long id) {
Comment comment = commentRepository.findById(id).orElseThrow(()-> new NoSuchElementException(String.format("comment[%s] 댓글을 찾을 수 없습니다",id)));
return comment;
}
}
어댑터에서 리포지토리 메소드를 예외처리한 후에 전달해 주기 떄문에 서비스 레이어에서 별도로 예외처리를 할 필요가 없어진다는 단점이 있다.
언뜻보면 리포지토리가 엔티티에 의존하는 것 처럼 보이지만, 사실 Jpa에 종속적이므로 사용기술이 바뀌면 엔티티도 바뀌게 되어서 테이블과 매핑되는 엔티티와 자바 객체로 사용할 엔티티의 분리가 필요할지도 모른다는 생각이 들었다. 다만 아직은 규모도 그리 크지 않고, 오히려 개발 비용이 클 것 같아 실행에 옮기진 않았다.