호주에 살고 있는 소프트웨어 개발자입니다. 30년간 다양한 분야의 시스템과 서비스를 개발해본 경험이 있습니다.
스프링 프레임워크와 관련 기술을 좋아하고 JVM 기반 언어를 주로 사용합니다.
한국스프링사용자모임(KSUG)을 설립하고 활동했고, 토비의 스프링이라는 책을 쓰기도 했습니다.
개발과 관련된 다양한 주제에 관해 이야기하는 것을 좋아합니다.
강의
수강평
- 토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 2
- 토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 2
- 토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 2
- 토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1
게시글
질문&답변
핵사고날 아키텍처 기반으로 멀티 모듈 설계 시 질문드립니다..
Part1 강의에서 설명드렸던 것처럼 헥사고날 아키텍처에서 Repository는 인터페이스만을 가리킵니다. 실제 그 인터페이스를 구현한 코드는 기술과 환경에 종속되기 때문에 어댑터에 인터페이스를 구현하는 클래스를 두고 만들어야 하죠. 다만, 스프링 데이터 JPA를 사용하는 경우 이 구현 클래스를 개발자들이 직접 만들지 않아도 스프링이 런타임에 자동으로 코드를 작성하고 컴파일해서 구현 기능을 사용하게 해줄 뿐입니다. 다시 정리하자면, Repository는 헥사고날 애플리케이션이 필요한 기능을 정의한 required interface이고요. 실제 구현은 그 외부 어댑터 영역에서 일어납니다. 스프링 데이터를 쓴다고 하더라도 QueryDSL이나 다른 데이터 기술을 사용한다면 어댑터 쪽에 실제 기능을 담당하는 리포지토리의 구현 코드가 만들어지면 됩니다.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 55
질문&답변
내부 객체 직접 접근에 관하여.
좋은 질문을 해주셨네요. 애그리거트 안에 루트 외에 내부 엔티티가 존재할 경우에 내부 엔티티에 접근하거나 기능을 수행하는 것은 최대한 제한되는 것이 바람직합니다. 애그리거트 내부 구조가 노출되고 내부 엔티티의 기능까지 직접 사용하게 되면 애그리거트 단위로 작업을 수행하려고 한 애초의 목적을 놓칠 수 있게 됩니다. 그래서 모든 내부 엔티티에 대한 조작과 접근을 루트 엔티티로 제한하는 방법을 선택할 수 있습니다. 그래서 보여주는 방법처럼 내부 detail 엔티티가 가지고 있는 정보를 조회할 때도 루트 엔티티의 메소드를 통해서 정보를 가져오는 작업을 위임할 수 있습니다. 그런데 저는 이렇게 모든 정보의 조회까지 루트에 다 구현을 해두면 루트 엔티티가 너무 복잡해진다는 느낌이 들더라고요. 그래서 저는 트레이드 오프를 통해서 단순 조회인 경우, 그리고 변경이 크게 일어날 가능성이 없다고 판단되는 경우엔 조회 로직은 내부 엔티티에 접근해서 가져오는 방식을 선택했습니다. 물론 변경이 필요한 작업은 애그리거트 단위에서 일관성을 지키는 것이 중요하기 때문에 내부 엔티티의 변경 로직은 철저하게 차단을 해두고, 루트 엔티티가 외부에 공개한 퍼블릭 메소드를 통해서만 일어나게 제한을 두는 방식을 선택한 것입니다.보여주신 방식처럼 조회 메소드도 루트 엔티티로만 제한을 하는 방법도 선택이 가능한 옵션입니다. 가벼운 트레이드 오프를 통해서 접근 방식을 결정하고 일관되게 적용하시면 됩니다. 루트 엔티티가 커지는 문제 vs 애그리거트 내부 엔티티 구조가 변경됐을 때 조회 로직도 따라서 바뀌어야 하는 문제 중에서 어느 부분에서 손해를 감수하고 다른 장점을 취할 것인가를 실제 코드를 구현해보시면서 판단에 따라 트레이드 오프 해보세요.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 83
질문&답변
InvalidCurriculumException DIP 적용 여부 문의
날카로운 지적을 해주셨네요.말씀하신 대로 지금 구현에서는 예외 클래스 때문에 양방향 의존성이 남아있게 되네요. 현재 ArchUnit에서는 애플리케이션 컴포넌트 레벨에서 순환참조 확인이 안 되고 있었습니다. 도메인과 애플리케이션 계층에서 각각 체크하기만 해서 이 부분을 놓치고 있었네요. ValidationException은 shared 모듈에 정의를 해둔 것이 있으니 그걸 인터페이스에서 던지도록 정의해두고, CurriculumValidationException이 발생하는 CurriculumModifyService 클래스 내 validate() 구현 내부에서 이를 캐치해서 ValidationException으로 한번 wrapping해서 던지게 하면 됩니다.제가 미처 체크하지 못했던 부분을 알려주셔서 감사합니다. 피드백 반영할 때 이 내용도 추가하도록 하겠습니다.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 63
질문&답변
readme에 대한 자료는 따로 받아볼수 있나요?
말씀하신 readme가 어떤 내용인지 잘 모르겠습니다. 구체적으로 알려주시면 답변을 드릴게요. 강의자료를 보시면 전체 소스코드와 문서 다운로드는 확인하실 수 있습니다.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 73
질문&답변
Part 2 듣기전 복습하던 중, 아키텍처에 관한 질문이 있습니다.
제 생각엔 스프링 프레임워크를 사용해서 애플리케이션을 개발하면서 스프링 개발자들이 권장하는 방식으로 자연스럽게 코드를 설계하고 구성한다면, 대부분 헥사고날 아키텍처를 적용하고 있다고 봅니다. 아직 복잡하지 않은 시스템이라고 하더라도, 객체지향 설계원칙과 응집도/결합도라는 기본적인 원칙을 따르면 자연스럽게 헥사고날 아키텍처가 나오는 것이 정상입니다. 이건 스프링이 처음 등장했을 때부터 제안하고 지지했던 방식입니다.도메인 모델 패턴도 마찬가지입니다. 빈약한 도메인(getter, setter 외에는 없는 엔티티만 사용하는) 모델 같은 것도 스프링의 개발 철학과 맞지 않습니다. 따라서 간단한 애플리케이션이니 스프링을 사용하지 않겠다는 결정을 하실 수는 있겠으나, 스프링으로 개발하기로 결정하셨다면 헥사고날 아키텍처를 자연스럽게 사용하실 수 있습니다.한때 인기를 끌었던 클린아키텍처 책을 통해서 헥사고날 아키텍처가 알려지면서 그 책의 예제에 나온 방식으로 코드를 작성해야 헥사고날 아키텍처라는 잘못된 인식이 널리 퍼졌는데요. 그 방식은 너무 복잡한 관례를 요구합니다. 네이밍 규칙도 자연스럽지 않고 패키지가 아주 많아지죠. 그래서 Part1에서 헥사고날 아키텍처의 본래 의도와 적용 방법을 가능한 가장 단순한 방식으로 설명드리려고 했습니다. 그 부분을 잘 이해하고 따라가시면 되는데, 그래도 전통적인 자바 백엔드 개발 스타일과는 다른 부분이 있어서 고민이 되실 수는 있습니다. 포트 인터페이스를 의도 단위로 쪼개는 것들도 익숙하지 않으실 수 있습니다. 그냥 MemberService라는 인터페이스 하나에 Member 관련 기능을 다 넣어서 개발하는 것이 단순하기 때문에 아주 복잡해지지 않는다면 이런 방법을 선택하는 경우가 많았거든요. 이런 부분은 좀 고민이 필요한데, 사실 처음부터 완벽하게 포트를 나누고 시작하지 않으셔도 됩니다. 우선 Member 엔티티를 종심으로 Service, Port 인터페이스를 하나씩 만들고 시작하다가 나중에 메소드가 많아지면 그때 이걸 어떻게 나눠볼까 하고 인터페이스를 쪼개고, 구현 클래스도 복잡해지면 구현도 분리하는 방법도 가능합니다. 어쨌든 헥사고날 아키텍처가 지향하는 원칙은 포기하지 않으시면 좋겠습니다. 외부 시스템 연동이 없다면 required interface는 사실 스프링의 Repository 인터페이스 쓰는 것이면 충분합니다. 그것만 사용하셔도 이미 헥사고날 아키텍처를 하고 계신 거죠. 그런데 DDD는 많이 다릅니다. DDD는 상당히 복잡하고 거대한 철학과 원칙, 패턴의 집합이고 진정한 DDD는 아주 복잡한 도메인과 시스템에서 가치가 드러납니다. 그래서 제 강의는 DDD가 아니라고 굳이 강조를 하는 것이고요. 다만 거기서 소개된 좋은 패턴은 얼마든지 적극 사용할 수 있습니다. 엔티티, 값 객체, 서비스, 리포지토리, 애그리거트 같은 것들이죠. 질문에 답변을 드리자면, 스프링을 쓰신다면 헥사고날 아키텍처로 개발하시면 됩니다.도메인과 관련된 로직을 담은 코드에 기술적인 코드가 혼재되지 않도록 해주시고요. 이미 리포지토리 인터페이스를 쓰면 그 부분은 90%쯤 성취된 것이죠. 반대로 UI(API 등등)쪽 기술이 도메인 로직을 담은 서비스 쪽으로 들어오지만 않게 해주시면 됩니다. ServletRequest, Cookie 같은 것들이죠. 그리고 도메인의 핵심 로직, 가장 중요한 불변식 등은 엔티티 안에 넣어주시면 됩니다. 그러면 도메인 모델 패턴까지도 적용하는 것입니다.답변이 도움이 되었으면 좋겠습니다. 더 궁금하신 게 있으시면 질문해주세요.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 87
질문&답변
JPA 엔티티로 만들었다고 기술종속이 아닌지에 대한 질문
날카로운 질문을 해주셨네요. 언급하신 제약사항이 JPA로 만든 도메인 엔티티의 상속 가능성, 생성 방식, 불변성 설계에 일부 영향을 미치는 것이 사실입니다. final을 사용하지 못해서 완전히 닫힌 타입을 만들지 못하는 것, 사용 가능성은 극히 낮지만 어쨌든 기술적으로 디폴트 생성자 호출이 가능해지는 것 등은 분명히 JPA 기술의 제약 때문에 도입되는 현상입니다. 만약 언어가 주는 특성을 모두 이용해서 강력한 캡슐화를 진심으로 원한다면 트레이드 오프라고 고민할 수도 있겠죠. 포기해야 할 것들이 있으니까요.그런데 이건 도메인 모델을 코드에도 반영해서 구현하는 도메인 모델 패턴 방식에서 제약이라고는 보지 않습니다. JPA를 사용해도 private 필드를 쓰고, setter를 최소화하고, 도메인 메소드를 이용해서 상태 변경을 사용할 수 있습니다. 도메인 로직을 구현하고 기능을 넣는 것에서 방해되는 것은 없습니다. 상속도 자유롭게 사용할 수 있고요. 그런면에서 JPA가 주는 제약이 도메인 설계를 방해하는 것은 거의 없다고 봅니다. 기술 종속도 마찬가지입니다. 이런 제약을 가지고 만들어진 JPA 엔티티가 JPA 외의 기술로 대체되거나, 아예 데이터 모델을 독립시키고 매핑을 한다고 했을 때 기존의 JPA를 사용하는 도메인 엔티티가 방해를 하나요? 기존 코드를 그대로 두고 기술을 바꿔도 무방합니다. 강의에서 처럼 애노테이션은 도메인 설계의 의미를 담은 주석 역할로 의미있는 것만 남기고, 매핑은 XML로 빼둔다면 더더욱 그렇습니다. JPA를 사용하지 않게 되면서 굳이 기본 생성자가 만들어지는 게 싫다면 Lombok 설정 한 줄 제거하면 됩니다. 그 외에는 어떤 저장 기술로 바뀐다고 하더라도 도메인 로직을 충실하게 구현한 코드가 JPA에서 다른 기술로 대체되었을 때 이를 방해하거나 어렵게 만들 이유는 전혀 없습니다. 그런면에서 저는 기술에 종속되었거나 설계에 영향을 미쳤다고는 생각되지 않습니다. 구현에 약간의 군더더기를 남긴다 정도면 충분할 듯합니다.
- 좋아요수
- 0
- 댓글수
- 3
- 조회수
- 99
질문&답변
설계 트레이드 오프 링크 접속 안됨
제가 다시 확인해본 강의영상이나 강의자료에는 블로그 글 링크가 바르게 들어간 듯 합니다.혹시 잘못 나온 부분이 어디였는지 다시 확인해주실 수 있을까요.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 125
질문&답변
설계 트레이드 오프 링크 접속 안됨
안녕하세요. 블로그 글 링크가 잘못 들어간 듯 합니다. 아래 주소로 들어가시면 됩니다.https://eternity-object.tistory.com/43강의 영상의 내용은 수정해서 업데이트 해두겠습니다.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 125
질문&답변
헥사고날 아키텍처와 DDD를 적용할 때, 화면에 강하게 연관된 조회 데이터를 어떻게 다루는 게 좋은지 궁금합니다.
우리가 도메인 중심의 개발을 한다고 할 때는 보통 변경이 일어나는 상황을 주로 다룹니다. 대부분의 로직은 변경 로직이고, 조회는 보안과 관련이 없다면 도메인 모델의 변경 없이도 새로운 화면이나 클라이언트 요구사항에 따라서 계속 추가될 수 있겠죠.그런데 말씀하신 경우처럼 보다 복잡한 조회 로직이 필요한 경우가 많이 존재합니다. 엔티티 구조로 커버할 수 있는 조회라면 기준이 되는 엔티티와 필요하다면 연관관계에 있는 엔티티까지 함께 페치하고, 이를 응답 데이터로 만드는 것은 웹 어댑터에서 진행해도 됩니다.그런데 리포트성 쿼리라든가, 복잡한 화면이나 테이블에서 필요한 정보 등은 DB 쿼리에 의존하는 경우가 많고, 필요하다면 네이티브 쿼리를 사용해야 합니다.헥사고날 애플리케이션 구조에서 이런 경우 내용에 따라 적절하게 분리된 조회 의도를 담은 Port를 설계하고, 구현 클래스는 필요에 따라 기본적인 조회용 query service를 같이 활용해도 되고 분리해도 좋습니다.QueryDSL은 스프링 데이터를 쓴다면 커스톰 repository 인터페이스를 구현해서 어댑터 쪽에 만들면 되니까, 헥사고날 쪽에서 볼 때는 인터페이스만 보고, 형식만 잘 맞춰주면 될 겁니다. 어떤 경우에는 가져온 데이터를 애플리케이션 내부에서 가공해서 최종 응답을 만들어야 할 수도 있습니다. 여기에도 결국 도메인 조회 로직이 만들어지는 것이겠죠. 조회가 복잡하고 많은 경우엔 엔티티 단위의 리포지토리 외에 추가로 구분된 리포지토리를 두는 것이 좋을 것 같습니다. 복잡한 분석을 다루는 경우 애그리거트/애플리케이션 서비스 경계를 넘어서게 되는데 이럴 때는 그런 조회를 담당하는 리포트 컴포넌트를 따로 추가하는 것도 좋습니다. 기술에 종속되는 코드는 어댑터에 두고, 헥사고날 애플리케이션 내부에는 도메인 정보로 해석되는 로직을 담은 코드를 두고, 의도 단위로 구분된 Port를 통해서 서비스를 제공한다는 헥사고날 아키텍처의 기본 원칙을 따른다면 크게 고민할 부분은 없을 것입니다.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 172
질문&답변
Request DTO에서 Entity를 생성할 때 의존성 방향을 반대로 하면 어떨까요?
좋은 질문을 해주셨네요.말씀하신 방법도 실무에서 많이 사용하는 것이긴 합니다. 저도 그런 스타일로 개발한 적도 있고요.그럼에도 이번 강의 시리즈에선 도메인 계층에 있는 Member 엔티티의 생성자에 동일한 타입의 파라미터가 여러개 연속으로 등장하는 근본적인 문제를 해결하는 것이 좋다고 생각을 했습니다. Part 1에서 말씀드렸던 것처럼 타입이 달라서 보호되지 못하는 String과 같은 타입이 연속으로 나오면, 이후 코드 변경에 취약해지기 쉽습니다. 그래서 parameter object 패턴 같은 것을 활용하려고 했던 것이지요.그런데 말씀해주신 것처럼 DTO에서 바로 복잡한 생성자/생성 메소드를 가진 엔티티를 생성하는 것을 개발 규칙으로 두는 경우 다음 두 가지 문제가 있다고 생각을 합니다.근본적으로 Member 엔티티를 다른 곳에서 DTO 없이 직접 생성하는 것을 막지는 못합니다. 웹 API를 사용하는 경우 한가지 경우만 있다면 문제가 되지 않을 수 있지만, 이후에 데이터 마이그레이션, 배치 등에서 다른 경로로 Member의 생성자(생성메소드)를 사용하는 경우가 나오면 동일 타입의 긴 파라미터를 가진 메소드 사용의 위험은 그대로 가지고 있게 됩니다.Part 2에서 리팩터링을 결정하면서 설명했듯이 서비스로 전달되는 Request DTO는 이후 Member 생성에 필요한 정보 외에 다른 데이터도 추가될 수 있습니다. 이런 경우 DTO가 단순히 Member 생성 정보 이상의 역할을 하게 됩니다. 그래서 이런 DTO에서 Entity를 바로 생성하는 방식으로 코드를 작성하면 직접적으로 생성 로직을 바로 이해하기가 여러울 수 있습니다. 도메인 엔티티가 어떻게 만들어지는가는 애플리케이션 서비스 코드에 잘 드러나면 좋겠고, 그 정보는 도메인 계층으로 오브젝트로만 구성되어 있는 것이 응집도가 높은 코드가 됩니다. DTO에 엔티티 생성 정보 이상의 정보를 넣게 되는 경우 DTO가 사용되는 방식이 아닌 다른 경로로 엔티티를 생성할 필요가 있을 때도 필요하지 않은 정보까지 넣어서 DTO를 만들어 쓰도록 강제하는 것도 어렵겠지요. 이런 이유로 말씀하신 방식을 이번 강의에서는 선택하지 않았습니다. 강의 제목에서 볼 수 있듯이 이번엔 도메인 계층을 가능한 도메인 지식을 그대로 드러내도록 순수하게 구성해보는 것이 강의 목적이어서 그런 것이기도 합니다. 물론 현실에서는 얼마든지 말씀하신 방법을 선택하실 수 있습니다. 더 이득이 있다고 판단되면 트레이드오프를 할 수 있겠죠.개인적으로는 코틀린처럼 named parameter가 지원된다면 긴 파라미터를 가진 메소드를 그대로 사용해도 괜찮았습니다. 파라미터 이름과 전달되는 변수나 프로퍼티 이름이 직접 매칭이 되기 때문에 이후 파라미터 변경에 취약해지지 않습니다. 하지만 자바는 아직 지원되지 않는 방식이라 아쉽긴 합니다.Lombok을 이용해서 생성자 빌더를 사용하는 것도 써본적이 있습니다. 코드는 간결해지지만 결국 setter를 사용하는 것과 같은 한계를 가지고 있기 때문에 역시 아쉬움이 있었습니다.제가 선택한 방식이 어떤 경우든 최선이라고 생각하지는 않습니다. 다만, 그 결정 과정에서 어떤 이유를 고민했고, 왜 선택을 했는지, 그 방식이 도메인 코드 개발에 주는 유익은 무엇인지 정도만 한번 생각해주시면 좋겠습니다.여러 선택지 중에서 어떤 것을 고르셔도 분명한 개발 규칙을 정의하고 일관성 있게 사용되도록 하시면 좋겠습니다.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 152





