목차
이 글은 ['객체지향 시스템 디자인 원칙'을 읽으며 정리한 내 생각 - LLM과의 Q&A 기반 사고 확장] 글을 챕터별로 나눈 시리즈 중 하나 입니다. 전체 흐름을 보고 싶거나, 다른 챕터를 확인하고 싶으시다면 위 통합글을 참고해주세요.
이 글은 책 내용을 요약하거나 정리한 글이 아닙니다. 책을 읽고 난 뒤 제 생각을 중심으로 정리한 글 입니다. '객체지향 시스템 디자인 원칙'을 읽으며, 느낀점이나 좋았던 점 및 책에 제시된 연습문제를 LLM에게 알려줬습니다. 이를 바탕으로 제 생각을 기반으로 책의 연습문제를 변형하거나, 새로운 질문을 생성하게 했습니다. 그 후 제가 그 질문에 답하고, LLM과 토론하는 방식을 사용해 사고를 확장해봤습니다. 이 글은 그 질문과 답변을 모아둔 글 입니다. 자세한 작성 배경은 통합글에서 확인하실 수 있습니다.
※ Q1, Q2와 같은 질문 외의 모든 답변은 LLM을 쓰지 않고 제가 직접 답변한 내용입니다. 이후 LLM과 토론하며 생각이 조금씩 바뀐 부분들도 있으나, 최대한 초안 답변으로 올렸습니다.
※ 제 생각을 적은 것으로, 일반적인 개발자들의 생각이나 사실과 다소 다를 수 있습니다.

5장. 추상화 잘 디자인하기
Q1. 현재 사용하는 소프트웨어 시스템에서 잘 디자인된 확장 지점을 얼마나 자주 접하는가?
배경:
책 5.1장에서는 추상화와 확장 지점을 설계 시점에 의도적으로 고려해야 한다고 강조했지.
하지만 실무에선 보통 다음 중 하나야:
- 처음부터 확장을 고려하고 구조를 설계한 경우 (적절한 전략/템플릿 메서드/이벤트 기반 설계 등)
- 처음엔 단순하게 시작했지만, 나중에 리팩터링하며 추상화 지점을 잡은 경우
- 아예 추상화 없이 구현되다가 조건 분기만 늘어나며 망가진 경우
너는 객체지향 설계에 신경을 많이 쓰는 편이고, OCP 기반 구조를 중시하는 만큼
네가 직접 만든 구조에는 어느 정도 의도적인 확장 지점이 있을 가능성이 높다고 생각해.
질문:
- 너가 설계한 시스템 중에서,
- 예: 다양한 조건의 배지 발급, 전입 예약 타입 분기, 알림 방식에 따른 로직 분리 등
- 초기에 추상화를 염두에 두고 설계해서 이후 유지보수가 쉬웠던 구조가 있어?
- 반대로, 다른 사람이 짠 코드에서 “확장 지점이 잘 만들어져 있다”고 느낀 경험은?
- 무엇이 그렇게 느끼게 만들었는지도 궁금해.
내 답변
최근 했던 프로젝트에서 외부 API를 많이 호출해야 했거든. 하위 이용기관 N개 및 결제 관련, 인증관련, OAuth 관련, 오픈API 등등 많은 외부 API가 필요했어. 이 때 필요한 스펙도 다르고 호출 방식도 조금씩 달랐어.
예를들어 하위 이용기관은 주로 DB 프로시져를 호출하게 되는데, 그 중간에 별도의 에이전트를 두고 프로시져에서 데이터를 받아오는 방식이야. 이 때 DB 프로시져는 CURSOR의 리턴 테이블 칼럼명에 대소문자를 구분하지 못해. 근데 난 Map으로 대충 받고 내부에서 쓰는 용어로 바꾸긴 싫고, record 형태로 받고 싶었어. 그래서 하위 이용기관 호출하는 부분은 별도의 HTTP 라이브러리를 사용했어. DTO에 값을 매칭시켜주는 부분을 갈아끼울 수 있는 구조로 된 라이브러리를 쓰려고 한거지.
추가로, 외부 API 호출부는 호출방식에 따라 여러가지 방식이 존재하지만, 그 구현은 널리 퍼트리고 싶지 않았어. 즉, 최대한 한 객체에서 호출에 대한 실제 구현을 책임지게 하고 싶었어. 근데 그렇다고해서 클라이언트가 본인이 쓸 외부 API 호출 방식 외의 것을 알고싶게 하진 않았어. 예를들어 결제 처리를 호출하는 클라이언트가, 인증과 관련된 인터페이스를 알게하고 싶지 않았어. 인터페이스더라도 필요없는걸 알게된다면 그건 캡슐화가 실패한거라 생각해.
그리고 위에서 말한 우리 프로젝트와 이용기관 DB 또는 SAP 사이의 ‘별도 에이전트’ 또한 추상화 대상이었어. 이건 하위 이용기관이 DB를 선호할지, SAP을 선호할지, 아니면 HTTP를 선호할지와 관계없이, 우리쪽의 메인 서비스 서버에 영향을 끼치지 않게하는게 주 목적이었어. 따라서 이번엔 한 프로젝트 내의 추상화가 아니라, 멀티 모듈 사이의 추상화가 필요하지. 메인 서비스는 동일한 방식으로 에이전트를 호출하지만, 에이전트는 하위 이용기관이 선택한 방식으로 알아서 DB를 호출하거나, SAP을 호출하거나, HTTP로 호출해줘야 해.
또, 에이전트는 독립된 별도 웹페이지에서 API가 어떤 방식으로 동작할지 설정할 수 있게 했어. 왜냐면 메인 서버에 API가 추가되거나 수정됬다고 모든 이용기관의 에이전트를 재배포하고싶진 않았거든. 이렇게되면 에이전트 코드에 MyBatis나 JPA는 사실상 쓰기 힘들어져. JDBC 혹은 JCO를 사용해 RAW하게 짜야하지. 그래서 오히려 재밌었어. 마음껏 디자인패턴 넣으면서 POJO하게 짜기 더 쉽거든 ㅋㅋ. 뭐 과하게 넣진 않았고, 간단히 말해보면 다음과 같아. 호출에 대한 부분은 FACADE 형태로 하나로 빼고, FACADE가 Strategy + Adapter 패턴 느낌으로 다형성으로 DB, SAP, HTTP 모듈을 갈아끼우도록 했어. 서로 다른 방식이더라도 하나의 interface를 상속해서 구현한거지.
그리고 또 DB라 하더라도 여러 DB를 지원해줘야 해. 이 때, DB마다 프로시져의 스팩이 다른 부분이 있어. 예를들어 오라클 DB는 CURSOR를 OUT 파라미터로 여러개 리턴하는게 가능해. 반면에 MySQL은 CURSOR는 OUT 파라미터로 뺄 수 없고, 프로시져 자체의 리턴으로 최대 1개만 리턴하는게 가능해. 근데 이거 외엔 호출방식이 거의 비슷하거든. 그래서 서로 약간씩 차이가 나는 부분만 template method 패턴으로 각 DB 모듈이 abstract를 구현하게 했어.
전체적으로 보면 최근 프로젝트에서 외부 API 호출에 대해 내가 설계한 추상화 지점은
1.외부 API 호출 라이브러리 추상화
2.외부 호출 API의 종류에 따른 추상화 (구현체는 응집도 있게, 클라이언트는 자기가 관련있는 인터페이스만 알게)
3.DTO 매핑 로직에 대한 추상화
4.DB, SAP, HTTP 에 대한 추상화
5.DB 종류에 따라 서로 달라지는 부분에 대한 추상화
이렇게 되겠네.
실제로 외부 API 호출부분이 계속 추가됬지만, 그냥 interface 하나 추가해서 끝날 수 있었고. 저 에이전트도 이후 다른팀으로 운영이 이관된 후 업그레이드가 되었는데 그 경우에도 큰 문제가 없었다고 알고 있어. 뭐 일단 프로젝트 오픈하고 몇십만 이상의 회원이 사용중인데, 아직 오픈이후로 백엔드 한번도 비정상 동작 없었음.
다른 사람이 짠 코드에 대해서는 말할게 많지 않긴해. 애초에 내가 운영업무를 거의 한적이 없거든. 했더라도 내가 만든거 운영팀에 이관되기 전까지 정도만 해봤어. 그래서 실제 업무 중 그런걸 느낀 경험은 잘 없어. 주로 느낀건, 궁금한게 있어서 스프링 자체를 까본 경우라던지, 강의를 볼 때 인것같아. 개인적으로 가장 기억에 남는건 대학교때 리눅스 커널 분석해봤을 때 크게 느꼈어. C언어지만 객체지향 스타일로 짜여져 있는 코드야. C 자체도 raw하다고 하지만, 커널은 더하거든. 파일 시스템을 갈아끼우는 지점을 유연하게 처리한다거나 그런 부분에서 사실 처음 확장지점에 대한 맛을 봤던 것 같아. 그 때 커널 스타일 C 코드도 어느정도 익혀서 몇 가지 자료구조를 C언어로 객체지향 관점에서 짜둔 코드가 있어. 그 중 하나 보여줄께. '[자료구조] C언어 - 이중 연결 리스트(doubly linked list) 구현 - 객체지향'
Q2. 도메인 로직은 ‘판단’과 ‘행위’를 나눠야 할까?
배경:
책의 BadgeGiver 예제에서는
- BadgeRule.give(employee)로 조건을 체크하고
- 조건을 만족하면 employee.winBadge(...)로 수여하는 구조야.
하지만 세영은 이 흐름이 어색하다고 봤지.
→ 조건 체크 + 수여 행위를 하나의 트랜잭션이자 메시지로 묶어야 하고,
Rule이 도메인 객체라면, rule.attemptToGive(employee)처럼
내부에서 모든 책임을 지는 구조가 더 자연스럽다고 본 거야.
세영의 관찰:
- 단순히 CQS를 따른다고 해서 판단과 행위를 나누는 건 아니라고 생각함
- 나누더라도 명확한 명분(중복 체크, 다른 도메인과의 협력 등) 이 있어야 납득함
- 도메인 객체에 위임할 수 있다면 거기에 판단과 실행 모두를 맡기는 구조를 선호함
내가 궁금한 것:
- 세영은 “이건 하나의 메시지야”라고 판단하는 기준이 뭐야?
- 어떤 상황에서 판단과 실행을 굳이 나눠야 한다고 생각해?
- 혹시 CQS 원칙을 따르려다 되려 책임이 분산되고 코드가 흐릿해졌던 경험은 없었어?
- 그걸 어떤 식으로 되돌리거나 리팩터링했는지도 궁금해.
내 답변
일단 책의 예제에서 내가 코드만 보고 생각한건 다음과 같아.
1.Rule에 조건 검증과 배지부여 책임이 모두 있다.
2.어차피 facade에서 도메인 객체로 예상되는 employee를 이미 ‘조건 검증’시엔 주입하고 있다.
3.facade에서는 Rule로 조건검증 후, 바로 Rule에게 배지 부여를 시키고 있다.
여기서 문제라 생각한건 다음과 같아. facade에서 이미 도메인 객체를 Rule에 주입해주고 있는데, 굳이 facade에서 조건검증 결과를 받고 조건 검증이 통과하면 배지 부여를 facade에서 호출하고 있다는 점. 만약 facade가 도메인 객체를 넘기지 않고, 조건 검증 방식만 위임받아 facade에서 도메인 객체를 가지고 판단한 후 배지 수여를 위임한거라면 그건 괜찮을만해. 또는 facade에서 배지가 중복됬는지 여부를 확인하는 등 추가 로직이 들어갔다면 그것도 괜찮을만해.
근데 이미 모든 책임을 Rule로 넘긴 상태에서, 굳이 코드 제어 흐름만 facade에서 처리하고 있으니 어색했던거야. 어차피 도메인 객체를 Rule에 넘길꺼면, 그냥 Rule에게 ‘조건 맞으면 배지를 수여해라’ 라는 하나의 command로 가는게 단일 transaction이라 생각한거지. CQS는 command와 query를 분리하는걸 원칙으로 삼아 코드의 사이드 이펙트를 줄이려는거지. 뭐든 맹목적으로 따르면 오히려 복잡해진다고 생각해.
하나의 메시지라고 판단하는 기준은 결국, 요구사항이 변경되었을 때 코드 변경의 이유가 같은가에 달려있다고 봐. 하나의 원자적 단위여야 하는가라고도 말할 수 있을 것 같아. 판단과 실행을 나눠야 하는 경우는 반대로 서로 다른 변경의 이유가 있을때라고 생각해. 즉, 책임이 서로 다른거지. 예를들어 사용자 정보를 화면에 보여주려고 조회하려는데, 서비스가 알아서 아직 로그인 안된 경우 자동으로 로그인 후 사용자 정보를 가져오게 한다. 이건 이상하지.
CQS는 좋아하는 개념이긴하지만, 항상 따르는건 아니야. 어쨋든 난 CQS보다는 책임을 중심으로 설계하다보니 딱히 기억나는 CQS로 인해 책임이 분산된 경험은 없는 것 같아.
Q3. 잘못 디자인된 추상화로 인해 고통받은 경험이 있는가?
배경:
5.3장은 “단순한 추상화”를 강조하면서도,
- 너무 일찍 하는 일반화
- 모든 것을 다형성으로 풀려다 실패
- 데이터에 불필요한 동작을 몰아넣기
- 이런 실패 패턴도 함께 소개했지.
세영은 원래 단순성과 명확성을 추구하는 편이지만,
“미래 확장 가능성” 때문에 일부 구조를 복잡하게 만든 적도 있을 것 같아.
또, 다른 사람이 만든 잘못된 추상화 구조를 고치느라 고생한 경험도 있었을지 몰라.
질문:
- 너 스스로 만들었던 구조 중에 “이건 괜히 일반화했었다”는 경험 있어?
- 예: 나중에 보면 if문만 느는 추상화, 책임 경계 애매했던 파사드 객체 등
- 혹은 누가 만들어둔 추상화를 보고,
- “차라리 if로만 짰으면 더 나았을 것” 같았던 구조가 기억나는 거 있어?
- 그 경험을 통해 어떤 추상화 경계선을 학습했는지 말해줄 수 있을까?
내 답변
이건 우선 배경설명부터 해주는게 맞을 것 같아. 난 주로 플랫폼 서비스의 초기 개발을 맡고, 개발한 후 1~2군데 정도의 하위 이용기관이 도입할 때 까지만 운영해. 실제 운영으로 넘어갔을 때도 문제없이 동작하는지 확인하는 과정이지. 그 후 운영팀으로 서비스 운영이 넘어가서 더 많은 하위 이용기관이 붙게 돼. 해당 서비스가 큰 서비스라면 별도 운영팀이 생기기도 하고 이런 방식으로 일하고 있어.
그래서 내 경우, 운영상황을 보고 나중에 리팩터링하며 추상화 지점을 잡는건 어려운 상황이야. 이건 나도 아쉽긴 한데, 트레이드 오프기도 하지. 운영 상황에 따른 리팩터링 경험은 적어지는 반면, 초기 개발로 다른 요구사항을 가지고 개발하는 경험이 많아진거니까. 아무튼 그래서 요구사항을 보고 초기 개발 때 확장지점을 예상해서 미리 구조를 설계해두는 경우가 많아. 배경 설명은 이정도면 될 것 같아.
이전에 짰던 프로젝트 중 헥사고날 아키텍처 기반으로 짠 프로젝트가 있었어. 명분은 확실했다고 생각해. 필요한 서비스모듈마다 여러 API를 지원해줘야 했어. 예를들어 뭐 준비, 요청, 상태확인, 검증 이런 단계를 기본적으로 서비스모듈마다 지원해줘야 했어. 그리고 이 서비스모듈이 초기 요구사항상 몇개월 뒤에 기능이 추가될거라고 했단말야? 멀티모듈 프로젝트까진 아니었지만 아무튼 한 프로젝트 내에서 서비스 모듈 단위의 추상화가 필요했으니. 내 생각에 가장 적절한 아키텍처는 헥사고날 아키텍처였던거지.
근데, 이후 추가될거라 예상되었던 서비스 모듈이 성격이 약간 달라지면서 별도 프로젝트로 분리된거야. 즉, 난 이미 넘겼고, 운영팀 입장에선 이후 서비스 모듈이 추가되지도 않았으니깐, 이유없이 분석하기 어려운 헥사고날 형태의 구조만 남은거지 ㅋㅋ. 운영팀에서 그래서 구조 어렵다고 하고. 그래서 나도 구조 설명해준다고 자료 만들고 그랬어.
뭐 사실 요구사항상 명분은 괜찮았다고 보긴 해. 결과적으론 괜히 일반화 한 것도 맞지. 저 배경 설명으로 보아 알 수 있겠지만, 주로 내가 짠 코드에 대해 운영팀에 넘기기 전까지 우리쪽에서 운영을 하는 방식이야. 그래서 남의 코드로 운영해본 경험이 거의 없어. 그래서 ‘누가 만들어둔 추상화를 보고.. ‘부분은 답변하기 좀 어려워. 일반적으로 보는 남의 코드는 프레임워크나 라이브러리 혹은 강의의 추상화코드거든. 걔네들은 당연히 이미 어느정도 정제되어 있어서, 크게 이상하지도 않고.
학습한 추상화 경계선이라면 뭐.. 사실 잘 모르겠네. 항상 명분에 따라 하려곤 하는데, 위 내 경험과 같은 경우라면 사실 내 예측이 틀린 경우라. 반대로 내가 “이거 이후에 서비스 모듈 추가 안될꺼같은데?” 라고 생각해서 유연하지 않은 구조로 짰는데 실제론 추가 되었다면, 또 그걸로 운영팀에선 힘들었겠지. 뭐 항상 정답은 없고 트레이드오프 생각하고 확률 높은 쪽으로 잘 판단해봐야 할 것 같아. 물론 여기서 헥사고날 아키텍처까지 넣은게 맞았냐? 다른 방식으로도 얼마든지 추상화가 가능했지 않냐? 라고 한다면 또 할말이 없긴해. 사실 당시 클린 아키텍처쪽을 좋아했어서 좀 과하게 넣은 점도 인정해.
'Study > 객체지향 시스템 디자인 원칙' 카테고리의 다른 글
| [객체지향 시스템 디자인 원칙] 내 생각 Q&A - 7장. 모듈화 달성하기 (0) | 2025.07.16 |
|---|---|
| [객체지향 시스템 디자인 원칙] 내 생각 Q&A - 6장. 외부 의존성과 인프라 다루기 (1) | 2025.07.16 |
| [객체지향 시스템 디자인 원칙] 내 생각 Q&A - 4장. 의존성 관리하기 (1) | 2025.07.16 |
| [객체지향 시스템 디자인 원칙] 내 생각 Q&A - 3장. 객체의 일관성 유지하기 (1) | 2025.07.16 |
| [객체지향 시스템 디자인 원칙] 내 생각 Q&A - 2장. 코드를 작게 유지하기 (0) | 2025.07.16 |
댓글