Replies: 4 comments
-
글 잘 읽었습니다!😄 여유로울 때 글을 자세히 읽어보고 싶어 comment가 늦었습니다..! 팜님의 의견처럼 해당 방식은 코드의 중복을 줄이고 중앙 집중형 방식이 되는 장점이 있으나, 강한 결합이 생기고 복잡성이 증가합니다. 모종의 이유로 메서드에 문제가 생긴다면 그 메서드를 사용하는 모든 메서드가 동작하지 않습니다.. 다음으로 본문의 방식은 중복을 피하고자 했으나 또 다른 중복을 불러일으킵니다. 예를 들어 또한 신뢰성 있는 엔티티는 이해하였지만, 의존성을 낮추었다는 말에는 모순이 존재한다고 생각합니다. 추가로 캡슐화가 된다고 하셨는데, 클래스 외부에서 접근하려면 모든 메서드를 public으로 풀어야 합니다. 그렇다면 데이터 은닉이 제대로 되지 않는 문제가 생길 것 같습니다. 그리고 순환 참조 문제가 발생할 수 있는 위험이 있습니다. 정리하면 해당 방법은 도메인 간 의존성을 낮추고자 하였으나 컴파일 타임 의존성이 증가하는 방법인 것 같습니다. |
Beta Was this translation helpful? Give feedback.
-
추가로 코드가 중복되는 부분은 모듈 서비스 클래스 개발을 통해 해결할 수 있을 것 같습니다!
예를 들어 해당 코드를 리팩토링하면 다음과 같습니다.
public Member findById(final Long memberId){
return memberRepository.findById(memberId).orElseThrow(()-> new MemberException(NOT_FOUND_MEMBER));
}
public void createTeam(final TeamCreateRequest request, final MultipartFile file, final Long memberId) {
Member member = memberFindService.findById(memberId);
final Team team = Team.builder()
.name(request.name())
.description(request.description())
.imageUrl(imageUrl)
.build();
teamRepository.save(team);
createMemberTeam(member, team.getId());
assignTeamRole(team.getId(), memberId);
}
(createMemberTeam, assignTeamRole코드는 생략) |
Beta Was this translation helpful? Give feedback.
-
답변을 요약했을때 제가 제시한 방법을 사용할시 아래와 같은 문제가 발생할 수 있다고 이해했습니다.
또한 미나님의 의견은 Service에서 다른 도메인의 Service의 참조를 지양하고 대신 Repository를 참조하자는 것으로 이해했습니다. 의존성 & 결합도에 대해
위 두 의견은 서료 연관지어 설명할 수 있을 거 같아 함께 설명하겠습니다. 의존성이란자바 객체 지향 원리에서는 의존성에 대해 아래와 같이 설명합니다.
…(본문)… DI를 마무리하기 전에 마지막으로 언급할 사항이 하나있다. 의존관계가 new라고 단순화했던 부분이다. 사실 변수에 값을 할당하는 모든 곳에 의존관계가 생긴다. 즉, 대입연산자(=)에 의해 변수에 값이 할당되는 순간에 의존이 생긴다. 변수가 지역 변수이 건 속성이건, 할당되는 값이 리터럴이건 객체이건 의존은 발생한다. 의존 대상이 내부에 있을 수도 있고, 외부에 있을 수도 있다. DI는 외부에 있는 의존대상을 주입하는 것을 말한다. 의존대상을 구현하고 배치할 때 SOLID와 응집도는 높이고 결합도는 낮추라는 기본원칙에 충실해야 한다. 그래야 프로젝트의 구현과 유지보수가 수월해진다. 또한 *<개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴>*에서는 의존성을 아래와 같이 설명합니다. public class FlowController {
public void process() {
FileDataReader reader = new FileDataReader(fileName); //객체 생성
byte[] plainBytes = reader.read(); // 메서드 호출
...
}
} *이렇게(위 코드와 같이) 한 객체가 다른 객체를 생성하거나 다른 객체의 메서드를 호출할 때 이를 그 객체에 의존(dependency)한다고 한다. 객체를 생성하든 메서드를 호출하든 또는 파라미터로 전달받든 다른 타입에 의존을 한다는 것은 의존하는 타입에 변경이 발생할 때 나도 함께 변경될 가능성이 높다는 것을 뜻한다. 아래는 제가 디스커션에서 제시했던 수정 전 코드 & 수정 후 코드입니다. public void createTeam(final TeamCreateRequest request, final MultipartFile file, final Long memberId) {
Member member = validateExistMember(memberId);
final Team team = Team.builder()
.name(request.name())
.description(request.description())
.imageUrl(imageUrl)
.build();
teamRepository.save(team);
final TeamRole teamRole = TeamRole.builder()
.memberId(memberId)
.teamId(team.getId())
.teamRoleType(ROLE_팀장)
.build();
teamRoleRepository.save(teamRole);
final MemberTeam memberTeam = MemberTeam.builder()
.member(member)
.isDeleted(false)
.teamId(team.getId())
.build();
memberTeamRepository.save(memberTeam);
} (수정 전) public void createTeam(final TeamCreateRequest request, final MultipartFile file, final Long memberId) {
MemberResponseDto memberDto = MemberQueryService.validateExistMember(memberId);
final Team team = Team.builder()
.name(request.name())
.description(request.description())
.imageUrl(imageUrl)
.build();
teamRepository.save(team);
teamRoleCommandService.assignTeamLeaderRole(memberId, teamId);
memberTeamCommandService.addMemerTeam(memberId, teamId);
} (수정 후) 우선 제가 언급한 의존성이 낮아진다라는 말은 다른 도메인에서의 불필요한 객체 할당이 사라지며 다른 도메인의 객체에 대한 의존성이 줄어든다는 뜻이었습니다. 서비스 레이어를 참조하며 발생하는 의존성그러나 미나님의 의견을 읽어봤을 때 객체보다는 서비스 레이어를 참조하는 것에서 발생한 의존성을 말씀하시는 것 같아 이에 대해서도 설명하겠습니다. 발췌한 위 두 책 중 _<자바 객체 지향 원리>_에서는 이러한 의존성으로 발생하는 결합도를 낮추는 방법으로 **의존성 주입(Dependency Injection)**에 대해, _<개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴>_에서는 의존성으로 발생할 수 있는 구현 변경의 비유연성에 대해 경고하며 이에 대한 해결 방안으로 캡슐화에 대해 설명합니다. 짧게 말하자면 Service 레이어의 의존성과 그 의존성으로 인해 발생하는 결합도는 어쩔수 없이 발생하는 것이라는 생각합니다. 미나님이 제시하신 방법도 결국 그 의존 대상이 Service에서 Repository로 바뀌었을 뿐 여전히 의존이 발생하고 있으니까요. 계층형 설계를 따르며 코드 구조를 작성한 만큼 하위 계층이 아닌 동일 계층의 Service끼리 의존성을 가지게 되는 것에 대해 거부감을 느낄 수도 있겠다는 생각이 듭니다. 상위 계층은 하위 계층에 의존해야하며 상위 계층을 알지 못해야 한다는 이야기만 들어왔지 동일한 계층끼리의 의존이 적절한가 부적절한가에 대한 이야기는 들어본적 없으니까요. 저도 예전에 이 부분에 대해 의문을 가져 관련된 내용을 찾아본적이 있습니다. DAO에 대한 참조는 있어선 안된다!라 주장하는 것은 아니고, 프로젝트의 성향에 따라 Service가 아닌 Repository만을 참조하는 것이 좋은 설계일 수도 있을 거라 생각합니다.(미나님의 다른 프로젝트가 그러하듯이) Keeper 코드 중 Repository만을 참조한 Service는 달리 규칙이 있어 그렇게 코드를 작성한 것이 아니라 그냥 다른 도메인간 의존할 일이 없었다는 대답을 들었습니다. Keeper 코드에서도 필요에 따라선 다른 도메인의 Service를 참조해 오기도 했고요. 사실 Service에서 다른 Service를 참조해오는 것은 Keeper가 아니라 타 프로젝트에서도 흔히 볼 수 있는 방식입니다. 그러나 Serivce에서 Service를 참조해오는 것이 좋다/Repository를 참조하는 것이 좋다에 관해서는 정답이 없는 부분이라 다른 프로젝트를 예시로 들기보다는 Doore의 상황에 따라 판단하는 것이 좋다 생각합니다. 일단 제 의견은 DooRe처럼 도메인간 확실한 상호작용이 필요한 코드에서는 어떤 도메인에 대한 기능이 해당 도메인에서만 일어나도록 관심사를 분리하는 것이 좋다는 것입니다. Service 레이어가 다른 Service를 의존해도 되는가에 대한 부분은 사실 제 의견보다는 전문가? 분들의 말씀이 더 신뢰가 갈 거 같아 링크 몇개를 첨부해봅니다,,,, (사실 제가 하고 싶은 말은 여기까지입니다. 아래는 미나님이 의문을 가지신 만큼 설명은 해드려야 할거 같아 일단 적어두긴 했지만 크게 중요한 부분은 아닙니다…) 특정 서비스의 메서드에서 발생한 문제가 해당 메서드를 사용한 다른 서비스에 영향을 미치는 경우에 대해
public void createTeam(final TeamCreateRequest request, final MultipartFile file, final Long memberId) {
MemberResponseDto memberDto = MemberQueryService.validateExistMember(memberId);
final Team team = Team.builder()
.name(request.name())
.description(request.description())
.imageUrl(imageUrl)
.build();
teamRepository.save(team);
teamRoleCommandService.assignTeamLeaderRole(memberId, teamId);
memberTeamCommandService.addMemerTeam(memberId, teamId);
} 이 부분은 public void createTeam(final TeamCreateRequest request, final MultipartFile file, final Long memberId) {
Member member = memberFindService.findById(memberId);
final Team team = Team.builder()
.name(request.name())
.description(request.description())
.imageUrl(imageUrl)
.build();
teamRepository.save(team);
createMemberTeam(member, team.getId());
assignTeamRole(team.getId(), memberId);
}
(createMemberTeam, assignTeamRole코드는 생략) (미나님이 제시하신 코드) 미나님이 제시해주신 코드의 미나님이 제시하신 코드를 따른다면 확실히 memberTeamCommandService의 그러나 이는 동일한 기능에 대한 관리 포인트를 나누게 됩니다. 만일 권한을 지정하는 코드나 팀원을 생성하는 코드의 로직이 수정될 경우 저희는 해당 기능이 존재하는 모든 Service코드를 찾아 직접 로직을 수정해야 할 것입니다. 또한 이러한 관리포인트의 분할은 오히려 문제의 발생 가능성을 높인다고 생각합니다. 미나님의 말씀대로 그러나 동일한 기능을 하는 메서드가 여러 Service에 중복되어 작성되어 있는 경우 한 Service의 정상 작동이 다른 모든 기능이 정상 작동을 보증할 수 없습니다. 권한 기능의 작동을 보장하기 위해서는 해당 기능이 작성되있는 모든 Service를 찾아 코드를 확인해야 할 것입니다. 여전한 중복의 발생 public void createStudy(final StudyCreateRequest request, final Long teamId, final Long memberId) {
validateExistMember(memberId); //회원 검증
validateExistTeam(teamId);
checkEndDateValid(request.startDate(), request.endDate());
final Study study = studyRepository.save(request.toStudy(teamId));
studyRoleRepository.save(StudyRole.builder()
.studyRoleType(ROLE_스터디장)
.studyId(study.getId())
.memberId(memberId)
.build());
participantCommandService.saveParticipant(study.getId(), memberId, memberId);
} (StudyCommandService) public void saveParticipant(final Long studyId, final Long memberId, final Long studyLeaderId) {
validateExistStudyLeader(studyId, studyLeaderId);
validateExistStudy(studyId);
final Member member = validateExistMember(memberId); //회원 검증이 두 번 발생한다.
final Participant participant = Participant.builder()
.studyId(studyId)
.member(member)
.build();
participantRepository.save(participant);
assignParticipantRole(studyId, memberId, studyLeaderId);
} (PartcipantCommandService) 이 부분은 제가 이해를 잘 했는지 모르겠습니다. 미나님이 제시한 대로라면 StudyService 아래에 오히려 현재 방식을 유지하며 도메인간 의존성 방향을 정하는 것이 검증을 중복해서 발생시킬 실수를 줄이지 않을까 생각이 듭니다.(Study→Participant로 의존성 방향을 잡아 최종적인 검증은 participant에서 발생하는 것으로)(물론 participant를 사용하지 않는 다른 study 메서드에는 해당되는 말이 아닙니다.) 메서드의 유연성에 관해
_<개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴>_에서는 의존성으로 발생할 수 있는 구현 변경의 비유연성에 대해 경고하며 이에 대한 해결 방안으로 캡슐화에 대해 설명합니다. 어떤 메서드가 다른 곳에서 호출될까 걱정하기보다는 오히려 재사용이 권장하는 코드를 작성하는 것이 좋지 않나 생각합니다. 재사용하기 좋은 코드란 내부 구현이 변경되더라도 기능을 사용하는 곳의 영향을 최소화 하는 것이고 이는 곧 유연성있는 코드를 뜻합니다. 유연성있는 코드를 작성하기 위해 사용하는 것이 캡슐화입니다. 캡슐화를 위해선 두 가지 규칙을 사용할 수 있습니다.
Tell, Don’t AskTell, Don’t Ask 규칙이란 데이터를 물어보지 않고 기능의 실행만을 요청하는 것입니다. if(member.getExpiryDate().getDate() < System.currentTimeMillis()) {
//만료 되었을 때의 처리
} (캡슐화가 되지 않은 코드) if(member.isExpired()) {
//만료 되었을 때의 처리
} (캡슐화된 코드) 이렇게 데이터가 아닌 기능을 요구하는 것으로 해당 기능의 구현을 캡슐화 하고 있습니다. 만일 만료 여부를 따지는 로직이 변경되더라도 캡슐화된 기능을 사용하는 코드의 수정은 불필요해질 것입니다. 데미테르의 법칙데미테르의 법칙은 Tell, Don’t Ask규칙을 지킬수 있도록 도와주는 규칙입니다. 데미테르의 법칙은 아래 세가지 규칙으로 이루어집니다.
if(member.getDate().getTime() < ...) { // 법칙 위반 (캡슐화가 되지 않은 코드) if(member.getTimeFromDate() < ...) { (캡슐화 된 코드) 메서드의 비유연성은 해당 메서드가 다른 서비스에서 온 것이기 때문에 발생하는 것이 아니라고 생각합니다. 데이터 은닉에 관한 문제다른 service를 참조한다고 해서 모든 service의 메서드를 public으로 만들 필요는 없다 생각합니다. 일부 메서드가 public하게 되는 건 다른 도메인과 자주 상호작용이 일어나는 권한, 팀원, 텃밭 정도가 되지 않을까요?? 그중에서도 일부는 Controller에서 사용되기에 이미 public하게 사용될 메서드라 생각됩니다. 또한 제가 제시한 방법은 신뢰성있는 객체를 위한 것이기도 하기에 메서드에서 순수한 객체가 아니라 DTO를 반환하길 권장하는 바입니다. 따라서 데이터 은닉에 대해서는 크게 걱정 안해도 될 것 같다 생각합니다. DTO를 통해 객체값이 변할 일은 없을테니까요. 순환 참조 문제전에 미나님이 작성해주신 리뷰에 제가 첨부한 포스팅에도 동일한 문제에 대해 언급하고 있습니다. 위 포스팅에서도 저의와 동일한 문제에 대해 고민하고 있는데
위 세가지 사항에 대해 고려하고 있습니다. 이때 3번 방식은 서비스 레이어의 의존성 방향을 정하고 파사드 패턴 (Facade Pattern)을 사용하는 것으로 순환 참조를 방지합니다. 그러나 사실 그냥 생각없이 서비스 레이어를 참조하더라도 저희가 도메인 설계를 잘 했다면 순환참조 문제는 발생하지 않지 않을까 생각합니다... 저희가 의도적으로 나눠둔 것은 아니지만 Member나 Role과 같은 Service는 명백하게 Study나 Team과 같은 도메인에는 의존할 일이 없으니까요. Curriculum같은 부분을 걱정하실수도 있을 거 같은데 이 부분은 저희가 Study라는 하나의 큰 도메인으로 묶어놓은 상태라 어차피 Service 참조가 아닌 DAO 참조를 할수밖에 없을 겁니다. (Study 객체를 생성하기 위해선 CurriculumItem객체가 필요함) 결론언급했다 싶이 제가 하고 싶은 이야기는 의존성 & 결합도에 대해 부분에 다 적혀있습니다. 일단 요약해보자면 아래와 같습니다.
|
Beta Was this translation helpful? Give feedback.
-
comment 잘 읽어보았습니다!😊
의존성에 대해팜님의 말씀처럼 '불필요한 객체 할당'으로 인한 의존성 감소는 이해하였습니다! 하지만 제가 앞서 말씀드렸던 컴파일타임 의존성은 여전히 해결되지 않은 것으로 보입니다. 예를 들어 의존이 발생하는 것은 동일하다.객체 지향을 정의하면, 의존성 관리입니다.
테스트의 어려움저희도 있었던 상황인 것 같은데요, curriculumItem에서 cropCommandService를 호출하며 crop 테스트 작성 시 curriculumItem에서 진행 + curriculumItem 상황이 다 갖추어 져 있어야 하는 문제가 발생한 적이 있었습니다! 😅😅 참조해주신 링크들우선 첫번째 링크는 제가 말씀드렸던 File이라는 특이 케이스인 것 같습니다. 결론
다음과 같은 이유로 여전히 같은 일을 처리하는 Service(각 Controller에서 사용되는 서비스 메서드)를 의존하는 방향으로 발생하는 문제를 해결해야 하는가 에 대해 좀 더 생각해보아야 할 것 같다는 의견입니다! 저도 여러가지 책이나 링크를 참조하고 싶었지만, 시간 이슈로 인해 제가 백엔드를 공부하며 정리해두었던 자료를 참고로 말씀드린 점 양해부탁드립니다.😢😢😢 (저도 여기까지가 제 답변이며 이후 내용은 팜님이 추가로 작성하셨던 부분에 대한 답변입니다.) 특정 서비스의 메서드에서 발생한 문제가 해당 메서드를 사용한 다른 서비스에 영향을 미치는 경우에 대해안정성 vs 관리포인트에 관한 말씀이신 것 같습니다. 이 부분은 저희가 간접 참조 방식을 사용하고 있는 부분을 예시로 들 수 있을 것 같아요~! 저희가 간접 참조를 이용함으로 인해 직접 삭제 로직을 다 구현해야하는 일이 발생하였습니다 (관리포인트가 많아지는 일) . 하지만 저희는 데이터베이스 최적화를 위해 관리포인트가 늘어나도 해당 방법을 채택하였습니다! 저는 그게 저희 서비스의 핵심이라고 생각합니다. 이 부분은 human issue가 발생할 위험이 크지만, 프로젝트 단위로써는 이득이 큰 방법을 선택하였던 것 처럼요! 여전한 중복의 발생
메서드 유연성에 관해이 부분에서 말씀하신 2가지 원칙은 저도 처음 들어보는 거라 공부해보겠습니다!😅😅 데이터 은닉 관한 문제저도 이 부분에 대해서는 이미 순환 참조 문제제가 잘 이해한 건지는 모르겠으나 해당 링크의 3번에서 말한 방식이 제가 제안드린 방식 같은 느낌이었습니당! |
Beta Was this translation helpful? Give feedback.
-
#184 (comment)
미나님이 남겨주신 해당 리뷰를 보고 Doore 코드의 의존관계에 대한 생각을 해보았습니다!
특정 도메인의 서비스레이어가 다른 도메인의 서비스코드를 호출한다는 점에 초점을 맞춰 도메인간 분리가 가능할지 의문이라 답했는데, 오늘 다시 한 번 생각해보니 서비스레이어간 분리가 아니라 레파지토리 레이어 관점에서의 도메인 분리가 필요할거 같다는 생각이 들었습니다.
예를 들자면 아래와 같은 코드가 있습니다.
(이해의 편의를 위해 FILE과 관련된 코드는 삭제했습니다.)
위 코드는 Team 도메인의 서비스 코드입니다.
그러나 Team 뿐만 아니라 다른 도메인의 Member와 memberTeam, TeamRole을 모두 날것의 엔티티 그대로 조회하고 있습니다.
이는 Team 도메인에서 더티체킹으로 인해 다른 도메인의 엔티티의 상태를 변경을 발생시키는 실수가 일어날 수 있다는 뜻입니다.
실제로 Team 도메인에서 TeamRole, memberTeam 도메인의 상태를 변경시키고 있고요
저희는 한 도메인이 자신의 역할에만 집중할 수 있도록 간접 참조를 사용하고 있습니다.
(Team은 MemberTeam과 다대다 관계에 있습니다.)
그러나 위와 같이 Team 도메인이 자신과 관계없는 Member, memberTeam, TeamRole의 다른 도메인의 엔티티를 조회하여 실수의 가능성을 높이고, 실제 변경을 일으키는 것은 설계 의도와 맞지 않다 생각합니다.
제가 도메인간 의존성을 낮추고, 신뢰성있는 엔티티를 만들기 위해 제안하는 방법은 아래와 같습니다.
이러한 방식을 사용하면 도메인간 독립성을 높힐 수 있을 뿐더러 캡슐화를 시킬수도 있고, 엔티티가 신뢰성을 가지며, validateExistMember와 같이 여러 도메인에서 사용하고 있는 코드이 중복을 줄일수 있다는 장점이 있습니다.
다른분들의 의견이 궁금합니다!
Doore에 대한 설계를 시작할때는 제가 백엔드에 막 입문한 뉴비였던 터라 잘 몰랐는데 그때 저희가 사용한
도메인형 패키지 구조
와 설계방식이 DDD 모델과 굉장히 흡사한거 같더라구요DDD 모델을 적용해 애그리거트를 사용하는것도 도메인 간 분리에 크게 도움이 될 거 같은데 이 부분은 구조가 전체적으로 수정될거 같아, 일단 서로다른 도메인의 서비스레이어와 레파지토리가 분리가 된 이후 의논해보는 것도 좋을 거 같습니다.
https://mangkyu.tistory.com/318
Beta Was this translation helpful? Give feedback.
All reactions