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

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

by Nahwasa 2025. 7. 16.

목차

     

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

     

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

     

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

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

     

     


     

    3장. 객체의 일관성 유지하기

    Q1. “객체의 일관성”이란 말이 애매하게 들릴 때, 세영은 어떤 관점으로 정의하고 있나?

    • 단순히 “데이터가 맞는 상태”를 넘어서, 객체가 어떤 책임을 제대로 수행하고 있는지를 기준으로 삼아?
    • 혹은 도메인 로직, DB 상태, 외부 시스템까지 고려한 “전체 맥락의 정합성”이 포함된다고 봐?
    • 실무에서 객체의 일관성이 깨졌던 기억이 있다면 어떤 사례였고, 그걸 어떻게 해결했어?

     

    내 답변

      객체의 일관성은 결국, ‘해당 객체를 신뢰하고 일을 맡길 수 있는가’라고 생각해. 해당 객체가 어떠한 역할이나 책임을 가질 때, 그 협력이 신뢰 가능한가의 문제라 생각해. 이런 관점에서 너가 물어본 1,2번째 모두 포함된다고 생각돼.

     

      이 책이나 너가 말한 ‘객체’는 일반적으로 도메인 객체겠지만, 내 경우 결합성을 떨어뜨리는 쪽으로 많이 생각해. 그래서 꼭 도메인 객체가 아니더라도 협력하는 객체라면 모두 일관성을 가져야 한다고 봐. 즉, 신뢰할만한 객체여야 한다고 봐.

     

      예를들어 난 프로젝트 진행할 때 내 관점을 따로 정리하면서 짜는편인데, JWT payload에 대해 다음과 같이 메모해둔 부분이 있었어.

    유저 정보는 결국 Payload에 있음. 따라서 DTO 형태로 사용하기 쉽게 만들어둘 필요가 있음.
    하지만 통신에 사용하기 위해 토큰에 넣을 땐 
    Payload만 사용 시, 암호화 되어있는지 아닌지 알기 어려움.
    따라서 EncryptedPayload를 따로 두었고, 여러 제한을 통해 외부에서 사용 불가하게 해서
    Factory를 통해서만 생성할 수 있게 만들어둠.
    이제 암호화되지 않은 Payload는 구조적으로 만들어질 수 없음이 보장됨.
    protected로 패키지 내에서만 사용가능한데, 혹시 모를 사용을 대비해 테스트로 의존성 확인함.
    

     

      외부에서 실수로, 혹은 이런 상황을 모르는 유지보수자가 실수로 암호화되지 않은 객체를 쓸 수 없도록 해서 객체의 일관성을 보장하려고 했지.

     

      또, 마찬가지로 JPA Repository가 너무 강력하기 때문에, 그 기능을 제한해서 DB 자체의 일관성을 유지하기 위해 repository를 래핑한 interface를 만들고 사용해. 이건 물론 초기 개발땐 Map 같은걸로 간단히 구현해두고, 기술의 선택을 늦추기 위해서 그런 점도 있지만, 너무 강력한 라이브러리나 프레임워크의 행동을 제한하기 위해서도 사용중이야.

     

      객체의 일관성은 내게 중요한 부분이기 때문에 내가 짠 곳에서 깨진 기억은 딱히 없어. 이전에 팀원이 PR 올린 부분에서 일관성이 깨질뻔한 곳을 발견해 장애를 미리 막은 기억은 있어. API는 stateless해야 하는데, PR 올라온 코드에는 private String clientId; 이런 부분이 있었어. 단계가 A, B, C 가 있다고 할 때, A단계에서 clientId를 받고 그걸 저장해뒀다가 B, C에서 처리하고 싶었나봐. 당연히 approve 하지 않았고, 왜 그렇게 하면 안되는지 설명해주고 혹시 다른 사람도 모를까봐 사내 세미나를 관련 내용으로 진행했었어. 특히 동시성 문제에 관련된 이런 부분은, 초반에 잡지 못하면 이후 아무도 알 수 없는 에러가 되기 쉬워서 더 주의해야 한다고 봐.

     

     

    Q2. 일관성을 유지해야 할 책임은 누구에게? — “엔터티 vs 서비스 vs 검증 컴포넌트”

    • 세영은 어느 정도까지 엔터티에 책임을 맡기고, 어디서부터는 외부에서 조율한다고 생각해?
    • 예를 들어 “수강 중복 여부”처럼 다른 컨텍스트가 관여되는 일관성은 어떤 구조로 해결하는 게 좋아?
    • 실제로 서비스가 너무 복잡해져서 검증 컴포넌트를 따로 만든 적이 있다면, 그 기준은 뭐였어?

     

    내 답변

      언제나 동일한 기준으로 개발할 순 없다고 봐. 우선 이하 도메인 객체라고 얘기하는건 어쨌든 ORM에 종속되는 엔터티와 도메인 객체를 우선 분리해서 얘기하고 싶어서 그랬어. 실제론 분리하지 않긴 하지만 설명의 편의를 위해 도메인 객체라고 할께. 예를들어 어떤 도메인 객체 중 한 속성은 반드시 암호화된 데이터가 들어가 있어야 한다고 해볼께. 일반적으로는 이걸 서비스에서 암호화해서 도메인 객체에게 넣어주겠지. 근데, ‘어떤 암호화 방식으로 암호화 할 지’ 또한 비즈니스 로직이 아닌가? 그럼 도메인 객체에서 처리해야하는게 아닌가? 근데 그렇게 되면 외부 라이브러리에 종속되는게 아닌가? 별거 아니긴 한데, 이전에 짤 때 이걸로 꽤 많이 고민했어 ㅋㅋ.

     

      결론은 그때그때 다르다고 봐. 엔터티는 어디까지 책임을 맡아야 한다. 서비스는 일반 서비스와 도메인 서비스로 나눠서 생각하고, 일반 서비스 객체에서는 비즈니스 로직이 들어가면 안된다. 도메인 객체에서 처리하기 좀 애매한데, 비즈니스 로직이 들어가면 도메인 서비스라고 불러야한다. 이런건 리팩터링의 대상이라 생각해. 명확한 기준보다는, 내 현재 지식을 기준으로 이정도면 됐다고 생각되는 지점까지 계속 리팩터링 해나가는거지.

     

      검증 컴포넌트 질문의 경우 아무래도 최근 타팀에 기능 추가 요청을 보내면서 예시코드를 짜서 보냈거든. 이런식으로 동작하는걸 너희팀의 룰에 맞게 짜달라 이런 요청이었지. 파일 관련된 로직이었는데, 거기에 ‘확장자를 제한하는’ 검증 컴포넌트를 따로 넣었어. 그렇게 한 이유는, 어떤 확장자를 허용할지는 어찌보면 비즈니스 로직이긴 한데, 사실 관리자가 관리자페이지에서 설정한대로 동작되는 부분이거든. 그러니 그냥 DB 값에 의해 제한되는 것 뿐이야. 그러니 도메인 객체에 넣을만한건 아니라고 봤어. 그리고 서비스로 넣기엔 또 확장자 별로 동작이 좀 달라져야 하는 부분이 있다보니 그런것까지 처리할겸 확장자 검사도 별도 컴포넌트로 만들어서 요청넣었지.

     

     

    Q3. “null은 그냥 무시해도 된다”는 설계 방식에 동의해?

    • 책에서는 “이미 완료된 청구서에 결제를 또 하려고 하면 그냥 무시해도 된다”고 했는데, 세영은 이걸 “클라이언트 문제를 숨기는 무책임한 설계”라고 봤지.
    • 그럼 세영은 어떤 조건에서 “아무 반응 없이 무시하는 설계”를 인정할 수 있을까?
    • 혹은 항상 명시적으로 에러를 반환하거나 예외를 던지는 쪽을 선호해?

     

    내 답변

      예를들어 클라이언트가 뭔가 상태를 변경할 수 있는 화면이 있다고 해볼께. 기본적으로 ‘확인 전’ 상태이고, 이걸 ‘A 처리중’, ‘B 처리중’, ‘완료’ 이렇게 바꿀 수 있다고 할께. ‘완료’ 에서 ‘확인 전’ 상태로 바꾸는게 가능할까? ‘A 처리중’ 에서 ‘B 처리중’으로 바꾸는건? ‘A 처리중’에서 ‘A 처리중’으로 바꾸는건? 이거에 대한 룰은 결국 설계자의 몫이야. 일반적으로 생각해봤을 때, ‘완료’에서 ‘확인 전’으로 바꾸는건 예외에 해당하지 정상적인 로직은 아니라 생각돼. 이런건 당연히 에러를 내고, 클라이언트한테 알려주는게 맞지. A 처리중에서 A 처리중처럼 상태가 변하지 않는 요청이고 로직적으로 별다른 문제가 없다면 이런건 무시해도 된다고 봐. 로직에 따라 다르겠지만, 화면에서 ‘현재 상태’를 보여주지 않는다고 할 때, 클라이언트가 현재 상태를 모르고 A로 변경요청할 수도 있지. 보통 상태 변경은 비즈니스 로직에 해당하니, 어떤 변경을 에러로 볼지 말지는 비즈니스 로직에 따라 다르긴 할텐데, 일반적으로 저 상황은괜찮을 가능성이 높아.

     

      항상 명시적으로 에러를 반환하거나 예외를 던지는 쪽을 선호하는건 아니지만, 적어도 비즈니스 로직적으로 에러라고 판단되면 어떤방식으로든 알려주는게 맞다고 생각해. 책에서 말한 이미 완료된 청구서에 결제를 또 하려고 하는 경우, 무시할 때 클라이언트가 어떻게 반응해야 할지에 대한 통제성이 너무 떨어져보여. 또 위에서 말한 A→A 처리와는 다르게, 결제로직은 클라이언트 입장에서도 중요한 비즈니스 로직일꺼야. 재결제가 시도되었다는건 클라이언트쪽의 로직이 탄탄하지 못했기 때문이라고 생각해. 클라이언트쪽에 “이미 결제된 청구서입니다.” 이런식으로 에러코드가 가는게 맞다고 생각해. 그렇게 하지 않고 무시한다면, 클라이언트 입장에서 현재 사용중인 API에 대한 신뢰성이 떨어진다고 봐.

     

      물론 이런 관점에서 초창기 자바의 라이브러리들이 Checked Exception을 남용해서 개발자들을 힘들게 한 경우도 있어. 그러니 너무 과한 방식은 또 안좋겠지만, 적어도 클라이언트가 현재 상황을 알고 판단할 수 있도록 어떤 방식으로든 알려줘야 한다고 생각해. 책에서 나온 완료된 청구서 재결제 상황이라면, 뭐 설계자가 무시하는 쪽으로 판단할수도 있지. 그렇더라도 에러까진 아니더라도 클라이언트 개발자쪽에서도 이런건 중요한 상황일 것이니, 간단하게라도 알려줘야 클라이언트쪽에서 UX를 개선하는데 쓸 수 있을꺼야.

     

      API를 만드는 입장에서 보자면 클라이언트가 어느정도 유연하게 사용할 수 있게 API를 설계하는건 좋은 방식이야. 또, 클라이언트마다 요구사항이 다를수도 있으니 그렇게 해야 이후 유지보수도 편해지겠지. 그치만 비즈니스 로직적으로 ‘에러’로 판단되는 상황이거나, ‘클라이언트의 실수’가 될 수 있는 상황이라면 그걸 명확히 알려주는 것도 API의 역할이라고 봐. 그런 의미에서 책의 예시가 무책임하다고 생각했어. 좀 더 덜 중요해보이는 상황으로 예시를 들었다면 내가 무책임하다고 얘기하지 않았겠지.

     

     

    Q4. 내부 구조를 외부에 노출하지 않으려 한 적 있어? — 캡슐화의 실전 적용

    지금까지 만든 객체 중에서, 내부 컬렉션(List 등)이나 구조를 그대로 외부에 리턴하지 않아야 했던 상황이 있었을까?

    • 있다면 어떤 방식으로 처리했어?
      • 예를 들어 List를 그대로 리턴하지 않고 복사본을 주거나,
      • 아예 Read-only View 객체를 따로 만든 경우도 포함해서 말해줘.
    • 반대로, 그걸 의식하지 않고 넘겼다가 문제가 생긴 적은 없었어?

    혹은 “이건 그냥 직접 줘도 된다”고 판단한 기준이 있다면, 그건 뭐였을까?

     

    내 답변

      이건 내가 책에서 ‘내부 구조를 노출하지 않으려 할 때, 복사본을 주는 방법도 있다’ 라는 부분이 신선했다고 말해서 너가 나한테 물어본걸로 생각돼. 내가 그게 신선했다고 생각한 이유는, 난 내부 구조를 그대로 줄 생각 자체를 해본적이 없어서 그랬어. 혹시 그래야 한다면, unmodifiable 콜렉션으로 래핑해서 복사본을 준다고 한 발상 자체가 신선하다고 생각했던거지.

     

      왜 내가 내부 구조 자체를 줄 생각을 못했는지 생각해봤어. 난 객체지향 구조에서, 내부 구조를 그대로 줘야하는 경우가 발생하는 것 자체가 설계 실패가 아닐까라고 생각이 들어. 솔직히 말해서 그에 해당하는 적절한 예시나 비유를 못찾겠어. 이건 반대로 너가 그런 예시를 나한테 알려주면 다시 생각해볼께.

     

      객체지향 구조에서 외부의 객체에서는 메시지를 보내서 뭔가 일을 시켜야 해. tell, don’t ask 라는 유명한 개발자 밈도 있지. 내부 구조를 보내줘야 한다는건 저 ask에 해당될꺼야. 그게 아니라 객체한테 일을 직접 시키는게 맞다고 생각해. 내부 구조에 해당하는 데이터를 외부에서 원할때도 있다? 근데 그럼 외부에서 객체의 내부 구조를 알고 있다는 얘기야? 그건 맞지 않는 것 같아.

     

      물론 나도 알고리즘 짤 땐 tell, don't ask를 따르지 않아. 절차지향적으로 짜니까 내부 구조의 복사본을 주는게 뭐야, 그냥 애초에 외부에서 접근 가능하게 할때도 많아. 근데 업무로 가면 얘기가 달라져. tell, don't ask를 항상 따르려고 노력하는 편이야. 그렇다보니 실무에서 내부 구조의 복사본을 줘야하는 경우의 예시가 정말 궁금해져서 너한테 물어본거야. 나도 어느 한쪽이 항상 맞다고 하지 않는 편이야. 트레이드 오프에 따라 균형을 맞추는 편이지. 그래서 너가 예시를 들어주면 내가 잊고 있던 점이 있는지 확인하고 균형을 너랑 맞춰보고 싶어.

     

     

    '내 답변'에 따른 chatGpt의 예시

    🧪 복사본 제공이 필요한 실무 예시 (토론용 소재)

    ✅ 1. UI 단의 일괄 조회 처리

    • 예: 게시글 조회 API가 댓글 목록을 포함해서 내려줘야 함.
    • 클라이언트는 단순히 보여주기만 하면 되므로, List<Comment>를 그대로 넘기고 싶을 수 있음.
    • 하지만 이걸 그대로 넘기면 외부에서 List.add() 가능.
    • 그래서 Collections.unmodifiableList(comments)로 감싼 다음 리턴.

     tell할 수 없음. UI는 단순히 보여주기만 하면 되는데, 객체에게 “댓글을 순회해서 출력해줘”라고 시킬 수는 없으니까.


    ✅ 2. 설정값 제공 객체

    • 예: SystemConfig 객체에 allowedFileExtensions 라는 리스트가 있음.
    • 이건 단순히 설정 조회용이지만, 외부에서 수정을 막아야 함.
    • 설정 주입 후 List<String>를 외부로 넘기되, 불변으로 복사해서 리턴.

    → 이 경우도 ask에 해당하지만, 불변성을 보호하려고 복사 필요.


    ✅ 3. 리포트 출력 API

    • Report 객체 내부에 계산된 결과 리스트가 존재 (List<SummaryResult>)
    • 외부에서는 출력용 JSON으로 직렬화만 하면 되므로, 내부 데이터를 꺼내야 함
    • 하지만 이 리스트가 캐시된 값이라 조작되면 안 됨

    → DTO로 변환하거나 복사해서 주지 않으면 객체의 내부 상태 보호가 깨질 수 있음


    🎯 핵심 포인트 정리

    • “복사본을 준다”는 건 대부분 Ask 상황에서 벌어지는 일이야
    • 그럼에도 불구하고 내부 무결성 보호, 불변성 유지, 실수 방지 등 실용적 이유로 사용돼
    • 따라서 tell/don’t ask 원칙을 무시한다기보다는, **“Ask하더라도 방어는 철저히 한다”**는 맥락에 가까워

     

     

    내 추가 답변

      아.. 너의 예시를 보니 알겠어. 관점의 차이였네. 너가 말한 예시들 나도 분명 자주 하는거야. 근데도 내가 그런적 없다고 했던 이유를 말해줄께.

     

      난 클린 아키텍쳐 기반으로 생각하는걸 좋아해. 즉, ‘도메인 객체를 제외한 모든 것은 외부’ 라는 관점이야. 도메인 객체는 서비스의 메인 로직이고, 이때 내부 구조는 최대한 추상화되어야 해. 외부 객체는 내부를 몰라도 메시지를 통해 tell, don’t ask 를 해야해. 그러다보니 ‘내부 구조를 외부에 보여줘야 한다면, 설계가 잘못된거 아닐까?’ 라고 말한거야. 즉, 복사본을 줄 필요성 자체를 못느낀거지.

     

      그럼에도 너가 말한 예시를 나도 해봤다고 했잖아. 그건 내 기준에서 너가 말한 예시들은 '도메인 객체'로 생각하지 않기 때문이야. 그냥 외부 영속성에서 데이터를 꺼내오는거지. 즉, CQRS 관점에서 그냥 DAO, DTO의 외부 계층에서 꺼내서 전달해주는 느낌인거고, 도메인 객체의 역할로 생각하지 않은거야.

     

      애초에 도메인 객체의 복사본을 외부에 전달할 경우, 물론 수정은 막을 수 있겠지. 근데 그럼 도메인 객체가 오히려 외부에 결합성이 생겨버리는거라고 생각해. 도메인 객체의 비즈니스 로직을 고치기 위해 속성을 바꾸고싶은데, 외부에서 이 복사본을 쓰고있다고 못바꾼다? 그건 본말전도야.

     

      실제로도 도메인 객체에 뭐 Entity 어노테이션을 써서 영속성과 연결되어 있다고 해도, 여기서 꺼내는게 아니라 별도로 조회용 모델을 만들어서 그걸로 꺼내써. 그리고 DTO 형태로 자료를 리턴해줘. 그래서 애초에 내부구조를 드러내고 수정될 수 있다는 생각을 안한거였어. 관점의 차이였고, 그걸 알게된 지금도 책의 내용을 동의하진 않지만, 실무적으로 쓸 수 있는 하나의 스킬 정도론 생각하고 있어야겠어.

    댓글