캠프에서 강의를 들으며 궁금했던 부분을 해당 튜터님께 여쭤보고 답변을 받았다.
왜 controller -> service에 데이터를 넘겨줄 때 DTO가 아니라 값을 쪼개서 주는걸까?
controller
@PostMapping
public ResponseEntity<BoardResponseDto> save(@RequestBody CreateBoardRequestDto requestDto) {
BoardResponseDto boardResponseDto =
boardService.save(
requestDto.getTitle(),
requestDto.getContents(),
requestDto.getUsername()
);
return new ResponseEntity<>(boardResponseDto, HttpStatus.CREATED);
}
service
public BoardResponseDto save(String title, String contents, String username) {
Member findMember = memberRepository.findMemberByUsernameOrElseThrow(username);
Board board = new Board(title, contents);
board.setMember(findMember);
boardRepository.save(board);
return new BoardResponseDto(board.getId(), board.getTitle(), board.getContents());
}
지금이야 간단한 CRUD만 구현하는 프로젝트라서 상관없지만, 나중에 규모가 커지면 해당 서비스의 메서드를 여러 곳에서 호출할 수 있다. 이 때 특정 DTO에 의존하게 되면 코드를 재사용하기 힘들다.
DTO 객체를 넘겨주는 것과 파라미터를 일일이 넘겨주는 것에는 각각 장단점이 있다.
DTO 방식
값 전달
DTO를 넘겨라
→ Controller와 Service 간 파라미터 수가 많거나, 데이터를 하나의 개념으로 묶어 처리하는 경우
→ 비즈니스 로직에서 해당 데이터를 객체로 다루는 게 자연스러운 경우
값을 쪼개라
→ 파라미터가 적고, DTO에 종속되지 않는 Service 레이어를 만들고 싶을 때
→ Service 레벨이 정말 도메인 중심이고, DTO는 Controller에서만 사용하는 일회용일 때
왜 repository 에서 예외를 throw 하도록 하는걸까? repository는 DB와의 상호작용만 하는게 낫지 않나?
service에서 처리해주는게 더 명확한게 아닌가?
코드 중복이 문제라면 서비스에서 private 메서드를 만들어서 사용하면 되는거 아닌가?
public interface MemberRepository extends JpaRepository<Member, Long> {
default Member findByIdOrElseThrow(Long id) {
return findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id));
}
}
repository를 여러 개의 서비스에서 사용하게 되면 비슷한 코드를 각 서비스마다 구현해야 한다. (코드 중복)
예외를 throw 하는건 항상 서비스에서 하겠다! 라고 정해두는게 아닌 이상 repository 단에서 이런 작업을 처리해도 된다.
조원분들께도 위 내용을 공유드렸더니 repository에 직접 default 메서드를 구현하는 것보다는 다른 방법을 사용하신다고 한다. 그런데 내용을 이해하지 못해서 이 부분은 따로 조사해야 한다.
서비스 클래스 간의 순환참조를 방지하기 위해 RetreivalService를 만들었는데 어떰? ScheduleService에서 Member 객체를 사용할 일이 있어서 이렇게 구현했는데... 이 서비스는 ScheduleService, MemberService 두 곳에서 모두 사용할거다.
/**
* 서비스 클래스들의 순환 참조를 방지하기 위해 Member 검색용 클래스 분리
*/
@Service
@RequiredArgsConstructor
public class MemberRetrievalService {
private final MemberRepository memberRepository;
public Member findById(Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 ID입니다. = " + memberId));
}
}
가장 안 좋은 패턴이다. (나름 고민했던 방식인데...)
애초에 순환참조가 일어나지 않게 설계하는 것이 가장 좋은 방식이다.
위와 같은 방식은 굳이 클래스를 하나 더 만드는거고 뎁스만 생긴다. 차라리 MemberService를 주입 받거나 MemberRepository를 사용해라.
순환참조는 서비스 간 관계를 제대로 설계하지 못 했을 때에나 생기는 문제다. 개발자가 제대로만 하면 일어날 일이 없다.
"내가 작성한 일정, 댓글" 등의 목록을 조회하는 기능을 구현한다고 했을 때, member, schedule, comment 도메인 중 어떤 곳에 코드를 작성하는 것이 좋을까? "내"가 작성한 일정이니까 member?
리소스 별로 구분하는 것이 가장 좋다.
내가 작성한 "일정"을 조회하는 기능을 구현한다면 "일정(schedule)" 컨트롤러에서 입력 받아 schedule service에서 처리해라. 이렇게 리소스를 기준으로 기능들을 모아두는 것이 가장 직관적이고 편하다.
이런 것들의 예외 케이스라면 "관리자" 기능이 있다.
사실 강의 내용이나 지금 진행하는 프로젝트는 규모가 작아서 이런 내용들에 대해 크게 와닿지가 않기는 한다. 오히려 여기에 시간을 과투자하는 느낌이 들기도 하고...
또 오늘 질문한 내용에 대해 튜터님이 무조건 옳다고 생각하지는 않는다. 당장 조원들이나 다른 튜터님들도 말씀이 다 다르더라.
일단은 진도를 후딱후딱 나가고 나중에 피드백을 받아서 다음 프로젝트에 적용시키는게 좋을 것 같다. 지금 이런 사소한 내용들에 붙잡혀서 너무 느린 것 같다.