헥사고날 아키텍처로의 시스템 아키텍처 전환을 프로젝트에 적용하면서, 가장 어려우면서 동시에 가장 중요하게 처리해야한다고 느낀 부분은 다른 도메인과 회원 도메인 간의 연관관계다.
현재 프로젝트에서는 크게 게시글, 바버샵, 예약, 채팅, 회원의 도메인이 나온다. 회원 도메인은 발생할 수 있는 모든 유즈케이스의 주체다. 따라서 나머지 세 개의 모든 도메인과 연관되어있다. 결국, 가장 중요한 도메인은 회원이 된다. 그래서 회원은 독립적으로 존재시키는 것이 좋다. 다른 도메인과 연관관계를 맺는 순간 다른 도메인의 복잡도가 회원에 전파된다.
그럼 다른 도메인은 회원에 어떻게 의존해야할까. 이 고민을 시작한 것은 게시글 수정 기능을 리팩토링하면서다. 게시글을 수정하기 위해서는 게시글의 작성자와 수정 요청을 보낸 사용자를 대조해야 한다. 이 과정은 도메인 영역에서 이뤄져야한다. 그러기 위해서는 게시글 도메인 엔티티에 사용자 도메인 엔티티를 넣어주고, 이를 위해 게시글 애그리거트에 사용자 도메인 객체를 가져와야 한다. 문제는, 그래서 어디서 사용자를 가져올 것이냐는 거다.
회원에 의존하기 위해서는 결국 어디선가 회원 객체를 가져와야한다. 회원 객체를 가져오는 방법은 크게 세 가지가 있었다.
- GetMemberController에 의존해서 가져온다.
- GetMemberUseCase(GetMemberService)에 의존해서 가져온다.
- LoadMemberPort(MemberPersistenceAdapter)에 의존해서 가져온다.
위의 세 가지 방법 중에 비교적 빠르게 3번을 선택했다. 다른 애그리거트의 관점에서 봤을 때, 회원 애그리거트는 외부 영역이다. 따라서 회원 애그리거트에 접근하기 위해서는 Port를 통해야한다. 또한 만약 1번의 선택지를 선택한다면, 애플리케이션과 어뎁터의 의존 관계가 역전된다. 어뎁터가 애플리케이션을 일방적으로 알고 있어야 하는데, 애플리케이션이 다른 애그리거트의 어뎁터를 알게되버리는 것이다. 2번과 3번이 Port에 해당하는데, 사용자 도메인 객체가 필요했기 때문에 이 중에서 사용자의 도메인 객체를 가져오는 3번 선택지를 선택했다. 사용자 도메인 객체가 필요한 이유는, 단순 id값의 비교뿐만 아니라, 해당 회원이 관리자인지에 대한 판단이 필요했기 때문이다. UseCase에서 반환하는 값에는 도메인 객체가 존재하지 않는다. API 영역에 도메인 객체를 담고싶지 않았기 때문이다.
LoadMemberPort를 이용하기로 결정한 뒤로, 이제 게시판 애그리거트의 어느 영역에서 해당 Port에 의존할 것인가를 선택해야 했다. 크게 두 가지 방법이 있었다. 하나는 외부 영역인 Output Adapter에서 회원에 의존하는 방법이었고, 하나는 내부 영역인 애플리케이션에서 회원에 의존하는 방법이었다.
나는 두 번째 방법인 애플리케이션에서 회원에 의존하는 방법을 선택했다. 가장 큰 이유는 의존 방향이 어디로 설정되어야하는가에 있었다. 또한, 다른 애그리거트의 도메인 객체를 알고있기에 적절한 영역이 어딘가를 생각하면 애플리케이션에 의존성을 설정하는 것이 더 적절해보였다.
만일 Output Adapter에서 외부 Port에 대한 의존이 생기면, 해당 Port가 수정될 때 변경이 Output Adapter로 전파된다. 유지보수의 관점에서 의존 방향을 일관화하는 것이 더 좋아보였다. 애플리케이션에서 해당 Port에 의존하면, Port에서 메서드 시그니처를 수정할 때 어느 부분으로 변경의 여파가 전파될지를 예측하기 쉽다. 애플리케이션이 Output Port에 의존한다는 약속이 시스템 전체에서 지켜지기 때문이다. 물론 Output Adapter도 Output Port를 구현하는 영역이기 때문에 Output Port의 변경이 영향을 미친다. 그러나 이는 같은 애그리거트 내에 그친다. 구현의 영역이기 때문이다.
또한 Member라는 도메인 객체를 다른 애그리거트의 Output Adapter에서 알고있다는 것은 애플리케이션 전체의 약속을 어긴다. 도메인 객체는 애플리케이션에서만 알고있어야 한다. 물론 Output Adapter에서 데이터베이스 객체를 도메인 객체로 전환해 반납하지만, 역시 같은 애그리거트 내에서의 일이다. 사용자 도메인 객체가 필요한 이유는 비즈니스 로직을 수행하기 위함이고, 이를 위해서라도 사용자 도메인 객체를 알고 있는 것은 애플리케이션이 돼야 시스템 전체의 약속을 지킬 수 있다.
이러한 생각을 바탕으로 자유 게시글 수정 기능의 리팩토링을 진행했다. 사실 결과물은 이번 포스팅에서 크게 의미 없긴한데 그래도 이걸 어떻게 사용했는지는 적어 놓는게 낫는 것 같아서 함께 기술한다.
@Service
public class PutFreeBoardService implements PutFreeBoardUseCase {
private final LoadMemberPort loadMemberPort;
private final LoadFreeBoardPort loadFreeBoardPort;
private final SaveFreeBoardPort saveFreeBoardPort;
public PutFreeBoardService(LoadMemberPort loadMemberPort, LoadFreeBoardPort loadFreeBoardPort, SaveFreeBoardPort saveFreeBoardPort) {
this.loadMemberPort = loadMemberPort;
this.loadFreeBoardPort = loadFreeBoardPort;
this.saveFreeBoardPort = saveFreeBoardPort;
}
@Override
@Transactional
public FreeBoardCommandResponse putFreeBoard(PutFreeBoardCommand command) {
FreeBoardQueryResult findQueryResult = loadFreeBoardPort.loadFreeBoardById(command.getFreeBoardId());
Member writer = loadMemberPort.findMemberById(findQueryResult.getWriterId());
FreeBoard freeBoard = findQueryResult.getFreeBoard();
freeBoard.setWriter(writer);
Member requestMember = loadMemberPort.findMemberById(command.getWriter().getId());
freeBoard.checkMemberAuthority(requestMember);
freeBoard.modify(command.getTitle(), command.getContent(), command.getModifiedAt());
FreeBoardQueryResult savedQueryResult = saveFreeBoardPort.save(freeBoard);
FreeBoard updatedFreeBoard = savedQueryResult.getFreeBoard();
return FreeBoardCommandResponse.of(updatedFreeBoard.getId(),
updatedFreeBoard.getTitleValue(),
updatedFreeBoard.getContent(),
updatedFreeBoard.getViewCount(),
updatedFreeBoard.getCreatedAt(),
updatedFreeBoard.getModifiedAt(),
WriterInfo.of(writer),
new ArrayList<>());
}
}
가장 중요한 Service만 가져왔다. 코드를 순서대로 살펴보자.
- LoadFreeBoardPort를 통해 수청 요청이 들어온 게시글 객체를 가져온다.
- QueryResult에는 생성된 게시글 객체와 게시판 데이터베이스 엔티티에서 가져온 작성자 id가 담겨있다.
- LoadMemberPort를 통해 해당 게시글 객체의 작성자 도메인 엔티티를 가져온다.
- 아마 추후 QueryResult의 형태로 리팩토링 될 것이다.
- 가져온 게시글 객체에 작성자를 넣어준다.
- 이 부분에서 고민이 됐다. 일반적인 흐름을 생각하면 작성자가 먼저 생기고 게시글이 생기는게 맞다. 그런데 코드 레벨에서 이런 흐름을 반영하기 위해서는 Output Adapter에서 직접 Member 도메인 객체를 찾고 넣어줘야 한다. 결과적으로 위의 고민의 과정을 거쳐 이렇게 사용하는 게 옳지 않다고 판단했다. 코드가 현실을 100퍼센트 반영하기는 어렵다. 사실 이 부분 때문에 어디다 의존 설정을 할지에 대한 고민이 시작됐다.
- LoadMemberPort를 통해 수정 요청을 보낸 사용자 객체를 찾는다.
- FreeBoard 도메인 객체에서 요청한 사용자가 수정 권한이 있는지 파악한다.
- 있으면 넘어가고 없으면 예외처리한다.
- 이후 수정 및 API 계층에 반환할 값을 보내준다.
전체 코드는 링크에서 확인할 수 있다.
'Programming > TDD Project' 카테고리의 다른 글
Pull Request 027. 로그인 / 로그아웃 로직 리팩토링하기 - Http Header 대신 Cookie를 사용해보자 (0) | 2024.01.09 |
---|---|
Pull Request 026. 자유 게시글 테스트 코드를 리팩토링하며 느낀 점 (0) | 2023.12.29 |
Pull Request 024. 아키텍처 리팩토링 및 패키징 수정 - 자유 게시글 등록 기능에 헥사고날 아키텍처 적용하기 (0) | 2023.12.15 |
Pull Request 023. 인증된 사용자 객체 사용 TDD로 구현하기 - 자유 게시판 등록 시 작성자 등록하기 (0) | 2023.12.08 |
Pull Request 022. 사용자 인증 및 권한 확인 기능 TDD로 구현하기 - Spring Security 설정 마무리 (0) | 2023.12.07 |
댓글