클라이언트에서 요청을 받는 RequestDto 객체 Service에 노출되지 않게 하기
@PutMapping("/{lineId}")
public ResponseEntity<Void> updateLine(@PathVariable Long lineId, @RequestBody LineRequest lineRequest) {
lineService.update(lineRequest.toEntity(lineId));
return ResponseEntity.ok().build();
}
JSON 형태의 클라이언트 요청을 받는 Dto 객체인 LineRequest를 컨트롤러에서 엔티티 객체로 변환시켜 Service 레이어로 넘겨주고 있다.
이렇게 설계한 이유는 LineRequest라는 Dto는 어떤 UI에 사용되는지에 따라, UI에서 넘겨주는 값이 바뀔 때마다 영향을 받는 UI 의존적인 Dto이기 때문에 비즈니스 로직을 처리하는 Service 레이어까지 침투하기를 원치 않았다.
하지만 그렇다고 Service에서 Dto를 쓰지 말아야 하는 것은 아니다. Service 레이어 패키지에 위치하면서 Controller로 넘겨주기 위한 Dto였다면 Dto를 사용해도 된다고 생각한다.
하지만 LineRequest는 Controller 패키지에 위치하고 있었고 Controller의 상위 계층이라고 할 수 있는 클라이언트(프론트)로 전달하기 위해 만든 Dto이다.
'Controller -> 클라이언트'의 방향성을 가진 Dto라고 생각해서 다른 계층까지 넘나드는 것이 썩 좋지 않다고 느껴졌다.
인수 테스트도 꼼꼼히!
// then
assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());
assertThat(response.header("Location")).isNotBlank();
인수 테스트를 작성할 당시 이미 꼼꼼한 테스트나 객체 내부를 검증하는 테스트는 ServiceTest에서 모두 이루어졌기 때문에 인수 테스트에서는 응답 코드 등과 같은 값만 검증하면 된다고 생각했다.
하지만 인수 테스트에서도 응답 body를 확인하는 등 꼼꼼한 검증이 이루어져야 한다고 리뷰를 받고 생각했다.
실제로 ATDD(인수 테스트 주도 개발)을 한다면 인수 테스트가 제일 먼저 작성될 것이다. ServiceTest나 도메인 테스트가 존재하지 않은 상태이기 때문에 꼼꼼하게 작성할 필요가 있을 것 같다.
@DirtiesContext는 느리다
처음 제공받은 코드에서 인수 테스트에서의 테스트 격리를 위해 @DirtiesContext가 붙어 있었고 때문에 테스트 속도가 많이 떨어졌었다.
그래서 @DirtiesContext를 제거하고 테스트 메서드가 끝날 때마다 @Sql 어노테이션을 사용해서 모든 테이블을 지우는 방식을 선택했다.
Dao와 Repository
도메인 객체의 연관관계를 표현하느라 dao가 다른 dao를 의존하는 설계가 되었었다.
dao가 dao를 의존해도 될까요?
설계하면서 궁금한 점이 dao가 dao를 의존해도 될까하는 문제였습니다.
dao에 도메인 객체를 바로 저장하고 불러오고 하고 있는데 Line 객체가 Section의 리스트를 갖고 있고, Section이 Station을 갖고 있습니다.
이런 구조를 가짐으로써 도메인 객체가 비즈니스 로직에서 제 역할을 충실히 해준다는 느낌은 받았습니다.
그런데 Section을 불러오면 Station도 셋팅을 해줘야 하고 Line을 불러오려면 Section의 리스트도 세팅을 해줘야 합니다.
그래서 dao에서 객체를 생성할 때 의존이 있는 객체를 넣어주기 위해 LineDao가 SectionDao를 의존하고, SectionDao가 StationDao를 의존하고 있는 상황입니다
그래서 위와 같은 질문을 남겼고 아래와 같은 답변을 받았다.
Line이 Sections를 가지고있어 도메인 구조와 최대한 db 접근 구조를 맞추다 보니 lineDao가 sectionDao를 의존하는 상황이 생길 수 있습니다.
특히 lineDao.save(line)을 할 때 자동으로 section도 저장이 되고 lineDao.find(id)를 할 때 자동으로 line에 sections이 초기화되길 원한다면 충분히 그럴 수 있을 것 같아요. 저는 나쁘지 않다고 봅니다!
하지만 다음 두 가지를 말씀드릴게요.
1. dao vs repository 차이
repository라는 개념이 있어요. 두 차이가 무엇인지 한번 확인해보면 좋을 것 같아요 😄
2. saveSection, updateSection, removeSection
물론 repository를 추가한다고 끝나는 건 아닙니다. 오히려 더 헷갈릴 수도 있으니 적용하지 않으셔도 괜찮아요 😅 그렇다면 이 3개의 함수는 과연 lineDao에 있는 게 맞을지 생각해보셨으면 좋겠어요. 🤔
lineDao가 sectionDao를 의존하는 이유는 line를 저장하면 자동으로 section도 저장되길 원해서에요.
하지만 세 함수들은 단순히 section만 저장하고 업데이트합니다. line객체를 이용하지도 않고 lineDB에도 접근하지 않는데 lineDao에 존재하는 게 맞을까요?
그래도 뭔가 시원하지않고 여러 고민이 들 거예요. 정답이 없으니 여러 방법과 장단점을 생각해보시면 좋겠습니다 😄 그리고 지금 했던 고민을 잘 기억해서 나중에 사용 중인 라이브러리들은 어떻게 해결했는지 비교해보는 것도 좋을 것 같습니다 💪
위 답변을 받고 고민하고 알아본 결과 아래와 같은 결론을 얻었다.
찾아보니 dao와 repository에 대해서 좀 더 자세히 알 수 있었습니다.
요약하자면 dao는 데이터베이스 테이블과 일치하는 데이터를 다루고, repository는 도메인(엔티티) 객체와 일치하는 데이터를 다룬다는 것입니다.
제 설계를 보면 dao에 테이블과 일치하는 데이터가 아닌 도메인(엔티티)에 일치하는 데이터를 컬렉션에 넣고 빼듯 다루고 있어 dao 보다는 repository에 개념상 가깝다고 느꼈습니다.
그래서 repository를 만들고 repository에 dao나 다른 repository를 사용하게 하였습니다.
그리고 LineRepository(LineDao 였던 것)에서 Section의 저장, 수정, 삭제 기능을 제거하고 LineService에서 SectionRepository를 사용해서 Section만의 저장, 수정, 삭제를 담당하도록 했습니다.
Dao는 데이터베이스에 접근하는 객체이고 데이터베이스의 상세 사항을 노출시키지 않고 데이터의 동작을 제공한다고 위키백과에 나와있다. 즉 도메인 로직과 데이터베이스 접근 로직을 분리하기 위한 디자인 패턴이라고도 볼 수 있다.
Repository는 DDD(도메인 주도 개발)에서 나온 개념으로 도메인 객체의 영속성을 관리하기 위한 객체이다. 그 내부가 데이터베이스로 구현이 되어 있든, 컬렉션으로 구현이 되어있어도 상관없다.
위에서도 적었듯이 Dao는 일반적으로 데이터베이스 테이블에 따라 표현되며, Repository는 도메인 객체 중심으로 컬렉션 지향으로 주로 설계된다.
하지만 컬렉션 지향으로 데이터베이스를 사용한 Repository를 설계할 때 문제가 있었다.
도메인의 변경을 추적하기
컬렉션에 도메인 객체를 저장하고 관리하면 장점이 있다. 바로 객체를 조회하고 상태 변경이 일어나도 따로 다시 저장하거나 수정하는 작업이 필요 없다는 것이다.
@Transactional
public void addSection(Long id, Section section) {
Line line = lineRepository.findById(id);
line.addSection(section);
}
자세한 로직은 알 필요는 없지만 위 코드에서 line에 section을 추가하면 line 안에 여러 section 중 상태 수정이 일어나는 section이 생길 수도 있다. 만약 LineRepository가 컬렉션으로 구현되어 있다면 위 코드만으로 충분할 것이다.
하지만 위 Repository는 데이터베이스에 접근하고 있기 때문에 수정되거나 저장되는 section에 대해 따로 save 처리와 update 처리를 해주어야 한다.
그래서 아래와 같이 line 도메인 객체에게 addSection() 메서드의 반환 값으로 Optional<Section>을 반환하게 했다.
@Transactional
public void addSection(Long id, Section section) {
Line line = lineRepository.findById(id);
line.findUpdatedSectionByAdd(section)
.ifPresent(lineRepository::updateSection);
lineRepository.saveSection(id, section);
}
로직상 수정되는 Section은 많아야 1개이기 때문에 위와 같이 만약 수정 Section이 있으면 Repository의 update 메서드로 수정하게 하였다. 그래서 이름도 길어졌다. findUpdatedSectionByAdd()라는 이름이 길기도 하고 데이터베이스 문제만 아녔어도 반환 값도 필요 없는데 도메인 객체가 DB 때문에 이상해진 것 같아 불편했다.
그래서 도메인 객체의 상태를 다른 객체가 추적해서 DB에 반영하는 방법을 생각했고 아래와 같은 코드로 리팩터링 했다.
SectionsDirtyChecker 클래스는 line의 List<Section>이 변경되기 전의 상태를 가지고 있다가 변경된 이후 다시 List<Section>을 받아서 수정되거나, 저장되거나, 삭제된 Section 객체를 List로 반환하는 메서드들을 가지고 있다.
@Transactional
public void addSection(Long id, Section section) {
Line line = lineRepository.findById(id);
SectionsDirtyChecker checker = SectionsDirtyChecker.from(line.getSections());
line.addSection(section);
executeDirtyChecking(id, line, checker);
}
private void executeDirtyChecking(Long lineId, Line line, SectionsDirtyChecker checker) {
checker.findUpdated(line.getSections())
.executeEach(sectionRepository::update);
checker.findDeleted(line.getSections())
.executeEach(section -> sectionRepository.remove(section.getId()));
checker.findSaved(line.getSections())
.executeEach(section -> sectionRepository.save(lineId, section));
}
이렇게 하니 변경을 추적하는 코드가 길어지기는 했지만 Line 객체는 DB에 영향받지 않는 도메인으로 남게 되어서 나름 만족하고 있다.
더 좋은 리팩터링 방법도 있을 것이고 나중에 JPA를 사용하면 안 해도 될 고민이지만 데이터베이스를 사용하더라도 도메인 객체는 온전히 도메인 객체로써 있길 바라는 마음에 고민한 결과인 것 같다.
https://github.com/woowacourse/atdd-subway-map/pulls?q=is%3Apr+is%3Aclosed+%EB%8D%94%EC%A6%88
'우아한테크코스' 카테고리의 다른 글
레벨 4 톰캣 만들기 미션 서버 소켓 코드 분석 (0) | 2022.09.03 |
---|---|
Level2 Spring 체스 피드백 정리 (0) | 2022.05.06 |
Level1 체스 미션 피드백 정리 (0) | 2022.04.10 |
수업 따라하기 (Gradle 프로젝트에 Docker로 mysql 접속) (0) | 2022.03.30 |
Level1 블랙잭 미션 피드백 정리 (0) | 2022.03.21 |