호주에 살고 있는 소프트웨어 개발자입니다. 30년간 다양한 분야의 시스템과 서비스를 개발해본 경험이 있습니다.
스프링 프레임워크와 관련 기술을 좋아하고 JVM 기반 언어를 주로 사용합니다.
한국스프링사용자모임(KSUG)을 설립하고 활동했고, 토비의 스프링이라는 책을 쓰기도 했습니다.
개발과 관련된 다양한 주제에 관해 이야기하는 것을 좋아합니다.
강의
수강평
- 토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1
- 토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1
게시글
질문&답변
email과 패스워드 VO 질문이 있습니다.
안녕하세요.이메일의 검증 로직은 이메일이라는 형식은 고정되어 있고 이를 검증하는 로직이 시간이 지남에 따라 발전할 가능성은 없을 것이라고 봤기 때문에 검증 로직은 그대로 고정했습니다. 다만, 이메일 VO의 경우 회원 뿐 아니라 다른 엔티티에서도 사용될 가능성이 높기 때문에 이메일을 공통 VO로 분리했던 것이죠.반면에 비밀번호 암호화는 기술적으로 여러가지 변경 가능성이 높습니다. 물론 서비스가 정식으로 운영되기 시작한 시점 이후에는 바뀌지 않을 수도 있지만, 여러가지 이후로 변경이 필요할 가능성이 있고(같은 종류의 비밀번호 로직이라도 성능을 위해서 암호화 전용 서비스를 API로 호출하거나 별도의 라이브러리를 이용하도록 외부화 할 수도 있겠죠), 서비스 개시 전까지는 보안 점검을 하면서 다양한 구현 알고리즘과 로직 구성 방식을 적용할 수 있어야 합니다. 그래서 외부에서 주입하기에 적당하기 때문에 이를 주입 받아 사용하도록 일종의 전략 패턴 사용했습니다.비슷하지만 분명한 차이가 있는 두 가지 대상이라고 저는 판단했는데요. 혹시 다른 의견이나 제안이 있으시면 알려주세요.
- 0
- 2
- 37
질문&답변
템플릿 콜백 패턴 관련하여 궁금한 것이 있습니다!
어떤 오브젝트가 의존(사용)하는 오브젝트를 동적으로 결정한다는 것은 우선 코드 레벨에서 의존 관계가 고정되어 있지 않다는 의미입니다. 일단 애플리케이션이 시작한 뒤에 @Configuration 클래스 등에서 프로퍼티나 다른 조건을 가지고 의존성을 주입해주는 경우도 동적이라고 볼 수 있습니다. 당연히 코드는 인터페이스에만 의존해야겠죠. 이런 경우는 어셈블러라고도 불리는 이런 빈 구성정보를 담은 다른 코드에 의해서 의존 관계가 결정됩니다. 프로퍼티나 심지어 이 시점에 DB를 액세스해서 어떤 빈을 사용할지 결정할 수도 있겠죠.여기서 한단계 더 들어가면 전형적인 전략 패턴 스타일로 의존 오브젝트가 결정될 수도 있습니다. 예를 들어 사용자의 요청마다 어떤 조건을 보고 이미 준비되어있는 여러 같은 타입의 오브젝트 중에서 하나를 선택하는 것이죠. 템플릿-콜백 패턴에서 콜백의 경우 아예 매번 new로 생성되어 전달되기도 합니다. 이때는 이것도 동적인 방식이겠죠. 또 다른 방법으로는 미리 준비해둔 여러가지 빈 중에서 원하는 것을 선택해서 사용하는 방법이 있습니다. 이게 전략 패턴에 가장 가까운 방식인데요. 알고리즘을 교체해서 사용하거나, 정책 등을 바꾸는 경우에 이 방식을 많이 씁니다.이때는 해당 알고리즘을 담은 인터페이스를 구현한 스프링 빈이 여러개가 될 수도 있습니다. 이를 사용하는 서비스 빈 같은 데서 하나만 주입을 받을 수는 없으니 이 때는 List나 Map 등으로 모든 빈을 컬렉션으로 받아둘 수 있습니다. 그리고 데이터나 코드의 로직에 의해서 이 중에서 적절한 것을 선택해서 직접 호출하거나 콜백 형태로 템플릿에 주입해주기도 합니다. Map으로 하면 key를 잘 세팅해서 한번에 원하는 오브젝트를 찾을 수도 있고요. List라면 매번 어떤 조건에 해당하는 빈을 찾는 로직을 한번 더 거쳐야 할 겁니다. 데이터를 기준으로 선택한다면 Map으로 가지고 있는 빈 들의 key에 해당하는 값을 데이터로부터 만들 수 있으면 가장 편리합니다.
- 0
- 2
- 19
질문&답변
UseCase 메서드 단위에 대한 Best Practice
헥사고날 아키텍처에서의 포트는 인터페이스로 표현됩니다. 인터페이스 이름에 UseCase라는 접미사를 붙이는 유행이 있긴하지만 이것은 별로 좋은 네이밍은 아닙니다. 질문하신 내용은 헥사고날 아키텍처의 경계로 노출되는 포트의 범위를 어떻게 잡는가라고 이해하고 제 생각을 말씀드릴게요. 포트는 헥사고날 애프리케이션을 액터가 사용하는 의도(intention)를 담고 있어야 하고, 이 의도를 기준으로 정의합니다. 물론 의도를 세분화해서 쪼갤 수도 있고, 유사한 의도를 묶어서 하나의 포트로 정의할 수 있습니다. 그에 따라 포트를 표현한 인터페이스의 메소드 갯수가 달라지겠죠.보통 비즈니스 로직을 담고 있는, 특히 변경이 일어나는 기능을 담은 것은 의도를 묶기가 상대적으로 쉽습니다. 반면 예로 드신 조회 로직은 이보다 세분화될 수 있고, 그래서 그룹핑할 수 있는 단위를 정하기가 애매하기도 합니다. 가장 쉽고 이상적인 건 모든 인터페이스가 하나의 메소드만 가지는 것입니다. 하지만 이러면 코드가 장황해지겠죠. 특히 조회처럼 세부 조건이 달라질 때마다 메소드를 추가하기도 하는 경우에는 이런 구분이 오히려 코드를 이해하는데 인지 부하를 더 줄 겁니다. 일단 응집도나 SRP는 오브젝트의 설계 원칙이므로 인터페이스레 이를 적용하는 건 적절하지 않습니다. 응집도가 높으면 변경이 일어날 때 전체가 같이 변경되어야 합니다. 조회 인터페이스에 이를 기계적으로 적용한다면 메소드 시그니처 하나 변경할 때 나머지도 변경되어야 응집도가 높은 것인데, 그렇기는 쉽지 않죠. 하지만 그렇다고 이게 설계의 문제가 되지는 않습니다. 대체로 이런 조회 인터페이스를 구현한 오브젝트는 구현체 내부가 대부분 독립적이라서 일부분만 변경된다고 해서 변경 전후에 코드를 이해하는데 어려움이 생기거나 나머지 코드에 영향을 줄 가능성이 적기 때문입니다.SRP는 이런 경우라면 좀 크게 해서하는게 적절합니다. 이 인터페이스의 구현 클래스는 "Concert를 조회한다"는 이유 하나 때문에만 변경된다면 그것으로도 SRP가 잘 충족되었다고 볼 수 있습니다. 너무 세부 조회로직 구현의 변경에 SRP를 기계적으로 적용한다고 더 나은 설계가 될 것으로 보이지 않습니다. 그보다는 조회 인터페이스는 ISP가 더 중요할 수도 있습니다. 이걸 사용하는 클라이언트가 항상 전체를 같이 사용할 가능성이 높은가, 아니면 사용 용도에 따라서 클라이언트가 명확히 구분되는가겠죠. 이건 한번쯤 생각해보면 어떻게 분리하는 것이 적절한지가 그려집니다. 그에 따라 테스트를 만드는 것도 자연스러워지겠죠. 하지만 이게 여러 다른 모듈에서 사용되어질 가능성이 높은 공통 정보를 다루는 경우라면 클라이언트를 제한하기가 어려울 수도 있긴 합니다.또, API 설계에 따라서 이걸 단순하게 바꿀 수도 있습니다. ID로 조회하는 것은 이미 명확하게 어떤 엔티티에 대한 레퍼런스를 확보해서 빠르게 결과를 성공적으로 얻어오는 케이스에서 쓰이겠지만, 나머지 3개는 좀 더 검색 목적의 조회일 가능성이 있기도 하겠네요. 이런 경우 파라미터를 이용해서 좀 더 동적이고 확장 가능한 조회 로직으로 설계할 수도 있을 겁니다. 이러면 메소드가 2개가 될 수도 있겠죠. 결국 최종 결정은 액터 혹은 클라이언트가 이 포트를 구현한 애플리케이션을 어떤 의도를 가지고, 어떤 단위로, 어느 시점에 사용할 것인지, 또 이후에 얼마나 더 많은 조회 로직이 추가될 것인지 등을 기준으로 결정하면 될 것 같습니다.지속적으로 추가 변경될 가능성이 높다면 꾸준히 이를 다듬어 가도 좋을텐데, 그렇다면 일단은 조금 큰 단위로 만들고 시작하는게 좋겠습니다. 당연히 인터페이스를 한번에 크게 만들지는 말고 필요할 때마다 추가해나가는 거죠. 테스트도 마찬가지고요. 그러다 쓰임새가 구분이 되는 느낌이 들고, 구현 부분이 달라지는 메소드가 드러난다면 그때 분리해도 좋습니다. 반대로 인터페이스를 미리 세분화하고 시작한다면 구현 클래스는 일단 하나로 만들었다가 구현이나 의존성에 따라서 구현 클래스를 구분해도 됩니다. 그런데 같은 클라이언트가 이렇게 구분한 인터페이스 이러개를 매번 구현하는 모습이 보이면, 그때는 클라이언트를 기준으로 묶는 것이 필요합니다. 이것도 사실 테스트까지 잘 만들어져 있다면 별로 어려운 리팩터링은 아닙니다만, 초반에 좀 번거롭게 시작하는 느낌이 들기도 하죠. 우선은 이정도로 설명을 드리겠습니다. 이후 강의에서 제가 어떻게 초기에 설계를 하고 이를 단계적으로 바꿔가는지 보여드리고 싶은데, 그때 기회가 되면 이에 대한 언급을 더 해보겠습니다.
- 0
- 2
- 60
질문&답변
멀티모듈
멀티 모듈 구조에 대해서는 이후 강의에서 다루겠지만, 항상 이렇게 해야한다라는 절대적인 기준은 없습니다.여기서 모듈은 아마도 Gradle 모듈이겠죠? 이걸 계층 레벨에서 분리한다면 지금 말씀하신 api / core(application, domain)이 가장 간단하게 처음 시도해볼 수 있는 방법입니다.모듈을 나눴을 때 중요한 건 모듈 레벨에서 의존관계가 명확해야 한다는 것이죠. 애초에 adapter, application(hexagon)의 의존관계가 잘 설정되었다면 모듈로 분리해도 의존 관계가 단 방향이 됩니다. 여기서 domain까지 별도 모듈로 분리하는 방법도 있습니다만, 그냥 같은 모듈에 두고 ArchUnit같은 도구로 패키지 레벨에서 의존방향을 체크해도 좋습니다. 이러다 기능이 많아지면 core를 다시 기능별 모듈로 나눌 수 있겠죠. 다음 강의에서 다루겠지만, 이때도 feature/slice의 의존관계가 단 방향으로 유지되는 게 좋습니다.api 어댑터를 분리하고 나면, 아마도 이후에 batch라든가, admin 등의 다른 종류의 프라이머리 어댑터 계층을 독립적인 모듈로 추가할 수 있습니다. 각각이 하나의 Spring Batch 애플리케이션이 되기 때문에 분리되는 것이 이상적입니다.반대로 세컨더리 어댑터들은 core와 이후 분리된 슬라이스에서 공유되는 경우가 많습니다. 이것도 단계적으로 모듈로 분리할 수 있습니다. DB와 관련된 persistence, 그리고 외부 서비스와의 통합을 담당하는 integration, 또 security 등등이죠. 이후에 또 내부, 외부 연동을 위해서 더 세분화 하기도 합니다. 혹은 MSA로 확장되면 더 큰 단위로 분리가 되기도 하죠. 이후 강의에서 이런 케이스들을 계속 다뤄보겠습니다.
- 0
- 2
- 73
질문&답변
DTO를 서비스 레이어에서 사용할 수 밖에 없다면
엔티티가 아니면서 데이터를 담는 모든 오브젝트가 DTO는 아닙니다. DTO는 오브젝트가 계층이나 시스템의 경계를 넘어서 데이터를 전달하는 것에 주 목적인 경우에 DTO라고 할 수 있습니다. 만약 JDBC 등을 이용해서 쿼리 결과를 매핑해서 담는 오브젝트는 활용 용도에 따라서 여러가지로 부를 수 있습니다. 만약 도메인 모델 패턴을 적용하고 싶다면 JDBC를 사용하더라도 도메인 모델로 설계된 오브젝트에 매핑할 수 있습니다. 이 경우 JDBC를 이용해서 도메인 오브젝트를 만들어 그 안에 로직을 담아 활용할 수 있습니다. 물론 이때도 서비스 계층에서 리턴할 때 이 오브젝트를 사용하겠죠. 이런 경우엔 DTO라기 보다는 도메인 오브젝트를 리턴한다고 해도 됩니다. 물론 목적이 단순 조회인 경우에, 그리고 도메인 모델과 직접 연결되지 않는 경우엔 일종의 DTO에 담아서 리턴할 수 있습니다. 이 경우 DTO가 애플리케이션 로직을 담고 있고 애플리케이션 로직에 의존하고 있기 때문에 서비스 계층에 DTO를 두는 것이 자연스럽습니다.
- 0
- 2
- 66
질문&답변
강의 업데이트 내역 질문
아직은 기존 강의 내용에 업데이트는 없습니다.그런데 혹시 업데이트가 표시됐다면 질문 주신 분이 계셔서 강의자료의 디스코드 링크를 제가 다시 확인하고 저장을 해서 업데이트로 나온게 아닌가 싶네요.이후에 강의 내용에 변경이나 추가된 게 있으면 새소식 기능을 통해서 알려드리겠습니다. 새소식이 등록되면 메일을 받으실 겁니다.
- 0
- 1
- 60
질문&답변
"MemberFinderTest, MemberRegisterTest" record관련
@Transational은 두 가지 다른 목적으로 사용됩니다. 이 중에서 프록시를 만들어서 AOP로 트랜잭션 기능을 부여하는 경우의 사용에는 final 클래스를 쓰면 안 됩니다. 따라서 record도 사용할 수 없죠. 그런데 테스트 코드에 붙는 @Transactional은 동작 방식이 전혀 다릅니다. 테스트 프레임워크가 JUnit의 테스트 실행 프로세스에 참여해서 트랜잭션을 시작하고 테스트를 수행하는 방식으로 동작하기 때문에 상속을 통해서 트랜잭션을 만드는 메인 코드의 @Transactional과 제약조건이 전혀 다릅니다. 따라서 record와 같은 final 클래스를 사용해도 아무런 문제가 없습니다.IntelliJ에는 이에 대한 검증 경고를 에러처럼 표시하는 버그가 있습니다. 하지만 실행해보면 아무 문제 없이 트랜잭션이 적용된 테스트가 수행되죠. 강의에서 한번 이에 대해서 설명을 드리긴 했습니다. 저는 IntelliJ의 스프링 코드 검증 기능은 가능한 끄는 것이 좋다고 생각됩니다. 의존관계 관련 에러나 경고 표시도 불필요한 것이 너무 자주 등장합니다. 이럴 때는 해당 에러 메시지에 디테일에서 validation을 off 시키는 것을 권장드립니다. IntelliJ의 이런 버그들이 빨리 고쳐지면 좋겠네요.
- 0
- 2
- 53
질문&답변
인터페이스
인터페이스를 구현한 클래스라고 모든 메소드가 다 인터페이스에 있어야 한다는 것은 잘못된 생각입니다. 객체지향의 설계 원칙인 캡슐화를 생각해보면 됩니다. 자주 변경되지 않고 다른 오브젝트에게 공개할 기능은 public으로 만듭니다. 반대로 내부에서 여러가지 이유로 분리해서 만들었지만, 이건 외부에 공개할 이유가 없고, 그리고 변경될 가능성이 있거나 구현 상세를 노출하면 안 되는 것들은 private으로 만듭니다. 이건 인터페이스를 떠나서 일반 클래스를 설계할 때도 적용되는 것입니다.그러면 인터페이스를 구현해서 만든 경우는 어떻게 해야할지에 말씀드릴게요. 당연히 인터페이스를 구현했다고 모든 메소드를 public으로 만들고 인터페이스에 클래스의 모든 메소드를 넣어야 하지 않습니다. 이건 클래스 레벨서의 이유와 동일합니다.그런데, 클래스의 모든 public 메소드는 인터페이스에 있어야 할까요? 일반적으로 단일 인터페이스를 구현해서 만드는 ServiceImpl 같은 경우엔 그럴 가능성이 높지만, 꼭 그래야하는 것은 아닙니다. 오브젝트를 외부 모듈에 인터페이스를 통해서 공개할 때는 그 인터페이스를 이용하는 클라이언트(인터페이스를 통해서 기능을 호출하는 코드를 다 클라이언트라고 부를 수 있습니다)의 의도에 맞춰서 인터페이스를 정의하고 구현하도록 하면 되는데, 종종 하나의 오브젝트가 한 개 이상의 인터페이스를 구현하기도 하고 이게 필요하거나 자연스러운 구현 방법일 수도 있습니다. 이 경우 특정 인터페이스가 그걸 구현한 클래스의 모든 public 메소드를 다 가지고 있지 않게 되죠. 자바에서 자주 쓰는 ArrayList라는 클래스를 보시면 구현하고 있는 인터페이스가 List, Iterable, Collection, RandomAccess, Cloneable, Serializable로 여러개 입니다. 이 클래스로 만든 오브젝트를 사용하는 클라이언트가 어떤 인터페이스를 바라보고 쓰느냐에 따라 필요한 인터페이스 타입으로 사용하도록 만들면 됩니다. 심지어 메소드가 없는 마커 인터페이스도 있죠. 이것도 해당 타입으로 오브젝트를 인식해서 필요한 용도의 기능으로 사용할 수 있습니다. 객체지향설계원칙 SOLID 중에서 인터페이스 분리 원칙(ISP)이 있습니다. 이걸 잘 지켜서 설계하면, 여러가지 이유로 구현 클래스는 하나이지만 하나 이상의 인터페이스를 구현하도록 만드는 경우가 종종 발생합니다. 그래서 클래스의 public 메소드를 하나의 인터페이스에 반드시 다 담아야 하는 것은 아닙니다. 다만, 예로 드신 서비스 클래스와 (단일) 서비스 인터페이스인 경우엔 특별한 이유가 없다면 클래스의 public 메소드는 인터페이스에 정의된 메소드로만 구성될 것입니다. 스프링 기준으로 종종 발견되는 예외가 있다면 setter 주입을 사용하는 경우 setter 메소드는 public이지만 인터페이스에 포함되지 않겠죠. 요즘은 보통 생성자 주입을 많이 쓰지만, 선택적인 의존 오브젝트나 정보인 경우엔 생성자가 아니라 setter를 사용하기도 합니다.
- 0
- 2
- 51
질문&답변
배포 시 테스트 코드가 돌아갈때 사용하게 될 RDB 셋팅에 관하여..
중요한 질문을 해주셨네요. 강의 시리즈에서 언젠가 다룰 내용입니다.이번 강의에선 우선은 스프링이 지원하는 메모리DB를 이용해서 빠르게 테스트를 수행하는 방법을 선택했습니다. 테스트 수행 속도도 빠르고, JPA의 다이얼렉트 지원으로 각 DB에 맞는 SQL이 생성되기 때문에 나중에 MySQL에서 동작할 코드이더라도 우선은 h2와 같은 가벼운 DB에서도 거의 차이가 없이 기능을 수행하게 할 수 있어서 제법 충분한 검증이 가능합니다.하지만 운영 시스템으로 배포하기 전에는 실제 사용할 DB를 이용하는 최종 테스트를 수행하는 것이 바람직합니다. 또, 성능을 위해서 특정 DB의 네이티브 쿼리와 같은 테스트용 메모리 DB로는 수행이 불가능한 기능을 테스트하기 위해서도 운영에서 쓰기로 한 MySQL 테스트 DB를 준비해서 테스트를 하는 것이 바람직합니다.배포 전에 진행할 테스트에 사용할 DB는 CI/CD 파이프라인에서 도커를 이용해서 환경을 준비하고 사용할 수 있고, 테스트가 병렬적으로 수행되지 않는 것이 보장된다면 미리 설치해둔 테스트용 DB에 대해서 테스트를 할 수 있습니다. Jenkins에서 배포를 준비하면서 테스트를 수행하는 경우라면 도커를 이용해서 셋팅된 MySQL에 대해서 테스트를 하시는 것이 좋을 듯합니다. 빌드와 배포 파이프라인이 여러 단계가 있고 복잡하다면 각각 다른 전략을 선택할 수 있겠지만, 반드시 지켜야 할 것은 테스트 수행 성능을 위해서 메모리DB를 사용하기로 결정했더라도, 중요한 배포 이전에, 혹은 팀의 결정에 따라 PR 생성이나 배포 준비 단계에서라도 실제 DB로 테스트를 수행하는 것입니다. 경우에 따라서 로컬에서도 MySQL을 로컬 설치한 DB나 Testcontainer를 이용해서 띄우고 매번 테스트하는 방법을 선택할 수도 있습니다. DB 준비 과정이 복잡하지 않다면 테스트 수행 성능은 큰 차이는 없습니다.
- 1
- 2
- 77
질문&답변
Exception 정의 기준
Profile, Email에서 발생하는 오류는 일반적인 형식 검증을 통과하지 못한 경우에 발생하는 것이고 이에 대한 예외는 자바에서 범용적으로 쓰이는 것이 존재하기 때문에 그걸 선택했습니다. 두 가지 이유가 있는데요. 하나는 프론트엔드에서 충분히 검증하고 걸러져야 하는 경우인데 이게 서버까지 넘어왔다면 일종의 버그입니다. 버그에 대해서 도메인 지식을 담은 커스톰 예외를 굳이 선언하는 것은 의미가 없고, 낭비라고 보기 때문입니다. NPE에 대해서 매번 의미있는 예외를 만들지 않는 것과 같습니다. 아직 서버가 던지는 예외에 대한 표준 가이드를 논의하고 결정하지 않았습니다. 파트 2나 시리즈 후반에 이를 본격적으로 검토하고 표준을 잡아서 가이드에 넣을 때 변경될 수도 있습니다. 반면 이메일 중복 예외는 도메인 로직을 표현한 코드입니다. 도메인 지식을 담고, 도메인에서 정한 제약조건을 나타내야 하고, 이건 프론트엔드에서 사전 검증이 불가능하거나 어렵고, 또 복구 가능한(중복되지 않는 다른 이메일로 재시도) 예외이기 때문에 커스톰 예외로 정의된 것입니다. 이건 자바가 가지고 있는 일반적인 예외 클래스로는 그 의미를 담을 수 없기 때문이기도 합니다. 일단은 이 정도 기준으로 Part 1에서는 예외 사용을 결정했습니다. 앞으로 더 논의해볼 부분이 생기면 그때 다시 예외 정책에 관해 이후 강의에서 다뤄보도록 하겠습니다.
- 0
- 1
- 63