호주에 살고 있는 소프트웨어 개발자입니다. 30년간 다양한 분야의 시스템과 서비스를 개발해본 경험이 있습니다.
스프링 프레임워크와 관련 기술을 좋아하고 JVM 기반 언어를 주로 사용합니다.
한국스프링사용자모임(KSUG)을 설립하고 활동했고, 토비의 스프링이라는 책을 쓰기도 했습니다.
개발과 관련된 다양한 주제에 관해 이야기하는 것을 좋아합니다.
강의
수강평
- 토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1
- 토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1
- 토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1
- 토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1
게시글
질문&답변
"MemberFinderTest, MemberRegisterTest" record관련
@Transational은 두 가지 다른 목적으로 사용됩니다. 이 중에서 프록시를 만들어서 AOP로 트랜잭션 기능을 부여하는 경우의 사용에는 final 클래스를 쓰면 안 됩니다. 따라서 record도 사용할 수 없죠. 그런데 테스트 코드에 붙는 @Transactional은 동작 방식이 전혀 다릅니다. 테스트 프레임워크가 JUnit의 테스트 실행 프로세스에 참여해서 트랜잭션을 시작하고 테스트를 수행하는 방식으로 동작하기 때문에 상속을 통해서 트랜잭션을 만드는 메인 코드의 @Transactional과 제약조건이 전혀 다릅니다. 따라서 record와 같은 final 클래스를 사용해도 아무런 문제가 없습니다.IntelliJ에는 이에 대한 검증 경고를 에러처럼 표시하는 버그가 있습니다. 하지만 실행해보면 아무 문제 없이 트랜잭션이 적용된 테스트가 수행되죠. 강의에서 한번 이에 대해서 설명을 드리긴 했습니다. 저는 IntelliJ의 스프링 코드 검증 기능은 가능한 끄는 것이 좋다고 생각됩니다. 의존관계 관련 에러나 경고 표시도 불필요한 것이 너무 자주 등장합니다. 이럴 때는 해당 에러 메시지에 디테일에서 validation을 off 시키는 것을 권장드립니다. IntelliJ의 이런 버그들이 빨리 고쳐지면 좋겠네요.
- 0
- 2
- 23
질문&답변
인터페이스
인터페이스를 구현한 클래스라고 모든 메소드가 다 인터페이스에 있어야 한다는 것은 잘못된 생각입니다. 객체지향의 설계 원칙인 캡슐화를 생각해보면 됩니다. 자주 변경되지 않고 다른 오브젝트에게 공개할 기능은 public으로 만듭니다. 반대로 내부에서 여러가지 이유로 분리해서 만들었지만, 이건 외부에 공개할 이유가 없고, 그리고 변경될 가능성이 있거나 구현 상세를 노출하면 안 되는 것들은 private으로 만듭니다. 이건 인터페이스를 떠나서 일반 클래스를 설계할 때도 적용되는 것입니다.그러면 인터페이스를 구현해서 만든 경우는 어떻게 해야할지에 말씀드릴게요. 당연히 인터페이스를 구현했다고 모든 메소드를 public으로 만들고 인터페이스에 클래스의 모든 메소드를 넣어야 하지 않습니다. 이건 클래스 레벨서의 이유와 동일합니다.그런데, 클래스의 모든 public 메소드는 인터페이스에 있어야 할까요? 일반적으로 단일 인터페이스를 구현해서 만드는 ServiceImpl 같은 경우엔 그럴 가능성이 높지만, 꼭 그래야하는 것은 아닙니다. 오브젝트를 외부 모듈에 인터페이스를 통해서 공개할 때는 그 인터페이스를 이용하는 클라이언트(인터페이스를 통해서 기능을 호출하는 코드를 다 클라이언트라고 부를 수 있습니다)의 의도에 맞춰서 인터페이스를 정의하고 구현하도록 하면 되는데, 종종 하나의 오브젝트가 한 개 이상의 인터페이스를 구현하기도 하고 이게 필요하거나 자연스러운 구현 방법일 수도 있습니다. 이 경우 특정 인터페이스가 그걸 구현한 클래스의 모든 public 메소드를 다 가지고 있지 않게 되죠. 자바에서 자주 쓰는 ArrayList라는 클래스를 보시면 구현하고 있는 인터페이스가 List, Iterable, Collection, RandomAccess, Cloneable, Serializable로 여러개 입니다. 이 클래스로 만든 오브젝트를 사용하는 클라이언트가 어떤 인터페이스를 바라보고 쓰느냐에 따라 필요한 인터페이스 타입으로 사용하도록 만들면 됩니다. 심지어 메소드가 없는 마커 인터페이스도 있죠. 이것도 해당 타입으로 오브젝트를 인식해서 필요한 용도의 기능으로 사용할 수 있습니다. 객체지향설계원칙 SOLID 중에서 인터페이스 분리 원칙(ISP)이 있습니다. 이걸 잘 지켜서 설계하면, 여러가지 이유로 구현 클래스는 하나이지만 하나 이상의 인터페이스를 구현하도록 만드는 경우가 종종 발생합니다. 그래서 클래스의 public 메소드를 하나의 인터페이스에 반드시 다 담아야 하는 것은 아닙니다. 다만, 예로 드신 서비스 클래스와 (단일) 서비스 인터페이스인 경우엔 특별한 이유가 없다면 클래스의 public 메소드는 인터페이스에 정의된 메소드로만 구성될 것입니다. 스프링 기준으로 종종 발견되는 예외가 있다면 setter 주입을 사용하는 경우 setter 메소드는 public이지만 인터페이스에 포함되지 않겠죠. 요즘은 보통 생성자 주입을 많이 쓰지만, 선택적인 의존 오브젝트나 정보인 경우엔 생성자가 아니라 setter를 사용하기도 합니다.
- 0
- 2
- 33
질문&답변
배포 시 테스트 코드가 돌아갈때 사용하게 될 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 준비 과정이 복잡하지 않다면 테스트 수행 성능은 큰 차이는 없습니다.
- 0
- 2
- 40
질문&답변
Exception 정의 기준
Profile, Email에서 발생하는 오류는 일반적인 형식 검증을 통과하지 못한 경우에 발생하는 것이고 이에 대한 예외는 자바에서 범용적으로 쓰이는 것이 존재하기 때문에 그걸 선택했습니다. 두 가지 이유가 있는데요. 하나는 프론트엔드에서 충분히 검증하고 걸러져야 하는 경우인데 이게 서버까지 넘어왔다면 일종의 버그입니다. 버그에 대해서 도메인 지식을 담은 커스톰 예외를 굳이 선언하는 것은 의미가 없고, 낭비라고 보기 때문입니다. NPE에 대해서 매번 의미있는 예외를 만들지 않는 것과 같습니다. 아직 서버가 던지는 예외에 대한 표준 가이드를 논의하고 결정하지 않았습니다. 파트 2나 시리즈 후반에 이를 본격적으로 검토하고 표준을 잡아서 가이드에 넣을 때 변경될 수도 있습니다. 반면 이메일 중복 예외는 도메인 로직을 표현한 코드입니다. 도메인 지식을 담고, 도메인에서 정한 제약조건을 나타내야 하고, 이건 프론트엔드에서 사전 검증이 불가능하거나 어렵고, 또 복구 가능한(중복되지 않는 다른 이메일로 재시도) 예외이기 때문에 커스톰 예외로 정의된 것입니다. 이건 자바가 가지고 있는 일반적인 예외 클래스로는 그 의미를 담을 수 없기 때문이기도 합니다. 일단은 이 정도 기준으로 Part 1에서는 예외 사용을 결정했습니다. 앞으로 더 논의해볼 부분이 생기면 그때 다시 예외 정책에 관해 이후 강의에서 다뤄보도록 하겠습니다.
- 0
- 1
- 46
질문&답변
개인적질문
안녕하세요. 1번은, @Embedded는 기본 타입이 아닌 사용자 정의 타입(클래스)으로 값 객체를 만들 때 주로 사용합니다. 이렇게 하면 개념적인 명확성을 줄 수 있습니다. String email과 Email email은 코드를 읽을 때 뿐 아니라 이게 파라미터로 전달 되거나 사용되어질 때, 비록 String 하나 뿐이더라도, 이게 어떤 타입이고 어떤 정보가 저장되는지가 명확하게 드러납니다. 또, 유효성 검사와 변환, 기타 이 값을 사용하는 로직을 담은 메소드를 하나의 클래스에 모아둘 수 있어서 응집도가 올라갑니다. Member 클래스에 isValidEmailAddress()가 있는 것보다는 Email 클래스에 있는 게 낫겠죠. Email처럼 여러군데 사용될 가능성이 있는 경우엔 중복도 피할 수 있고요. 파라미터로 넘길 때 자연스럽게 타입 안정성도 보장됩니다. String은 너무 많이 쓰이자나요. 그런데 email을 넘겨야 하는데, 사용하는 쪽에서 실수로 name을 넘겨도 컴파일도 잘 되고 기능도 돌아갑니다. 실제 메일 발송을 해야 없는 메일이라는 경고가 뜰 뿐이죠. 이런 논리적인 버그를 피하는데도 도움이 됩니다. 따라서 여러 개의 함께 다니는 정보를 묶는 경우에도 쓰지만 값 객체가 가지는 장점을 살리기 위해서도 타입을 클래스로 정의하고 @Embedded로 쓸 수 있습니다. User type을 정의하는 복잡한 방법도 있는데 그 보다는 임베딩 시키는게 깔끔하죠.2번은 애그리거트에 여러 엔티티가 연결되어 있는 경우에 단순 조회라면 체이닝을 써도 괜찮습니다. 코드도 자연스럽게 읽히죠. getMember().getAddress().getStreet() 뭐 이런 식이죠. 중간에 널이 들어갈 수 있다면 Optional로 안전하게 호출할 수도 있습니다.반면 로직을 적용해서 변경이 일어나는 작업인 경우, 또는 어떤 조건 판단을 위한 메소드를 호출하는 경우라면 로직을 루트에 두고 루트가 내부 체이닝 구조에 따라서 필요한 기능을 수행하고 리턴하게 하는 것이 바람직합니다. 그러면 나중에 내부 구조의 변경이 일어나도 사용하는 코드를 수정할 필요가 없고, 내부 엔티티가 불필요하게 노출이 되어서 그 내부 기능을 바로 사용하는 것도 피할 수 있겠죠.3번은 애그리거트라는 용어는 DDD 책을 통해서 처음 소개된 것으로 알고 있습니다. 하지만 이렇게 항상 하나의 트랜잭션으로 묶여서 처리되거나 내부를 공개하지 않는 것이 더 나은 설계인 경우엔 유사한 방식을 얼마든지 따를 수 있습니다. 단순하게 데이터 저장 기술로 JPA를 쓰는 경우에도 애그리거트처럼 cascade를 적극적으로 활용하는 예는 아주 많습니다. 4번은 경우에 따라 다릅니다. 관계가 어떻게 되어있는지는 중요하지 않습니다. 각 엔티티의 의미와 변경 주기, 시점, 방식에 따라서 애그리거트를 어떻게 묶을지는 고민해봐야죠. 그리고 애그리거트를 묶는 단위는 시간이 지나고 바뀔 수도 있습니다. 모든 걸 처음부터 완벽하게 설계하라는 보장은 없으니까요. 좀 애매하다 싶으면, 우선 결정을 하나 해보고, 코드가 발전하면서 그 선택이 어떤 영향을 주는지 보고 나중에 리팩터링해도 됩니다. 중요한 건 도메인 모델을 더 깊이 연구해보는 것이죠.
- 0
- 2
- 47
질문&답변
어플리케이션 , 도메인 계층질문
의존성 역전이 아니라면 애플리케이션 코드는 기술에 의존적인 외부 인터페이스에 의존합니다. 기술과 관련된 코드가 어디에 존재하는지를 따지는 이유는 해당 코드가 기술에 의존하고 있는가, 즉 기술의 변경이 코드에 영향을 미치는가로 판단해야 합니다. 의존성 역전으로 스프링 데이터 JPA의 관례를 따르는 코드가 애플리케이션의 Port가 되었지만, 이 인터페이스는 특정 기술에 의존적이지 않습니다. 오히려 도메인 지식과 용어가 도메인 로직에 충실하게 드러나는 인터페이스일 뿐입니다. 물론 JPA 기술에서 제공하는 애토네여션이 메타 정보로 부여되지만 이 때문에 해당 인터페이스가 JPA에 의존되지는 않습니다. 도메인과 무관한 JPA API가 사용된 적도 없으며 해당 기술이 JPA가 아니라 다른 스토리지 기술로 변경된다고 하더라도, 만들어진 인터페이스의 코드는 변경되지 않습니다. 기술이 침투한다는 것이 어떤 의미인지, 어떤 효과가 있는 것인지를 잘 따져보고 초기 개발 맥락에서 최선이라고 한 선택을 한 것입니다. 관련된 내용은 강의에서 그리고 Q&A의 여러 질문을 통해서 말씀드렸습니다. 도메인 모델의 내용을 최대한 표현해낸 코드가 도메인 계층이 되고, 애플리케이션(헥사곤) 외부와의 연동과 책임을 담당하는 부분을 애플리케이션 서비스로 개발합니다. 이 둘 다 도메인 지식과 로직에 충실하게 작성되었고, 일부 외부 기술의 애노테이션이 노출되는 것이 이런 개발 원칙을 깨뜨리지 않는 선에서 적절하게 선택했습니다. 기술의 침투, 기술에 의존이라는 건 기술의 변화, 그 기술을 적용하는 환경이 바뀔 때 그 "코드"도 바뀔 가능성이 높은가로 판단하면 됩니다.
- 0
- 3
- 84
질문&답변
어플리케이션 , 도메인 계층질문
의존성 역전이 아니라면 애플리케이션 코드는 기술에 의존적인 외부 인터페이스에 의존합니다. 기술과 관련된 코드가 어디에 존재하는지를 따지는 이유는 해당 코드가 기술에 의존하고 있는가, 즉 기술의 변경이 코드에 영향을 미치는가로 판단해야 합니다. 의존성 역전으로 스프링 데이터 JPA의 관례를 따르는 코드가 애플리케이션의 Port가 되었지만, 이 인터페이스는 특정 기술에 의존적이지 않습니다. 오히려 도메인 지식과 용어가 도메인 로직에 충실하게 드러나는 인터페이스일 뿐입니다. 물론 JPA 기술에서 제공하는 애토네여션이 메타 정보로 부여되지만 이 때문에 해당 인터페이스가 JPA에 의존되지는 않습니다. 도메인과 무관한 JPA API가 사용된 적도 없으며 해당 기술이 JPA가 아니라 다른 스토리지 기술로 변경된다고 하더라도, 만들어진 인터페이스의 코드는 변경되지 않습니다. 기술이 침투한다는 것이 어떤 의미인지, 어떤 효과가 있는 것인지를 잘 따져보고 초기 개발 맥락에서 최선이라고 한 선택을 한 것입니다. 관련된 내용은 강의에서 그리고 Q&A의 여러 질문을 통해서 말씀드렸습니다. 도메인 모델의 내용을 최대한 표현해낸 코드가 도메인 계층이 되고, 애플리케이션(헥사곤) 외부와의 연동과 책임을 담당하는 부분을 애플리케이션 서비스로 개발합니다. 이 둘 다 도메인 지식과 로직에 충실하게 작성되었고, 일부 외부 기술의 애노테이션이 노출되는 것이 이런 개발 원칙을 깨뜨리지 않는 선에서 적절하게 선택했습니다. 기술의 침투, 기술에 의존이라는 건 기술의 변화, 그 기술을 적용하는 환경이 바뀔 때 그 "코드"도 바뀔 가능성이 높은가로 판단하면 됩니다.
- 0
- 3
- 84
질문&답변
39. 문서와 코드 다듬기 updateInfo 테스트 질문 있습니다.
테스트 코드도 리팩터링이 필요한데 제가 강의 진행하면서 빠르게 넘어갔던 부분을 멋지게 정리를 해주셨네요.미래에 테스트 코드의 가독성을 고려해서 검증 목적에 따라 더 잘게 쪼개보는 방법도 가능할 것 같긴합니다. 테스트 코드를 좀 더 개선한다면 아마도 앞으로 자주 쓰이게 될 회원 오브젝트를 일정 상태로 준비하는 부분을 공통 코드로 추출해서 반복적으로 사용할 수 있게 하면 좋을 것 같네요. 여러가지 아이디어가 떠오르지만, 정상적인 케이스를 검증하는 것과 실패하는 케이스에 집중하는 방식으로 코드를 나누신 것은 아주 좋아보입니다. 코드 읽기도 좋네요. 감사합니다.
- 0
- 1
- 37
질문&답변
섹션 6 -2강
안녕하세요. 제가 강의에서 orm.xml의 기본 XML 구성은 복잡해서 강의자료로 남겨드린다고 했는데 깜빡했습니다. ㅠㅠ예제 코드에서 찾으실 수도 있겠지만, 우선 여기에 답변으로 올려드릴게요. 영상 강의에 자료를 업로드하는 기능이 있는데 거기에도 올려두도록 하겠습니다. 인터넷 검색을 해도 최신 버전인 JPA 3.1과 Hibernate의 스키마가 같이 적용된 orm.xml 샘플을 찾기가 쉽지 않더라고요. 다음과 같이 사용하시면 됩니다. 내부에 들어가는 JPA 매핑 정보 설정은 강의 내용과 GitHub에 올라가 있는 예제 코드를 참고하세요.
- 0
- 1
- 54
질문&답변
unique-constraint 설정 질문드립니다.
단일 컬럼에 대해서라면 와 모두 고유한 값에 대한 제약조건을 걸고, 이를 위해서 인덱스도 만드는 효과가 있습니다. 이 중에서 는 JPA 2 이후에 도입된 방식입니다. 이름에서 알 수 있듯이 는 제약조건(고유값)에 더 의미를 두는 설정이고, 는 검색 성능을 위해서 인덱스를 생성하는 것을 강조하는 방식이겠죠. 거기에 unique 조건을 추가할 수 있긴하지만요. 제가 아는 바로는 차이점이 있다면 이 부분일 겁니다. 이번 강의에서는 email의 중복을 방지해야 한다는 도메인 로직을 적용하면서 설정을 추가했기 때문에 uniquie-constraint를 사용했습니다.
- 0
- 2
- 63