강의

멘토링

로드맵

Inflearn brand logo image

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

MASKUN님의 프로필 이미지
MASKUN

작성한 질문수

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

회원 애플리케이션 기능 추가

동일 계층의 애플리케이션 서비스가 서로의 인터페이스를 사용하여 의존하는 경우에 대해서

해결된 질문

작성

·

1.2K

·

수정됨

5

안녕하세요? 존경하는 토비님 작은 질문하나 드리겠습니다.

 

회원 애플리케이션 기능 추가의 25:00~26:00 에서는 MemberFinder 인터페이스를 정의하고 기존 MemberService에 혼재된 멤버 조회로직을 CQRS에 따라 구현클래스를 분리했습니다. 그리고 이를 MemberModifyService에서 사용하고 있습니다.

말씀 주신 것처럼 단순히 조회이고 변경이 없다는 가정하에 이처럼 인터페이스를 정의하고 이를 통해 사용을 하는 것은 잘 이해가 됩니다!

다만 저는 같은 계층에서 있는 서비스 빈이 서로를 의존하지 않는 방향으로 개발을 해오고 있었는데요. 그래서 토비님의 방식에 대해 공부하면서 같은 계층에서 인터페이스 포트를 통해서 호출하는 것은 괜찮을까 고민이 들었습니다.

만약 그게 설계적으로도 문제가 없다면 앞으로도 토비님의 방식으로 개발하고 싶은데요. 분명 토비님께서 오랫동안 고민하신 개발 원칙/기준이 있을 것 같아서 여쭤봅니다.

같은 계층에 있는 애플리케이션 서비스 빈이 서로를 의존, 또는 인터페이스를 통해 사용되는 경우 지켜야할 기준이나 원칙이 있을까요?



*추신
저는 토스의 김재님의 [블로그](https://geminikims.medium.com/%EC%A7%80%EC%86%8D-%EC%84%B1%EC%9E%A5-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EA%B0%80%EB%8A%94-%EB%B0%A9%EB%B2%95-97844c5dab63)를 접해서 위와 같이 개발해오고 있었습니다. 저는 계층을 하나 두고 유틸성 빈을 만들고 이를 사용하게끔 했습니다.

(블로그내용 아래 일부 발췌)
> 네 번째 규칙
동일 레이어 간에는 서로 참조하지 않아야 한다.
(다만, Implement Layer는 예외적으로 서로 참조가 가능합니다.)

답변 2

4

안녕하세요 :D 토비님에게 해당 질문에 대해서 전해들어서 저도 짤막히 생각을 달아봅니다🙂

그 전에 먼저 오래 된 글인데도 봐주시고 언급까지 해주셔서 감사합니다!


핵심 질문에 대한 얘기를 먼저 해보면 (다만,, 제가 현실이 너무 바빠서 강의를 아직 못 봤습니다ㅜㅜ 이 점 양해를... 토비님 사랑합니다)

같은 계층에서 인터페이스 포트를 통해서 호출하는 것은 괜찮을까 고민이 들었습니다.

 

적어 주신 질문 글의 맥락으로 보면 저런 케이스의 호출에 대한 것은 전략을 정할 수 있는 영역으로 보입니다. 코드를 못 봤지만 맥락 기준으론 허용이 충분히 가능하다고 봅니다, 특히 구현체가 아닌 인터페이스 기반이라면 더욱 허용이 쉬워진다고 생각합니다.

 

다만, 이런 호출의 기반에는 올바른 개념(도메인)의 정립과 아래 설명한 '격벽'(단어와 상관없이 개념간 경계 영역 의 대하여 고민이 선제 되어야한다 생각합니다.

(단순히 해당 규칙을 허용하는 순간 잘못 된 개념끼리도 엮일 수 있으니까요, 물론 이 부분도 코드 리뷰나 도메인 리뷰가 철저하다면 괜찮겠지만요)


[해당 글의 '레이어'에 대하여]

과거의 제 글에서 ‘레이어’라고 소개 된 부분은 사실 '레이어드 아키텍처' 를 강조하고 설명하고자 한 글은 아닙니다

사실 이런 오해(?) 때문에 저는 요즘 ‘격벽’(2024인프콘 에서 발표한..) 이라는 용어를 더 많이 쓰는데요

 

결국 다양한 관점이 있지만 공통 분모로 개발자들이 말하는 것은 우리가 소중히 여겨야하는 부분(그걸 '개념'이라 부르건 '도메인'이라 부르건)을 어느정도 의존시키고, 어떻게 협력시킬지에 대하여 어떤 전략을 가져갈지 결정하는 것이 핵심인 것 같습니다.

 

저는 이런 측면에서 레이어드 / 포트&어뎁터 이렇게 무 자르듯이 아키텍처 결정하고 나누는 것은 선호하지 않습니다

제 기준에서는 잘 만든 레이어드진화하면 건강한 도메인이 나올 수 있고 필요에따라 포트&어뎁터로 진화하는 것은 상당히 쉬운 일이라고 생각하기 때문입니다.

 

추가로 해당 글의 비즈니스로직 + 레이어 챕터의 핵심은 토비님이 적어 주신 것이 정확합니다.

상세 구현 로직은 잘 모르더라도 비즈니스의 흐름은 이해 가능한 로직이어야 한다.

 

제가 여러 조건 때문에 현재 강의를 찍을 순 없지만, 추후에 이런 내용을 종합하여 한번 강의로 찍어보고 싶네요 :D

 

[동일 레이어 참조 허용 규칙 관련]

저도 재민님께서 저렇게 제안하신 이유는 아마 설계의 단순성을 통해서 여러사람이 같은 영역을 작업하기 편하게 한 것 같다는 생각이 듭니다.

MASKUN 님이 답변 주신 것 처럼 해당 글의 저 규칙은 특정 현실 상황에 상당히 유리한 면이 있습니다.

  • 개발 팀원 수

  • 개발 팀원들의 역량 수준

  • 개발 팀원들을 코드 리뷰/코칭 해줄 수 있는 여유가 있는가

  • 각개전투로 인당 프로젝트를 1개씩 해야하진 않는가

     

등등 개발할때에는 현실적인 상황을 종합적으로 고려해서 해야합니다, 그렇다면 저 글의 최대 효율은 언제 나올까를 생각해보면 대부분의 조건이 열악한 상황에서 최선의 효율정도 까지는 보장이 됐던 것 같습니다 .(온전히 제 경험 기준입니다 :D )

최악의 경우로 말하면... 누군가 코드를 망쳐놔서 그로인해 회사에 심각한 문제가 있을때, 제가 직접 가서 해결해야하는 상황에서 그나마 최선의 코드를 만날 수 있습니다. 😅

 

모쪼록 좋은 질문과 토비님의 깔끔한 답변과 질문자 분의 정리까지 즐거운 글 같습니다 :D

제 글 뿐만 아니라 토비님의 강의에서도 장점과 단점 트레이드 오프에 대하여 충분히 이해하시고 처해계신 상황에 맞는 전략을 결정해서 펼쳐나가보시길 바라겠습니다.

 

다양한 관점을 융합해서 시도해보고 실험하다보면 MASKUN님 만의 새로운 전략 및 철학이 탄생할 수 있을 것이라고 생각하고, 저는 그런 것이 수 많이 이미 정립된 이론 보다 멋진 것 이라고 생각합니다

 

다시 한번 알찬 질문과 글 언급 감사드립니다 :D

 

추가로 아래 내용은 저는 잘 만들면 이런 문제는 없다 라고 생각하긴 하는데ㅎㅎ (그런 문제가 생기기 쉬운 것은 팩트이긴합니다) 이건 저도 추후에 코드로 보여줄 수 있으면 좋을 것 같네요!

도메인 모델의 빈혈을 유발할 수 있겠다는 생각이 드네요.

 

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

재민님! 유투브 및 블로그 자료를 항상 잘 보고 있습니다. 😊

점점 더 바빠지시는 것으로 알고 있는데 이렇게 시간내서 답변 주신 것과
오래된 글을 이렇게 발굴(?) 해서 언급한 것에 대해서 이렇게 직접 A/S를 해주셔서 더욱 감사드립니다.  

"최선의 효율" 및 그밖에 답변 주신 내용 100% 공감합니다.

또한 추신하신 내용 또한 앞으로 기대하겠습니다!

날씨가 무척 덥고 습하네요! 늘 건강하시길 바랍니다! 감사합니다.

3

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

안녕하세요

링크해주신 토스 김재민 님의 블로그 글 잘 읽어봤습니다. 최근 몇 년간 개발자들의 코드에서 종종 보던 방식이네요. 이 글이 쓰여진게 4년 전이군요. 사실 재민 님은 친한 분이라 어제 연락해서 이 질문에 대해서 이야기를 해봤습니다. 전에도 이 아키텍처를 적용한 코드가 많아지고 있다는 얘기를 했더니, 너무 기계적으로 reader, writer를 쓰는 코드는 의도한 바가 아니었고, 주의해야 한다는 의견을 내기도 했네요. 이 글도 오래전에 쓴 것이라고 했고요. 아마 조만간 업데이트 된 이야기가 나오지 않을까 싶습니다.

재민 님이 소개한 아키텍처는 도메인 로직/비즈니스 로직을 트랜잭션 스크립트 패턴으로 작성하는 방식으로 보입니다. 사실 전체 예제가 없어서 공개된 메소드 하나만 가지고 판단하기엔 한계가 있긴하지만, 어쨌든 이번 강의에서 소개해드린 도메인 오브젝트(엔티티)에 로직을 두고 이를 사용하는 애플리케이션 서비스/도메인 퍼사드를 사용하는 방법은 아닙니다.

 

이 글의 핵심은 이 부분입니다.

상세 구현 로직은 잘 모르더라도 비즈니스의 흐름은 이해 가능한 로직이어야 한다.

"애플리케이션 수준의 도메인 로직(비즈니스 로직)을 코드를 보고 빠르게 이해할 수 있어야 한다. 디테일은 필요한 경우에 보면 되지만, 그렇지 않고 추상화된 상위 수준의 로직 흐름은 간결하고 손쉽게 이해할 수 있도록 리팩터링이 필요하다. " 제 강의에서 계속 이야기한 내용이죠.

문제의식과 목표, 맥락은 같습니다. 다만 접근 방법에서 저는 이 글에 소개된 계층 분리와제약조건에 대해서는 동의할 수 없습니다.

우선 구현 계층이라는 말이 이해가 잘 안 됩니다.

만약 저에게 초기 businessPay() 메소드를 보여주고 이걸 리팩터링 하라고 하면 우선 메소드를 추출해서 같은 클래스 내의 private 메소드로 분리하고, 각 추출된 메소드에 의미있는 이름을 부여하겠습니다. 디테일한 작업은 모르겠지만 어떤 일을 하는지 이해할 수 있는 좋은 이름을 부여할 겁니다.

그러다가 그 분리된 로직을이 해당 클래스에 소속되기 보다는 다른 클래스로 분리하고, 유사한 목표와 성격, 그리고 의존관계를 가지는 로직과 함께 별도의 클래스를 만들고, 이걸 스프링의 DI로 주입 받아서 사용할 겁니다.

그 외에도 적용할만한 다양한 리팩터링 기법이 있습니다.

어쨌든 중요한 건, PaymentService 빈의 businessPay는 이 계층의 앞단으로(혹은 헥사고날이라면 바깥으로) 공개되는 일종의 계층 인터페이스입니다. 제 강의에서 처럼 헥사고날 원칙을 따른다면 명확한 의도 단위대로 포트 인터페이스로 분리를 하고 그걸 사용했겠죠.

여기서는 인터페이스를 쓰지 않고 클래스 수준의 public 접근 권한만 주고 이걸 UI 계층 등에서 사용하는 하나의 비즈니스 업무 처리를 담당하는 인터페이스와 같이 취급을 하는 코드인 것이죠. 그렇게 디테일을 최소화하고 리팩터링 한 코드가 하단에 있는 businessPay() 메소드이겠네요.

제가 이 비즈니스에 대해서, 또 전체 코드에 대해서 알 수가 없어서 추측하는 것이지만 이 코드는 너무 단순해서 사실 비즈니스 로직을 잘 이해하기 쉽지 않습니다. 원래 코드를 보면 이보다는 좀 더 충실한 이름을 부여해도 좋지 않았을까 싶기도 하고요.

사실 리팩터링 자체에 대한 부분 보다는 비즈니스 로직(애플리케이션 로직)을 다루는 코드를 공개된 인터페이스(또는 퍼블릭 메소드를 가진 빈 컴포넌트)와 상세 구현이라는 구조로 한 계층에 두어도 충분한 것을 계층으로 분리한 것은 좀 과도한 작업이 아닌가 싶습니다.

제가 계층형 아키텍처의 기본 원리를 설명하면서, 모든 계층은 이를 사용하는 상위 계층에 노출된 인터페이스를 가져야 하고, 그 내부 구현과 디테일은 감춰야 한다는 아주 단순한 원리가 적용된다고 말씀드렸습니다. 아키텍처 계층의 캡슐화이죠.

그래서 구현이라는 이름이 들어간 컴포넌트들을 하나의 독립적인 계층으로 분리할 필요는 없다고 봅니다. 외부에서 직접 접근하면 안 되는 계층 내부 컴포턴트(접근을 제어하는 방법은 여러가지가 있습니다)로 만들어 하나의 비즈니스 로직을 처리하는 코드로 만들어도 됩니다.

포인트 관리자(PointManager)가 하는 일과 PaymentAppender가 하는 일은 비즈니스 로직이 아닌가요? 저는 그 안에도, 비록 트랜잭션 스크립트로 만들어지지만 중요한 도메인 로직이 있다고 보는데, 그걸 계층까지 분리해서 독립시켜야 할 이유를 잘 모르겠습니다.

지금까지 수 십 년 간 제가 배우고 경험한 엔터프라이즈 애플리케이션의 아키텍처에서 서비스 계층은 트랜잭션 스크립트 단독으로 만들거나, 도메인 계층을 하나 더 추가하고 그 앞에 이를 조합해서 애플리케이션 로직을 구성하는 도메인 퍼사드 계층 또는 애플리케이션 서비스 계층을 쓰는 구조 뿐입니다.

구현 이라는 이름으로 로직을 대부분 계층 레벨로 분리하다보니 read, user, append라는 언어로 구성된 짧은 코드에서 businessPay의 비즈니스 로직과 흐름이 잘 읽히지가 않습니다.

헥사고날 관점에서 외부와의 소통은 애플리케이션 서비스가 담당하면 충분합니다. 리포지토리 인터페이스는 충분히 추상화 되고 도메인의 개념을 담아 작성되고, 엔티티 또는 프로젝션 DTO로 정보를 주고 받을 수 있게 되어있습니다. 그런데 왜 Reader, Writer 같은 것을 추가해서 읽고 쓰는 작업을 또 한번 추상화 해야하는지, 이게 어떤 유익을 주는지 잘 이해가 안 됩니다.

 

질문하신 비즈니스 로직 계층은 동일 레이어 간에 서로 참조(호출)하지 않습니다라는 원칙은 아키텍처 레벨의 원칙이라고 보기 어렵습니다. 아마도 이건 모든 외부에 공개되는 하나의 로직은 하나의 "트랜잭션 스크립트"에 다 담고 그 메소드에서 끝내야 한다는 사상을 담은게 아닌가 싶습니다. 설령 그렇다고 하더라도, 예제에서처럼 포인트, 또는 페이먼트를 처리하는 appender(이게 결국 새 payment를 만들어서 리포지토리에 저장하는 코드가 들어가는 것이겠네요)를 일일히 별도의 코드로 만들어 쓸 필요가 있을지 모르겠습니다.

비즈니스 로직은 아주 범용적인 공통 서비스 등을 컴포넌트화 한 것을 제외한다면, 도메인 계층의 엔티티 등에 규칙과 같은 상세 로직을 적용하고, 그 로직을 정의한 인터페이스를 구현한 클래스 내부 또는 이와 협력해서 일을 하는 다른 서비스 계층에게 위임하는 식으로 동작하면 됩니다.

이 코드에서라면 포인트는 그 자체로 독립적이 서비스로 구성하는 게 낫습니다. 포인트도 하나의 중요한 도메인 개념이니까요. 혹은 처음 코드의 3줄 정도에 담긴 수준이라면 간단한 리팩터링으로 코드를 분리해두고 작업 내용을 이해할 수 있는 코드로 만드는 것이 좋겠습니다.

하지만 저라면 포인트라는 애그리거트를 헥사곤으로 분리해서 정의하고 PointUsage라는 인터페이스를 만들고 그 안에 usePoint(member, points) 라는 메소드를 만들겠습니다. 기록을 남기기 위해서 Payment를 먼저 생성하고 그 ID 값을 사용 레퍼런스 정보로 넘겨도 좋겠네요.

이렇게 어떤 비즈니스 작업의 메인 관문 역할을 하는 코드에서 당연히 함께 연결되어진 다른 도메인 로직을 다루는 서비스 등을 사용하는 데 아무 문제 없습니다. 재민 님 글에서 왜 그걸 제한을 했는지는 그 기원을 여러가지 유추해볼 수 있을 것 같긴한데, 어쨌든 그 규칙 적용을 강제해버리면 너무 단순합니다. 이런식으로 하면 소위 구현 패턴 코드로 중요한 로직이 유출되고, 흩어지기 쉽고, 단일 비즈니스 로직 메소드에서 여러개의 구현 오브젝트의 메소드를 사용한다는 방식이 코드 전체의 변화를 어렵게 하지 않을까도 생각이 듭니다.

 

얘기가 길어지긴 했는데, 안그래도 Part2에서 이런 스타일의 아키텍처와 개발 방법(특히 Reader, Writer)를 쓰는 문제에 대해서 한번 이야기를 하려고 했는데 마침 질문을 주셔서 감사합니다.

저 네번째 규칙이 왜 나왔는지, 분명 어떤 배경이 있을 것 같긴한데요. 재민 님도 답을 바로 못해주시더라고요. 나중에 더 캐서 물어보고 알려드리겠습니다.

어쨌든 구현 계층을 쓰지 않고, 트랜잭션 스크립트 패턴 대신 도메인 모델 패턴을 사용해서 도메인 계층을 구분하는 방식에 헥사고날 아키테처를 적용한다면, 같은 계층에서 상호 호출은 안 된다는 제약 조건은 전혀 신경쓰실 필요가 없습니다. 얼마든지 상호 협력해서 자기가 맡은 책임을 담당하도록 만드는 것이 권장됩니다. (나중에 이 결합을 느슨하게 하기 위해서 도메인 이벤트를 쓰는 것ㄷ 나오겠지만요)

기회가 되면 제 유튜브 채널에서 이 질문을 가지고 좀 더 깊은 이야기를해볼 수도 있을 것 같네요. 그때 재민 님도 한번 초대해보겠습니다. 좋은 질문 해주셔서 감사합니다.

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

제가 지금까지 개발해온 방식에 대해서 다시 돌아보게 하는 답변 감사드립니다.

"구현 계층을 쓰지 않고, 트랜잭션 스크립트 패턴 대신 도메인 모델 패턴을 사용해서 도메인 계층을 구분하는 방식에 헥사고날 아키테처를 적용한다면, 같은 계층에서 상호 호출은 안 된다는 제약 조건은 전혀 신경쓰실 필요가 없습니다. 얼마든지 상호 협력해서 자기가 맡은 책임을 담당하도록 만드는 것이 권장됩니다. (나중에 이 결합을 느슨하게 하기 위해서 도메인 이벤트를 쓰는 것ㄷ 나오겠지만요)
기회가 되면 제 유튜브 채널에서 이 질문을 가지고 좀 더 깊은 이야기를해볼 수도 있을 것 같네요. 그때 재민 님도 한번 초대해보겠습니다. 좋은 질문 해주셔서 감사합니다."

-> 저야말로 이렇게 정성스럽게 답변해주셔서 감사합니다. 또한 앞으로 이와 관련된 얘기를 들으면서 좀 더 명확하게 관련한 내용을 배울 수 있다니 기대가 되네요. 관건은 역시 느슨한 결합에 대한 것이라는 생각이 듭니다.


"재민 님 글에서 왜 그걸 제한을 했는지는 그 기원을 여러가지 유추해볼 수 있을 것 같긴한데, 어쨌든 그 규칙 적용을 강제해버리면 너무 단순합니다. 이런식으로 하면 소위 구현 패턴 코드로 중요한 로직이 유출되고, 흩어지기 쉽고, 단일 비즈니스 로직 메소드에서 여러개의 구현 오브젝트의 메소드를 사용한다는 방식이 코드 전체의 변화를 어렵게 하지 않을까도 생각이 듭니다."
-> 저도 재민님께서 저렇게 제안하신 이유는 아마 설계의 단순성을 통해서 여러사람이 같은 영역을 작업하기 편하게 한 것 같다는 생각이 듭니다. 계층형 아키텍쳐에 대부분의 동료 개발자가 익숙하니까 이를 조금 응용하고 비지니스 레이어를 구현계층의 파사드로 구성한게 아닌가 싶네요. 누구나 쉽게 개발할 수 있는 규칙을 적용했다고 생각합니다. 물론 그 과정에서 로직이 유출되고 흩어지기 쉬울 수 있다는 점을 토비님이 지적하셨고 저는 이부분은 생각하지 못했네요. 좋은 지적인 것 같습니다.

또한 토비님의 글을 읽으면서 이렇게 구현계층을 두고 로직을 쪼개는 방식이 도메인 모델의 빈혈을 유발할 수 있겠다는 생각이 드네요.

많은 인사이트 주셔서 감사드립니다! 좋은 하루되시길 바랍니다.

MASKUN님의 프로필 이미지
MASKUN

작성한 질문수

질문하기