호주에 살고 있는 소프트웨어 개발자입니다. 30년간 다양한 분야의 시스템과 서비스를 개발해본 경험이 있습니다.
스프링 프레임워크와 관련 기술을 좋아하고 JVM 기반 언어를 주로 사용합니다.
한국스프링사용자모임(KSUG)을 설립하고 활동했고, 토비의 스프링이라는 책을 쓰기도 했습니다.
개발과 관련된 다양한 주제에 관해 이야기하는 것을 좋아합니다.
講義
受講レビュー
- トビーのクリーンスプリング - ドメインモデルパターンとヘキサゴナルアーキテクチャ Part 1
- トビーのクリーンスプリング - ドメインモデルパターンとヘキサゴナルアーキテクチャ Part 1
- <トビーのスプリング6>発売記念ライブ映像
- トビーのクリーンスプリング - ドメインモデルパターンとヘキサゴナルアーキテクチャ Part 1
- トビーのクリーンスプリング - ドメインモデルパターンとヘキサゴナルアーキテクチャ Part 1
投稿
Q&A
도메인 모델에서의 검증과 애플리케이션 레벨 검증의 경계
좋은 질문해주셔서 감사합니다.이것도 제가 개발팀과 자주 하는 이야기입니다. 어떤 경우엔 도메인쪽에 상세한 검증 규칙을 다 넣기도 합니다. 특히 자리수 같은 것들도 체크를 하죠. 그러면 애플리케이션 서비스, 심지어는 앞의 API에서는 체크하지 않아도 되나요. 혹은 체크가 여러번 중복되는데 이게 좋은 것인가요라는 질문을 할 수 있겠죠. 이번 강의 코드를 만들면서 우리 스프런 개발팀에서는 이런 이야기를 했습니다(물론 가상입니다). 도메인 모델을 만들 때 닉네임의 최소 자리수, 최대 자리수, 허용 문자열(영문과 숫자만 허용)과 같은 것들을 이야기하던가요? 그럴 수도 있고 아닐 수도 있다는 생각이 들었습니다. 이메일은 메일 주소 형식을 지켜야 한다라는 것은 이메일이라는 언어를 사용할 때 본질적으로 녹아져 있다고 봅니다. 그러니 도메인에서 체크할 필요가 있겠죠. 그런데 닉네임이 최소 4자리, 이건 도메인의 본질적인 속성인가 라고 물었을 때, 100% 그렇다고 답을 하지는 못했습니다. 회원의 닉네임은 필수이다까지죠.그래서 이번 개발의 정책에서 도메인 클래스에서는 금액, 수치와 같은 불변식을 지켜야 하는 경우가 아니라면, 단순 문자열의 길이 등을 직접 체크하지는 말자. 그건 애플리케이션 또는 UI(API) 로직이다라고요. 그래서 그건 앞의 애플리케이션 서비스 쪽에서 표준 빈 벨리데이션 애노테이션과 스프링의 @Validated 지원 기능을 이용해서 처리하게 했습니다. 사실은 API 레벨에서 먼저 이 빈 검증 기능을 이용해서 자리수 등을 체크할 겁니다.그럼 그 외의 경로로 회원이 만들어지는, 도메인 클래스만 사용되는 경우엔 어떻게할 것인가라고 하실 수도 있겠죠. 예를 들어 배치 작업이나 마이그레이션 같은 것들이죠. 그래서 살짝 고민이 되네요. 하지만 아직 그쪽 개발을 하고 있지 않으니 일단은 위에서 이야기한 수준으로 개발을 먼저 진행하기로 했습니다. 나중에 도메인 엔티티를 사용할 때 좀 더 엄격한 검증 로직을 더해야한다라고 하면, 그때는 아마도 MemberValidator와 같은 분리된 도메인 서비스를 만들어서 사용할 것입니다. 그러면 자릿수 같은 속성을 스프링 부트 프로퍼티 등으로 분리시켜서 지정하게 만들 수도 있을 것이고요.그래서 아직은 엔티티에선 필수 값이 들어왔는지 여부만 체크하는 것으로 만들었습니다. 이런 논의를 해볼 수 있어서 너무 즐겁네요. 정말 개발팀과 이야기를 나누는 것 같은 느낌입니다.
- 1
- 2
- 46
Q&A
테스트 준비 과정에서 서비스 메서드 호출
좋은 지적이십니다. 회원 가입 뿐 아니라 서비스, API 수준의 테스트를 하게 되면 DB와 내부 로직 적용 외에 외부 서비스 호출 등이 포함될 수 있습니다. 메일, SMS, 카톡 메시지 전송, 심지어는 은행 계좌 이체를 위한 API 실행 등도 들어갈 수도 있겠죠. 이러면 테스트 실행 성능도 떨어지고, 심지어 위험할 수 있습니다. 테스트용 이메일 발송으로 인해 이메일 전송 서비스 비용이 많이 나올 수도 있겠죠.이런 것들이 헥사고날 아키텍처를 사용하는 이유의 하나 입니다.애플리케이션 테스트를 외부 환경에 독립적으로 가능하게 하는 것이죠. 아직 이메일 발송 기능을 정식으로 구현하지 않았습니다(Part 2에서는 실제 메일이 발송되도로 구현할 겁니다). 그렇지만 메일 발송 기능이 있다고 하더라도 테스트에서는 이게 실행되지 않도록 해야 합니다. 메일 전송과 같은 헥사곤 외부에 요청할 기능은 required interface로 정의해두고요. EmailSender 인터페이스로 만들었죠. 그리고 테스트용 스프링 설정에 이 구현을 넣습니다. 그러면 메인 코드에서 실제 이메일이 발송되도록 구현한 코드가 있더라도, 그걸 무시하고 테스트쪽에 넣은 일종의 mock/stub 오브젝트에 해당하는 스프링 빈 코드가 실행됩니다. 제가 코드를 찾아보니 SplearnTestConfiguration 클래스로 만들어졌네요.@TestConfiguration public class SplearnTestConfiguration { @Bean public EmailSender emailSender() { return (email, subject, body) -> System.out.println("Sending email: " + email); } @Bean public PasswordEncoder passwordEncoder() { return MemberFixture.createPasswordEncoder(); } }이렇게 테스트가 실행될 때는 외부 기능이 운영 환경과 다르게 동작하도록 재구성하는 방법을 사용하면 됩니다. 그러면 순수하게 회원 가입 기능을 담당하는 코드와 DB(테스트용 메모리 DB)에 대한 테스트를 빠르게 진행할 수 있습니다. 그래서 마지막 질문에 대한 답은 테스트에서 실행될 필요가 없는 외부 기능은 테스트용 목 오브젝트를 사용하면 되기 때문에 MemberRegister.register()는 테스트 만들고 매번 실행하는데 아무 문제가 없습니다.
- 0
- 1
- 40
Q&A
Member#register() 메서드명이 모호하게 느껴집니다.
가장 어려운 게 이름을 정하는 것인데요. register라는 말의 일반적인 의미로 생각하면 뭔가 더 많은 단계를 거쳐야 할 것처럼 느껴지기도 하죠. 그런데 굳이 create를 쓰지 않고 register라는 말을 선택한 이유는 이 개발팀의 도메인의 보편 언어에서 그런 선택을 하기로 결정했기 때문입니다. 물론 제가 일방적으로 한 것이긴하지만요. ㅎㅎ 어쨌든 엔티티가 생명주기로 볼 때 그 온전한 기능을 사용할 수 있게 되는 활성 상태가 되는게, 이를 최초로 만드는 작업에 의해서 결정되는 경우(아직 구현은 안 했지만 강사가 강의를 만드는 것)와 생성 이후 어떤 조건을 충족해야 비로소 그 엔티티의 기능을 수행할 수 있게 되는 것으로 구분하기로 한 것입니다. 그때 선택한 우리 개발팀의 보편 언어로 "등록"과 "생성"을 구분한 것입니다.뭔가 누구나 딱 들으면 완벽하게 이해가 되는 단어를 선택할 수 있으면 좋겠다 싶지만, 많은 경우 그러기는 힘들죠. 그래서 우리가 말하는 등록/register라는 것은 이런 의미이고 이런 규칙을 가지고 사용하자라는 스프런 도메인 모델을 이해하는 것이 먼저 선행되어야 할 필요가 있습니다. 아마도 그런 논의를 하는 과정을 제가 강의에서 충분히 느끼시도록 시연 같은 것을 하지 못해서 단어의 의미를 평소 알고 계신 그 이해로 생각하고 보시면 뭔가 어색할 수도 있을 겁니다. 그래도 강의에서도 말씀드렸고, 위에 적은 것처럼 등록/register라는 스프런의 보편 언어는 이렇게 쓰인다고 이해해주시면 좋겠습니다. 등록/reigster는 생성후 일정 조건이 충족되는 것을 대기하는 상태로 만들어지는 것을 부르는 우리 도메인의 언어이다라고요.
- 0
- 1
- 43
Q&A
EntityManager#flush()를 검증하면 더 좋을 것 같습니다!
createMember() 테스트에서 flush()는 그 기능을 검증하기 위해서 넣은 것이 아닙니다. JPA 테스트가 처음 나오는 부분이라 제가 flush를 할 때 SQL이 어떤 게 날라가는지 눈으로도 볼 수 있다는 설명을 드린 것 뿐이고요. flush()에 의해서 SQL이 실행되는지를 검증하려는 목적이 아닙니다.강의에서 말씀드렸을텐데 테스트가 하나의 트랜잭션으로 묶이는 경우에 flush()를 하지 않으면 DB까지 SQL이 날라가지도 않은채로 테스트가 끝날 수 있고요. findById()를 해봤자 캐시에서 읽어오기 때문에 런타임에 실제 DB와 상호작용했을 때 문제가 없다는 것을 검증할 수 없기 때문에 넣은 것입니다. clear()는 캐시를 지우기 위한 것이고요.flush(), clear()는 검증할 대상이 아니라, 이후 이어지는 테스트의 검증 기능이 제대로 작동하게 하기 위한 보조 작업일 뿐입니다.
- 0
- 2
- 37
Q&A
테스트클래스명이 테스트 목적을 잘 나타내지 못하는 것 같습니다.
스프링 데이터 JPA와 같이 코드를 자동 생성하는 기술을 사용할 때 고민이 되는 부분입니다.테스트는 오브젝트를 실행해야 하기 때문에 구현 클래스를 사용해야 하는데 스프링 데이터는 클래스를 런타임에 자동으로 만들어주기 때문에 이걸 테스트 할 수 있는 방법은 @DataJpaTest와 같이 스프링 컨테이너를 이용해서 JPA 환경을 만들고 인터페이스 타입의 빈을 가져와 테스트하는 것 뿐이죠. 그런데 특정 클래스 구현에 대한 단위 테스트가 아니라 통합 테스트라고 본다면 리포지토리 인터페이스에 현재 매핑되는 빈(bean)을 테스트 하는 것으로는 RespositoryTest가 자연스럽다고 생각됩니다. 그 인터페이스로부터 출발해서 뒤에 (메모리) DB까지 다 검증하는 테스트 코드이기 때문이죠. 비슷하게 애플리케이션 계층의 provided 인터페이스를 타고 테스트 하는 경우에도 스프링 컨테이너를 띄우는 통합테스트인 경우엔 테스트를 수행하면서 여러 계층과 기술이 통합되어서 테스트가 됩니다. 그게 테스트 만들기도 좋고, 실제와 유사하게 기능의 동작을 검증하는 것이 자연스럽기 때문에 애플리케이션 서비스에 대한 단위 테스트보다 더 선호되는 것이죠.사실 인터페이스만 테스트할 방법이란 없습니다. 거기에 스텁을 만들어 넣고 테스트 한다면 아무런 가치가 없을 것이니까요. 대체로 리포지토리, DAO와 같은 코드는 DB 기능을 사용하는 애플리케이션의 인터페이스(자바의 인터페이스가 아니라 DB에 대한 시스템 입장에서 인터페이스)이기 때문에 그 자체로 테스트 한다는 것도 별 가치가 없습니다. 리포지토리 구현 클래스가 있다고 하더라도, EntityManager를 목킹해서 테스트 하는 건 테스트로서 별 가치를 주지 못하기 때문에 그렇습니다.리포지토리에 정의된 기능이 비록 스프링 데이터가 자동으로 구현을 만들어주긴 하지만, 그 구현을 이끌어내는 관례가 인터페이스에 반영되어 있기 때문에 지금 코드가 충분히 인터페이스 테스트이기도 하다는 생각인데요. 혹시 더 나은 이름을 생각하신 게 있으신가요.
- 0
- 2
- 37
Q&A
도메인 로직으로 분리해도 되나요?
작성하면서 살짝 고민했던 부분이네요. 강의에서 만든 코드는 사실 애그리거트 내부의 정보를 꺼내오는 코드가 외부에서 사용된다는 점에서 아쉽긴 합니다. 물론 애그리거트 데이터를 조회하는 것 자체는 문제는 아니지만요. 제안해주신 것처럼 Member에 프로필 값을 체크하는 메소드를 만드는 것도 하나의 방법입니다. 그런데 이 방식을 선택하지 않은 이유는 회원 도메인이 가지는 로직으로 이런 것까지 만들어야 하는가라는 의문이 들었기 때문입니다. 왜 Member는 프로필을 비교하는 메소드를 가져야 할까 생각해보면, 좀 과도하지 않은가라는 생각이 들었던 거죠.그래서 우선은 프로필 업데이트에 적용할 검증 로직을 서비스 계층으로 빼뒀습니다. 리포지토리를 사용해야 하는 것도 이유이고요. 그래도 프로필 값을 비교하는 부분을 도메인쪽으로 빼서 좀 더 단순하게 구성한다면, 이럴 땐 차라리 검증을 담당하는 MemberValidator 갈은 도메인 서비스를 하나 사용하는 게 더 적절할 것 같긴합니다. 근데 그렇게까지 만드는 게 당장엔 조금 과도한 작업으로 느껴져서 이건 나중에 리팩터링을 고민해보자는 생각으로 우선은 넣지 않았습니다. 그렇게 도메인 서비스까지 도입을 하지 않더라도 다음 두 줄은 메소드를 추출해서 더 의미를 드러내는 게 낫지 않았을까 싶네요. Profile currentProfile = member.getDetail().getProfile(); if (currentProfile != null && currentProfile.address().equals(profileAddress)) return;파트 2에서 리팩터링을 한번 해보겠습니다. 좋은 제안 해주셔서 감사합니다.
- 1
- 1
- 41
Q&A
MemberRepositoryTest 중 Member table이 생성이 안됨
해결하셨다니 다행입니다. 그런데 어떤 이유였는지 궁금하네요.
- 0
- 3
- 51
Q&A
도메인에 대한 개인적인 경험이 다음 설계에 영향을 주는 경우가 많습니다. 토비님께서는 설계를 하실 때, 이전 경험에서 비롯된 도메인을 어느 정도까지 설계에 반영하시나요?
이미 익숙한 분야를 다룰 때는 당연히 기존의 경험이나 지식이 도메인 설계에 영향을 줍니다. 이미 작동하는 비즈니스가 있는 경우가 아니라, 새로운 서비스를 구상하고 만들 때는 더욱 그렇죠. 온라인 강의 서비스에서 수강신청과 결제라는 개념은 너무 자연스럽게 연결됩니다. 그 정도로 부담을 가지실 필요는 없습니다. 혼자 도메인을 정리하면 다양한 아이디어가 떠오르기도 하죠. 그래서 개발팀과 기획자, 현업 담당자들과 함께 이야기를 나눌 필요가 있습니다. 그때는 자연스럽게 결제 이야기도 나오지 않을까요? 제가 결제를 도메인 이야기에 넣지 않은 것은 이건 꽤 흥미롭고 복잡한 개념이기 때문에 조금 뒤로 미루기로 결정을 했기 때문입니다. 상용 비즈니스를 만드는데, 그러면 우리는 어떻게 수익을 낼 것인가 이야기는 자연스럽고 당연히 결제까지 이어질 수 있을 겁니다. 그러니 오버엔지니어링이 아닌가 고민하실 필요는 전혀 없습니다. 다만, 결제라는 도메인 개념을 깊이 탐구하려면 꽤 많은 시간을 들이고 고민을 해야 합니다. 이런 경우엔 결제 도메인을 더 연구해볼 수 있을 때까지는, 수강신청후 정해진 조건을 만족하는 경우에 라고 좀 더 추상적으로 일단 이야기해두고 뼈대가 되는 기능을 개발하는 걸 먼저 하는 것이 제 스타일입니다. 이미 실무가 돌아가는 비즈니스의 도메인이 아닌 신규 서비스를 만들 때라면, 복잡한 것은 뒤로 미룬다가 제 방식이긴합니다. 다만 이번 Part 1 강의는 개발 초반 2-3일 정도에 일어나는 일이라서 결국엔 조만간 결제 얘기도 나오는 게 자연스럽겠죠.
- 1
- 2
- 83
Q&A
인터페이스 위치를 결정하는 기준에 관해
안녕하세요. 강의 들어주셔서 감사합니다.PasswordEncoder를 어디에 둘지 결정할 때 사용한 기준은 도메인 모델을 이야기할 때 이게 등장하는가 입니다. 도메인을 탐구하고 모델로 정리하면서 "회원의 비밀번호는 혹시 DB가 유출되더라도 회원의 비번이 노출되지 않도록 암호화해서 저장해둔다"라고 했죠. 그래서 도메인 계층에 비밀번호를 암호화한다라는 모델을 반영해서 PasswordEncoder 인터페이스를 정의해둔 것입니다. 또, Member라는 엔티티가 이 PasswordEncoder에 의존(사용)하기 때문이기도 하죠. 이런 경우 PasswordEncoder를 애플리케에션 계층에 두면 도메인이 그 밖에 있는 애플리케이션 계층에 의존하는, 계층 아키텍처의 의존 규칙을 위반하게 됩니다. 그걸 피하려면 비밀번호를 인코딩하는 걸 서비스 계층에서 하고, 그렇게 얻은 해시값을 다시 Member에 넘겨야 하는데, 이렇게 코드가 만들어지면 도메인 로직이 두 계층에 흩어지는 문제가 있기도 하죠.그런데 말씀하신 대로 PasswordEncoder의 구현은 특정 기술이나 환경에 의존적이라 헥사곤 밖에 있는 어댑터에서 구현 또는 연동(비번 인코딩 서비스가 외부에 있다면)을 해야 합니다. 그렇다면 PasswordEncoder는 required interface가 아닌가라고 생각이 되겠죠.맞습니다. PasswordEncoder는 헥사곤(애플리케이션) 입장에서 볼 때 기능 요구 인터페이스(required interface)가 맞습니다. 원래 헥사고날 아키텍처는 내부에 도메인 계층을 분리하는 것을 지정하지 않았습니다. 그건 우리가 내부에 다시 계층을 하나 더 넣기로 결정한 것일 뿐이죠. 그렇게 보면 도메인 계층에 있는 PasswordEncoder도 결국 헥사고날의 포트가 되는 겁니다. 포트는 헥사곤 내부에 정의해 두고, 그 밖에서 구현한다라는 헥사고날의 원칙을 지키는 것이죠.다만, 도메인 모델 패턴을 적용하기 위해서 계층을 하나 더 내부에 만들었기 때문에 도메인 계층에 정의된 인터페이스는 required 패키지에 직접 둘 수는 없을 뿐입니다. 이게 처음에는 살짝 혼동이 될 수도 있습니다만, 계속 개발을 해보면 자연스러워질 것입니다.인터페이스로 정의한 도메인 서비스의 구현은 어떤 경우엔 도메인 계층 안에 구현이 들어가기도 하고, 어떨 때는 애플리케이션 서비스에 구현을 둘 수도 있습니다. 그 구현 코드 성격에 따라서 결정되는 것이죠. 이번처럼 도메인 로직 밖으로 분리하는 것이 적당한 경우라면 외부로 빼둔 것이고요. 이에 대해서 설명을 했다고 생각했는데 좀 부족했던 것 같습니다. 더 궁금하신 게 있으시면 알려주세요.
- 1
- 2
- 60
Q&A
record 생성 시 휴먼 에러 발생 가능성
안녕하세요.네. 말씀하신대로 record도 생성자 호출로 값을 넣는 거라서 같은 타입이 연속으로 나오면 실수할 가능성이 있습니다. 저도 코틀린으로 개발할 때 named parameter를 사용하는 게 무척 편했는데요. 자바에서는 Builder를 사용하기엔 필수 값을 모두 받지 못할 수도 있어서 기피하게 됩니다. 어떨 땐 차라리 setter로 넣는게 코드에서 혼란이 없지 않나 싶기도 하고요. 이건 참 풀리지 않는 문제입니다.그래도 도메인 계층으로 넘길 때는 record를 사용하고, 이 record를 컨트롤러에서 API로부터 전달되는 값을 받을 파라미터로도 사용하면, 이때는 API로 전달되는 값을 이름으로 매핑해서 받을 수 있으니 조금 더 안전하지 않을까 싶습니다. 어쨌든 주의해야 할 곳이 줄어드는 것만으로도 도움이 될 것 같고요. 사실 named parameter나 setter를 쓰더라도 실수하기도 합니다. 비슷한 이름의 프로퍼티가 있는 경우나, 정말 순간 헷갈려서 다른 값을 넣기도 하죠. 이런 코딩 실수를 최대한 막아줄 방법을 계속 궁리를 해봐야겠습니다.
- 0
- 2
- 49