강의에서도 잠깐 언급되지만, 전체적인 강의 내용이 만들면서 배우는 클린 아키텍처의 저자가 주장하는 내용과는 지향하는 바는 같지만 그 구현에 있어서 어느정도 거리가 있다고 느껴지는데요..! "만들면서 배우는 클린 아키텍처" 이 책을 읽으면서도 저자가 주장하는 내용이 좋은 아키텍처라는 생각이 들었었고, 그 지름길에 대해서도 많은 생각이 들었습니다. 그러다보니 강의에서 언급해주시는 일부 내용들은 이 지름길을 사용했다는 것으로도 저한테는 느껴지기도 하는데요. 하지만 또 다른 측면으로 토비 선생님께서 강의에서 말씀하시는 내용들이 매우 타당하고 이것 또한 매우 클린하며 헥사고날 아키텍처의 장점을 매우 잘 살리고 있다는 생각도 들었습니다. 제가 궁금한 건 "만들면서 배우는 클린 아키텍처" 에서 저자가 주장하는 내용들에 대해 토비 선생님께서는 어떻게 생각하고 계시는지 궁금합니다! 회사에서 많은 시니어 분들이 헥사고날과 관련해서는 저자마다 주장하는 내용이 다른 경우가 많으니 최대한 많이 접하고 스스로 기준을 삼는 것이 좋다라고 조언을 주시기도 하셔서 더욱 궁금하네요 ㅎㅎ:)!
질문이 있어 남깁니다. 30. 회원 애플리케이션 기능 추가 강의 중 Activate 메서드를 작성하면서 설명해주신 Spring Data Jpa 사용시 save를 사용해야 한다고 공식문서에 나와있다고 하셨는데 해당 문서에 대한 링크를 알수있을까요? save를 안티패턴이라고도 설명을하고 불필요한 오버헤드 발생에 대해서는 어떻게 생각하시나요? 강의의 내용 처럼 JpaRepository가 아닌 Repository를 사용 하는경우에는 필수적으로 save를 해야 하나 JpaRepository의 경우는 Jpa 자체에서는 save라는 것이 없기 때문에 새로운 엔티티를 생성할때만 사용을하고 업데이트의 경우는 생략을 해야하는 것일까요?
안녕하세요 🙂 "도메인 모델을 직렬화 했다가, RDB에 저장했다가" 하는 경우도 JPA와 도메인모델 분리가 필요한 경우라고 생각되는데 어떻게 생각하시나요? 도메인모델이 생성되었을때 영구보관이 필요한게 아니라, 어느정도 상태머신이 진행된 후 영구 보관이 필요하여 그전에는 레디스나 다이나모 같은 저장소에 보관하다가, 이후에 RDB에 영구보관을 하는 경우가 좀 더 자세한 예시일 것 같아요. 이 경우 JPA에서 DB 성능등을 이슈로 양방향맵핑을 하는 경우 순환참조로 인한 직렬화 이슈가 생기기 때문에 어떻게 해결할 수 있을지 고민하다가 이때 모델 분리를 선택한 경험이 있습니다. -- 무조건적인 지향을 하는게 아니라 필요에 따라 기술을 선택할 수 있게 강의를 진행해주시는 점 너무 많이 배우고 있습니다. 감사합니다.
토비님, 항상 좋은 강의 감사드립니다. 강의 내용을 학습하던 중 궁금한 점이 생겨서 질문드립니다! 강의 코드에서 MemberRegisterRequest 가 domain 패키지 안에 직접 정의되어 있고, 이 동일한 객체를 adapter 계층의 컨트롤러에서 @RequestBody 로 직접 받는 것을 확인했습니다. 제가 접해온 일반적인 계층형 아키텍처에서는, 웹 계층을 위한 DTO를 별도로 두고 서비스 계층에서 이를 도메인 객체로 변환하여 도메인 계층이 웹 DTO에 의존하지 않도록 분리하는 방식을 주로 사용했습니다. 그래서 강의에서 보여주신 설계 방식에 대해 궁금한 점이 두 가지 있습니다. 이처럼 요청(Request) 자체를 도메인의 일부로 보고 domain 패키지에 포함시키는 설계 가 갖는 이점은 무엇인지 궁금합니다. 이러한 설계가 계층 간의 결합도를 높일 수 있다는 우려 에 대해서는 어떻게 생각하시는지, 그리고 어떤 상황에서 이러한 실용적인 접근이 더 효과적이라고 판단하시는지, 토비님의 설계 철학이나 기준 에 대해 여쭙고 싶습니다. 감사합니다.
강의와는 관련이 없는데, 개인적인 질문이 있습니다. 저는 자바 스프링 신입 개발자를 준비하고 있는 학생입니다. 지금까지는 MVC 패턴만 사용하고, 모놀리틱 아키텍처를 사용해서 배포를 진행하고 프로젝트를 해왔습니다. 제가 알기로는 학습적으로나 포트폴리오적으로나 필요성을 느껴서 하는 공부가 제일 좋다고 들었습니다. 근데, 최근에는 어떤 필요성을 느끼지 못하면서 대규모 시스템 강의,헥사고날 아키텍처가 중요하다고 하니 강의 등을 듣고 있습니다. 왜냐하면, 본격적인 취업은 내년이고 시간이 좀 남았습니다. 그래서 해당 강의들을 들어두면 언젠가 개인 프로젝트나 현업에서 사용할 수 있지 않을까하고 듣고있습니다만, 제가 사용했던 MVC 패턴의 장단점 등 기본적인 것들도 알지 못하는 상태에서 계속 진도 나가듯이 이런 저런 강의를 듣고 하는게 괜찮을까요? 즉, 아직 기본도 잘 모르면서 계속 새로운 걸 배우는 과정들이 괜찮을까하는 걱정이 드네요. 하지만, 한편으로는 CS지식이 너무 방대해서 기초를 다 잡아두고 다음 단계로 넘어간다는 것도 솔직히 엄두가 안납니다. 그래서 우선은 쭉 이것저것 배워두고 나중에 필요하면 다시 찾아보면서 공부하면 되지 않을까 싶은데, 토비님은 어떤 방향이 더 괜찮다고 생각하시나요??
안녕하세요, 토비님. 현재 MemberRegister 인터페이스는 register 뿐만 아니라 활성화/비활성화, 정보 업데이트 등의 오퍼레이션을 제공하고 있는데요, 해당 인터페이스에 작성된 주석은 회원의 등록과 관련된 기능을 제공한다 여서 ' register 를 제외한 오퍼레이션은 다른 인터페이스에 위치해야 하지 않나?' 하는 생각이 듭니다. 혹은 회원 등록 외 오퍼레이션이 해당 인터페이스에 존재하는게 의도하신 바지만, 주석 내용이 수정되지 않은 걸까요? 궁금한 점을 요약해서 정리하자면, xxxFinder 는 조회와 관련된 오퍼레이션을, xxxRegister 는 생성/수정/삭제와 관련된 오퍼레이션을 가지는 걸까요? 완강 후 회사에서 강의로 알려주신 것들을 이것저것 적용해보며 재밌게 일하고 있습니다, 정말 감사드립니다.
안녕하세요 Toby! 먼저 좋은 강의 올려주셔서 감사 말씀 드립니다. 열심히 강의를 듣던 와중에 궁금한 점이 있어서 질문드립니다. 회원 애플리케이션 기능 추가 강의에서 보면, MemberModifyService 클래스에서 MemberFinder 빈을 주입받아 사용하도록 구현해주셨습니다. 관련해서 같은 application 계층에서 서로 DI 받는 구조로 구성하게 될 경우, 발생하는 순환 참조와 같은 문제점들에 대해서는 어떻게 관리하는게 좋을까요? 그리고 그런 문제점이 발생하지 않도록 예방하기 위해서는 어떤 방법이 있을까요?
안녕하세요 프로젝트 초기 설정을 토비님 강의 진행대로 따라하고 있는 중입니다. 제가 docker 는 잘 몰라서 강의 자료에 있는 Rancher Desktop 을 그냥 설치했구요 서버 구동하기 전에 먼저 켜고 토비님 강의 순서 대로 진행했습니다 compose.yaml 의 내용을 수정하기 전에는 오류없이 잘 되는데 토비님이 작성하신 대로 수정을 해서 서버를 구동하면 계속 오류가 발생하고 있습니다 제 디비에 문제가 있나 싶어서 mysql를 완전 삭제하고 재설치 까지 해서 다시 해봐도 이전과 계속 같은 오류가 발생하는데 ai 를 통해서 해결 해보려고 해도 해결을 못하고 있습니다 혹시 확인해보시고 알려주셨으면 합니다
학습 관련 질문을 남겨주세요. 어떤 부분이 고민인지, 무엇이 문제인지 상세히 작성하면 더 좋아요! 먼저 유사한 질문이 있었는지 검색해 보세요. 서로 예의를 지키며 존중하는 문화를 만들어가요. 강사님 안녕하세요. 🙂 우선, 테스트 코드에 이어 양질의 강의 제공해 주셔서 감사합니다! 🙏🏻 프로젝트 리팩토링 단계에서 본 강의를 접한 덕분에 많이 배우고 있습니다! 혹 제가 이해한 내용을 바탕으로 블로그 혹은 깃허브에 정리해도 될지요? 출처는 기재할 예정이며, 블로그 수익과는 전혀 관계없습니다. 더불어 강의 자료 제작 시 사용하시는 드로잉 툴?이 어떤 것인지도 궁금합니다. 답변 기다리겠습니다, 감사합니다!
안녕하세요, 토비님. 강의를 들으며 많은 인사이트를 얻고 있습니다. 강의를 완강한 후에도 내면적으로 정리되지 않은 부분이 있어 조심스럽게 질문을 드리게 되었습니다. 생성과 관련된 설계는 강의에서 잘 이해가 되었지만, 업데이트(update) 와 관련된 내용은 직접적으로 다뤄지지 않아 고민이 생겼습니다. 특히, 제 고민은 다음과 같습니다. "도메인의 비즈니스 규칙이 Web API 설계에 어느 정도까지 직접적으로 드러나야 하는가?" 현재 도메인 로직에서는 사용자의 여러 정보를 변경할 수 있는 비즈니스 규칙이 존재합니다. 예를 들어: 비밀번호 변경 기타 세부정보 변경 비즈니스적으로는 각각의 규칙이 잘 정의되어 있고, 각각의 변경 로직도 Member 객체 내에 명확히 메서드로 존재합니다. 여기서, 이러한 비즈니스에 대해서 API에 어떻게 노출시켜야 하는가에 대해서 두 가지 선택지가 고려됩니다. 1. 비즈니스 정의를 역할 별로 구성한다. POST /api/v1/members/{id}/change-password POST /api/v1/members/{id}/change-nickname 생각이 나는 장단점은 다음과 같습니다. 장점 : 비즈니스에 따라 API를 관리하여 클라이언트가 이해하기 용이합니다. 단점 : 수정 가능한 필드가 많아질수록 API의 개수가 증가하며, 유지보수가 어려워질 수 있고, Restful 규칙에 위배됩니다. 2. 하나의 update API로 통합한다. PATCH /api/v1/members/{id} { "password": "originalPassword123!", // nullable "detailRequest": { // nullable "email": "user@example.com", "nickname": "nickname123", "password": "newPassword456!" } } 장점 : API가 간결하여 확장이 용이하며, 클라이언트는 필요한 값만 상황에 따라 요청하면 됩니다. 단점 : API가 비즈니스 책임에 명확하지 않을 수 있습니다. 결론적인 질문은 다음과 같이 정리 할 수 있을 것 같습니다. 비즈니스 로직이 도메인 레이어에 잘 분리되어 있는 경우, API 계층에서도 분리하여 표현하는 것이 좋은가요? 도메인의 역할만 명확하다면 API는 통합해서 update 형식으로 만들어도 괜찮은가요? 만약, 후자로 처리를 한다면 어디서 처리를 하는게 좋아보이시나요? 서비스 계층 도메인 계층 // MemberModifyService public void update(Long memberId, MemberUpdateRequest request) { Member member = memberFinder.find(memberId); if (request.password() != null) { member.changePassword(request.password()); } if (request.detailRequest() != null) { member.updateInfo(); } } -------- // MemberModifyService public void update(Long memberId, MemberUpdateRequest request) { Member member = memberFinder.find(memberId); member.update(request); } // Member public void update(MemberUpdateRequest request) { if (request.password() != null) { changePassword(request.password()); } if (request.detailRequest() != null) { updateInfo(); } } 뭔가, 이런 고민이 계속 드는 이유가 외부 계층에 종속적이지 않고 도메인에 의존하여 개발을 하더라도 실제로 저희가 처한 상황은 대부분 WebAPI 계층에서의 요청이 많다보니 외부의 행위 또한 도메인에 종속 되어야 하는가 하는 고민이 생긴 것 같습니다. 양질의 강의 제공해주셔서 감사드립니다!
'25. 엔티티의 자연키 지정' 영상의 후반부에 적용한 @NaturalIdCache 에 대해 추가로 학습한 내용이, 저처럼 해당 애노테이션을 처음 접한 분들에게 도움이 될 것 같아서 글을 작성합니다. 강의에서 오해가 있을 수 있는 부분, 그리고 자연키에 캐시를 적용하는 방법을 정리해 보았습니다. 강의 내용과 실제 동작의 차이점 강의에서는 “같은 트랜잭션 안에서 같은 아이디 값을 가지고 여러 번 조회 시 Persistence Context 에 캐시된 값을 꺼내오는 것 처럼. @NaturalIdCache 를 적용하면 이것도 영속 컨텍스트에 캐싱이 된다.”고 말씀하셨습니다. 해당 내용에 대한 이해를 돕기 위해 Hibernate의 두 가지 캐시에 대해 간단히 짚고 넘어가겠습니다. 1차 캐시 (First-Level Cache): 세션(영속성 컨텍스트) 범위의 캐시입니다. 같은 트랜잭션 안에서만 유효하며, 트랜잭션이 끝나면 사라집니다. Spring Data JPA에서는 기본적으로 @Id 에 대한 조회를 1차 캐시합니다. 2차 캐시 (Second-Level Cache): 세션 팩토리 범위의 캐시로, 여러 세션에서 데이터를 공유할 수 있습니다. 적용하려면 별도의 의존성 추가 및 캐시 관련 설정( @Cache 등)이 필요합니다. 따라서, "같은 트랜잭션 안에서 캐시된 값을 꺼내온다."는 말은 세션 범위의 1차 캐시로 해석됩니다. 하지만 제가 직접 테스트해 본 결과, @NaturalIdCache 는 1차 캐시 가 아닌 2차 캐시 와 관련이 있었으며, 1차 캐시를 적용하기 위해서는 다른 방법이 필요했습니다. 테스트를 통한 확인 자연키에 대한 1차 캐시 동작을 확인하기 위해, 강의에서 적용한 Member 엔티티의 @NaturalIdCache 를 제거 하고, 자연키(Email)에 @NaturalId 만 적용 한 상황에서 두 가지 방식으로 테스트를 진행했습니다. 테스트1: findByEmail 메서드를 사용한 조회 @Test void NaturalIdFirstLevelCache() { Member member = Member.register(createMemberRegisterRequest(), createPasswordEncoder()); memberRepository.save(member); entityManager.flush(); entityManager.clear(); System.out.println("회원 저장 및 persistence context 초기화 완료"); // 같은 email(Natural ID)로 두 번 조회 Member findMember1 = memberRepository.findByEmail(member.getEmail()).get(); Member findMember2 = memberRepository.findByEmail(member.getEmail()).get(); assertThat(findMember1).isSameAs(findMember2); } Spring Data의 쿼리 메서드를 사용하여 이메일로 조회하는 findByEmail 메서드를 만들고, 한 트랜잭션에서 같은 회원을 두 번 조회했습니다. 자연키에 대한 1차 캐시가 동작한다면, SELECT 쿼리는 한 번만 실행되어야 합니다. 결과는 SELECT 쿼리가 두 번 실행 되었습니다. 즉, 자연키에 대한 1차 캐시가 동작하지 않았습니다. 테스트2: Hibernate의 자연키 관련 API를 사용한 조회 @NaturalId 를 다루는 글들을 찾아본 결과 Hibernate가 제공하는 자연키 관련 API가 있다는 것을 확인했고, 이를 적용하기 위해 커스텀 리포지토리를 구현했습니다. @Repository @RequiredArgsConstructor public class CustomizedMemberRepositoryImpl implements CustomizedMemberRepository { private final EntityManager entityManager; @Override public Optional<Member> findByNaturalId(Email naturalId) { return entityManager.unwrap(Session.class) .bySimpleNaturalId(Member.class) .loadOptional(naturalId); } } 그리고, 테스트 1과 같은 방식으로 테스트를 진행하였습니다. @Test void NaturalIdApi() { Member member = Member.register(createMemberRegisterRequest(), createPasswordEncoder()); memberRepository.save(member); entityManager.flush(); entityManager.clear(); System.out.println("회원 저장 및 persistence context 초기화 완료"); Member findMember1 = memberRepository.findByNaturalId(member.getEmail()).get(); Member findMember2 = memberRepository.findByNaturalId(member.getEmail()).get(); assertThat(findMember1).isSameAs(findMember2); } 결과는 SELECT 쿼리가 한 번만 실행 되었습니다. 이를 통해 자연키에 대한 1차 캐시는 @NaturalIdCache 애노테이션과 무관하게, 전용 API를 사용해야만 동작 하는 것을 확인했습니다. @NaturalIdCache의 용도 @NaturalIdCache Javadoc에는 다음과 같은 설명이 있습니다. Specifies that mappings from the natural id values of the annotated entity to the corresponding entity id values should be cached in the shared second-level cache. …중략 This annotation is usually used in combination with Cache , since a round trip may only be avoided if the entity itself is also available in the cache. 대략 “natural id와 상응하는 id에 대한 매핑을 2차 캐시에 저장 하는 애노테이션이고, 엔티티가 캐시되어있어야 하기 때문에 일반적으로 Cache 와 함께 사용 된다.”라고 해석됩니다. 즉, 1차 캐시가 아닌 2차 캐시를 위한 애노테이션입니다. 정리 2차 캐시 관련 설정 및 테스트를 마저 진행한 후 최종 정리한 내용은 다음과 같습니다. 자연키의 1차 캐시 @NaturalIdCache 애노테이션과 관련 없습니다. 자연키에 @NaturalId 만 붙이면 됩니다. 반드시 Hibernate Session의 bySimpleNaturalId() 같은 전용 API를 사용해야 적용 됩니다. 자연키의 2차 캐시 @Cache 와 @NaturalIdCache 를 함께 사용해야 동작합니다. @Cache 만 사용 시 @Id 로 조회할 때만 2차 캐시가 동작합니다. @NaturalIdCache 만 사용 시 자연키와 ID에 대한 매핑 정보는 캐시 히트 되는 걸 확인했지만, ID와 엔티티에 대한 캐시가 없어서 캐시가 적용되지 않았습니다. @Cache 와 @NaturalIdCache 모두 사용 시 ID를 통한 조회와 자연키를 통한 조회 모두 2차 캐시가 적용됩니다. 참고 자료 Hibernate6.6 공식 문서 NaturalCache javadocs baeldung: Hibernate Natural IDs in Spring Boot Spring Custom Repository 글의 오류나 부족한 내용을 알고 계신 분은 코멘트를 달아주시면 감사하겠습니다.
토비님 안녕하세요. 강의 너무 즐겁게 잘 수강하고 있는 한 개발자 입니다. 강의 수강 중에 궁금한 점이 생겨서, 토비님의 의견이 궁금해서 한 가지 질문 드리고자 합니다. 33. 엔티티 클래스와 JPA 매핑 정보 분리 강의에서 분리를 xml로 분리해서 매핑 하는 예를 들어주셔서, 이 부분에 대해서 궁금한 점이 있습니다. 개인적으로 xml 매핑작성 생산 비용과 jpa/도메인 엔티티를 분리해서 작성하는 비용이 크게 차이나지 않는것 같단 생각이 들긴합니다. 결국 xml이든, 코드든 분리해서 작성 비용이 필요한 것 같아요. 그렇다면, 여기에서 관리 포인트를 이중(xml,코드)으로 가져가는게 나을지, jpa/도메인 엔티티를 분리해서 코드에서 관리하는게 나을지? 고민이 되는데요. 토비님의 의견은 어떠신지 궁금합니다. ai 자동완성 기능 활용 코드도 마찬가지로, 애노테이션 빼줘, 붙여줘 하면 어느정도 잘 만들어주긴 하더라구요. 이 부분도 어떻게 생각하시는지 궁금합니다. 코파일럿에 xml 매핑정보 만들어줘 하는 내용과, 코드로 애노테이션 붙여줘, 빼줘 해서 복/붙하는 행위 자체가 크게 다르지 않은것 같다는 생각이 들긴해서 이 부분은 어떻게 생각하시는지도 궁금합니다. 유지보수 관점에서 생산비용 보다, 개발 완료 후 유지보수를 하는데 있어서 그래도 xml/코드 두가지 중 1개를 선택해야한다면 어떻게 관리하는것이 나을까요?? 바쁘신 와중에도 질문 확인하고 답변 주시는 점 미리 감사드립니다.
안녕하세요. 토비님! 현재 저는 강의 내용을 Kotlin springboot 로 따라가고 있습니다. JPA Entity 클래스를 생성할 때, 자바에선 롬복까지 이용해서 Getter 만 만들어놓고 setter 는 닫아놓는 게 쉽게 되는데, 이걸 코틀린에서는 롬복을 사용하지 않다보니 코틀린스러우면서도 깔끔하게 사용하는 방법에 대해서 애를 먹고 있습니다. 찾아보니 3가지 방법 있는 것 같습니다. 방법1. 자바랑 가장 비슷하게, 필드를 모두 private 으로 생성하고 getter 는 롬복 대신 직접 선언. @Entity open class Member( @Column(name = "email", unique = true, nullable = false) private var email: String, @Column(name = "nickname", nullable = false) private var nickname: String, @Column(name = "passwordHash", nullable = false) private var passwordHash: String, @Column(name = "status", nullable = false) @Enumerated(EnumType.STRING) private var status: MemberStatus = MemberStatus.PENDING, ) { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L fun getEmail(): String = email fun getNickname(): String = nickname fun getPasswordHash(): String = passwordHash fun getStatus(): MemberStatus = status } 방법2. getter 를 좀 더 코틀린스럽게 사용하기 위해 내부 필드를 _를 붙여서 선언 @Entity class Member2( @Column(name = "email", unique = true, nullable = false) private var _email: String, @Column(name = "nickname", nullable = false) private var _nickname: String, @Column(name = "passwordHash", nullable = false) private var _passwordHash: String, @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false) private var _status: MemberStatus = MemberStatus.PENDING, ) { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L val email: String get() = _email val nickname: String get() = _nickname val passwordHash: String get() = _passwordHash val status: MemberStatus get() = _status } 방법3. protected set 사용 @Entity open class Member3( email: String, nickname: String, passwordHash: String, status: MemberStatus = MemberStatus.PENDING, ) { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L @Column(name = "email", unique = true, nullable = false) var email: String = email protected set @Column(name = "nickname", nullable = false) var nickname: String = nickname protected set @Column(name = "passwordHash", nullable = false) var passwordHash: String = passwordHash protected set @Column(name = "status", nullable = false) @Enumerated(EnumType.STRING) var status: MemberStatus = status protected set } 방법4(?). 전부 public val 로 선언하고, 변경시 새로운 객체 생성 @Entity @Table(name = "members") class Member4( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L, @Column(name = "email", unique = true, nullable = false) val email: String, @Column(name = "nickname", nullable = false) val nickname: String, @Column(name = "passwordHash", nullable = false) val passwordHash: String, @Column(name = "status", nullable = false) @Enumerated(EnumType.STRING) val status: MemberStatus = MemberStatus.PENDING, ) { fun updateNickname(newNickname: String): Member4 { require(newNickname.isNotBlank()) { "Nickname cannot be blank" } return Member4( id = this.id, email = this.email, nickname = newNickname, passwordHash = this.passwordHash, status = this.status, ) } } 어느 방식을 선택하는게 현명할까요? 토비님은 평소에 어떻게 하시는지 궁금합니다.
안녕하세요. 토비님! 먼저 제가 정말 오랜 기간 고민해온 주제에 대해서 이렇게 강의를 내주셔서 너무 감사드립니다!! 특히 강의에서 여러 차례에 걸쳐서 깊게 다룬, "JPA Entity Class 와 Domain Model Entity Class 를 분리해야 하는가?" 에 대한 토비님의 의견이 너무 공감되고 유익했습니다! 감사합니다! 그리고 이에 대해서 두 가지 질문이 있습니다. 첫 번째는, 저는 사실 지난 3년 간 스타트업에서 Domain Entity 와 JPA Entity 를 분리해서 사용해왔습니다. 그런데 강의에서 다뤄주신 내용이 제가 겪으며 갖게 된 생각과는 결이 조금 다르다고 느껴서 이렇게 질문을 드리게 되었습니다. 제가 느꼈던 "Domain Entity 와 JPA Entity 를 분리해서 사용했을 때의 장단점"은 이랬습니다. 장점 1. 도메인 모델의 수정이 DB 데이터 마이그레이션을 꼭 강제하는 게 아니라서, 도메인 모델 수정이 매우 자유롭습니다. 상황에 따라 의도적으로 JPA Entity 클래스와 도메인 모델 클래스를 다른 모양으로 만들어두고 유지할 수 있습니다. (ex. User, UserDetail 이 쪼개져 있었는데, 어떠한 의사결정으로 인해 User 하나로 합쳐서 관리하는게 맞다는 판단이 든 경우, UserEntity, UserDetailEntity 는 놔둔 채로 User 만 합치는 게 가능.) 장점 2. 도메인 모델들을 완전히 정규화된 구조 로 가져갈 수 있습니다. 회사에서 비즈니스가 발전하다보면 종종 어쩔 수 없이 테이블에 성능을 위한 반정규화 필드나 soft delete 용 deleted_at 필드 등을 넣게 됩니다. 하지만 사실 이런 필드들은 순수 비즈니스 로직을 기술하는데에는 방해가 될 뿐인, 너무 Technical 한 부분들입니다. 이러한 반정규화 or 기능 필드들을 도메인 모델에는 넣지 않고 JPA Entity 에만 넣어서 어댑터에서 처리하면, 도메인 모델 내에서는 항상 순수 비즈니스 로직만을 기술할 수 있게됩니다. 단점 1. JPA 의 lazy loading 을 활용할 수 없습니다. 항상 도메인 모델 객체는 완전한 상태로만 존재합니다. 그래서 성능 문제로 CQRS 패턴이 강제됩니다. 특히 저는 GraphQL 을 사용중이라 더욱 더 CQRS 가 필요했습니다. 단점 2. Repository 의 save(= upsert) 로직을 직접 구현해야 합니다. 특히 복잡한 비즈니스의 핵심 도메인 모델들은 관계된 테이블도 많아가지고 이 save 구현이 엄청나게 길어집니다. 저는 이 save 로직을 직접 구현하고 관리할 때 회의감이 가장 많이 들었습니다. Spring Data JPA 가 그 동안 얼마나 압도적인 생산성을 제공해주고 있었는지도 느끼게 됐습니다. 저는 스타트업에서 일하고 있다보니 특히 생산성을 중요하게 생각합니다. 그런 점에서도 위의 장점들과 단점들이 모두 너무 치명적으로 느껴졌습니다. 그래서 이 관점들에 대한 토비님의 의견과 토비님이 제 상황이시라면 어떤 선택을 하셨을지가 너무너무 궁금합니다. (참고로 저는 Kotlin Springboot + Spring Data JPA + Kotlin JDSL + GraphQL-Kotlin(code first)을 사용하고 있습니다.) 두 번째는, 사실 저는 Kotlin Springboot 를 사용중인데, 코틀린 언어와 JPA 가 너무 안 어울린다는 생각을 종종 합니다. 예를 들면, DB table 에 정의된 column default 값을 쓰려면 JPA Entity 의 해당 필드에 null 을 넣어서 보내야하는데, 보통 테이블 컬럼에 default 를 쓰는 경우는 대부분 해당 컬럼이 not null 타입입니다. 그래서 당연히 JPA Entity 에도 필드 타입은 not null 로 하고 싶어집니다. 물론 @Column(insertable=false) 를 사용하긴 하지만, 결국 그럼 이 JPA Entity 객체를 생성할 때 해당 필드에 실제로 저장되지도 않을 값을 거짓으로 넣어야 하는 상황이 생깁니다. 그래서 조금 더 Kotlin 에 잘 맞는 Exposed 를 고려하자니, 도메인 모델 클래스와 JPA Entity 클래스 분리가 강제되는 느낌이고, 이게 과연 JPA 의 생산성을 따라올 수 있을까? 올바른 선택이 맞을까? 하는 의문이 듭니다. 그래서 궁금한 점은, 혹시 토비님이 추천하시는 좀 더 Kotlin 언어에 잘 맞고 Kotlin Springboot 를 생산적으로 사용할 수 있는 방법이 있을까요?
안녕하세요? 존경하는 토비님 작은 질문하나 드리겠습니다. 회원 애플리케이션 기능 추가의 25:00~26:00 에서는 MemberFinder 인터페이스를 정의하고 기존 MemberService에 혼재된 멤버 조회로직을 CQRS에 따라 구현클래스를 분리했습니다. 그리고 이를 MemberModifyService에서 사용하고 있습니다. 말씀 주신 것처럼 단순히 조회이고 변경이 없다는 가정하에 이처럼 인터페이스를 정의하고 이를 통해 사용을 하는 것은 잘 이해가 됩니다! 다만 저는 같은 계층에서 있는 서비스 빈이 서로를 의존하지 않는 방향으로 개발을 해오고 있었는데요. 그래서 토비님의 방식에 대해 공부하면서 같은 계층에서 인터페이스 포트를 통해서 호출하는 것은 괜찮을까 고민이 들었습니다. 만약 그게 설계적으로도 문제가 없다면 앞으로도 토비님의 방식으로 개발하고 싶은데요. 분명 토비님께서 오랫동안 고민하신 개발 원칙/기준이 있을 것 같아서 여쭤봅니다. 같은 계층에 있는 애플리케이션 서비스 빈이 서로를 의존, 또는 인터페이스를 통해 사용되는 경우 지켜야할 기준이나 원칙이 있을까요? *추신 저는 토스의 김재님의 [블로그]( https://geminikims.medium.com/%EC%A7%80%EC%86%8D-%EC%84%B1%EC%9E%A5-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EA%B0%80%EB%8A%94-%EB%B0%A9%EB%B2%95-97844c5dab63 )를 접해서 위와 같이 개발해오고 있었습니다. 저는 계층을 하나 두고 유틸성 빈을 만들고 이를 사용하게끔 했습니다. (블로그내용 아래 일부 발췌) > 네 번째 규칙 동일 레이어 간에는 서로 참조하지 않아야 한다. (다만, Implement Layer 는 예외적으로 서로 참조가 가능합니다.)