-
[Agarang] 지금까지 내가 잘 못 알고 있었던 점 (1) - 문제제기개발 ing 2025. 2. 8. 01:29
해당 글은 SSAFY12기 공통 프로젝트 Agarang 서비스를 만들면서 했던 내용이다
이 글은 코드 작성 과정에서 내가 고민했던 점과 잘못 알고 있었던 개념을 정리한 글이다.
일단 코드를 한번 보자
BabyService 클래스의 로직이다.
public BabyResponse createBaby(Integer userId, BabyRegisterRequest babyRegisterRequest, MultipartFile multipartFile) { User user = userRepository.findById(userId) .orElseThrow( () -> new BusinessException(ErrorCode.USER_NOT_FOUND) ); String image = getImage(multipartFile); Baby baby = new Baby( babyRegisterRequest.getName(), babyRegisterRequest.getBirth(), babyRegisterRequest.getSex(), image ); Baby saveBaby = babyRepository.save(baby); Custody custody = new Custody(baby, user, CustodyType.MAIN); custodyRepository.save(custody); return babyMapper.mapToResponse(saveBaby); } public BabyResponse updateBaby(Integer userId, BabyUpdateRequest babyUpdateRequest, MultipartFile multipartFile) { User user = userRepository.findById(userId) .orElseThrow( () -> new BusinessException(ErrorCode.USER_NOT_FOUND) ); Baby baby = babyRepository.findByBabyIdAndFlagDeletedIsFalse(babyUpdateRequest.getBabyId()) .orElseThrow( () -> new BusinessException(ErrorCode.BABY_NOT_FOUND) ); Custody custody = custodyRepository.findByUserAndBabyAndDeletedAtIsNull(user, baby) .orElseThrow( () -> new BusinessException(ErrorCode.CUSTODY_NOT_FOUND) ); if (!CustodyType.MAIN.equals(custody.getCustodyType())) { throw new BusinessException(ErrorCode.CUSTODY_NOT_AUTHORIZED); } updateImage(babyUpdateRequest.getBeforeImage(), multipartFile, baby); baby.setName(babyUpdateRequest.getName()); baby.setBirth(babyUpdateRequest.getBirth()); baby.setSex(babyUpdateRequest.getSex()); return babyMapper.mapToResponse(baby); }
코드를 보면 비즈니스 로직이 있는 Service 클래스의 메서드들이다.
위에 코드를 작성하면서 들었던 의문점을 볼 것이다.
- xxxx.findById() 의 중복 코드가 존재 한다
- 현재 이 Service 의 domain은 Baby인데 다른 domain의 Repository를 참조해서 사용하는게 맞을까?
- 현재 이 Service 의 domain은 Baby인데 다른 도메인인 Custody 를 생성도 하고 있다 이게 맞을까?
일단 위에 의문점 1번 과 2번을 볼것이다. 3번에 대해서는 나중에 설명 하겠다.
그리고 내가 왜 이렇게 작성 했는지에 대한 기존에 알고 있었던 잘못된 지식 이다.
- 3계층 아키텍처 : Controller -> Service -> Repository 에 너무 의존해 있었다.
- Service 계층 안에서 비즈니스 로직을 나누기 위해 여러 개의 repository를 참조해서 사용했다.
- 왜냐하면 service 간의 결합도나 계층 구조 때문이라고 생각했다.
쉽게 말하자면 Spring에서는 크게 Controller, Service, Repository 계층이 존재하고 이게 흔히 사용하는 패턴이다.
그래서 나는 같은 layer 끼리 참조를 하면 안된다고 생각 했었다. (같은 layer 끼리 결합도 때문이라고도 생각 했었다.)
이런 이유로 한 Service 클래스가 다른 Repository 클래스를 가지고 비즈니스 로직을 구현한다.
다시 정확하게 알게 된 지식이다.
새로 알게된 지식 (고쳐 먹게 된 지식)
- Service 계층 안에서 비즈니스 로직을 나누기 위해 여러개의 서비스를 둘 수 있다.
- 서로 다른 도메인 로직을 담당하는 서비스가 필요에 따라 서로 호출하는 것도 흔히 일어나는 패턴이다.
- 하지만 여기서 중요한 점은 단방향 의존이여야 한다.
- BabyService -> UserService 를 참조 하고 또 UserService -> BabyService 를 호출 한다면 순환 의존성이 생겨난다.
- 일반 적으로 계층이 깨진다는 말은 다음과 같다
- 하위 레이어를 상위 레이어를 역으로 호출하는 상황 ex) Repository -> Service
- 상위 레이어가 하위 레이어를 직접 호출하는 상황 ex) Controller -> Repository
결합도 문제는?
Service에서 Service를 참조하면 당연하게 결합이 생겨난다
하지만 연관관계를 가지고 있는 다른 도메인 클래스만을 호출 한다는 점은 대부분의 서비스 간 호출에서 자연스럽게 발생한다.
- User 관련된 로직(예: User를 조회하고, 없으면 예외를 던지는 로직)을
별도의 UserService가 관리하도록 하여, BabyService 안에서 중복을 줄이는 이점이 더 크다.
위 글을 정리하자면
1. 같은 Layer 끼리 참조하면 안된다 → ❌ 잘못된 개념
- 틀린 말은 아니지만 중요한 것은 결합도를 낮추는 방향으로 설계가 되어야 한다.
- Service 간의 참조는 허용되지만 단반향 의존 관계가 유지되어야 한다.
2. 비즈니스 로직을 나누기 위해 여러 개의 Repository를 참조했다 → ❌ 잘못된 개념
- Repository는 엔티티를 저장하고 조회하는 역할이고, Service는 비즈니스 로직을 수행하는 계층이다.
- 여러 개의 Repository를 참조할 수 있지만 이것이 비즈니스 로직의 흐름을 복잡하게 만들거나 특정 Service가 많은 역할을 맡게 되면 문제가 된다
- 따라서 Repository를 여러 개 참조하는 것이 문제가 아니라 비즈니스 로직이 명확하게 분리되지 않는 것이 문제이다.
3. 서비스 간 호출을 하면 안된다 -> ❌ 잘못된 개념
- Spring에서는 계층 간 의존성 관리뿐만 아니라 서비스 간 호출을 허용하는 방식도 일반적이다.
- Service 간 호출 시에도 역할이 명확해야 한다
- 예를 들어 BabyService가 UserService를 호출하는 것은 자연스럽지만 UserService가 다시 BabyService를 호출하면 순환 참조(Circular Dependency) 문제가 발생할 수 있다.
- “서로 다른 도메인 로직을 담당하는 서비스가 필요에 따라 호출할 수 있지만 단방향 의존성을 유지해야 한다.”
4. 결합도 문제
- 서비스 간 결합도는 낮추는 것이 중요하지만 완전히 분리하는 것보다 적절한 관계를 유지하는 것이 더 중요하다.
- 오히려 User 관련 로직을 UserService가 관리하면 중복이 줄어들고 코드 재사용성이 높아진다.
- 위에 코드 처럼 도메인 로직이 섞이는 경우 오히려 유지보수성이 떨어질 수 있다.
이에 대한 내 생각
- 객체 지향 적으로 역할과 책임을 생각 한다면 user를 찾기 위해 repository의 findById()를 호출하고 exception를 던지는건 UserService 클래스에서 수행 하는게 더 자연스러운 현상인 것 같다.
- 계층 구조에 대해서 다시 한 번 이해 하게 되었다.
- AService 에서 BService를 참조 한다는 건 흔히 일어나는 패턴이지만 순환 참조만 피하면 된다는 것을 알게됨
다음 글은 이런 문제를 가지고 어떻게 해결 할건지에 대한 생각과 해결 방법을 설명할 것 이다.
이 글을 읽고 다른 개발자 분들은 어떻게 생각하는지 댓글에 자유롭게 작성 해줬으면 좋겠다. 토의 해보고 싶다.
'개발 ing' 카테고리의 다른 글
[Agarang] 지금까지 내가 잘 못 알고 있었던 점 (2) - 아키텍처 재구성 (0) 2025.03.02 [DOSI:RAK] 부하테스트 시작 - CPU, 메모리, 디스크 (0) 2025.02.17 [DOSI:RAK] “DOSI:RAK 서비스 개발 : Spring EventListener로 객체지향 설계 문제 해결하기” (1) 2024.12.16 [Backend] application.properties, application.yml 보안 관리와 협업 효율성 높이기 (0) 2024.12.10 [Solitour] 비용과 성능의 트레이드오프 (2) 2024.10.13