본문 바로가기
Study/객체지향 시스템 디자인 원칙

[객체지향 시스템 디자인 원칙] 내 생각 Q&A - 4장. 의존성 관리하기

by Nahwasa 2025. 7. 16.

목차

     

      이 글은 ['객체지향 시스템 디자인 원칙'을 읽으며 정리한 내 생각 - LLM과의 Q&A 기반 사고 확장] 글을 챕터별로 나눈 시리즈 중 하나 입니다. 전체 흐름을 보고 싶거나, 다른 챕터를 확인하고 싶으시다면 위 통합글을 참고해주세요.

     

      이 글은 책 내용을 요약하거나 정리한 글이 아닙니다. 책을 읽고 난 뒤 제 생각을 중심으로 정리한 글 입니다. '객체지향 시스템 디자인 원칙'을 읽으며, 느낀점이나 좋았던 점 및 책에 제시된 연습문제를 LLM에게 알려줬습니다. 이를 바탕으로 제 생각을 기반으로 책의 연습문제를 변형하거나, 새로운 질문을 생성하게 했습니다. 그 후 제가 그 질문에 답하고, LLM과 토론하는 방식을 사용해 사고를 확장해봤습니다. 이 글은 그 질문과 답변을 모아둔 글 입니다. 자세한 작성 배경은 통합글에서 확인하실 수 있습니다.

     

     Q1, Q2와 같은 질문 외의 모든 답변은 LLM을 쓰지 않고 제가 직접 답변한 내용입니다. 이후 LLM과 토론하며 생각이 조금씩 바뀐 부분들도 있으나, 최대한 초안 답변으로 올렸습니다.

     제 생각을 적은 것으로, 일반적인 개발자들의 생각이나 사실과 다소 다를 수 있습니다.

     

     


     

    4장. 의존성 관리하기

    Q1. 도메인 객체가 클라이언트 요구를 알아야 할까?

    배경: Offering → OfferingSummary 변환 책임 위치에 대해

    • 저자: OfferingSummary.convert(Offering) (클라이언트 입장 책임)
    • 세영: Offering.toSummary() (도메인 객체가 알고 있어야)

    질문:

    너는 왜 toSummary()가 더 자연스럽다고 생각했어?

    도메인 객체가 클라이언트의 요약 형태를 알아도 된다고 판단한 기준은 뭐야?

    혹시 그런 판단을 뒤집었던 경험은 없었어?

     

    내 답변

      뒤집었던 경험 보다는, 저 두가지에 대해 고민을 꽤 했었어. 사실 당시 명확한 답은 안나왔고, 그냥 내가 좀 더 중요하게 생각하는걸 기준으로 선택한거라고 보면 돼. 실제로 저자도 둘 다 괜찮은 방법이라고 하기도 했고.

     

      ‘도메인 객체가 클리이언트의 요약 형태를 알아도 된다’ 라기 보다는, 말장난이긴 한데 관점을 좀 바꾸는거지. 비즈니스 로직적으로 도메인 객체가 자기 자신에 대해 Summary를 제공하는 로직이 있다고 생각하는거지. 즉, 표현 책임도 일종의 도메인 로직일 수 있다는거지. 이 관점에서 보면 확실히 convert에 도메인 객체를 넣는 것 보다는, 그냥 도메인 객체가 toSummary()를 제공하는게 더 맞다고 느낄꺼야. 물론 내 생각에 대한 반론도 스스로 가능해. 참조 방향에서의 어색함이나, 이런 요약 로직이 여러개 필요할 때 도메인이 더 더럽혀질 수 있다는 점이 있겠지.

     

      사실 정답은 없긴해. 그리고 애초에 이 경우 더 자주 사용한 방식은, OfferingSummary에 대한 별도의 서비스를 만드는 방식이야. CQRS 관점에서 도메인 로직이 아니라, 그냥 영속성에서 필요한 정보를 가져와서 보여주는 Query 느낌의 usecase로 보는거지.

     

      물론 이건 JPA Entity, DB Entity, Domain Entity의 역할과 분리와도 연관된 내용이긴 해. 도메인 엔터티는 순수해야 한다는 파가 있고, 도메인 엔터티를 JPA 엔터티로 만들어도 상관없다 파가 있지. 양쪽 다 틀린부분은 아니야. 전자는 도메인을 POJO로 만들어 더욱 일관성있게 만들겠다는 거고, 후자는 JPA의 강력한 기능을 버릴 수 없다가 크겠지. 후자에 대한 또 다른 관점에서 토비님은 애초에 “기술을 만든 공식적인 의도가, JPA에서 Entity는 도메인 객체이고, Spring Data에서 Repository<T>의 T는 aggregate root” 라고 했어. 그리고 “맘에 안 들면 다른 걸 쓰든가” 라고 꽤 쌔게 말하셨어.

     

      어쨌든 도메인 객체를 어디까지 연결된 것으로 보느냐에 따라서도 저자가 말한 2가지 방식 혹은 다른 방식에 대한 호감도가 달라질 것 같아. Summary가 도메인의 일부인지, 혹은 하나의 도메인 객체에서 처리가 안되는건지, 단순히 외부 요구사항인지, 레이어간 참조 방향이 괜찮은지 등 다양하게 생각해볼 수 있겠지.

     

      또, Summary가 어느정도의 중요성을 가진거냐에 따라 달라질수도 있어. ‘결제 결과 요약’ 이런 내용이라면 이건 도메인 객체의 역할로 볼 수도 있어. 이 경우라면 난 도메인 객체에서 toSummary()를 더 선호하는거고. 반면 통계 등 정말 단순한 조회용일수도 있어. 이런건 난 그냥 영속성에서 데이터를 가져오는 usecase 정도로 보는거니, 이 경우라면 애초에 Summary를 도메인 객체에서 만들꺼냐, Summary에서 도메인객체를 변환할꺼냐 이런 생각 자체를 안하는거지.

     

     

    Q2. 외부 의존성 감싸야 할까? — 스프링 의존성 포용 문제

    • 스프링 같은 프레임워크가 내부에 깊숙이 들어와도 괜찮다고 판단하는 경계는 어디까지야?
    • 예를 들어 @Transactional, @Entity, @Autowired, RestTemplate, WebClient, Spring Event, JpaRepository 등…
    • 이 중 일부는 직접 쓰고, 일부는 감싸는 이유가 있다면 그 기준을 말해줄 수 있을까?

     

    내 답변

      전반적으로 내가 판단하는 기준은, 프레임워크가 제공하는 메인 기능인지, 외부와의 접점 기능인지야. 예를들어 Autowired 같은건, 의존성 주입 프레임워크인 스프링을 쓰는 상황에서 이걸 래핑하고 싶다는건, 그냥 스프링 말고 다른 프레임워크를 찾아보는게 낫다고 봐. 반면에 ResTemplate, WebClient, JpaRepository 이런건 외부와의 접점에 해당한다고 봐.

     

      즉, 기술의 선택에 해당하는 부분이고 이런곳은 최소한 interface로라도 감싸는 편이야. 즉, 어댑터 느낌으로 감싸는거지. 필요에 따라 얼마든지 프레임워크에서 제공하는 것 외의 라이브러리를 쓸 수 있거든. 또 그래야 모킹 같은걸로 테스트하기도 편해. 최근 만든 프로젝트에서도 HTTP 통신에 다른 라이브러리를 썼어. 하위 이용기관의 DB 프로시져로부터 데이터를 가져와야 하는 부분이 필요했는데 이 경우 DB에서는 프로시져로 리턴되는 CURSOR의 칼럼명에 대소문자 구분이 포함되지 않아. 그렇다고 Map 같은걸로 받고싶진 않았어. record로 받고싶은데, 그렇다고 대문자로만 리턴된다고 대문자로 변수명을 설정하는것도 안멋있는거지. 그래서 외부라이브러리 중 record에 매칭시켜주는 부분을 갈아끼울 수 있는 라이브러리를 선택해서 개발했어.

     

      또, JpaRepository 처럼 기능을 제한하지 않을 경우 너무 큰 기능을 제공할 수 있는 부분도 감싸는 편이야. 이 경우, 기술의 선택을 늦추면서도, 너무 과한 기능 제공이 안되게 막는 역할까지 포함된거지.

     

     

    Q3. “정적 메서드가 의존성 그래프를 단순화한다”는 말에 대한 생각

    이 주제에 대해 실무에서 누군가 반대 의견을 낸다면,

    예를 들어 “정적 메서드면 인스턴스 생성 안 해도 되니까 간단하고 좋잖아?”라고 한다면

    어떻게 설명할래? (즉, 실제 설득 상황을 가정해서 답변해줘)

     

    내 답변

      ‘진리’와 같은 기능이라면 그래도 된다고 생각해. 이후로 바뀔일이 없고, 그렇게 복잡하지 않아서 언제나 믿을 수 있는 그런거. 예를들어 public static int sum(int a, int b) 정도라면, MathUtil.sum(1,2) 이런식으로 정적 메서드로 써도 되겠지.

     

      근데 일반적인 비즈니스 로직에서 이런건 잘 없어. 요구사항 변화에 따라 변경될 가능성이 있지. interface 혹은 의존성 주입을 통해 만들어놓는게 아니라면, 이 정적 메서드의 구현 변경에 직접적으로 관련된 모든 객체가 영향을 받게되버려. 또 테스트도 어려워지지.

     

      다른 관점에서, 의존성 그래프를 단순화한게 아니라, 숨겨진 의존성을 만들었다고 보는게 더 맞을 것 같아. 결국 변경의 여파가 어디까지 전달되냐를 결합도라고 봤을 때, 정적 메서드는 결합도를 떨어뜨리는데 도움이 되지 않아. 오히려 그래프가 외관상으로만 단순화되면서, 숨겨진 의존성, 결합도가 되버리겠지.

     

      ‘간단하다’에 대해서도 쉽게 반박되는데, 어차피 의존성 주입 프레임워크인 스프링을 쓰고 있는 입장에서 굳이 어디서 생성 책임을 맡을거냐 생각도 안해도 되. IDE까지 잘 되어있으니 사실 딸깍 한번이면 알아서 주입문도 만들어져. 혹시 ‘간단하다’가 ‘코드가 짧다’라는 의미라면, 본말전도야.

     

     

    Q4. 의존성 주입 프레임워크를 싫어하는 사람에 대한 생각

    배경:

    책 연습문제 3번 그대로야.

    “일부 개발자는 DI 프레임워크(Spring DI 등)를 좋아하지 않는다. 이에 대한 너의 생각은?”

    질문:

    너는 프레임워크에 대한 거부감은 없지만, POJO 기반을 좋아하는 편이지.

    이걸 바탕으로 프레임워크 없는 순수 DI와 프레임워크 기반 DI에 대해

    • 어떤 장단점이 있다고 보이고
    • 너는 왜 프레임워크를 받아들이기로 했는지 (혹은, 어디까지 받아들이는지)
    • 한 번 정리해줘.

     

    내 답변

      POJO를 좋아하지만, 그렇다고 프레임워크를 거부하는 것도 아니야. 위선이라 볼 수도 있지만 실제로 2천개 넘게 푼 알고리즘 문제는 대부분 POJO로 짰어. 심지어 자바에서 제공하는 라이브러리로 해당 기능이 있더라도 직접 짜는걸 선호하는 부분들도 많아. 이건 내 취향이 이런거야.

     

      반면, 일에 있어서는 프레임워크를 받아들이는데 거부감이 없어. 우선 단순히 DI만 봐도, 의존성 주입 프레임워크가 해주는 역할이 커. 내 죄책감을 덜어주는 역할을 하기도 해. 아무리 SOLID 원칙으로 짜고, 의존성 주입하도록 짜고 해봐야 결국 의존성 주입 프레임워크 없이는 어디선가 생성의 책임을 질 수 밖에 없어. POJO라면 이 책임을 Factory 패턴이라는 이름으로 약간의 회색지대로 보고 거기에 죄책감 혹은 책임을 몰아넣지.

     

      그치만 의존성 주입 프레임워크가 있다면, 한 곳에 죄책감을 몰아넣지 않고도 “싫으면 프레임워크 쓰지 말던가” 라는 쉴드로 이 죄책감을 덜어낼 수 있어 ㅋㅋ. 그 외에도 AOP 기능도 좋고, 특히 Gradle, Maven을 통한 의존성 추가는 대체하기 힘들정도로 편하지.

     

      웹 프레임워크로써의 기능을 보면 더욱 심해. Dispatcher Servlet을 직접 구현한다? 와 이건 정말 어렵지. 어디까지 POJO해야 하냐는 질문까지 들어갈 수도 있어. JPA는 순수해? 그럼 MyBatis는? 그럼 JDBC를 직접 호출해서 쓰는건? POJO는 어디까지나 도메인 로직에 대해 선호하는거지, 그 외적인 요소에 대해서는 프레임워크의 생산성을 이길 방법이 없어. 오히려 전부 POJO하게 한다면 코드가 더 더러워질꺼야.

     

      애초에 EJB(Enterprise JavaBeans)에서 POJO를 외치며 나온게 스프링이야. 즉, POJO 프레임워크이기도 해. Hibernate도 마찬가지야 EJB의 Entity Beans가 쓰기 힘드니, POJO 기반으라고 나온게 Hibernate야. 바퀴를 새로 발명하지 말라는 말이 있지. 물론 프레임워크는 ‘범용적으로’ 좋은 기능들인거고, 뭔가 특화된게 필요하다면 직접 짤 수도 있어. 그치만 모든걸 순수하게 해야된다는건 오히려 역으로 유연성을 잃을 수 있다고 생각해. 위에서 말한 그럼에도 알고리즘은 POJO로 짰다는건, 사실 업무 로직이 아니라서 그런거야. 내 취향대로 가는거지. 참고로 저 '스프링이 POJO 프레임워크'라는 거에 대해서는 관련 세미나를 한 적 있어. ‘대강 살펴보는 자바부터 스프링부트까지의 역사 (자바, CGI, Servlet, DispatherServlet, JSP, Thymeleaf, J2EE, EJB, POJO, Hibernate, JPA, Spring Data JPA, Spring, Spring Boot)

     

      물론 지금 생각은 위와 같은데, 스프링부트 처음 접했을 땐 실수한적이 많았어. 당시 뭔가 디자인 패턴은 순수해야 하는 것 같은거야. 그러니까 굳이 ~~Manager 라고 만들면서 여긴 프레임워크가 안묻어야된다고 생각하면서 막 getInstance() 이런 정적 메서드 만들고 그랬지. 아니 애초에 IoC 컨테이너가 다 해주는건뎈ㅋㅋㅋ 당시 지식이 부족했어.

     

      물론 프레임워크가 너무 침투한다고 생각하는 경우도 있어. 개인적으로 AOP를 그렇게 좋아하진 않아. 물론 참 좋은 개념이고 좋은 기능이지. 근데 개인적으로는 응집도가 떨어지는 느낌이 있어. 또, 숨겨진 의존성 느낌도 들어. 그래서 꼭 필요하거나 효율적인 곳 아니면 잘 안쓰는 편이야.

    댓글