해결된 질문
작성
·
442
·
수정됨
7
안녕하세요. 토비님!
먼저 제가 정말 오랜 기간 고민해온 주제에 대해서 이렇게 강의를 내주셔서 너무 감사드립니다!!
특히 강의에서 여러 차례에 걸쳐서 깊게 다룬, "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 를 생산적으로 사용할 수 있는 방법이 있을까요?
답변 2
4
중요하고 어려운 질문을 해주셨네요. 우선 모든 경우에 통하는 항상 장점만 가진 이상적인 방법은 없다는 걸 전제로 해야할 겁니다. 대부분의 기술이 그렇지만 케이스에 따라서 다르고, 컨텍스트에 따라서 트레이드 오프를 결정해야 합니다.
그럼에도 제가 시작하는 프로젝트라면 우선 도메인 모델 클래스를 JPA 엔티티로 사용하는 것이 좋다는 의견을 드렸고 여러가지 설명을 했는데요.
우선 저는 JPA에 대한 상당한 신뢰를 가지고 있습니다. 스프링이 나오기도 전인 2003년에 오픈소스 하이버네이트를 접하고, 이게 자바의 모든 오브제트-DB 매핑 관련 기술을 다 제치고 표준이 되어가는 과정을 꾸준히 지켜보면서 여러 확신을 가지게 되었죠. 물론 모든 것이 다 완벽하게 들어맞는 상황은 없을테니, 여러가지 비판들이 있었고, 그 중 대표되는 것이 처음부터 이상적으로 설계된 정규화된 DB 구조가 아닌 경우엔 ORM/Hibernate가 맞지 않는다 였습니다.
그래서 JPA 표준으로 준비되는 즈음에 나온 Hibernate 3에서는 우리는 반정규화된, 엉망인 DB도 매핑해낼 수 있다는 것을 강조했던 기억이 있습니다.
세월이 흘러 그와는 별개로 도메인 모델은 순수해야 한다면서 JPA 엔티티를 도메인 오브젝트로 사용하는 것을 거부하는 흐름도 생겨났습니다. 제가 기억하기로는 자바 스프링 쪽에서는 만들면서 배우는 클린 아키텍처라는 책에서 언급한 방식이 빠르게 퍼져나갔던 것 같습니다. 닷넷 계열의 개발자들의 영향도 물론 있었고요.
이게 워낙 복잡하고 어려운 주제라 한번에 제 생각을 다 적기에는 시간이 좀 부족합니다. 제가 내일 한국으로 출장을 가게 되어서 지금 준비중이거든요. 🙂 그래서 시간을 두고 이야기해주신 것들, 아마 다른 분들이 주시는 피드백도 더 반영해서 계속 답을 이어가 보겠습니다.
기존 분리 방식의 장점이라고 하신 것들을 먼저 생각해볼게요.
장점 1번은 전형적으로 오브젝트와 DB 스키마의 불일치 문제로 일반화해서 생각해볼 수 있습니다. 사실 단순 SQL 매퍼를 넘어서, 이걸 극복하기 위한 기술이 ORM이죠. 실제 예를 가지고 생각해봐야겠지만, JPA는 꽤나 다양한 매핑 기법을 제공합니다. 많이 나오는 이야기 중에, 레거시 DB라서 테이블이 쪼개져 있는데, 도메인 모델 관점에서는 두 개 이상의 테이블에 흩어진 것을 하나읜 오브젝트로 만드는 것이 이상적인 경우가 있습니다. 예로 드신 User - UserDetail 이 테이블이 나뉘어있는데 도메인 오브젝트는, 어떤 이유로든, 하나로 만드는 것이 더 나은 설계가 되는 경우죠.
이럴 때는 JPA가 제공하는 @SecondaryTable을 사용하면 됩니다. 두 개 또는 그 이상의 테이블에 흩어진 정보를 모아서 하나의 엔티티와 매핑을 해줍니다. 테이블이 나뉘든 다시 합쳐지든 매핑 정보만 수정하면 되니 도메인 오브젝트 관점에서는 일관성을 유지할 수 있습니다.
JPA 표준 외에 하이버네이트가 별도로 제공하는 매핑 기법들도 많이 있습니다. 많은 경우 이런 매핑 방식을 활용하면 레거시 DB 또는 다른 이유로 DB 테이블 구조의 변경이 일어나는 경우에도 도메인 모델을 유지시킬 방법들이 제법 있습니다.
제가 자주 쓰는 방법 중에는 같은 테이블에 대해서 읽기용 엔티티를 따로 만들어서 특정 로직에 특화된 JPA 엔티티 오브젝트로 가져오기도 합니다. 보통 읽기 쓰기를 다 해야 하느 경우 외에 특별한 로직을 사용해야 하는 경우, 각종 프로젝션 기법들이 도움이 되죠. 하나의 테이블에 대해서로 하나 이상의 엔티티와 매핑을 시킬 수도 있습니다. 이게 같은 트랜잭션 안에서 두 개가 충돌이 나지 않는 시나리오라면요.
그래서 말씀하신 장점이 특별한 경우엔 있겠지만, 많은 경우 JPA로도 어렵지 않게 지원이 되기도 한다는 점을 말씀드리고 싶네요. 사실 이런 걸 다 알고 필요에 따라 적용해야 하기 때문에 JPA와 관련 기술을 지속적으로 익히고 적용하는 학습 부담이 있기도 합니다. 그럼에도, 제가 JPA 엔티티를 분리해서 만들고 매핑하는 어댑터를 수동으로 만들었을 때, 너무 단순한 중복 코드가 늘어나고, 빠른 변화에 잘 대응하지 못하고 종종 버그가 만들어지는 걸 여러번 체험해보니, 우선은 JPA 엔티티를 도메인 오브젝트로 사용하고, 도메인 설계에 따라 데이터 구조와 직접 매핑되지 않는 특별한 형태가 존재할 경우에만 도메인 오브젝트를 가공해서 넘기는 어댑터를 만들어 사용하는 방식을 우선해왔습니다.
장점 2번도 어떤 내용인지 잘 알고 있습니다. 만들면서 배우는 클린 아키텍처 책에서 JPA 엔티티를 분리해야 한다는 이유로, 도메인 로직으로는 굳이 필요없지만 단순 감사 목적으로 들어가는 created_at 같은 것이나, 낙관적락을 위해서 추가하는 version 같은 것들이 도메인 모델을 반영해야 하는 도메인 엔티티에 들어가는 것이 좋지 않다는 이야기가 나오죠. 반정규화 때문에 추가한 필드를 매핑하려고 넣은 프로퍼티도 그럴테고요.
그런데 저는 이런 생각을 해봤습니다.
사실 이런 필드들은 순수 비즈니스 로직을 기술하는데에는 방해가 될 뿐인, 너무 Technical 한 부분들입니다
얼마나 어떻게 방해가 되는지 따져보고 싶더라고요. 이건 도메인에 있으면 안 되는 거야, 우리 도메인은 순수해야 해라는 명분이 그렇게 중요할까요? 어떤 분들은 DDD 책에서 도메인 모델은 비개발자인 도메인 전문가들도 이해할 수 있는 것이어야 한다, 그런데 낙관적락을 위한 버전, 오딧팅 목적의 필드 등의 기술적인 문제 해결을 위한 코드가 들어가면, 그걸 어떻게 이해하겠냐고요.
궁극적으로는 코드가 도메인 모델을 그대로 담고 있는게 이상적입니다. 그렇다고 비개발자가 자바 코드를 이해할까요? public, class, List, int, if, switch 이런게 나열된 코드지만 도메인 전문가들이 척 보면 도메인 모델이 보여야 한다, 아주 추상화된 DSL 언어를 만들지 않는 이상 불가능하죠. 결국은 개발자들과 함께 코드를 보며 도메인 지식을 꺼내서 이야기를 나누면 충분합니다.
거기서 왜 도메인 모델인데 interface, class 같은 키워드가 들어가는가, 순수하지 않다. 왜 @Lombok 같은 애노테이션이 나오는가, 이런 걸 따지는 것은 별 가치가 없어보입니다.
그래도 최적화나, 동기화, 감사 목적으로 DB 액세스 관점이 더 많이 담긴 정보가 들어가 있는 것은 아니지 않냐라고 하실 수 있겠죠.
근데 그게 들어 있으면 도메인 엔티티의 로직을 사용하는 클라이언트 코드(아마도 애플리케이션 서비스쪽 코드) 작성에 혼란이 올까요. 버전이나 감사용 프로퍼티는 굳이 public getter를 만들지 않아도 됩니다. 엔티티 코드에는 존재하지만 이를 사용하는 다른 코드에서 실수할 여지를 주지 않으면 됩니다. 남은 건 개발자가 이후 로직을 추가하는 필요가 있을 때, 이런 기술적인 목적을 위한 일부 프로퍼티들이 로직을 기술하는데 어떤 방해가 되던가요? 저도 여러가지 입장에서 다양한 토론을 해보기도 하고, 실제 한가지 방법을 선택한 뒤에 팀원들과 코드를 만들면서 많은 회고를 해봤습니다만, 그것 때문에 도메인 로직을 구성하는데 방해가 되더라, 도메인 코드를 이해하기 어렵게 만들더라라는 얘기는 들어본 기억은 딱히 없습니다. 제가 강의에서 하나 지적했던 JPA 매핑 애노테이션이 너무 많으면 도메인 코드를 읽으려고 할 때 자꾸 시선을 뺐어가서 불편했던 것 하나 정도 남습니다.
대체로 공통적인 기술적인 프로퍼티는 공용 수퍼 클래스로 빼내도 되고요(모든 엔티티에 다 감사 필드를 적용하겠다는 식인데 기술 정책에 따라 이건 꽤 많이 쓰게 되더라고요). 코드에 안 보였으면 좋겠다 싶으면 임베딩을 해서 별도 클래스에 담아두고 도메인 관점으로 코드를 다룰 때는 이건 볼 필요없다고 마킹해둬도 됩니다.
어쨌든 저는 순수한 도메인 코드에 가깝게 만드는 것을 지향해야 하지만, 그게 조금이라도 흠집이 없어야 하므로, 다른 많은 불편함을 감수하고라도 모든 경우에 JPA 엔티티를 도메인 엔티티에서 분리하자라는 의견에는 동의하지 못하겠습니다.
좀 더 구체적인 예를 보여주시면 더 생각해볼 수 있겠습니다만, 분리쪽으로 트레이드 오프를 해야 한다면 비즈니스 로직을 기술하는 데 엄청난 방해가 된다라는 걸 보여줄 수 있어야 하지 않을까요.
JPA로는 아주 유명한 분과 오래 전에 관련해서 얘기를 나눠봤습니다. 본인의 의견은 공개를 하지 말아달라고 해서 누군지는 말씀드리지는 않겠습니다. 아무튼 그분도 아주 복잡한 도메인과 대용량 서비스에 JPA를 적용할 때, 분리 방법을 한번 썼다가 급한 변경에 대응을 빠르게 못하고, 매핑 코드의 누락 때문에 데이터가 망가지고, JPA의 장점은 대부분 포기해야 하는데, 아무리 생각해도 다른 NoSQL로 바뀔 일은 없고, 그래서 그렇게 안 쓰기로 했다고 하시더라고요.
단점으로 예로 드신 것에는 모두 동의합니다. 제가 강의에서 우리는 JPA를 쓰는 것이 아니라 Spring Data (JPA)를 사용한다라고 여러번 강조해서 말씀드렸는데, 벤치마킹을 해보면 그냥 JPA API 쓰는 것보다 더 느리다고 나오는 스프링 데이터를 사용하는 이유가 있죠. 심지어 업데이트에서 save()를 강조하는 그 구조가 처음에는 JPA의 설계를 무시하는 것 아닌가 싶어 못마땅했던 적도 있지만, 지금은 관점은 다르지만 아주 좋은 설계라는 생각을 많이 하게 됩니다.
다른 답변을 주시거나 제가 생각해볼 시간이 나면 더 얘기를 적어볼게요.
그리고 코틀린은.. 이번 강의 내용은 아니지만, 그래도 생각을 남겨보겠습니다.
저도 한 6년 이상 실무에서의 대부분 프로젝트는 코틀린 스프링으로 개발해왔습니다. Spring Boot + Spring Data JPA + Kotlin JDSL을 사용합니다. GraphQL은 아직 실무에선 사용하지 않았습니다.
말씀하신 것과 마찬가지로 코틀린 언어와 JPA가 참 안 어울린다는 생각을 종종 합니다. JPA는 철저하게 자바 언어의 특성에 맞춰서 만들어진 기술이니까요. 그럼에도 솔직히 대안이 아직까지 없습니다.
코틀린 컨퍼런스에서 Exposed를 만든 분의 발표를 보면서 이거 써보고 싶다는 기대를 가지기도 했지만, 다른 한편으로는 왜 개발자들이 안쓰는지 알겠더라고요. 지금은 모르겠지만 작년 초반까지도 Exposed의 전담 개발자가 없었고, 젯브레인스 직원이 개인 프로젝트로 리드를 했다고 합니다. 그래서 2년 넘은 이슈 수 백개가 그대로 쌓여있었죠. 그걸 커뮤니티에서 개발에 참여해서 풀어나가면 좋겠지만, 가장 큰 문제는 사용자가 없습니다. Ktor도 마찬가지죠. 사용자가 없고 생태계가 안 만들어지니 발전을 못합니다. JPA가 여러가지 비판을 받더라도 24년 가까운 세월동안 엄청난 사용과 피드백을 받으며 거대한 생태계를 만들어 놓았기 때문에 지금까지도 가장 인기가 높은 것이겠죠.
국내에 아주 유명한 기업의 어떤 개발팀에서 Exposed를 열심히 사용하는데, 당장 스프링 트랜잭션 관련 기능에도 문제가 많아서 애를 먹다가 결국은 자체적으로 프레임워크를 개선해서 제출하고 그걸 반영시켜서 사용했다는 얘기를 들었습니다. 실력이 좋은 사람들이 들어가고 싶어하는 좋은 회사에 많은 지원을 받는 팀이니까 가능한 얘기가 아닌가하는 생각이 들었죠. 그래서 아직까지는 Exposed를 흥미롭게 지켜보고 있고 기회가 되면 조금씩 써보긴 하지만 실전에 적용할 계획은 없습니다.
코틀린의 nullability는 처음에는 너무나 매력적으로 보이지만 실무에서 사용하다보면 너무 경직된 구조가 불편을 가져오는 경우도 많습니다. 지금 예로 드신 DB의 디폴트 값의 적용이 필요해서 null 값을 초기엔 허용하지만 이후엔 null을 허용하지 않는 not null로 취급되면 좋겠죠. 하지만 안 됩니다. 이건 JPA와 같이 쓸때만의 문제가 아닙니다. 그 외의 경우에도 null 값의 초기화 작업이 일어나는 시간이 조금만 길어져도 무조건 nullable로 만들어야 하는데, 그러면 이후에 !!을 쓰거나 지저분한 null safety 코드를 넣거나, 컨벤션을 적용하거나 해야 하죠. 사용 방법이 매끈하지 않습니다.
그래서 여러가지 궁리를 해봤지만 아직까지 답이 없습니다. 이건 사실 JPA라서 그런 건 아니고요. 어떤 DB 액세스 기술이라도 오브젝트 매핑 구조로 만들었을 때 동일한 문제가 생길 겁니다. 근본적으로 null 값을 넣어야 디폴트 값을 설정해주는 DB의 기능을 쓰면서 not null 타입으로 만든다는 것이, 그 요구사항 자체가 모순으로 보이기도 합니다. 코틀린이 일정 조건이 될 때까지는 nullable, 그 이후에는 not null로 다루는 컴파일러를 만들어주기까지는 개발자가 감당해야할 것 같네요.
급하게 적느라.. 내용에 빠진게 있는지 모르겠지만 일단 질문을 보고 떠오른 생각을 적어봤습니다.
좋은 질문 남겨주셔서 감사합니다. 나중에 제 유튜브 채널에 한번 출연하셔서 관련 이야기를 나눠봐도 좋겠습니다.
반정규화라고 언급하신 내용이 뭔지 이해가 되네요.
읽기에는 JPA 모델을 직접 사용하셨군요.
쿼리로 가져올 수 있는 데이터를 다른 테이블에 또 두는 게 일종의 정규화 위반이긴 합니다만, 현실적으로 모든 경우 그런 설계를 하는게 권장되지는 않겠죠. 은행 계좌 잔액은 지금까지의 모든 트랜잭션의 금액을 다 더하고 빼면 나오지만, 정규화를 기계적으로 맞추기 위해서 잔액 정보를 안 만드는 경우를 본 적은 없습니다.
더 중요한 건 그게 도메인 모델이기 때문이죠. 잔액이라는 도메인 개념과 속성이 많은 규칙과 로직에 사용되니까요.
마찬가지로 count를 단순한 통계성 정보로 취급하고, 지금은 도메인 지식에 포함되지 않지만 언젠가 데이터 분석 등에서 사용할 용도라면 도메인 모델에 굳이 들어가는게 의미없을 수 있습니다. 도메인에서 의미있는 생성 일자와 단순한 오딧팅을 위해서 기계적으로 부여한 created_at은 거의 비슷한 정보지만 그래서 개념이 다르겠죠.
회원별 리뷰 count를 User에 두신 이유를 잘 모르겠습니다. 그게 회원이라는 도메인 개념의 중요한 속성이거나 로직에 자주 등장하기 때문은 아니고, 단지 참고용 정보로 생성해두고 어느 UI에서 조회를 해야 하기 때문에 두신 것일 수도 있고요. 아니면 그 정보가 도메인의 중요한 정보일 수도 있습니다. 리뷰 카운트를 가지고 회원의 레벨에 반영을 한다거나, 또 다른 보상을 준다거나, 정기적으로 순위를 매겨서 노출한다거나 하는 것들도 있겠죠. 그렇다면 아무리 매번 aggregate 함수를 써서 가져올 수 있는 정보라 할지라도 그걸 기록해두는 것이 자연스럽습니다.
DB 설계의 핵심은 중복을 제거하는 것이긴하지만 어쩌다 필요할 때 한번 쿼리해서 가져오면 충분한 데이터가 아니고 도메인이나 애플리케이션 로직에 의미 있는 것이라면 엔티티에 두어도 충분할 겁니다. 다만, User와 생명주기나 쓰이는 시점, 이유가 다르다면 저라면 UserReviews라든가 UserActivity 같은 엔티티로 분리해두겠습니다.
등록 삭제와 카운트를 일치시키기 위해서 비관적 락을 거는 것은 이상적이지만 성능에 안 좋을 수 있죠. 그래서 보통 통계성 정보이고 실시간 일관성이 중요한 값이 아니라면 저는 보통 도메인 이벤트를 사용합니다. 트랜잭셔널 이벤트를 걸어서 기본 리뷰 등록을 우선 처리하고, 이후에 카운트 업데이트와 또 이어지는 다른 이벤트 처리 로직과 같이 다루겠습니다. 혹시 장애로 카운트 업데이트가 누락될 수 있다면, 그리고 그게 치명적이라면 아웃박스 패턴을 이용해서 이벤트 처리가 확실하게 완료되도록 만들테고요.
통계성 업데이트가 그렇게 실시간성 데이터가 아니라면, 일간 배치 정도에서 업데이트를 하는 방법도 있을 겁니다.
어쨌든 예로드신 카운트는 꽤나 도메인의 중요한 속성일 듯 싶어서, 반정규화로 고민하실 필요는 없지 않을까 싶습니다.
성능을 위해서 각종 트릭을 많이 써야하는게 요즘 시스템의 현실이기도 하고요. 어쨌든 일관성을 지키는데만 문제가 없으면 되지 않을까요.
토비님, 정성스런 답변 감사합니다!! 🙂
(위의 예시는 제가 보편적인 도메인으로 예시를 들기 위해 지어낸 것입니다.)
저도 평소에는 트랜잭셔널 이벤트를 많이 애용하는데요.
그럼에도 불구하고,
DDD 관점에서 "도메인 매서드가 해당 작업에 대한 완결성을 가지고 있어야 한다. (= 특정 비즈니스 단위에 대한 도메인 모델간 상호작용을 완전하게 표현해야 한다.)"고 생각하다보니, 위 (2)의 createProductReview
함수처럼 도메인 모델간 상호작용을 매서드 내에서 전부 표현하려면 count 때문에 비관적 락까지 걸어야만 한다는 게 불편하게 느껴졌습니다.
그래서 도메인 모델에서는 저런 반정규화 필드를 아예 배제해버려서 이렇게 불편할 일이 없게 만들자고 생각도 했던 것입니다만, 결국 DDD도 보통의 이상향들처럼 "추구는 하지만 현실도 중요하기에 기꺼이 타협한다"는 건 어쩔 수 없는 것 같네요.
마지막에 말씀해주신
성능을 위해서 각종 트릭을 많이 써야하는게 요즘 시스템의 현실이기도 하고요. 어쨌든 일관성을 지키는데만 문제가 없으면 되지 않을까요.
이 부분이 엄청 공감이 됐습니다. 🤣
답변 감사합니다!!
0
안녕하세요, 인프런 AI 인턴입니다. woonge님께서 제기하신 질문에 대해 답변드리겠습니다.
먼저, JPA Entity Class와 Domain Model Entity Class를 분리해서 사용하는 것에 대한 토비님의 견해에 대해 말씀드리겠습니다. 일반적으로 JPA Entity와 Domain Model을 분리하는 이유에는 말씀하신 것과 같은 장점들이 존재합니다. 도메인 모델을 더 자유롭게 수정할 수 있고, DB 마이그레이션의 부담을 덜 수 있으며, 도메인 로직을 더욱 순수하게 유지할 수 있다는 장점이 있습니다. 그러나 이로 인해 발생하는 단점도 무시할 수 없습니다. 특히, JPA의 Lazy Loading을 사용할 수 없고, 저장 로직을 직접 구현해야 하는 등의 생산성 저하 문제는 중요한 고려 사항입니다.
토비님은 강의에서 이러한 선택이 도메인의 복잡성이나 프로젝트의 특성에 따라 달라질 수 있다고 언급하셨습니다. 도메인 모델과 JPA Entity를 분리함으로써 얻을 수 있는 장점이 프로젝트의 목적과 맞아야 하고, 관리할 수 있는 수준이어야 합니다. 반면, 통합의 편리함을 제공하는 스프링 JPA의 생산성을 유지해야 할 필요가 있을 때는 통합된 모델을 사용하는 것이 더 유리할 수 있습니다.
두 번째 질문에 대해, Kotlin과 JPA의 조합 시 발생하는 문제는 많은 개발자들이 겪는 부분입니다. Kotlin은 null 안전성을 강조하는 언어이기 때문에 JPA의 null 처리와 종종 충돌하는 부분이 있습니다. 이를 해결하기 위해서는 옵션 타입을 사용하는 방법도 고려해볼 수 있습니다. Kotlin에서는 nullable 타입과 non-null 타입을 명확히 구분할 수 있기 때문에, JPA Entity에서는 nullable로 설정하되 비즈니스 로직에서는 non-null로 활용하는 방식도 있습니다.
또한, Kotlin-friendly 한 ORM인 Exposed를 사용하여 Kotlin의 장점을 최대화하는 방법도 고려해볼 수 있습니다. Exposed는 Kotlin DSL을 사용하여 더욱 코드가 간결하고 명확하게 설계되었기 때문에 Kotlin 라이브러리와의 궁합이 좋습니다. 다만, JPA처럼 자동화된 기능을 제공하지 않기 때문에, 필요한 기능을 직접 구현해야 할 수 있습니다.
Kotlin을 스프링 부트와 더 생산적으로 사용하고자 한다면, Kotlin DSL을 적절히 활용하고, 필요한 부분에서는 Spring Data의 익숙한 기능을 조합하여 사용하는 방법도 효과적일 수 있습니다.
아래의 토비님의 답변을 참고해 보시면 더 많은 인사이트를 얻으실 수 있습니다:
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.
이렇게 빠르게, 이렇게 정성스런 답변이라니!! 너무너무 감사합니다!! 😄
제 질문에 구체적인 예시가 부족했던 거 같아서 조금 더 첨언해서 말씀드려보고 싶습니다.
장점 2번에 대해서 저는
created_at
,version
,deleted_at
같은 필드 뿐만아니라, 반정규화 필드들을 도메인 모델에 넣지 않는 게 장점이라고 느꼈습니다.product_review_count
같은 필드를 예로 들 수 있을 것 같습니다.새로운 상품평
product_reviews
table)이 새로 생성될 때, 각users.product_review_count
,products.review_count
컬럼을 (1) 도메인 모델에도 정의할지? 와 (2) 어디에서 동기화할 지? 를 고민할 수 있습니다.(1) 도메인 모델에 Count 컬럼에 대한 필드를 정의할 것인가?
저는 Count 가 일반적으로는 join, group by, count, sum 같은 무거운 쿼리를 없애기 위해 선언하는 반정규화 필드일 뿐이다보니
따로 도메인 로직에서 중요하게 사용하는 게 아니라면 도메인 모델에 정의해놓기 애매하다는 생각이 들었습니다.
(2) 어디에서 동기화할 것인가?
만약 User 도메인 모델에 productReviewCount 필드가 있다면,
이런 식으로 처리해야할 것처럼 느껴지는데요.
이게 올바르게 처리되려면, 동시성 문제 때문에 Product 와 User 조회시 Pessimistic Lock 을 걸어야 하는데, 이 방식은 다른 로직들이랑 데드락에 걸릴 위험 때문에 피해야 합니다.
그래서 보통
UPDATE users SET product_review_count = product_review_count + 1 WHERE id = ?
같은 별개의 증분 쿼리를 정의해서 락 없이 사용하는데요.그러면 리뷰를 생성하는 서비스 로직에서 항상
이렇게 순차 호출해서 처리해야 합니다. 근데 이렇게 되면,
1) User 도메인 모델의
createProductReview()
함수에서this.productReviewCount++
와product.incrementReviewCount()
를 호출하기도, 안 하기도 둘 다 애매합니다.호출하면 어차피 반영이 repository 를 통해 반영이 안 되는 의미없는 로직이 되고 호출을 안 하면 필드가 정의는 되어있는데 의미상 관리가 안 되는 것처럼 느껴집니다.
2) save 가 insert 인 상황에서만 아래 두 increment 가 반드시 호출되어야 합니다. save 를 호출하는 곳이 늘어나면, 이 save 가 insert 가 맞는지 바로 알 수가 없어서 increment 호출이 적절하게 되고 있는 건지 판단할 때 가독성이 너무 안 좋습니다.
그래서 저는 현재 별다른 이유가 없다면 도메인 모델에 productReviewCount 같은 반정규화 필드를 정의하지 않고, 반정규화 필드들에 대한 관리 책임도 outbound adapter 에 완전히 위임했습니다. 덕분에 도메인 모델과 서비스에서는 완전히 정규화된 상황에서 비즈니스 요구사항만을 정확하게 서술할 수 있게 됐습니다.
즉, 어플리케이션 서비스에서는
productReviewRepository.save(productReview)
만 호출하고,구현 어댑터에서 직접 insert 케이스를 판단해서
users.product_review_count
와products.review_count
같은 반정규화 필드에 대한 일관성을 항상 보장해주게 처리했습니다.덕분에 도메인 모델에 Count 필드와 관리 로직이 없어서 createProductReview 매서드 내용도 의미상으로 완전해지고, 어플리케이션 로직에서도
productReviewRepository.save()
만 남게 되어서 너무 간결하고 깔끔해졌습니다. 이 save 매서드를 호출하는 곳이 많아져도 안심이 됐고요.그리고 이 부분이 헥사고날 구조에서 Domain Entity 와 JPA Entity 를 분리할 때 CQRS 도입이 강제되는 이유 중 하나라고 느꼈습니다. (READ 가 도메인 모델를 거치지 않고 JPA Entity 를 바로 사용해야 이 reviewCount 를 사용할 수 있으니까요.)
다만 한 편으로, "근데 반정규화 필드들에 대한 관리 책임을 어댑터가 가져도 되는 건가?" 에 대해서 사실 한 편으로 의문이 남아있기도 했어서, 이 부분에 대해서도 토비님의 의견이 궁금했습니다.
이 부분에 대해서 평소에 보통 어떻게 처리하시는지 궁금합니다.
나머지 답변해주신 내용에 대해서도 전부 잘 이해했습니다.
Exposed 는 아직 스타트업에서 도입을 고려할 수 있는 정도는 아니군요.
그리고 말씀해주신 내용중에 JPA 와 Hibernate 가 발전되어온 흐름에 대한 통찰이 너무 재밌네요. 이 부분 되게 자세히 듣고 싶은데 나중에 혹시 유튜브 영상이나 강의로 제작해주시면 너무 재밌게 들을 수 있을 것 같습니다. 🙂
토비님께 이렇게 여쭤볼 수 있는 기회가 있다는게 너무너무 좋네요.
감사합니다!