강의

멘토링

로드맵

인프런 커뮤니티 질문&답변

roovies님의 프로필 이미지
roovies

작성한 질문수

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

강의 자료

UseCase 메서드 단위에 대한 Best Practice

작성

·

40

0

안녕하세요! 토비님.

헷갈리는 개념이 하나 있어서 여쭤보고 싶습니다.

바로 헥사고날 아키텍처에서 UseCase의 책임 범위인데요.

우선 정답은 없다는 것은 알고 있습니다. 다만 Best Practice나 권장되는 방법이 있는지, 그리고 토비님의 고견이 궁금하여 질문드리게 되었습니다.

 

기능 단위로 UseCase 인터페이스 분리하기 vs 연관된 기능은 UseCase 인터페이스에 묶음으로 제공하기(메서드별로)

입니다.

 

전자는 SRP가 매우 엄격하게 준수되고, 테스트 용이성, 개별 인터페이스별로 정책을 다르게 적용할 수 있다는 장점들이 있지만 과도하게 인터페이스화를 하다 보니 관리할 포인트가 많아져 복잡해진다는 게 단점인 것 같습니다.

 

후자는 SRP가 엄격하게 준수되지 않더라도, 관련된 기능을 응집도 있게 관리하기 때문에 테스트 용이성이 조금 떨어지고, 일관된 정책을 관리하거나 인터페이스가 비대해질 수도 있다는 단점이 있지만, 응집도 있게 관리하여 유지보수에는 편한 장점이 있는 것 같습니다.

 

코드를 예시로 보면 아래처럼 콘서트를 조회한다고 했을 때, 일반적으로 PK를 기반으로 조회하지만,

아래와 같이 콘서트명도 unique하고, 가수도 1개의 진행 중인 콘서트만 가지고 있을 수 있을 때 조회 조건이 Id, Name, ArtistName으로 분류될 수 있다고 예시를 들어보겠습니다.

public interface GetConcertUseCase {
    ConcertResult findById(Long concertId);
    ConcertResult findByName(String name);
    ConcertResult findByArtistName(String ArtistName);
    ConcertResult findByIdWithSchedules(Long concertId); // Aggregate Member인 ConcertSchedule 목록 정보도 포함하여 조회
}

위에처럼 구성하는 게 후자 방식이고 응집도가 높다고 생각합니다.

 

그런데 해당 방식은 유스케이스가 비대해질 수 있고, 단일 책임 원칙에서 벗어날 수 있다는 의견 때문에 조회 목적별로 유스케이스 분리하는 것을 권장하는 의견도 있습니다. (전자 방식)

public class GetConcertByIdUseCase { ... }
public class GetConcertByNameUseCase { ... }
public class GetConcertByArtistUseCase { ... }
public class GetConcertByIdWithSchedulesUseCase { ... }

 

정답은 없어서 프로젝트 규모나, 각자의 스타일, 기능 분석에 의해 정해지겠다만, 보편적으로 이런 경우 어떻게 접근하는 게 Best Practice인지 감이 잡히질 않아 질문드리게 되었습니다.

 

답변 2

0

토비님의 프로필 이미지
토비
지식공유자

헥사고날 아키텍처에서의 포트는 인터페이스로 표현됩니다. 인터페이스 이름에 UseCase라는 접미사를 붙이는 유행이 있긴하지만 이것은 별로 좋은 네이밍은 아닙니다. 질문하신 내용은 헥사고날 아키텍처의 경계로 노출되는 포트의 범위를 어떻게 잡는가라고 이해하고 제 생각을 말씀드릴게요.

포트는 헥사고날 애프리케이션을 액터가 사용하는 의도(intention)를 담고 있어야 하고, 이 의도를 기준으로 정의합니다. 물론 의도를 세분화해서 쪼갤 수도 있고, 유사한 의도를 묶어서 하나의 포트로 정의할 수 있습니다. 그에 따라 포트를 표현한 인터페이스의 메소드 갯수가 달라지겠죠.

보통 비즈니스 로직을 담고 있는, 특히 변경이 일어나는 기능을 담은 것은 의도를 묶기가 상대적으로 쉽습니다. 반면 예로 드신 조회 로직은 이보다 세분화될 수 있고, 그래서 그룹핑할 수 있는 단위를 정하기가 애매하기도 합니다.

가장 쉽고 이상적인 건 모든 인터페이스가 하나의 메소드만 가지는 것입니다. 하지만 이러면 코드가 장황해지겠죠. 특히 조회처럼 세부 조건이 달라질 때마다 메소드를 추가하기도 하는 경우에는 이런 구분이 오히려 코드를 이해하는데 인지 부하를 더 줄 겁니다.

일단 응집도나 SRP는 오브젝트의 설계 원칙이므로 인터페이스레 이를 적용하는 건 적절하지 않습니다. 응집도가 높으면 변경이 일어날 때 전체가 같이 변경되어야 합니다. 조회 인터페이스에 이를 기계적으로 적용한다면 메소드 시그니처 하나 변경할 때 나머지도 변경되어야 응집도가 높은 것인데, 그렇기는 쉽지 않죠. 하지만 그렇다고 이게 설계의 문제가 되지는 않습니다. 대체로 이런 조회 인터페이스를 구현한 오브젝트는 구현체 내부가 대부분 독립적이라서 일부분만 변경된다고 해서 변경 전후에 코드를 이해하는데 어려움이 생기거나 나머지 코드에 영향을 줄 가능성이 적기 때문입니다.

SRP는 이런 경우라면 좀 크게 해서하는게 적절합니다. 이 인터페이스의 구현 클래스는 "Concert를 조회한다"는 이유 하나 때문에만 변경된다면 그것으로도 SRP가 잘 충족되었다고 볼 수 있습니다. 너무 세부 조회로직 구현의 변경에 SRP를 기계적으로 적용한다고 더 나은 설계가 될 것으로 보이지 않습니다.

그보다는 조회 인터페이스는 ISP가 더 중요할 수도 있습니다. 이걸 사용하는 클라이언트가 항상 전체를 같이 사용할 가능성이 높은가, 아니면 사용 용도에 따라서 클라이언트가 명확히 구분되는가겠죠. 이건 한번쯤 생각해보면 어떻게 분리하는 것이 적절한지가 그려집니다. 그에 따라 테스트를 만드는 것도 자연스러워지겠죠. 하지만 이게 여러 다른 모듈에서 사용되어질 가능성이 높은 공통 정보를 다루는 경우라면 클라이언트를 제한하기가 어려울 수도 있긴 합니다.

또, API 설계에 따라서 이걸 단순하게 바꿀 수도 있습니다. ID로 조회하는 것은 이미 명확하게 어떤 엔티티에 대한 레퍼런스를 확보해서 빠르게 결과를 성공적으로 얻어오는 케이스에서 쓰이겠지만, 나머지 3개는 좀 더 검색 목적의 조회일 가능성이 있기도 하겠네요. 이런 경우 파라미터를 이용해서 좀 더 동적이고 확장 가능한 조회 로직으로 설계할 수도 있을 겁니다. 이러면 메소드가 2개가 될 수도 있겠죠.

결국 최종 결정은 액터 혹은 클라이언트가 이 포트를 구현한 애플리케이션을 어떤 의도를 가지고, 어떤 단위로, 어느 시점에 사용할 것인지, 또 이후에 얼마나 더 많은 조회 로직이 추가될 것인지 등을 기준으로 결정하면 될 것 같습니다.

지속적으로 추가 변경될 가능성이 높다면 꾸준히 이를 다듬어 가도 좋을텐데, 그렇다면 일단은 조금 큰 단위로 만들고 시작하는게 좋겠습니다. 당연히 인터페이스를 한번에 크게 만들지는 말고 필요할 때마다 추가해나가는 거죠. 테스트도 마찬가지고요. 그러다 쓰임새가 구분이 되는 느낌이 들고, 구현 부분이 달라지는 메소드가 드러난다면 그때 분리해도 좋습니다.

반대로 인터페이스를 미리 세분화하고 시작한다면 구현 클래스는 일단 하나로 만들었다가 구현이나 의존성에 따라서 구현 클래스를 구분해도 됩니다. 그런데 같은 클라이언트가 이렇게 구분한 인터페이스 이러개를 매번 구현하는 모습이 보이면, 그때는 클라이언트를 기준으로 묶는 것이 필요합니다. 이것도 사실 테스트까지 잘 만들어져 있다면 별로 어려운 리팩터링은 아닙니다만, 초반에 좀 번거롭게 시작하는 느낌이 들기도 하죠.

우선은 이정도로 설명을 드리겠습니다. 이후 강의에서 제가 어떻게 초기에 설계를 하고 이를 단계적으로 바꿔가는지 보여드리고 싶은데, 그때 기회가 되면 이에 대한 언급을 더 해보겠습니다.

roovies님의 프로필 이미지
roovies
질문자

감사합니다 🙂

0

안녕하세요, 인프런 AI 인턴입니다. roovies님께서 헥사고날 아키텍처에서 UseCase 인터페이스의 설계 방법에 대해 고민하고 계시군요. 여기서는 어떤 선택이 더 나은지, 각각의 장단점을 구체적으로 살펴보겠습니다.

UseCase 인터페이스의 구조화 - 두 가지 접근법

  1. 기능별로 UseCase를 분리하는 방식 (전자 방식):

    • 장점:
      • SRP(단일 책임 원칙) 준수: 각 UseCase는 하나의 명확한 책임을 가집니다. 이는 코드의 가독성을 높이고, 각 기능에 대한 테스트 용이성을 증가시킵니다.
      • 정교한 제어: 각 UseCase에 대한 정책과 검증 로직을 개별적으로 구성할 수 있어 변화에 민첩하게 대응할 수 있습니다.
    • 단점:
      • 관리할 UseCase가 많아짐에 따라 프로젝트의 복잡도가 증가할 수 있습니다.
      • 각 UseCase가 독립적이기 때문에, 연관된 로직이 있어도 이를 적절히 묶기 어려운 부분이 있습니다.
  2. 관련된 기능을 하나의 UseCase 인터페이스에 묶는 방식 (후자 방식):

    • 장점:
      • 응집도 증가: 비슷한 기능들을 하나의 UseCase 인터페이스로 묶음으로써 관련 있는 로직을 쉽게 관리할 수 있습니다.
      • 간결한 구조: 인터페이스가 비대해질 위험이 있지만, 프로젝트 전체적인 구조는 단순해질 수 있습니다.
    • 단점:
      • SRP 위반 가능성이 있으며, 관심사가 다양한 코드가 한 클래스에 포함될 수 있습니다.
      • 테스트와 유지보수 시 복잡도가 증가할 수 있으며, 변경이 있을 때 문제점이 발생할 가능성도 증가합니다.

토비님의 조언

토비님과 같은 전문가들은 특정한 Best Practice를 강력하게 추천하기보다는, 도메인의 특성을 고려해 설계하는 것을 강조합니다. 간단한 구조에서 시작하여 점차적으로 복잡도가 증가할 때, 애플리케이션의 필요성에 맞게 구조를 조정하는 것이 권장됩니다.

  • 핵심은 유연성: 프로젝트의 초기 단계에서는 응집도를 고려하여 연관된 기능을 묶어도 좋으며, 나중에 코드의 복잡도가 증가하거나 특정 기능이 확장될 때 더 세분화하는 것도 전략입니다.
  • 실제 운영 중인 시스템의 요구 분석: 각 UseCase가 나중에 얼마나 변화할 가능성이 있는지를 따져보는 것도 중요합니다. 변화 가능성이 큰 기능들은 개별적으로 사용하되, 그렇지 않은 부분들은 응집도를 높이는 것도 방법입니다.

관련 유사 질문을 참고하시면 도움이 될 수 있습니다:
- UseCase와 SRP에 대한 오해
- 헥사고날 아키텍처의 UseCase 작성법

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

roovies님의 프로필 이미지
roovies
질문자

두 유사 질문 링크 404 뜹니다.

roovies님의 프로필 이미지
roovies

작성한 질문수

질문하기