작성
·
226
·
수정됨
답변 2
1
안녕하세요.
행위가 외부 의존을 가지는 것은 여러가지 이유가 있겠네요. 기본적으로 오브젝트는 자신이 가진 정보(속성) 또는 주어진 정보(파라미터)를 가지고 기능을 수행합니다. 이때 외부의 다른 오브젝트의 기능이나 데이터가 필요한 경우가 있겠죠. 이렇게 다른 오브젝트를 사용하거나, 상속하거나, 참조해서 기능을 수행할 때 의존한다고 말할 수 있습니다.
이게 해당 도메인 오브젝트의 생애주기 동안에, 또는 일정 기간 동안에 요구된다면 다른 오브젝트에 대한 참조를 생성 시점 또는 특정 상태 변환 시점에 부여받아 내부에 저장해두고 필요할 때 작업을 위임할 수 있습니다.
그런데 외부 오브젝트에 대한 의존이 지속적으로 유지되야 할 필요가 없다면 행위를 가지는 메소드 실행 시점에서 기능을 제공할 외부 오브젝트를 전달하거나, 혹은 기능 자체만 함수(람다)로 뽑아서 전달할 수 있습니다.
보통 행위에 해당하는 메소드에 기능을 가진 여러 외부 오브젝트를 필요에 따라 바꿔쓰게 하는 것을 전략 패턴이라고 합니다. 이런 경우도 특정 기능을 정의한 인터페이스 또는 클래스 타입의 오브젝트를 메소드 파라미터로 전달하고, 이를 메소드 호출 주입이라고 부르기도 합니다.
행위 자체가 일정한 템플릿 구조를 유지하는데 필요에 따라 일부 기능만 변경을 해서 쓰고 싶은 경우도 있습니다. 이때는 상속을 이용하는 템플릿 메소드 패턴을 사용할 수 있습니다만, 이건 상속 기반이라 런타임에 교체하기가 어렵습니다.
행위가 어떤 방식으로, 어느 시점에 외부 오브젝트의 기능에 의존하는지에 따라서 여러가지 방법 중에서 선택하시면 됩니다.
그런데 간혹 어떤 행위가 자신이 가진 정보나 기능을 활용하지는 않고, 참조를 가진 다른 오브젝트에 위임하기만 하는 경우도 있습니다. 이런 때는 행위가 해당 오브젝트가 아니라 그 외부 오브젝트가 가지고 있어야 할 가능성이 있으므로, 따져보고 메소드를 옮기는 리팩터링을 해야 합니다.
순수성이 뭘 말하는지 모르겠습니다. 구체적인 주장을 알려주시거나 링크를 주시면 참고해보겠습니다.
이렇게 람다(펑션)을 메소드 호출에 전달해서 기능을 수행하는 방법을 고계함수(higher-order function)이라고 하고, 이건 프로그래밍 역사를 통해서 초기부터 꾸준하게 사용된 아주 대표적인 설계 방식입니다. 그만큼 유연한 설계가 가능하고 동적으로 기능을 확장하기 좋기 때문이죠. 사실 오브젝트를 넣는 것은 람다보다 더 많은 기능에 의존하도록 만드는 경우인데 그것이 어떤 경우엔 더 적절할 수 있습니다.
아무튼 말씀하신 의견에 대해서 출처나 상세한 설명 부탁드립니다.
아 네넵 다른건 아니고 저도 요즘 DDD 세계에 진입하기 위해서 여러 자료들을 찾고 있는데 항상 "도메인은 항상 의존적이지 않아야 한다" 라는 말을 자주 들어서 리서치 중에 있습니다.
https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness/
DDD 관련해서 트릴레마(세 가지 선택지 중에서 어느 것을 선택해도 바람직하지 않거나 불리한 결과가 초래되는 상황) 관련된 부분에 대한 링크를 최근에 접하게 되서 토비님은 어떻게 이 부분을 생각하시나 고민되어서 질문드리게 되었습니다.
도메인 주도 설계(DDD)에서는 도메인 모델이 비즈니스의 진짜 의미를 반영해야 하며, 가능한 한 순수하게 설계되어야 한다고 알고 있습니다.
그런데 메서드 호출 시점에 람다로 외부 로직을 주입받게 되면, 겉으로 보기엔 의존을 줄인 것 같지만 결국엔 비즈니스 로직의 일부가 외부로 분리되어 있고, 그 구성은 애플리케이션 레이어의 의사결정에 따라 달라질 수 있다는 점에서, 도메인의 자기 완결성이나 자율성이 흐려질 수 있다고 느꼈습니다.
특히, 특정 전략(예: 할인 정책, 알림 방식 등)이 도메인 규칙에 깊이 관여할수록, 그 의사결정을 외부로 위임하는 것이 도메인의 표현력이나 정체성을 약화시킬 수 있지 않을까요?
이런 관점에서 보면, 도메인 객체 내부에 전략을 고정하거나, 최소한 도메인 내부에서 전략을 결정하는 방식이 도메인 순수성 측면에서는 더 바람직하다고 생각했는데요.
이처럼 도메인 순수성 유지와 유연한 전략 주입 사이에서 어떻게 균형을 잡는 것이 좋은 설계라고 생각하시는지 궁금합니다.
도메인 계층이 독립적으로 존재하고 외부에 대한 의존성을 가지지 않도록 하는 이유는 도메인 로직을 보편 언어로 최대한 기술하고 이를 손쉽게 이해할 수 있도록 하는 것입니다. 많은 분들이 오해하는 것은 실행 시점에 도메인 계층의 인터페이스에 대한 구현도 도메인 내부에 있어야 순수하다고 하는 것인데, 이건 DDD가 말하는 도메인을 고립시키는 것에 대한 잘못된 이해입니다.
도메인 계층 안에 구현 클래스로 존재하는 것들이 있습니다. 엔티티와 값 객체가 대표적이죠. 반면 DDD 책에서 명시적으로 "인터페이스"라고 설명한 것들이 존재합니다. 도메인 지식을 최대한 표현해 내는 순수한 코드를 만들다보면 인터페이스만 도메인 계층에 존재하는 것들이 나옵니다. 도메인 계층 내의 코드는 이러한 인터페이스에만 의존하고, 인터페이스의 선언에는 도메인의 지식과 로직이 충분히 반영되어 있어서 이를 이해하기 충분합니다.
하지만 이런 인터페이스의 구현도 도메인 계층 내부에 두어야 하는 것은 아닙니다. 대표적인 것이 리포지토리죠. 리포지토리는 도메인을 구성하는 대표적인 구성 요소입니다. 그리고 인터페이스로 정의합니다. 그 선언에는 마치 도메인의 정보를 담은 오브젝트가 메모리에 모두 존재하는 것처럼 엔티티와 값 객체, 또는 컬렉션 등으로 정보를 조회하거나 저장합니다. 이걸로 도메인 언어를 통한 엔티티 등의 라이프사이클 관리나 조회, 추적 등이 자연스럽게 코드로 표현됩니다. 그런데 리포지토리 구현은 어쩌죠? 그게 수동으로 직접 구현 클래스를 만들든, 스프링 데이터 처럼 런타임에 동적으로 만들어지든 그 구현은 당연히 기술 서비스에 연결되어 있습니다.
도메인의 구성 요소인 리포지토리를 사용했더니 시스템 외부의 DB에 연결이 된다면 그것은 순수성을 해치는 것인가요? 전혀 아닙니다. 도메인 계층의 코드는 여전히 도메인의 관심사에 충실하게 만들어진 것이고, 리포지토리의 구현 메카니즘은 도메인 코드가 전혀 의존하고 있지 않고, 얼마든 독립적으로 변경될 수 있습니다. 여기서 어떤 면이 순수성을 해친다는 것인가요? 이건 완전히 DDD를 오해하고 멋대로 해석한 결과일 뿐입니다.
리포지토리는 인터페이스로 도메인 계층 내에 존재하며 도메인 모델의 의도를 반영하는 단순하고 명확한 메서드를 가집니다. 클라이언트는 도메인 모델의 용어로 엔티티를 요청할 수 있고, 기술적인 메커니즘이 아닌 모델 개념에 집중할 수 있게 해줍니다. 실행과정에서 각종 DB 저장 기술이 동작하고 사용되는 것은 이러한 리포지토리의 도메인 순수성을 전혀 해치지 않습니다.
마찬가지로 도메인 서비스도 도메인 계층 내에서 독립적인 인터페이스로 제공되는 연산이고, 단일 메소드를 가진 경우라면 람다식으로 전달될 수 있습니다. 도메인 모델 내에 정의되고 모델의 일부로 간주되고, 도메인 전문가들이 이해할 수 있는 언어로 표현됩니다. 그러면 순수성은 충분한 겁니다.
하지만 이 기능의 구현이 여러가지가 있고, 외부 설정이나 복잡한 조건, 또는 API 호출, DB 조회 등의 기술적인 구현이 필요할 수 있습니다. 그런 경우라면 람다식으로 전달되는 오브젝트의 실제 구현은 도메인 계층 밖에 존재합니다. 의존성의 방향은 여전히 도메인으로 향하고, 도메인은 그런 기술 구현에 영향을 받지 않습니다.
이런 인터페이스와 구현의 분리는 DDD의 순수한 핵심 전략 중의 하나입니다. 인터페이스는 도메인 계층에 남아 도메인 모델의 순수성과 보편 언어를 강화하고, 구현은 기술적인 사항을 처리하기 위해서 필요한 경우 인프라 계층(어댑터 계층)으로 이동시키는 것은 매우 자연스럽습니다.이렇게 하면 도메인 모델이 기술적 복잡성에 오염되지 않고, 비즈니스 본질에 집중할 수 있고, 독립적으로 진화할 수 있습니다.
구현이 도메인 계층 밖에 존재하고 기술적인 서비스에 연결된다고 해서 도메인 계층의 순수성을 해친다는 주장은 DDD의 계층형 아키텍처 및 책임 분리 원칙에 대한 이해가 부족한 것입니다.
그런데 위에 예로 드신 코드에서 이메일 변경 로직에 리포지토리를 통채로 주입하는 방식은 저는 좋은 설계라고 생각하지 않습니다. 수 많은 기능을 가진 리포지토리 인터페이스는 너무 과도합니다. 굳이 내부에서 중복 로직을 체크하는 기능을 활용하고 싶다면, 더 단순한 람다식을 정의하고 리포지토리의 해당 메소드만 매핑해서 전달하는 것이 낫습니다. 혹은 메일 주소 중복 여부 체크는 요청 파라미터에 대한 전제 조건 검증 로직으로 분류해서 밖으로 빼도 됩니다. 저는 리포지토리를 엔티티나 값 객체 내부에서 사용하는 것은 대체로 좋은 선택이 아니라고 생각합니다.
그런데 제 강의에서는 DDD를 사용하지 않는다고 말씀드렸는데..
좋은 말씀 감사드립니다! 사실 아직 저는 주니어 발도 못뗀다고 생각하는데 여러 정보들이 머리속에 들어오면 무엇이 정답인지 결론을 쉽게 내리지 못하는 것 같습니다. 그래서 많은 경험을 가지신분의 의견을 따르기 위해 해당 질문을 좀 길게 드린거 같네요... 자세한 답변 정말 감사드립니다!
0
안녕하세요, 인프런 AI 인턴입니다. 래셔널님!
도메인 모델에 속성과 행위를 모두 포함하는 것은 객체지향 프로그래밍의 핵심적인 특성 중 하나입니다. 도메인의 ‘행위’가 외부 의존성을 가져야 하는 경우, 몇 가지 설계 패턴과 원칙을 통해 문제를 해결할 수 있습니다. 특히, 헥사고날 아키텍처에서는 이러한 문제를 상당히 유연하게 해결할 수 있습니다.
도메인 서비스 사용: 외부 의존성이 필요한 행위를 직접 도메인 엔티티 내에 포함시키기보다는, 이러한 의존성을 관리하는 도메인 서비스를 따로 정의하는 것이 좋습니다. 도메인 서비스는 특정 도메인 로직을 처리하며, 외부 시스템과의 상호작용을 캡슐화할 수 있습니다.
인터페이스 분리: 외부 의존성을 갖는 행위는 인터페이스를 통해 정의하고, 이를 구현하는 클래스에서 구체적인 외부 의존성을 처리합니다. 이렇게 하면 도메인 로직이 구현 세부 사항으로부터 분리되어 깨끗하고 테스트하기 쉬운 구조를 유지할 수 있습니다.
헥사고날 아키텍처의 포트와 어댑터 패턴: 헥사고날 아키텍처에서는 포트와 어댑터 패턴을 사용하여 외부 의존성과의 결합을 느슨하게 유지합니다. 도메인 모델은 인터페이스(포트)를 통해 외부의 기능을 호출하며, 실제 구현(어댑터)은 인터페이스를 통해 주입됩니다. 이를 통해 도메인은 외부 시스템 변경과 관계없이 독립성을 유지할 수 있습니다.
실제 예제로, 만약 도메인의 행위가 외부 API를 호출해야 한다면, API 호출 로직을 도메인 엔티티에 직접 작성하지 않고, 별도의 서비스에 추출하여 인터페이스를 통해 도메인에서 사용하도록 설계할 수 있습니다.
아래 링크에서 관련된 답변을 참고하실 수 있습니다:
이러한 접근 방식을 통해 도메인이 외부 시스템에 종속되지 않고 고유의 책임을 잘 관리할 수 있도록 설계할 수 있습니다.
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.
답변 감사합니다. 호옥시 람다 또는 익명클래스(델리게이트) 주입은 “엔티티가 외부 의존을 직접 참조”하는 것보다는 나은 절충안이지만, 순수성 측면에서 완전히 자유롭지 못하다.라는 의견에 대해서는 어떻게 생각하시나요?