본문 바로가기

난 iOS개발자/iOS

클린아키텍처 적용하기-03 몇가지 팁과 고민들

728x90

클린아키텍처 적용하기-02 테스트 

이전 글에 이어서 마무리하는 글. 

Repository은 그저 중간자 역할뿐인가? 꼭 필요한가?

데이터 엑세스를 하는 Datasource와 UseCase를 연결하기 위한 adaptor 역할이라고 볼 수 있습니다. Datasource와 직접 연결해도 되는것을 불편하게 하나의 과정을 더 밟는게 아닌가 싶지만, 이는 데이터소스의 구체적인 구현 세부사항을 비즈니스 로직으로부터 숨길수 있게 해줍니다.

일관된 인터페이스를 제공해서 비즈니스 로직이 Datasource의 구체적인 구현에 의존하지 않도록 하는것입니다. 때문에 Datasource를 변경하거나 업그레이드 되어도 비즈니스 로직에 영향을 주지 않습니다.

또 데이터 소스로부터 받은 데이터를 애플리케이션이 사용하기 적합한 형태로 변환하는것도, Repository의 역할입니다.

어디까지 UseCase로 만들어야 할까?

시스템의 모든 유스케이스를 캡슐화하고 구현한다고 했습니다만 저 역시 가장 고민이 많이 되었던 부분이기도 합니다. 나름의 기준을 정리해보자면 재사용성이라던지 앱의 핵심정책과 같은 내용을 UseCase 로 만드는게 적합해 보입니다.

UseCase 는 도메인 영역이기 때문에 자주 변경되지 않는 로직이어야 합니다.

아래 UseCase 에 적합한 코드 예시입니다.

 

데이터 처리

  • 데이터 CRUD 작업은 전형적인 유스케이스입니다. 예를 들어 사용자 프로필을 업데이트하는 로직은 유스케이스로 분리될 수 있습니다.

비즈니스 규칙

  • 예를 들어 할인율을 계산하거나 주문합계를 결정하는 로직이 여기에 해당합니다. 주문합계나 할인율 계산 같은 로직에 대한 검증도 중요하기 때문에 테스트를 고려하여 UseCase로 분리하여 관리하는것도 좋은 방법입니다.

워크플로우나 트랜잭션 관리

  • 여러 단계를 거치는 복잡한 프로세스는 유스케이스를 통해 관리될 수 있습니다. 예를 들어, 주문 처리 과정에서 결제, 재고 확인, 결제 완료 등의 단계를 포함하는 유스케이스를 만들 수 있습니다. 이때 UseCase 를 내부적으로 중첩해서 사용한다면 그 복잡성이나 관리가 어렵게 꼬여가고 있는건 아닌지 주의해야합니다.

UI관련 코드는 도메인 범위를 벗어나기 때문에 UseCase로 만들기에 적절하지 않습니다. 또 데이터베이스 엑세스 코드는 데이터레이어 에서 처리되기 때문에 UseCase로 만들지 않습니다.

UseCase를 만들때 아래 내용을 참고해보세요.

  • 단일 책임 원칙을 위배하지 않는가
  • 각 UseCase는 하나의 기능이나 책임에 집중해야 합니다. 이는 테스트와 유지보수를 용이하게 합니다.
  • 재사용 가능성이 있는가
  • 다양한 상황에서 동일한 비즈니스 로직을 활용할 수 있다면 생산성 향상에 도움이 됩니다.
  • 독립성이 있는가 UseCase는 다른 UseCase나 시스템의 다른 부분과 가능한 한 독립적이어야 합니다. 이는 테스트를 단순화하고 시스템의 결합도를 낮춥니다.

UseCase 중첩해도 될까?

설치버전이 최신 버전보다 낮은지 확인하는 로직이 필요하다고 가정해보겠습니다. 이때 UseCase 관리 방법은 크게 두 가지로 나뉠것 같습니다.

첫번째는 이전 포스팅에서 테스트 용이성 에서 소개한 내용 처럼 DetermineUpdatePolicyUseCase 를 사용하는 방법입니다.

GetLastestVersionUseCase 와 GetInstalledVersionUseCase 를 따로 호출, 반환값을 매개변수로 전달하는 방법입니다. 버전을 가져와야하는 처리는 다른 UseCase에서 완료하고, 비교할 값만 전달하기 때문에 DetermineUpdatePolicyUseCase 에서는 버전을 비교하기만 하는 수준의 코드가 들어가게 됩니다.

 

두번째는 GetLatestVersionUseCase , GetInstalledVersionUseCase 를 DetermineUpdatePolicyUseCase 안에서 관리하여 반환값만 전달받는 방법이 있을 수 있습니다.

여러단계의 프로세스를 직접 관리하지 않고 DetermineUpdatePolicyUseCase 가 알아서 처리하고 그 결괏값만 받는 방법입니다.

사용자 입장에서는 DetermineUpdatePolicyUseCase 의 반환값만 확인하면 되기 때문에 편리하게 사용할 수 있을것 같습니다.

 

어느방법을 선택하든 나쁘지 않은 선택이라 생각되지만, 개인적으로는 전자의 방법을 택할것 같습니다.

UseCase를 조합하고 관리하는것은 Presentation 영역이 맡은 역할입니다.

첫번째 방법을 택하자면 개발자 입장에서 편할수 있으나, 중첩된 UseCase에 대한 에러처리를 좀 더 세분화해서 처리하기 어려울수 있다는 판단입니다.

또 UseCase 는 그 이름만으로도 내부구현을 어느정도 예측할수 있습니다. 중첩된 UseCase 로 인해서 내부 구현의 복잡성이나 관리하기 힘든 구조가 되지 않을까 우려스러운 부분도 있습니다.

Presentation 영역에서 여러개의 UseCase 를 연관지어 사용할 때 그 복잡도가 리스크를 안겨줄 정도가 아니라면 UseCase를 세분화하여 사용자(Presenation영역)가 컨트롤하는것이 괜찮아 보입니다.

 

이 내용을 팀원에게 공유했을때 팀원은 이런 의견을 내놨습니다.

"DetermineUpdatePolicyUseCase을 사용하기 위해 다른 UseCase를 뷰모델에서 조합해야 한다면 추상화가 덜 된 케이스라고 생각합니다."  개발자는 내부 구현에 의존하지 않고 인터페이스를 구현해야 한다는 의견이었습니다.

 

이처럼 보는이 마다 관점이 다를수 있기 때문에 논쟁의 여지가 있으며 규칙을 정해 나가면 될 문제로 보입니다.

 

DTO를 애플리케이션에서 사용할 도메인모델로 변환하는것은 어느시점에?

모델간의 변환은 데이터 엑세스의 일부로 볼 수 있기 때문에 레파지토리에서 그 역할을 하면 좋습니다. Repository가 Datasource로부터 받은 데이터를 애플리케이션의 도메인 모델에 맞게 변환하는 책임을 가짐으로써, 비즈니스 로직이 Datasource의 구체적인 구현에 대해 알 필요가 없도록 합니다.

도메인 영역에서 모델변환을 하면 어떤 문제점이 생길까요? 비즈니스 로직을 처리하는것이 핵심기능이지만 데이터 모델 변환의 역할까지 갖게 된다면 UseCase의 과도한 책임을 부여하는것으로 볼 수 있습니다. 게다가 특정 데이터 모델에 더 강하게 결합되는 문제가 발생합니다. 유연성을 저하시키게 되는것이죠. 데이터 변환을 분리함으로써 UseCase의 테스트가 보다 집중적이고 단순화할수 있습니다.

어려운 의존성 주입

고민중입니다. 위 본문에선 초점을 두지 않았지만, 작업하며 가장 걸림돌이 되었던것이 의존성 주입에 관한 내용이었습니다.

위에 작성했던 LatestVersionUseCaseImpl 로 예시를 들어보겠습니다.

서버로부터 최신 버전을 받아와야 하는 역할인 경우 데이터 엑세스를 위해 Repository와 DataSource 가 필요합니다.

각 객체들은 생성시 의존성을 주입받게 되었습니다.

그러므로 인스턴스를 생성할 때마다 필요한 재료들(의존성)을 주입해야 하는 코드가 한 없이 늘어나는 경우가 생겼습니다.

이 문제를 해결하기 위해 팩토리 메서드를 작성해봤습니다.

static func make() -> GetAlanTokenUseCaseImpl {
	.init(authorizationRepository: AuthorizeAlanTokenRepositoryImpl.make())
}

인스턴스 생성시 의존성을 직접 주입하지 않고 make()를 통해서 생성하는 방법입니다.

생성로직을 캡슐화하고 편리하게 사용할 수 있다는 장점이 있으나 다른의존성을 주입해야할 때는 유연성이 떨어지는 단점도 있습니다. 하지만 체감상 불편함을 느끼진 않았습니다.

이 방법 말고도 DI컨테이너를 활용하는 방법도 있습니다만 그것은 학습이 필요합니다.

싱크홀(architecture sinkhole) 안티패턴

요청이 한 레이어에서 다른 레이어로 이동할 때 각 레이어가 아무 비즈니스 로직도 처리하지 않고그냥 통과시키는 안티패턴을 말합니다.

예를 들어, 유저가 기본 고객데이터를 조회하는 단순 요청을 하면 프레젠테이션 레이어가 응답하는 아키텍처가 있다고 합시다. 프레젠테이션 레이어는 도메인 레이어에 요청을 전달하고, 도메인 레이어는 아무 일도 하지 않고 다음 레이어로 요청을 합니다. 쭉쭉 이동하여 DB에서 조회한 데이터가 다시 데이터 변환 등 일체의 로직 없이 다시 왔던 길을 거꾸로 돌아갑니다. 

 

이런 흐름은 불필요한 객체 초기화 및 처리를 빈번하게 유발하고 쓸데없는 메모리를 소모하게 합니다.

하지만 아키텍처 싱크홀 안티패턴에 해당하는 시나리오가 전무한 계층형 구조는 아마 하나도 없을 겁니다.

 

전반적으로 내 앱의 구조를 점검했을 때, 전체 요청의 20%이하가 싱크홀인 정도면 그런대로 괜찮은 수준입니다.(80대 20법칙)

그러나 80%가 싱크홀이라면 해당 도메인에는 이런 계층형 구조가 적합한 아키텍처가 아니라는 증거입니다.

728x90