목차
혹시 책에 대한 간단한 후기를 찾아 온 것이라면 '읽은 책 소감 - 객체지향 시스템 디자인 원칙 (마우리시오 아니체 저)' 이 글을 참고해주세요. 또한 이 글은 책 내용을 요약하거나 정리한 글이 아닙니다. 책을 읽고 난 뒤 제 생각을 중심으로 정리한 글 입니다.
서론
얼마 전 'LLM과 개발 책을 읽으며 든 내 생각 - 개발자식 하브루타 독서' 라는 글을 썼었다. 그 글에선 '육각형 개발자' 책을 읽으며
1.LLM에게 목차를 먼저 주고
2.목차만 가지고 질문을 생성하게 한 뒤
3.챕터 읽은 후 내 생각, 생성된 질문에 답변한걸 가지고 LLM 과 토론했다.

이번엔 '객체지향 시스템 디자인 원칙 (마우리시오 아니체 저)'을 읽으며 비슷하지만 약간 다른 방식으로 내 사고를 확장해봤다.
1.내가 먼저 챕터를 읽고 느낀점이나 좋았던 점을 LLM에게 알려줬다. 이 때 책에 제시된 연습문제(질문 형태)도 알려줬다.
2.내 느낀점과 좋았던 점, 책의 연습문제를 기반으로 LLM이 질문을 생성하도록 했다. 즉, 내 생각 중 LLM이 궁금해하던걸 물어보거나 연습문제를 기반으로 좀 더 심화된 질문을 해주도록 했다.
3.그리고 내가 그 질문에 답변하고 LLM과 토론하는 방식으로 진행했다.
이 글은 그 질문과 답변을 모아둔 글이다. 이전 글에서는 일부러 내 생각에 매몰되지 않은 상태로 LLM의 질문을 유도했다면, 이번엔 오히려 내 사고를 기반으로 LLM과의 대화를 설계한 셈이다.
사실 이미 LLM의 질문과 내 답변만해도 이미 5만자 수준이라 너무 길긴하다 (일반적으로 장문의 포스팅이 1만자 이내 정도). 그래서 책을 읽고 나서 느낀점이나 좋았던점 정리해둔 메모는 글 분량상 포함하지 않았다.
※ 연차가 그리 많지 않고, 배경지식도 계속 쌓여가는 중입니다. 현재 시점 기준으로 답변했다보니, 과거 저를 알고 계시던 분들은 "쟤가 저렇게 생각했다고? 아니었는데?" 하실 수 있습니다. 지금도 늅늅이지만, 더 늅늅이 시절이 있었으니까요. 또, 이후 제 생각이 바뀔 수도 있습니다.
※ Q1, Q2와 같은 질문 외의 모든 답변은 LLM을 쓰지 않고 제가 직접 답변한 내용입니다. 이후 LLM과 토론하며 생각이 조금씩 바뀐 부분들도 있으나, 최대한 초안 답변으로 올렸습니다.
※ 제 생각을 적은 것으로, 일반적인 개발자들의 생각이나 사실과 다소 다를 수 있습니다.
챕터별로 나누어 보기
이 글은 모든 질문과 답변이 합쳐진 통합본 입니다. 너무 길다고 느껴지거나, 책을 보며 챕터별로 확인하고 싶으신 경우 이하 챕터별로 나누어둔 글을 봐주시면 됩니다.
[객체지향 시스템 디자인 원칙] 내 생각 Q&A - 1장. 모든 게 복잡도 관리다
[객체지향 시스템 디자인 원칙] 내 생각 Q&A - 2장. 코드를 작게 유지하기
[객체지향 시스템 디자인 원칙] 내 생각 Q&A - 3장. 객체의 일관성 유지하기
[객체지향 시스템 디자인 원칙] 내 생각 Q&A - 4장. 의존성 관리하기
[객체지향 시스템 디자인 원칙] 내 생각 Q&A - 5장. 추상화 잘 디자인하기
[객체지향 시스템 디자인 원칙] 내 생각 Q&A - 6장. 외부 의존성과 인프라 다루기
[객체지향 시스템 디자인 원칙] 내 생각 Q&A - 7장. 모듈화 달성하기
[객체지향 시스템 디자인 원칙] 내 생각 Q&A - 8장. 실용적인 접근법

1장. 모든 게 복잡도 관리다
Q1. '복잡성'의 정체?
단순한 객체지향 디자인은 '복잡도 관리'라는 렌즈로 보면, 세영이 생각하는 ‘복잡성’의 정체는 뭐야?
- 시간에 따라 변화하는 요구사항
- 조직 구조에 따라 바뀌는 책임 분리
- 혹은 인프라, 프레임워크, 정책 등 외부 요소와의 연결
‘복잡성’이 실제 코드에 끼친 가장 강력한 흔적은 어떤 형태로 남았었는지 떠오르는 사례가 있다면?
내 답변
복잡성에는 내부 절차가 복잡하다, 초기에 너무 과도하게 설계해두었다 등 여러 외부 요인이 있을 수 있을거야. 어쨌든 그런 외부 요인은 빼고 현재 잘 동작하는 시스템을 기준으로, 복잡성은 요구사항의 변화에서 출발한다고 생각해. 요구사항은 외부로 보면 기능 추가에 대한 요청사항, 성능 개선에 대한 요청, 기능 수정, 타겟층 추가, UI 개선 같은게 있을 수 있지. 내부로 보면 프레임워크 버전업을 위한 마이그레이션, 이후 요구사항을 더 유연하게 적용하기 위한 구조 변경, 초기 사용량 예측 실패로 인해 최적화 예상 실패한 부분에 대한 최적화 진행, 보안을 위해 토큰 구조 변경, 테스트 커버리지 높히기, 외부 API가 버전 변경되면서 API 요청, 결과 파라미터가 바뀜 등이 있을 수 있겠지.
물론 조직 구조에 따라 역할과 책임이 변할 순 있겠지만, 근본적으로는 대외적인 요구사항 혹은 팀 내부에서 개선을 위한 요구사항 등 아무튼 요구사항이 발생할 때 복잡성이 발생할 수 있다고 봐. 그리고 이 요구사항에 얼마나 유연하게 대응할 수 있는지가 설계의 핵심이고.
복잡성이 코드에 끼친 사례를 생각해볼께. 나 개발자 극초반에 웹쪽을 처음 해보는 구간이 있었단말야. js, css, 스프링부트, html 전부 처음 해보는 구간이 당연히 있었을꺼잖아. 그 때, js에도 객체지향 설계 개념을 넣고 싶었어. 뭐 지금 생각해도 괜찮은 생각이긴 한데, 너무 과도하게 넣었어 ㅋㅋ. 현재 고객이 보고있는 지도 화면을 캡쳐한 후에 고객이 요청한 전송 방식(MMS, 메일, 팩스 등)으로 보내주는 거였거든. 해당 프로젝트의 js 코드 안에 class로 구성된게 하나도 없음에도 js에 class를 추가했어. 그리고 이후 유지보수는 생각 안하고, 해당 class를 깔끔하게 쓰면 된다는 생각으로 ‘전송 방식’ 부분을 비트마스킹으로 처리했어. 이게 자바이고, MSA 같은거였으면 사실 내부 구현을 캡슐화하는게 문제는 아니지. 근데 js이고, 이후에 전송 방식이 추가되면 유지보수 하는 사람이 수정해야 하는 부분이란 말야. 그 전송하는 부분을 하나의 ‘모듈’로 잡고 모듈의 사용성만 생각해서 비트마스킹으로 처리해버린거지. 애초에 코드의 일관성 없게 class를 쓴 것 자체도 문제였고 ㅋㅋ. 그거에 연결되어 백엔드에서 받는 부분도 굳이 스프링 환경임에도 POJO하게 막 Singletone으로 된 매니저 클래스로 전송 모듈 관리하고 그랬어. 당연히 실제로 이후 유지보수하는 곳에서는 힘들어했지.
사실상 사회생활 첫 프로젝트였고, 당시 파견 느낌의 회사라서 딱히 날 컨트롤해줄 사수도 없었다보니 막을 사람도 없던거지 ㅋㅋㅋ. 그래서 ‘왜 이렇게 코드를 짰을까? 제 코드의 선택과 이유들’ 이라는 세미나를 할 때, 현재와 다른 회사임에도 맨 처음 설명한 코드도 거기서 짠 코드의 레플리카였어. 자기반성용이지. 결국 유지보수를 생각하고 만든게 아니라, 내가 만든 구조의 품질 자체를 우선시한거였어.
Q2. ‘단순한 코드 / 좋은 추상화 / 좋은 모듈화’의 차이점?
여섯 가지 항목 중 ‘단순한 코드 / 좋은 추상화 / 좋은 모듈화’는 겉으로 비슷해 보여.
이 셋의 차이점을 세영의 언어로 다시 정의해보면 어떻게 구분돼?
(예: 추상화는 개념 정리, 모듈화는 책임 정리, 단순한 코드는 구현 정리…)
그 기준으로 봤을 때, 세영이 가장 약하다고 느끼는 부분은?
내 답변
단순한 코드는 ‘동일한 INPUT에 동일한 OUTPUT을 내면서도, 더 짧고 가독성 높은 코드’ 라고 생각해. 좋은 코드는 책처럼 술술 읽혀야 한다는 얘기가 있지. 그 외에 중복 제거 같은 부분도 필요하겠지만, 사실 if문 같은 분기가 없다면 보통의 코드가 몇백 줄 수준이 아니라면 읽기 어려운건 아니야. 그래서 난 단기 기억력의 부담을 최대한 줄여주는데 초점을 맞추는게 좋다고 봐. 또, 짧다고 무조건 좋은것도 아니겠지. if (a > b) return a; else return b; 이것보다 return a>b ? a:b; 가 짧긴한데 가독성이 꼭 좋다고 볼 순 없거든. 오히려 내가 선호하는건 if (a ≤ b) return b; return a; 이거긴 해. if 등 블록 depth가 늘어나는 부분에서 컨텍스트 스위칭 된 개발자의 단기 기억력을 최대한 빠르게 free 시켜주는게 중요하다고 보거든.
좋은 추상화는 단순히 abstract, interface 관점에서 뭐 SOLID 따져서 ISP를 위해 인터페이스를 만들고.. OCP를 지키기 위해 Template Method 패턴을 쓰려고 abstract 만들고 이런 관점에서도 볼 수 있겠지. 근데 내가 주로 쓰는 추상화라는 말은 어떠한 개념에 대한 추상화 수준 얘기야. 즉 복잡한 문제를 단순화해서 어느 수준에서 묶을꺼냐는 개념적인 얘기말야.
이것도 사람의 단기 기억력에 대한 얘기긴 한데, 이 책에서 얘기한 피플그로우 시스템을 예시로 생각해볼께. 높은 추상화 수준이라면 프론트엔드, 백엔드, 외부시스템 등등으로 나눌 수 있겠지. 낮은 추상화 수준이라면 좀 더 디테일하게 들어가서 뭐 강의 엔터티가 존재하고, 그 강의들을 묶은 커리큘럼이 존재하고.. 이런 수준에서의 생각이겠지. 더 깊게 들어가면 강의 엔터티 내에 어떤 속성이 필요하고.. 그런 수준일테고. 코드에서의 interface, abstract 같은 부분은 결국 이런 개념적 추상화의 구현체라고 생각해. 좋은 추상화를 위해선 개념적인 추상화 수준에서 생각해봐야해.
좋은 모듈화는 결국 결합도와 관련있다고 봐. 나도 항상 결합도를 떨어뜨리는 방향을 선호해. 역시 코드만의 얘기는 아니야. 처음 내가 파견업체로 개발자를 시작할 때, 업무 분담은 항상 수직적이었어. 무슨말이냐면, 어떠한 기능에 대해 A개발자는 1,2번 기능, B개발자는 3,4번 기능 이런식이지. 이 경우 해당 기능에 대해 프론트, 백엔드, DB 모두를 해당하는 사람이 짜야해.
현재는 수평적으로 분리해. 난 프로젝트를 시작할 때 백엔드와 프론트 사이의 인터페이스를 먼저 만들어. 서로 어떻게 구현하는진 오히려 몰랐으면 해. 서로 인터페이스를 기준으로 각자 최선의 구조로 코드를 짜는거지. 좋은 모듈화도 이런 관점이야. 물론 모듈을 어떤 추상화 수준에서 분리할지는 다양한 요소가 고려되어야 해. 뭐 최대 500명 쓰는 내부 관리 시스템을 만드는데, 관리자용 모듈, 대리용 모듈, 과장용 모듈, … 이런식으로 나누는건 오히려 복잡성만 증가시키는거겠지. 적절한 추상화 수준에서 서로의 결합도를 떨어뜨릴 수 있게 모듈화 하는게 좋은 모듈화라 생각해. 또, 해당 서비스가 발전함에 따라 이 모듈화 수준은 물론 달라질 수 있다고 보고.
내가 가장 약하다고 생각하는건 ‘단순한 코드’ 부분이야. 알고리즘 문제를 꽤 풀었다보니, 기본적으로 코드를 짤 때 해당 기능이 어느정도의 시간복잡도가 나올지 자동으로 계산하면서 짜게돼. 그렇다보니 복잡함 추가가 미미한 수준이라면, 소소한 최적화를 하는걸 좋아해. 예를들어 스트림 쓰면 코드가 단순해지는 부분이 있다고 해볼께. 근데 사실 자바 17 미만에서 스트림은 성능이 떨어지거든. 이 해당 부분의 최악 시간복잡도를 생각해보고, 스트림으로 할 시 기대 이하의 성능이 나올 것 같다면 스트림을 쓰지 않아. 어느정도 자제하긴 하는데, 아무튼 단순한 코드도 좋지만 기본적인 최적화도 아는데 안할순 없다보니 좀 약하다고 생각해.
Q3. 시스템이 성장해도 디자인을 단순하게 유지할 수 있다고 보는 편이야?
그걸 가능하게 하는 전제 조건이 있다면 어떤 게 있을까?
- 팀 문화?
- 코드리뷰 기준?
- 아키텍처 설계 철학?
그리고, 실제로 단순함을 유지하려다 실패한 경험이 있다면 공유해줘.
내 답변
운영 구조에 따라 좀 다를 것 같아. 사실 현재 내 구조라면 힘든편이야. 내 경우, 플랫폼 서비스를 만든 후에 하위 이용기관 한두군데 붙여서 실 사용에도 문제 없는지 확인해. 그 후 운영팀쪽으로 프로젝트를 넘기는 방식이야. 해당 프로젝트가 크다면 해당 프로젝트 전용 팀이 생기고, 좀 작다면 다른 운영팀 중 한곳으로 가고 그런 방식이야. 물론 커다란 기능 추가 건이 들어온다면 다시 우리팀으로 올 때도 있지만, 대부분의 경우 운영팀에서 처리하고 있어.
그러니 경험상 많은건 아니지만, 중간중간 하위 이용기관이 늦게 붙거나 어디로 이관할지 정해지지 않아 좀 오래 들고 있을때도 있어. 또, 내부에서만 쓰는 프로젝트 같은 경우에도 오래 들고 있는 경우가 있어. 그걸 기준으로 생각해볼께.
이 경우 역시 팀 문화가 가장 중요하다고 봐. 팀 분위기 자체가 다들 축 쳐져서 대충 일정이나 맞추자 분위기면 디자인을 단순하게 유지하려는 노력 자체가 이루어지기 힘들겠지. 반면에 못해도 팀의 20%정도만 이런 것에 관심이 있어도, 오히려 다수파쪽이 묻힌다고 생각해. 실제로도 팀원이 누구였냐에 따라서 분위기 차이가 많이 났던 것 같아. 즉, 단순하게 유지하고자 하는 사람이 공감대 형성할 수 있는 분위기여야해.
예를들어 타팀에 한 직원이 ‘속도가 느리다’ 라는 평가를 받고 있었어. 근데 내가 볼 땐, 리팩토링에 투자를 엄청 해서 그랬던거거든. 즉, 운영 요구사항을 바로 처리하는게 아니라 이후로 발생하지 않도록 만드는 직원이었어. 근데 팀에선 인정못받는 것 같아 우리팀으로 올 수 있도록 처리했어. 그리고 역시나 엄청난 고수였지. 이 케이스는 해당 팀의 팀원들의 의식 문제라기 보다는, 그 팀을 이끄는 사람의 무지에서 나온 나쁜 평가를 받은 평가긴한데, 어쨌든 팀 문화에 따라 고수가 안좋은 평가를 받을 수도 있다는 예시야.
단순함을 유지하려다 실패한 경험이라면, 역시 일정 때문인 경우가 많아. 당장 hotfix가 필요한데 '단순하게 유지'를 위해 시간을 많이 들여서 짜는건 오히려 안좋지. 물론 그 이후 TDD 책의 말 처럼 ‘죄악을 없애자’ 단계가 필요하긴 하겠지. 근데 어떨땐 너무 할게 많아서 도저히 시간을 못낼때도 있어. 그러다가 한두달 지나고, 운영팀에 넘어가고 그러다보면 사실 죄악을 없애지 못하는 경우가 생겨. TDD의 '죄악 없애기' 단계에 대한 예시는 'TDD, Mock, SOLID 얘기 - 도시 가스 요금 계산' 이 글에 약간 적어뒀어.
Q4. 책의 시스템 요구사항을 봤을 때, 세영이 가장 먼저 고려할 '복잡성의 중십축'은 뭐야?
피플그로우! 시스템 요구사항을 봤을 때, 이 시스템을 설계하면서 세영이 가장 먼저 고려할 “복잡성의 중심축”은 뭐야?
- 교육 일정 관리?
- 사용자 역할?
- 외부 시스템 연동 (위키, 캘린더, 채팅)?
- 관리자 UI?
그리고 그걸 “단순하게 설계하려면 무엇을 가장 먼저 잘라내야 할까?”
내 답변
내가 책에 나온 피플그로우 시스템의 요구사항을 받고, 그걸 설계하는 입장이라 생각하며 어떤 흐름으로 설계할지 생각해볼께. 우선 난 end point를 기준으로, 내가 생각하기 편한 추상화 수준으로 나누는걸 좋아해. 책에 나온 요구사항은 큰 시스템은 아니니 아래 정도로만 생각을 나누면 될 것 같아. 사실 추상화 수준이 동일하다고 느끼지 않을 수도 있어. 그냥 내가 생각하기 편한 그룹이라고 보면 될 것 같아. 애초에 어떤걸 기준으로 추상화하냐에 따라서도 또 얘기가 달라지는거라 정답은 없다고 봐.
비즈니스 로직, 피플그로우에서 시작해 접근하는 외부 시스템, 외부에서 시작해 피플그로우로 들어오는 외부 시스템, 관리자 UI, API 인터페이스, 클라우드 구성 등의 인프라.
추상화 수준을 나누고나서 보통 비즈니스 로직같은건 오히려 익숙한 부분이기도 하고 이미 요구사항이 잘 정의되어 있으므로 제쳐두고 외부 시스템 학습 테스트부터 할 것 같아. 기술 검증이라고 하지. 외부 시스템을 써보면서 어떤식으로 써야하는지, 필요한건 뭔지 파악하는거지. 단순히 외부 시스템의 가이드 문서만 보고 판단하기 힘든 부분이 많거든. 예를들어 결제 관련 외부 시스템의 경우, 전용선이 필요한 경우도 있어. 또 어떤 곳은 가이드만 보면 문제 없었는데, 실제로 확인해보니 지원하지 않는 기능일수도 있어. 이런 부분은 내가 잘 알지 못하는 블랙박스에 가까워서, 우선 블랙박스부터 열어서 확인해보는거야.
그 후에 비즈니스 로직을 설계해. 여기에서 사용자 역할, 강의, 커리큘럼(강의의 집합), 사용자, 관리자 등에 대한 전반적인 도메인 로직을 설계하겠지. 이 때는 DDD 설계 개념으로 접근하는걸 선호해. 현재 내가 가장 편하게 생각할 수 있는 방식이거든.
그리고 비즈니스 로직과 기술검증 해본걸 기반으로 러프한 API를 정의해. 범위는 상황에 따라 다른데, 일반적으로 외부에서 시작해 피플그로우로 들어오는 경우라면 내가 API를 정의하고, 그걸 외부 시스템 개발자가 사용하는 방식이니 그 부분도 필요해. 보통은 백엔드 - 프론트엔드 사이의 API 인터페이스를 정의하지. 여기엔 HTTP STATUS에 따른 동작 방식, 프론트의 동작 방식도 어느정도 정의해두는 편이야. 예를들어 UI에 띄울 모든 에러메시지를 백엔드에서 정의하도록 하는 경우도 있어. 심지어 공통 에러페이지를 띄울지, 팝업으로 띄울지, 그 메시지는 뭐일지까지 정해주는 경우도 있어. 그 외에 인프라도 내게 역할이 있다면 생각해보겠지. 뭐 사용량이 높을 것 같다면 DB write, read 연산을 나눠볼수도 있겠고, 로드밸런서나 서버의 갯수도 생각해봐야겠지. 전체적인 인프라 아키텍처도 크기가 크다면 생각해봐야겠고.
음 좀 질문에 대해 답변이 산으로 가긴 했는데, 결국 ‘복잡성의 중심축’은 내게 있어서 어떤 단위로 프로젝트의 요구사항을 나눠서 생각할지에 대한 부분이야. 위는 작은 시스템이라 그리 어렵지 않게 처음부터 나눠서 개별적으로 생각했지. 근데 통합 서비스로 막 여러 시스템이 붙은 하나의 큰 프로젝트라면 또 얘기가 달라져. ‘비즈니스 로직’ 이렇게 하나로 나눌수가 없지. A서비스 비즈니스 로직, B서비스 비즈니스 로직, … 이런식으로 될테니까. 결국, ‘단순하게 설계하려면’ 이 단위를 잘 정해야 한다고 생각해.

2장. 코드를 작게 유지하기
Q1. “작게 쪼개기”의 본질은 ‘책임 분리’인가, ‘가독성 확보’인가?
- 세영은 “작게 나누는 건 책임을 나누는 행위”라고 봤어.
- 그렇다면 함수/클래스를 나눌 때 1차 판단 기준은 '역할'이어야 할까, '코드 길이'일까?
실무에서 ‘길긴 한데 잘 읽히는 함수’ vs ‘짧지만 의도가 중첩된 함수’가 있을 때, 어떤 기준으로 판단했어?
위 LLM의 질문에서 '세영은 "작게 나누는 건 책임을 나누는 행위"라고 봤다고 한 이유
책을 읽으며 중간에 느낀점이나 좋았던 점을 노션에 적어두고 이후 LLM에게 그대로 줬습니다. 이 글은 질문, 답변만 들어가도 너무 양이 많아져서 해당 내용은 제외했습니다. 그것도 있고, 질문 답변은 애초에 블로그에 포스팅할껄 상정하고 적은거라 좀 부드럽게 적었는데, 느낀점은 좀 쌔거나 문맥 무지성으로 적은 경우가 많아서 그런거 걸러내기도 귀찮아서요 ㅋㅋ 아무튼 아래의 내용때문에 LLM이 저렇게 질문한겁니다.
- 저자는 작은 단위는 항상 큰 단위보다 낫다고 거의 확정적으로 말하고 있어. 물론 맞는 말이야. 근데 개인적으론 이런 생각도 해. 결국 함수나 클래스로 나뉜 부분은 각각의 책임을 나눠가졌다고 볼 수 있어. 함수가 isPrimeNumber(int num) 이렇게 생겼다고 해보자. 코드 중간에 이게 있다면, 당연히 소수 판별하는 함수겠거니 하고 넘어가게 돼. 근데 알고보니 개발자가 실수로 ‘1’도 true를 리턴하게 만들어둔거면, 버그가 날 수 있겠지. 즉, 저자의 말은 맞긴한데, 팀원들의 상향 평준화가 없다면 오히려 작은 단위가 더 버그를 만들수도 있다고 봐. 근데 이건 너무 억까 케이스인 것 같긴 하네 ㅋㅋ 어쨌든 내가 말하고 싶은건, 너무 단순하게 ‘작은 단위’가 낫다고만 말하는건 아쉬워. 추상화 수준이 서로 너무 다른 함수들이 모여있거나, 함수나 클래스로 빼는것도 일종의 책임의 분리라는걸 이해하지 못하고 단순히 ‘너무 기니까 분리한다’ 이렇게 생각할 수도 있다고 봐.
- 뒤로 가니 비슷한 얘기가 나오긴 하네!
내 답변
1차 판단 기준은 역할이 맞아. 넓게 볼 때 ‘역할’은 결국 interface와 같은 추상화이기도 해. 캡슐화의 관점에서 볼 때도 역할 먼저 따지는게 좋지. ‘코드 길이’는 로직이 만들어지고 난 후의 리팩터링 대상이지 최초 설계의 대상은 아니라고 생각해. 개인적으로 코드 길이가 적당히 긴 것 까지는 크게 문제있다고 보지 않아서 더 그런 것 같네.
알고리즘 짤 때는 의도적으로라도 ‘짧지만 의도가 중첩된 함수’ 형태로 많이 짜. 물론 알고리즘 문제도 객체지향 설계로 짜야 유연하게 짤 수 있는 문제들도 있어(예 : 백준 17081 - RPG Extreme). 근데 보통의 알고리즘 문제들은 절차지향적으로 짜도 문제 없고, 오히려 간편할 때도 많아. 즉, 객체들의 협력보다는 단순한 단위 기능에 대한 문제가 많아. 이 경우라면 사실 함수를 나누던 안나누던 한 모니터에 최대한 들어와야 내가 디버깅하기 좋긴 하더라고.
반면에 실무 코드 짤 때는 길더라도 잘 읽히는 함수를 선호해. 함수나 변수명을 일종의 주석으로 생각하고, 심지어 이렇게 짤 때도 있어. if (num > limit) 대신, if (isNumOverLimit(num)) 이런식으로. 물론 이건 주로 조건문이 복잡할 때 저렇게 하긴 하지.
‘어떤 기준’에서 판단하냐고 한다면, 그냥 이후 유지보수 하는 사람이 나한테 최대한 안물어볼 수 있게 하는걸 기준으로 해. 결국 가독성이나 로직의 단순함이 더 중요하단거지.
Q2. 좋은 주석의 조건?
주석에 대한 견해가 계속 바뀌어왔는데, 그럼 지금의 세영은 “좋은 주석”이란 어떤 조건을 갖춰야 한다고 생각해?
- 과거: Javadoc → 클린코드 영향 → 현재는 “그때그때 다르다”
- 지금까지의 경험을 돌아볼 때,
- 남이 쓴 주석 중 최고였던 것은 뭐였어?
- 반대로 아예 주석이 없거나 거짓 주석 때문에 생긴 사고가 있었다면?
내 답변
설계에 정답이 없듯, 좋은 주석에도 정답은 없다고 생각해. 예를들어 일반적으로 좋은 테스트 코드는 아니지만, 특정 경우에만 동작하게 매크로 같이 통합 테스트를 해주는 테스트 코드를 최근에 짠 적이 있어. 하위 이용기관이 붙을 때 마다 그 테스트 코드에 외부 시스템 주소 바꾸고, 컨디션 풀어서 돌리면 모든 외부 API에 대해 하위 이용기관이 제대로 만들었는지 테스트해주는 구조야. 테스트 코드 관점에선 좋은게 아니지만, 실제 제작과정 및 이후 유지보수가 엄청 편해지는거지. 물론 외부로 별도 시스템으로 만들어도 되지만, 추가 관리 포인트를 만들고 싶지 않았어. 이 테스트 코드엔 내가 주석을 위 내용으로 장황하게 달아놨어. 즉, 이런 테스트코드를 짜게 된 명분에 대해 설명해두었어.
주석도 마찬가지라고 봐. 사실 책마다 하는 얘기도 다르잖아? 물론 다들 예외케이스는 의례 붙이곤하지만, 저자들의 말투만 봐도 기본적으로 부정적인지 긍정적인지 정도는 판단 가능하지. 난 대학생때 연구 과제에서 프레임워크를 짤 때 해당 프레임워크를 사용하는 다른 학교나 업체분들이 IDE에 각 함수나 클래스 설명이 떠야하니 주석을 많이 썼어. 사회에서 개발자 시작하고 나선 클린코드 책 및 내 취향 변화로 주석을 잘 안썼어. 지금은 사실 써야한다, 말아야한다 이런 내 주관 자체가 없어. 왜냐면 그냥 써서 이득이 클 것 같으면 쓰고, 아닌 것 같으면 안쓰면 된다고 보거든. 그래서 ‘좋은 주석’의 조건은 현재 상황에 어울리는 주석인가? 라고 생각해.
만약 라이브러리를 만든다고 해볼께. 이 땐 모든 클래스 및 함수마다 주석을 상세히 다는게 맞아. 심지어 클린코드 등의 책에 있는, 주석에 html 태그같은거 쓰지 말라는 것도 무시해도 된다고 봐. 솔직히 태그까지 달아서 써야 라이브러리 사용하는 측에서 IDE에서 해당 함수 툴팁볼 때 훨씬 가독성 좋게 보이거든.
반면에 뭐 세그먼트 트리 자료구조를 프로젝트에 넣어야된다고 해볼께. 물론 세그먼트 트리는 복잡하고, 로직적으로 설명해줘야 할게 많아. 근데 클래스명이 SegmentTree 라면, 굳이 상세하게 주석을 달아야 할까? 달더라도 ‘세그먼트 트리 자료구조 이해 필요’ 정도만 달아도 되겠지.
남이 쓴 주석 중 최고였던 건 약간 밈 짤로 본건데, 예전 게임 개발자가 작성해다는 다음 주석이야. “Somehow things have been miraculously working, I’m not shure why”, “This is horrible. I’m not allowed to change the save structure, because we’re too close to final.” 정말 신뢰성 넘치고 진심이 담긴 좋은 주석이라 생각해 ㅋㅋ
장난이고, 애초에 내가 유지보수 경험이 거의 없고 대부분 내가 만든걸 내가 유지보수한 경우가 많아. 그래서 남이 쓴 주석 중 최고였던 것이라고 해봐야 일하면서 다른 직원이 쓴 것 보다는, 라이브러리의 설명쪽이 많았다고 봐. 그 중에 이게 아닐까 싶네. 자바 ArrayDeque 주석에 써 있는 “This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.” 이거. 이거보고 알고리즘 짤 때 ArrayDeque를 많이 썼었는데, 실제로도 더 빨랐어.
아예 주석이 없거나 거짓 주석때문에 생긴 사고는 사실 없어. 직전에도 말했듯 유지보수 경험이 많지 않기도 하고, 애초에 코드도 잘 안믿는데 주석은 더 안믿긴해. 물론 라이브러리는 기본적으로 믿고 보긴한데, 실무 코드는 기본적으로 안믿는 스탠스로 가거든.
Q3. 세영은 리팩터링 전에 “전체를 한눈에 보기” 위해 어떤 방법을 사용해?
- 단순히 호출 순서만 따라가?
- 아니면 흐름도 / 시나리오 / 역할 분리 등을 따로 그리거나 정리해?
- “지금 이게 왜 복잡한 거지?”를 판단할 때 참고하는 세영만의 단서 리스트가 있어?
예전 실무에서 복잡한 코드를 리팩터링한 경험 중, 이해나 설계를 돕기 위해 만든 너만의 기준이나 방식이 있다면 알려줘.
내 답변
리팩터링 전에 전체를 한눈에 보기에 대해 대답해보자면, 반대로 리팩터링을 어느 수준에서 할거냐에 따라 전체를 한눈에 보려고 하지 않는 경우가 많아. 난 코드 뿐 아니라 업무적으로 결합도를 줄이는 방향을 좋아해. 그때그때 내가 리팩터링 하려는 추상화 수준에 따라 어느 수준부터 볼지도 정해져. 프로젝트 자체의 아키텍처를 리팩터링 하려고 한다면, 프론트엔드, 백엔드, 인프라, 외부시스템, … 이런 큰 단위로 봐야겠지. 백엔드 패키지 구조를 리팩터링 하려고 한다면 백엔드의 각 모듈들을 보게 될거야. 뭐 기능이 추가될 부분에 대해 디자인 패턴을 넣어 OCP를 만족하게 해주도록 리팩터링 할 수도 있겠지. 아주 작게는 클래스명이나 함수명을 리팩터링 하려 할 수도 있겠지. 에를들어 난 AbstractChargeService, ChargeService, … 이런 식으로 이름에 기술적인 부분이 들어가는걸 별로 안좋아해. 그러니 ChargeService, RegularChargeService 이런식으로 바꾸겠지.
그래서 단순히 호출 순서만 따라가? → 호출 순서 보다는, 서로 다른 역할이나 책임을 가진 단위로 봤을 때 그 책임을 잘 수행하는지 위주로 봐. 즉, 책임과 경계 위주로 봐.
흐름도 / 시나리오 / 역할 분리 등을 따로 그리거나 정리해? → 응. 실제로도 난 코드 짜는 시간은 전체 개발 시간의 10%도 안되는 것 같아. 전체적인 구조 먼저 머리속이나 문서로 정리하고, 코드는 맨 마지막에 작성해. 클래스 다이어그램, 시퀀스 다이어그램을 특히 자주 그리는 편이야.
‘지금 이게 왜 복잡한 거지?’에 대한 단서 리스트 → 이 부분은 좀 직관에 의존하는 것 같아. 그냥 ‘이후 나한테 최대한 연락이 안오도록’을 생각하며 짜는 것 같아. 또, 당시 내 지식 수준에 따라서도 매번 달라지는거라 더더욱 그런 것 같네. 어쨌든 기본적으로 SOLID 중 OCP를 가장 중요하게 생각해. 즉, 이후 요구사항이 들어왔을 때 기존 코드를 얼마나 안건드리고 기능을 추가할 수 있냐이지. 그렇다고 너무 오버 엔지니어링이 되면 안되니까, 그런 요구사항이 나올만한 부분 위주로 처리해둬. 그러니 내 생각에 여기에 기능이 추가된다고 했을 때 유연하게 유지보수가 가능하냐를 직관적으로 판단한다가 내 기준이겠네.
실무에서 경험한 복잡한 코드 리팩터링에 대해 이해나 설계를 돕기 위해 만든 너만의 기준이나 방식 → 사실 ‘복잡한 코드 리팩터링’ 이라고 한다면, 전체 구조에 대한 변경이라 생각해. 예를들어 기존 방식을 이벤트 메시지 기반 방식으로 바꾼다고 해볼께. 이럴 때 난 논리적으로 문제 없을 때 까지 코드보다는 내 상상 혹은 다이어그램을 기준으로 먼저 생각해. 문제가 없다고 판단되는 방법을 찾아야 코드 수정으로 들어가.
Q4. 만약 세영이 ‘2장을 모르는 주니어’에게 설득해야 한다면…
"왜 코드를 작게 유지해야 하는지"를 단순한 은유나 비유로 설명해줘.
- 함수 = 마법 주문? / 클래스 = 부대 단위? / 코드 = 레시피?
- 또는 본인이 겪었던 “와 이건 너무 길어서 못보겠다…” 했던 코드 사례를 차용해도 좋아.
주니어가 "길긴 하지만 그냥 다 한눈에 보면 되는 거 아니에요?"라고 묻는다면 뭐라고 대답할래?
내 답변
나 알고리즘 짤때 절차지향적으로 짜둔 코드가 많아서 ㅋㅋㅋ 그거 보여주면서 어떤 코드가 더 좋아보이냐고 물어보는 식으로 할듯.
private void solution() throws Exception {
n = Integer.parseInt(br.readLine());
dp = new int[n+1][3];
v = new boolean[n+1];
edges = new List[n+1];
for (int i = 0; i <= n; i++) edges[i] = new ArrayList<>();
for (int i = 1; i < n; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
edges[a].add(b);
edges[b].add(a);
}
arr = new int[n+1][3];
for (int i = 1; i <= n; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
arr[i][0] = Integer.parseInt(st.nextToken());
arr[i][1] = Integer.parseInt(st.nextToken());
arr[i][2] = Integer.parseInt(st.nextToken());
}
int ans = 0;
edges[0].add(1);
for (int i = 0; i < 3; i++) ans = max(ans, find(0, i));
char[] selected = new char[n+1];
Stack<int[]> stk = new Stack<>();
for (int i = 0; i < 3; i++) {
if (dp[0][i] == ans) {
stk.push(new int[]{0, i});
break;
}
}
while (!stk.isEmpty()) {
int[] cur = stk.pop();
int idx = cur[0];
int color = cur[1];
selected[idx] = COLOR[color];
for (int next : edges[idx]) {
if (v[next]) continue;
v[next] = true;
int nextColor = -1;
int nextMax = 0;
for (int i = 0; i < 3; i++) {
if (color == i) continue;
if (nextMax < dp[next][i]) {
nextMax = dp[next][i];
nextColor = i;
}
}
stk.push(new int[]{next, nextColor});
}
}
StringBuilder sb = new StringBuilder();
sb.append(ans).append('\\n');
for (int i = 1; i <= n; i++) sb.append(selected[i]);
System.out.println(sb);
}
“위 코드로 보는게 좋아? 아니면 아래처럼 보는게 좋아?”
private void solution() throws Exception {
parseInput();
init();
findMaximumAnswer();
trackingColor();
printAnswer();
}
물론 저것도 절차지향적이긴하지만, 그렇다고 해도 코드 이해 측면에서 차이가 확 날듯.
길긴 하지만 그냥 다 한눈에 보면 되는 거 아니에요? → 솔직히 말해서 나도 그정도면 괜찮다고 보긴 해서 굳이 설득해야되나 싶네. 코드 길이가 중요한게 아니라, 클래스나 함수의 책임과 역할을 명확히 하는게 중요한거라고 봐. 물론 책임이 응집되어 있더라도 너무 길다면 그땐 기능단위로도 좀 빼거나 하겠지. 어쨌든 ‘이정도면 괜찮다’ 정도면 된다고 봐. 한 눈에 다 보여도 무슨 역할인지 모르겠다면 잘못짠거고, 한 눈에 다 안보일만큼 꽤 길어도 무슨 역할인지 명확하게 파악될 정도로 잘 짰다면 난 그걸 더 높게 평가해.

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 형태로 자료를 리턴해줘. 그래서 애초에 내부구조를 드러내고 수정될 수 있다는 생각을 안한거였어. 관점의 차이였고, 그걸 알게된 지금도 책의 내용을 동의하진 않지만, 실무적으로 쓸 수 있는 하나의 스킬 정도론 생각하고 있어야겠어.

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를 그렇게 좋아하진 않아. 물론 참 좋은 개념이고 좋은 기능이지. 근데 개인적으로는 응집도가 떨어지는 느낌이 있어. 또, 숨겨진 의존성 느낌도 들어. 그래서 꼭 필요하거나 효율적인 곳 아니면 잘 안쓰는 편이야.

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를 지원해줘야 했어. 예를들어 뭐 준비, 요청, 상태확인, 검증 이런 단계를 기본적으로 서비스모듈마다 지원해줘야 했어. 그리고 이 서비스모듈이 초기 요구사항상 몇개월 뒤에 기능이 추가될거라고 했단말야? 멀티모듈 프로젝트까진 아니었지만 아무튼 한 프로젝트 내에서 서비스 모듈 단위의 추상화가 필요했으니. 내 생각에 가장 적절한 아키텍처는 헥사고날 아키텍처였던거지.
근데, 이후 추가될거라 예상되었던 서비스 모듈이 성격이 약간 달라지면서 별도 프로젝트로 분리된거야. 즉, 난 이미 넘겼고, 운영팀 입장에선 이후 서비스 모듈이 추가되지도 않았으니깐, 이유없이 분석하기 어려운 헥사고날 형태의 구조만 남은거지 ㅋㅋ. 운영팀에서 그래서 구조 어렵다고 하고. 그래서 나도 구조 설명해준다고 자료 만들고 그랬어.
뭐 사실 요구사항상 명분은 괜찮았다고 보긴 해. 결과적으론 괜히 일반화 한 것도 맞지. 저 배경 설명으로 보아 알 수 있겠지만, 주로 내가 짠 코드에 대해 운영팀에 넘기기 전까지 우리쪽에서 운영을 하는 방식이야. 그래서 남의 코드로 운영해본 경험이 거의 없어. 그래서 ‘누가 만들어둔 추상화를 보고.. ‘부분은 답변하기 좀 어려워. 일반적으로 보는 남의 코드는 프레임워크나 라이브러리 혹은 강의의 추상화코드거든. 걔네들은 당연히 이미 어느정도 정제되어 있어서, 크게 이상하지도 않고.
학습한 추상화 경계선이라면 뭐.. 사실 잘 모르겠네. 항상 명분에 따라 하려곤 하는데, 위 내 경험과 같은 경우라면 사실 내 예측이 틀린 경우라. 반대로 내가 “이거 이후에 서비스 모듈 추가 안될꺼같은데?” 라고 생각해서 유연하지 않은 구조로 짰는데 실제론 추가 되었다면, 또 그걸로 운영팀에선 힘들었겠지. 뭐 항상 정답은 없고 트레이드오프 생각하고 확률 높은 쪽으로 잘 판단해봐야 할 것 같아. 물론 여기서 헥사고날 아키텍처까지 넣은게 맞았냐? 다른 방식으로도 얼마든지 추상화가 가능했지 않냐? 라고 한다면 또 할말이 없긴해. 사실 당시 클린 아키텍처쪽을 좋아했어서 좀 과하게 넣은 점도 인정해.

6장. 외부 의존성과 인프라 다루기
Q1. 도메인을 우회하고 DB 직접 접근해도 될까?
배경:
책에서는 “애그리게이트 루트를 통해 처리하는 것보다 DB에서 직접 처리하는 게 효율적으로 보인다면,
그 전에 도메인 설계가 잘못된 건 아닌지 먼저 검토하라”고 말했지.
세영도 이 주장에는 논리적으로 동의하지만,
실제로는 “도메인 로직을 DB에서 처리하는 것” 자체에 거부감을 가지고 있어.
DB 접근을 완전히 금기시하는 건 아니지만,
검증, 상태 판별, 일련의 비즈니스 로직을 쿼리나 DB 트리거 등으로 넘기는 건 매우 꺼리는 편이야.
질문:
- 만약 팀원이 “이건 굳이 애그리게이트까지 안 가도 되니까 DB에서 직접 처리하자”고 제안한다면,
- “그게 도메인 로직인지 아닌지”는 어떤 기준으로 판단해?
- 예를 들어 단순한 상태 변경 쿼리나 batch update도그런 판단 기준이 명확히 있는지 궁금해.
- 도메인 로직처럼 느껴질 수 있는데,
- 또, 효율과 설계의 균형에서 어디까지는 타협 가능하고, 어디부터는 안 된다고 생각해?
내 답변
설계에 정답은 없어. 팀원의 명분이 논리적이라면 팀의 합의로 일반적으로 안좋다고 보여지는 것이더라도 협의 결과에 따라 수용해도 괜찮다고 생각해. 어쨌든 그렇더라도, 도메인 로직이라면 DB에서 처리하는건 옳지 않다고 보긴 해. 즉, 직전에 괜찮다고 생각한건 구조를 바꿔서 해결해볼 수 있다이지, 도메인 로직이 확실히 맞다면 이건 생각을 바꾸긴 쉽지 않아. 이게 DB에서 뭔가를 처리하는게 나쁘다는 얘긴 아니고, 서비스의 핵심 로직이 DB에서 처리되는걸 싫어하는거야. 이후 서비스 확장될 때 스케일아웃하기도 힘들고, 일단 테스트나 디버깅도 어려워지지. DB에서 쿼리로 작업 시 매직넘버가 들어갈 확률도 너무 높아져. 이후 유지보수자도 DB에 있는 로직을 도메인 로직이라고 생각하기 쉽지 않아서, 맘편히 바꿔버릴수도 있어.
참고로 이론적으로만 DB에 로직이 있는게 싫다고 생각한건 아니고, 개발자 처음할 때 파견업체에서 시작했거든. 그 때 직접 해봤어. 심지어 당시 PL급이 "비즈니스 로직은 DB 프로시져에 전부 있어야한다"라고 한게 기억나. 이미 팀의 합의가 저렇게 되어있으니 어쩔수없이 따르긴 했고, 나도 지식이 그리 많지 않았을 때지만 당시에도 뭔가 위화감은 계속 들었었지. 지금은 뭐 명확히 왜 싫어하는지 이유를 댈 수 있게 된거고.
위에서 말한 ‘구조를 바꿔서 해결해볼 수 있다’에 대해서는 예를들어 뭐 애그리거트 루트에 있던 로직인데 너무 효율성이 안나오는거야. 애그리거트 루트에서 애그리거트 100만개 가지고 처리하는 로직이 있다고 쳐봐. 메모리나 시간복잡도 전부 저 멀리 가겠지. DB에서 처리하거나 배치처리하는게 맞을꺼야. 근데 그럼 이게 도메인 로직이 맞냐부터 생각해보는거지. 사실 설계에 정답은 없어서, 일반적으로 생각을 약간만 틀면 ‘응 이런식으로 생각하면 DB에서 처리해도 전체 시스템 정합성에 문제없게 가능하겠다’ 식으로 뺄 수 있어.
그럼 도메인 로직인지 아닌지 어떤 기준으로 판단하는지도 생각해볼께. 사실 아직 내가 코드를 설계하며 진행한 경험이 많은건 아니니까, 아직은 좀 직관적으로 판단하는 것 같아. 대충 다이어그램 그려봤을 때 ‘얘가 제대로 안되면 언젠가 서버나 나 둘 중 하나는 터지겠는데?’ 싶은걸 도메인 로직으로 보는 것 같네 ㅋㅋ 또는 뭐 ‘이 부분은 테스트 코드 안짜면 잠 못자겠는데?’ 싶은거. 뭐 글로야 여러가지 적을 수 있겠지. 서비스의 핵심 가치를 반영하고 있다거나, 서비스에서 메인 규칙을 반영한다거나, 비즈니스 로직의 상태 변경이나 제약 조건과 관련되었다거나, 그게 망가지면 서비스의 신뢰도 자체에 문제가 생긴다거나. 근데 아직 내 수준에선 직관적으론 알고 있지만, 누군가 이건 도메인 로직 아니죠. 이럴 때 명확하게 그게 맞다고 술술 말을 하진 못할 것 같아.
그러니 어디까지 타협 가능하고 어디부터는 안된다고 생각하는지도 좀 두루뭉술해. 내가 처음에 도메인 로직으로 생각했더라도, 이후 얘는 그냥 밖으로 빼도 되겠는데? 싶은것도 있고. 밖에 있더라도 나중에 어.. 얘는 안쪽으로 들여와서 보호하는게 맞겠다 싶기도 해. 결국 서로의 명분을 가지고 토론해서 결정하는게 맞겠지.
확실히 이 질문에 답변하다보니, 내가 이 기준에 대해서는 주관이 뚜렷하지 않다는게 나도 느껴지네. 이 부분은 좀 더 공부하고 생각해봐야겠어.
Q2. “자신이 소유한 것만 의존하라”의 경계
질문:
- 현재 프로젝트에서 ‘남의 것’을 의존하고 있는 코드 중 **“굳이 감쌀 필요 없다고 생각하는 것”**이 있다면 예시를 들어볼 수 있어?
- 반대로, “이건 꼭 감쌌어야 했는데, 그냥 쓴 걸 나중에 후회했던 경험”이 있다면?
- 그 두 가지를 나누는 기준은 뭐야? 클래스의 복잡성, 유지보수 가능성, 아니면 도메인 오염 여부?
내 답변
자바의 장점이자 단점이라 생각하는게 하위호환성을 왠만하면 지켜준다는 점이야. 그래서 자바 라이브러리 부분은 굳이 감쌀 필요 없다고 생각해. 변경 가능성이 매우 적거든. 그리고 자바와 달리 버전업 시 자주 deprecated 되거나 삭제되는 편이긴 하지만, 그래도 스프링 프레임워크의 라이브러리도 괜찮다고 봐. 일반적으로 스프링부트나 스프링 버전업은 별도 업무로 일정잡고 작업하는만큼 갑자기 바뀔 일이 거의 없다고 봐. 프레임워크나 메인 언어의 라이브러리를 ‘남의 것’으로 판단하고 래핑하기엔 피곤함이 더 크다고 생각해.
반면 이 글을 읽으면서도 느낀건데, 사실 json 관련 라이브러리들이 상당히 많거든. 그리고 막 JSONObject, JsonObject 등등 비슷한데 약간씩 다른 이름의 클래스들이 존재하지. 내 경우 지금 생각해보니 왜인지 모르겠지만 감쌀 생각을 안했었어. 아무래도 자주 쓰는 외부 라이브러리라 기준이 무뎌졌던 것 같아. 외부 라이브러리에 의존성 생기는 것 같고, 코드에 대한 개발자의 책임이 적어지는 느낌이라 lombok도 잘 안쓰는 내가 json 같은건 그냥 막 썼던 셈이지. 이건 다음부터 별도로 처리하는게 좋아보여. 사실 개발자 초기엔 막 json 파싱같은 재밌는걸 라이브러리한테 맡긴다고? 이 재밌는걸 안만드는건 손해다. 내가 직접 짠다! 이런적도 있는걸보면 나도 많이 발전한듯 ㅋㅋ
나누는 기준은 아무래도 '이후 유지보수 시 변경 가능성'이 가장 큰 것 같아. 뭐 도메인 오염이나 클래스 복잡성도 전부 유지보수 가능성과 관련있으니 하나로 퉁치면 될 것 같아. 예를들어 도메인이 외부 라이브러리에 오염됬을 때 문제는 외부 라이브러리가 변경되었을 때, 도메인까지 수정해야 된다는 점이지. 자바나 프레임워크의 라이브러리는 이후 변경 가능성이 적어. 혹시 변경될 경우, 보통 단순 유지보수가 아니라 업무로 잡고 일정잡고 진행되는 꽤 큰 작업이야. 그러니 괜찮다고 봐. 물론 LTS 버전 쓸 때 얘기겠지. 반면 서드파티 라이브러리 같은 경우 심심치 않게 변경될 수 있어. 비슷한 기능을 가진 라이브러리들이 많고, 기능 지원여부나 더 나은 기능등을 이유로 유지보수 시 변경될 수 있어.
Q3. 인프라 코드와 도메인 코드의 분리 실전 감각
배경:
책에서 “도메인과 인프라를 분리해야 한다”는 이야기를 중심으로,
너는 이미 DI나 포트-어댑터 구조를 적극 활용하고 있었고,
프레임워크나 라이브러리의 예외가 도메인 로직에 노출되지 않도록 처리하는 것도 신경 쓰고 있다고 했어.
질문:
- 도메인 코드에 스며든 인프라 코드(혹은 예외, 클래스)를 나중에 ‘지우기 힘들어졌던’ 경험이 있을까?
- 만약 지금 신입에게 “도메인과 인프라 코드를 분리해야 한다”고 설명해줘야 한다면,
- 어떤 기준이나 예시를 들어서 설명할래?
내 답변
나중에 지우기 힘들었다기보단, 이걸 지우긴 해야되는데 어떻게 지울지 생각이 안난 경우가 있었어. 도메인 내에 암호화된 데이터가 필요한 경우가 있었어. 뭐 물론 지금 생각해보니 애초에 도메인 객체에 암호화된 데이터가 필요했던거 자체가 문제긴 한데, 아무튼 당시엔 그랬으니 이건 넘어갈께. 이 때, 암만 생각해봐도 ‘암호화가 되어있어야 한다’ 자체가 도메인 객체의 책임으로 느껴지는거야. 심지어 어떤 암호화 알고리즘을 쓰느냐도 도메인 로직이라는 착각도 있었지.
즉, 외부에서 처리할게 아니란 생각이었지. 근데 그렇다고 암호화 로직을 직접 개발하는건 바퀴를 새로 만드는꼴이잖아. 이미 잘 만들어져있는 것들이 많은데 말야. 그렇다고 도메인 객체 자체에 추상화도 안된 외부 라이브러리를 넣을수도 없고.. 그렇다고 주입으로 받아봤자 아무튼 결국 도메인에 외부 라이브러리 간접적으로라도 결합도 생기는거라 이것도 아닌것같고.. 주변 개발자와 토론하며 어떻게 처리할지 고민했던걸로 기억해. 당시 결국 어떻게 처리했는지가 기억이 안나네. 내부에 외부 라이브러리에 대한 결합도를 남겼을 것 같진 않고, 타협해서 암호화된 값을 VO로 보고 서비스에서 처리해서 넣어줬던 것 같아.
지금 생각해보면 도메인이 굳이 기술적 수단(암호화 알고리즘 등)까지 판단할 필요는 없는데 당시 책임을 나누는 경계에 대한 감각이 부족했던 것 같아.
도메인과 인프라 코드를 분리해야 한다를 설명하려면, 그 반대되는 상황을 보여주는게 가장 베스트지. 외부의 변경이 도메인으로 전파되어 기존 도메인 로직에 대한 테스트 코드를 다 갈아엎고 정합성 새로 맞춰야하는 케이스를 보여주면 될 것 같아. 그리고 역으로 질문을 던져보면 좋을 것 같아. 예를들어 '여기서 Json 라이브러리를 다른걸로 바꾸면 어떻게 될 것 같아?'
Q4. “에러는 고수준 도메인 예외로 감싸야 한다”의 적용 한계
배경:
책에서 SDKBot의 예외를 예로 들며, 외부 예외를 고수준 도메인 예외로 감싸야 한다는 조언을 했지.
세영도 이 부분에서 ‘실제로 그렇게 처리하고 있다’고 했고, 테스트의 용이성이나 코드 오염 방지 측면에서도 동의했어.
질문:
- 실무에서 모든 외부 예외를 고수준 예외로 감싸는 게 현실적으로 힘들 때가 있잖아?이런 타협을 언제 허용하고, 언제는 반드시 감싸는지 기준이 있다면?
- 예를 들어 공통 유틸에서 예외를 바로 던지거나, 중간 레이어에서 catch 없이 propagate 하는 경우.
- 또, 너는 도메인 예외 클래스 설계할 때 어떤 기준으로 이름과 구조를 정해?
- (ex: DomainException, ExternalServiceException, JsonParsingException 이런 구분을 어떻게 잡는지)
내 답변
실제로 최근 프로젝트에서도 별도로 리팩토링할만한거 메모하며 코드 짤 때, 제일 많이 적었던게 특정 지점에서의 Exception 처리 문제였어. 예를들어 외부 API 호출하는 코드에서 암호화 모듈을 썼다고 해볼께. 특히 암호화 모듈의 경우 오래된 라이브러리들이 많아. 그리고 요즘 라이브러리들은 checked exception을 지양하는 반면, 예전 라이브러리들은 checked exception을 많이 쓴 경우가 꽤 있지.
이 경우 예외가 상위 모듈까지 올라간다면 외부 API를 사용하려는 클라이언트 코드에서, 갑자기 InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException 이런 익셉션들을 처리해야 되는 상황이 되버려 ㅋㅋㅋ. 뜬금없지.
참고로 실제 내 리팩터링 지점이었고, 다음과 같이 처리했어. 우선 테스트코드로 null이 들어간 경우를 포함해 우리 서비스에서 사용될만한 다양한 암호화 케이스를 전부 테스트했어. 그리고 생각해봤지. 애초에 InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException 이런 익셉션이 발생했을 때, 시스템적으로 이걸 복구할 방법이 있는가? 당연히 없지. 즉, 해당 에러들은 사실 코드를 잘 짜고 테스트코드에서 문제가 없었다면, 더이상 판단하지 않아도 되는 에러야. 저걸 보고 복구하는게 아니라 애초에 발생하면 안되는 에러들인거지. 따라서 그냥 암호화 모듈쪽에서 상위로 아무것도 전파안하도록 처리했어.
또, 우리의 메인 서비스 로직에 외부 API를 불러야 하는 경우가 있었어. 마케팅 동의에 따라 쿠폰을 발송하는 부분이었어. 이 때, 우리의 메인 서비스의 로직의 성공 여부가 쿠폰 발송 성공여부에 영향을 받아야 하냐? 라고 생각했어. 그래서 외부 API 호출이 실패한 경우, 그냥 로그만 남기고 무시하도록 해뒀어. 혹시 외부 API 에러 시 이정도는 그냥 운영적으로 수동으로 처리하거나, 이후 에러가 많이 나면 운영팀에서 별도 로직 만들면 괜찮겠다고 생각했지.
catch (Exception e) { … (로그 저장 등의 처리) // 마케팅 API 호출 실패는 외부 로직이므로, 메인 로직에 영향 없도록 별도 처리 하지 않음. }
저런식으로 주석 달아둠 ㅋㅋ 지금 생각해보니 비동기나 메시징으로 처리했어도 괜찮았겠네.
예외 클래스 설계 시 이름에 대해 생각해보면, 내 경우 서비스의 축약어를 이름에 넣는편이야. 이유는 간단한데, 일반적으로 개발자들이 다양한 라이브러리의 exception의 이름들을 전부 외우고 다니지 않는다는 점이지. 즉, 내부에서 만든 자체 exception인지 아닌지 판단하기 위해 그렇게 했어. 이게 일반적으로 좋은 방식인지는 잘 모르겠어. 내가 기존 봤던 책들 중에 커스텀 exception 이름 규칙은 없었거든ㅋㅋ 실제로 만든 exception 이름보면, MissingㅇㅇㅇTokenException, ExpiredㅇㅇㅇTokenException 이런거네. 아 물론 토큰 관련이라 기존 exception 그대로 써도 어색하진 않겠는데, 저렇게 한 이유는 특정 exception에 대해 프론트에서 어떻게 처리할지 로직을 내가 프론트 개발자들에게 지정해줬거든. 그래서 명시적으로 별도 exception으로 만든거야.
chatGpt의 추가 질문
이건 너와 더 이야기해보고 싶은 점인데:
🔹 외부 API 실패 시 “실패를 삼켜야 할지, 드러내야 할지”는 항상 주관적인 판단인데
너는 어떤 기준으로 삼켜도 된다고 판단해?
예를 들어:
- *“이건 운영으로 커버 가능하다”**는 건 정말 그렇게 되는가?
- 혹은 “재시도 로직이 없다면 감싸서 알려주는 게 낫지 않나?” 같은 질문도 할 수 있어.
개인적으로는,
- 필수 정보 전송 API면 실패해도 “예외를 감싸서” 도메인에 전파해야 하고,
- 부가적인 알림/마케팅 용도 API는 너처럼 삼켜도 괜찮다고 생각하는데
혹시 너는 이 기준을 좀 더 정제된 형태로 갖고 있는지도 궁금해!
내가 추가 답변
결국 해당 외부 API 실패를 내 서비스의 메인 로직 실패로 받아들일것이냐에 달려있어. 애초에 쿠폰 발송 로직의 경우, 설계때부터 실패해도 괜찮도록 되어 있었어. 고객한테도 “마케팅 동의 시 바로 발송됩니다.”가 아니라, “ㅇㅇ일 이내로 발송됩니다.”라고 안내가 나가. 즉, 좀 에러나서 바로 발송안되도 운영적으로 풀 수 있게 되어 있는 구조야.
반면에 이 프로젝트의 경우, 다양한 결제 방식으로 결제가 가능해. 이 때, 결제 관련 호출이 실패한 경우 애초에 메인 로직의 실패로 판단할수밖에 없어. 이런건 위 마케팅과 다르게, 운영적으로 이후에 처리할 수 없는 부분이야.
그러니 그때그때 다르다고 할 수 있긴한데, 솔직히 이건 단순히 직관적으로 판단해도 어려운 부분은 아니라고 생각해. 애초에 설계상 실패해도 되도록 했냐의 문제겠지.

7장. 모듈화 달성하기
Q1. "깊이 있는 모듈"이란 무엇이었나?
배경:
책에서는 "깊이 있는 모듈이란 적은 인터페이스로 많은 작업을 수행하는 모듈"이라고 정의하고, 관련 기능을 내부로 숨겨서 모듈을 안정적으로 유지하는 걸 강조했어.
질문:
- 지금까지 만들어본 모듈(또는 클래스) 중에서 *‘이건 진짜 깊이 있었지’*라고 느꼈던 경험이 있을까?
- 반대로, *‘겉보기만 번듯하고 깊이는 얕았던 모듈’*이 있다면 그 예시는?
- 어떤 기준으로 ‘인터페이스를 줄이고 내부 복잡도는 감추자’는 결정을 내렸는지도 궁금해.
내 답변
최근 프로젝트에서 토큰 발급 받는 부분이 꽤 잘만들었다고 생각해. 단순히 id,pw로 인증할 수 없는 요구사항을 가진 서비스야. 기본적으로 본인인증이 필요해. 이 때, 현재 고객이 접속한 환경에 따라 다른 인증 방식을 쓸 수 있어야 했어. 근데 난 이걸 프론트엔드에서 판단해서 인증 방식을 선택하게 하고 싶지 않았던거야. 인증에 대한 내부 구현을 외부에서 최대한 모르게 하고 싶었던거지. 물론 프론트엔드도 회사입장에선 내부지만, 아무튼 외부망에 언제나 열려있으니까. 또한 발급된 토큰은 이후 프론트엔드에서 고객이 행하는 모든 동작에 대해 백엔드로 들어올꺼야. 이 때, 인증 방식에 따른 차이를 백엔드 메인 로직에서 신경쓰게 하고 싶지 않았어. 그래서 이것들을 모두 래핑한 하나의 토큰 발급 API를 만들어뒀어.
실제로 프론트엔드에서 주는 정보는 하위 이용기관 구분값이랑 현재 고객의 접속환경 정도 뿐이야. 이걸보고 백엔드에서 어떤 인증방식을 사용할지 판단하고, 필요하다면 프론트를 리다이렉트 시켜서 처리해. 그리고 추가로 고객의 역할도 나뉘는데, 일단 본인 인증은 기본적으로 필요해. 그리고 추가로 하위 이용기관을 등록을 할 수 있어. 그러니 본인인증은 서비스를 진입하기 위한 필요조건인거고, 본인인증만 통과한 경우엔 익명인 셈이고, 1개 이상 등록했을 시엔 고객의 하위 이용기관 특정이 가능한 구조야. 또, 하위 기관 특정 됬을 시 이 고객이 유효한 고객인지도 확인하지. 이에 따라 발급되는 토큰도 달라져. 이런 부분까지 래핑해서 최종적으로 프론트로 그냥 토큰만 가는거야.
프론트엔드쪽에선 그냥 토큰달라고 해서 준거라 생각되겠지만, 실제론 분리된 코드들 다 합쳐보면 천 줄은 가볍게 넘을꺼야. 어찌보면 너무 책임이 많기도 한데, 어쨌든 이걸 통해 이후 백엔드의 모든 메인 로직 설계가 엄청 간편해졌고, 프론트는 뭐 보다시피 엄청 간단하지. 그냥 토큰 발급받고 그거 다른 API 호출할때마다 넣어주면 끝임 ㅋㅋ. 적절한 트레이드오프였다고 생각해.
겉보기만 번듯하고 깊이는 얕았던 모듈이라면, 주로 외부 API를 bypass한 경우야. 사실상 인프라에 대한 책임만 지는 단순 바이패스 API인거지. 역할은 그저 외부와 내부 사이의 DMZ 영역 정도의 역할이야.
어떤 기준으로 인터페이스를 줄이고 내부 복잡도를 감추자는 결정을 내렸는지는 사실 내 관점은 코드 내의 객체들의 협력이나, 외부와의 협력이나 동일하게 생각해. 단일 모듈 코드짤 때 생각한 방식 그대로 멀티 모듈에 대해서도 생각해. 보통 초기 개발할 때 코드개발보다는 전체적인 구조먼저 잡고 가는편이라, 최대한 ‘간단하게’, ‘이걸보고 누가 질문을 최대한 안하게’가 목표야. 간단하면 “아 이것만 보내면 되요? ㅇㅋㅇㅋ” 이걸로 끝나거든.
Q2. 세영이 생각하는 "모듈의 인터페이스 명세"는 어떤 구조여야 할까?
배경:
7.2는 모듈의 인터페이스를 단순하고 깔끔하게 유지해야 한다는 이야기였어.
특히 하위 호환성과 확장성, 소유권, 참여 규칙 등이 강조됐지.
질문:
- 지금까지 만든 모듈에서, 외부에 노출한 인터페이스를 설계할 때 어떤 기준을 갖고 설계해?
- 예를 들어 Service, Facade, Controller, UseCase 등에서 "이건 외부에 노출해도 되고, 이건 숨겨야 한다"고 나누는 기준이 있어?
- 또 "확장 지점을 어떻게 제공할 것인가"에 대해서 명시적으로 고민해본 적 있다면 그 예시도 궁금해.
내 답변
난 백엔드 개발을 위주로 하고 있어. 그리고 일부 네트워크나 DB 등 인프라에 대한 고려도 내 몫인 경우가 있어. 내 경우 기본적으로 외부의 요청을 최대한 믿지 않아. 예를들어 프론트엔드에서 주는 데이터는 언제든 변조될 수 있다고 생각해.
이런 관점에서 내 기준은 간단한데, 내가 처리할 수 있는 복잡한건 모두 내가 처리하고, 외부에 노출은 최소한만 한다는 기준이야. 이건 단일 모듈 내의 객체 설계시나, 외부 모듈과의 API 설계 시 모두 고려하는 점이야. 다만 좀 더 외부를 간단하게 만드는데 집중하긴 해. 내부의 복잡함은 우리쪽에서 처리하더라도, 내부에서의 문의는 사실 처리하기 쉽잖아. 반면에 외부로 노출된 API 에 대해서는 우리쪽 업무를 잘 모르는 사람도 쓸 수 있으니까. 최대한 간단하게 하는게 문의를 덜받는 방법인 것 같아.
너가 예시로 든 기준으로는 API 관점에서는 Controller만 노출해. 그리고 라이브러리 관점에서는 facade만 노출해. 그 외에는 노출을 최대한 꺼리는편이야.
확장 지점은 사실 불필요한걸 많이 두는건 그렇 게 좋아하지 않아. 예를들어 막 비고1, 비고2, 비고3, … 이런식의 확장지점은 안이쁘다고 생각해. 다만 설계시 초기엔 필요없지만, 이건 확장될만한데? 싶은건 미리 추가해두기도 해. 이 경우 문서상 ‘확장 대비’ 이런식으로 미리 적어두는편이야. 또 현재는 단일 값으로 받더라도 이후 N개로 늘어날만하다 하다면 미리 Array 형태로 구성해두는 이런식이야. 이 경우에도 현재는 1개이나, 이후 확장 가능하다는 식으로 적어두는 편이야. 사실 정말 큰 변경이 있다면 버전 방식의 API로도 괜찮다고 보거든.
Q3. 이벤트 기반 모듈 분리나 알림 시스템과 관련한 너의 경험
배경:
7.3.5~7.3.6에서는 이벤트 기반 분리와 알림 시스템 예제가 나왔어.
세영도 이벤트 기반 아키텍처 과제 경험이 있다고 했지.
질문:
- 지금까지 만든 이벤트 기반 시스템 중에서 "이건 설계 잘했다" 싶은 구조가 있었어?
- 이벤트 도입할 때 가장 힘들었던 점이나, 반대로 "이건 도입하길 잘했다"고 느낀 순간은?
내 답변
대학교 때 상황인지에 대한 국가 프로젝트에 참여한 적 있어. 10개 이상의 대학교와 여러 업체가 공동으로 진행한 프로젝트였어. 이 때 내가 맡은 역할은 안드로이드 기반 환경에서, 각 대학교 및 업체가 만든 컴포넌트 앱들이 이벤트 기반으로 동작할 수 있도록 프레임워크를 만드는거였어. 그리고 일부 데이터 수집 모듈도 여기에 포함되어 있었어.
컴포넌트들은 프레임워크 내에서, 데이터를 수집하는 컴포넌트앱도 있고 그걸 사용해 다양한 상황인지를 수행하는 모듈이 있고, 그걸 사용자에게 보여주는 모듈도 있었어. 각 기관이 독립적으로 만들 수 있도록 각자 별도의 앱을 만들었었어. 즉, MSA 구조라고 보면 되겠네. 이 때 서로간의 컴포넌트를 정확히 모르더라도, 그냥 원하는 수집 데이터를 구독하면 받을 수 있도록 설계했었어.
가장 힘들었던점은 사실 당시 교수님이 좀 완벽주의자셔서, 기간 얼마 안남아도 더 좋은 구조가 생각나면 무조건 갈아엎으셨어. 그리고 교수님은 절대 코딩에 손대지 않으셨어. 물론 코딩 엄청 잘하시는분인데도 토론만 딱 하고 가심. 교육상 그러시는건 알지만 힘들긴 했지. 솔직히 말하면 현재 회사에서 일하면서도, 힘듬 최대치로 보면 아직 대학교때가 더 빡쌨던거같아. 결론적으로, 모든 코드를 하나로 붙여보면 10만줄은 가뿐히 넘는 프레임워크 앱이 만들어졌고, 별 문제없이 잘 동작했어. 아 추가로 여러 대학교나 기업들이 참여하고, 내가 학년이 낮다보니.. 아무래도 당시 요구사항이 들어올 때 그걸 내가 잘 쉴드치지 못한점도 좀 힘들었네.

8장. 실용적인 접근법
Q1. 리팩터링은 ‘수익률 높은 투자’다. 이 말에 너는 얼마나 동의해?
- 실무에서 리팩터링을 자주 하는 편이야? 아니면 일정에 쫓겨 잘 못 하는 경우가 많아?
- 지금까지 리팩터링을 해서 ‘진짜 잘했다’ 싶었던 경험과, 오히려 ‘괜히 건드렸다’ 싶은 경험이 있다면?
- 리팩터링 타이밍은 어떤 기준으로 판단해?
- (예: 커밋 수 기준? 기능 분기점? 코드 읽기 난이도?)
내 답변
사실 내 업무 특성 때문에 많이 하진 못해. 다른 답변에서도 말했지만, 보통 플랫폼 서비스 초기 개발 후 1~2군데의 하위 이용기관 도입까진 내가 운영해. 그 후 서비스 크기에 따라 운영팀 혹은 별도팀으로 넘어가는 식이야.
그래서 초기 개발 시의 리팩터링이 대부분이야. 보통 일정이 그리 널널한 편은 아니야. 그러다보니 초기 개발하면서 우선은 ‘이정도면 괜찮지’ 수준으로 우선 만들면서, 이후 여유 있을 때 리팩터링 할만한 부분은 따로 적어둬. 물론 여유 있다면 바로바로 처리하지. 즉 내가 주로 해본건 장기적인 리팩터링보다는, 기술 부채를 내 현재 수준에서 최대한 관리 가능한 수준으로 유지시키는거였어. 예를들어 최근 프로젝트에서 적어두고 리팩터링했던 부분 중 일부를 적어보면 다음과 같아.
- 로그 부분 전부 람다식으로 변경 (참고 : 메모리 최적화 - 필요할때만 String 생성됨)
- 이후 서비스 커질 가능성 높음. 현재 모놀리식 기반이나, 기능 추가 시 멀티모듈도 가능하게 확장지점 마련
- 테스트 커버리지 높히기
- 외부 API 호출부에 들어간 암호화모듈때문에 checked exception 추가된 부분 상위 모듈로 전파안되게 처리
- 외부 API 호출부 병목구간 확인을 위해 시간 로그 다양화.
- 로그 저장 실패 시 파일로라도 내리기
이제보니 객체지향 설계적인 리팩터링은 없네. 애초에 코드 구조 설계부터 하고 실제 코딩은 마지막에 하는편이라 그런 것 같긴 해.
리팩터링 해서 잘했다고 생각한 부분은 외부 API 호출부 시간 로그를 다양하게 바꾼 부분이었어. API별로 실제 외부 API를 호출하기 이전까지 내부의 로직 시간, API 호출 후 응답이 오기까지의 시간, 응답을 받고 내부에서 처리에 걸린 시간 등 다양하게 로깅되도록 했어. 그리고 API 별로 N개의 하위 로그 형태로 외부 API 더라도 개별로도 확인할 수 있게 했지. 어느 부분이 병목지점인지 파악하기 정말 편했어.
괜히 건드렸다 싶은건 내 입장에선 딱히 없는 것 같네. 이후 운영팀에선 이걸 왜 건드려놨어 했을수도 있지만 ㅋㅋ
Q2. 더 우아한 디자인 해법으로 옮겨갈 타이밍은 어떻게 결정해?
- 단순히 “돌아가니까 OK”보다는 “좀 더 구조적으로 리팩터링할 타이밍이다”라고 느끼는 기준은?
- 어떤 상황에서 “이건 더 이상 이렇게 가면 안 된다”고 판단해?
- 직관적으로 판단하는 편이야, 아니면 로그/성능/구조 등 어떤 지표를 가지고 판단해?
내 답변
다른 리팩터링 말고 구조적 리팩터리만 생각해볼께. 타이밍은 다음처럼 판단해볼 수 있을 것 같아. 이후 바뀔 수 있다고 예상되는 지점, 이후 제어권이 내게서 벗어났을 때 휴먼에러 발생 시 문제가 클 수 있는 지점. 그 외에도 직관적으로 깔끔하지 못하다고 생각되는 부분인 것 같네. 뭐 이론적으로 기준을 들자면 더 들 수 있겠지만, 대부분은 초기 설계 때 이미 고려되어 있어. 그러니 이후 리팩터링 대상은 개발하다보니 요구사항이 자주 변경되는 부분에 대한 리팩터링, 개발하다보니 이후 여긴 기능 더 추가되거나 변경 가능하겠는데? 싶은 부분이 주로 내가 리팩터링 하는 부분인 것 같아.
제어권이 내게서 벗어났을 때 휴먼에러 발생 시 문제가 클 수 있는 지점도 마찬가지로, 실제 동작 자체엔 문제가 안돼. 다만 이후 운영팀으로 이관되었을 때, 잘못 손대면 안될만한 부분에 대한 안전장치를 말한거야. 예를들어 최근 프로젝트에서 고객의 정보를 담고 있는 Payload가 있었어. 백엔드 내부에서는 암호화가 풀린 상태로 쓰고 있지만, 네트워크 상에 나갈땐 무조건 암호화된 EncryptedPayload 형태로 나가야해. 우선 암호화에 대한 생성 책임을 Factory로 만들고, 패키지 형태나 접근 제어자를 적절히 사용해서 내가 원하지 않는 곳에서 사용하지 못하게 했어. 근데도 패키지 구조에 안맞게, 굳이 쓰겠다고 public으로 풀어버리거나 동일한 패키지 내에 클래스를 만들어서 억지로 사용할수도 있겠지. 그래서 ARCUnit으로 생성이 특정 클래스에서만 이루어질 수 있다는 제약을 테스트코드로 뒀어. 테스트코드까지 갈아엎을정도로 쓰고싶었다면, 이제 내 책임은 아닌거 같다고 생각했어 ㅋㅋ
‘이건 더 이상 이렇게 가면 안된다’ 이 부분은 항상 어려워. 코드 구조를 설계할 때 항상 생각하는 지점이야. 실제로도 일부러 타협해둔 부분도 많아. 예를들어 서비스 코드가 한 100개 있는데, 그 중에 하나만 길어. 근데 서비스 코드들 전부 비슷비슷하게 생겼는데, 얘만 길다고 구조를 바꿨을 때 이후 유지보수 하는 사람이 보기 더 편할까? 아니면 하나정돈 좀 길어도 코드 형태의 일관성을 맞춰두는게 좋을까? 이런 문제도 있지. 나 처음으로 개발자 관련 스터디할 때 스터디원이 “야그니”라는 말을 썼거든. 그게 뭔지 궁금해서 찾아보니 You Ain’t Gonna Need It 이더라고. 저 용어를 처음 들었다보니 기억에 많이 남아. 자연스레 짜면서 항상 되묻는편이야. 굳이 이거까지 해야할까? 정말 바뀔만한 부분이 맞을까? 가독성 너무 떨어지는거 아닐까? 오히려 싫어하지 않을까? 그런거지. 그래서 명확한 판단 기준은 없어. 그냥 현재 내가 가진 개발 지식 또는 현재 프로젝트에 대한 예상되는 상황에서 이 정도면 양심의 가책은 없겠는데? 싶은 정도까지만 처리하는 편이야.
직전 답변에서도 말했듯이, 보통 플랫폼 서비스 초기 개발 후 1~2군데의 하위 이용기관 도입까지만 내가 운영하고 운영팀에 넘기는 식이야. 그러다보니 로그, 성능을 가지고 리팩토링하긴 힘들어. 오히려 이후 로그나 성능으로 문제가 될 시 이런식으로 처리하라는 식으로 문서화는 가끔씩 해두는 편이야. 예를들어 최근 통계관련 로직에서 이후 인덱스 걸만한 지점 써뒀어. 그러니 내 기준은 주로 ‘구조’적으로 직관적으로 판단한다가 더 맞겠네. 이후 예상되는 서비스의 크기를 가지고, 어느정도 수준 까지만 해두는 편인거지. 참고로 아까 말한 통계 부분은, 운영팀에 넘기기 전에 이미 사용량이 많아져서 3초 이상 걸려서, 넘기기 전에 리팩토링해서 1초 이내로 줄여뒀었어. 더 최적화도 가능하지만, 사실 하위 이용기관 담당자들이 가끔씩 보는거라 굳이 더 줄일 필요는 없다고 생각했어.
Q3. ‘임시 해결책만큼 영구적인 것은 없다’ — 너도 그런 사례 많이 봤어?
- 실제 경험한 임시방편 코드가 영구 구조물처럼 자리잡은 경우는?
- 왜 그렇게 굳어졌다고 생각해?
- 그런 사태를 미리 방지하거나 나중에라도 해결하기 위해 어떤 노력을 하고 있어?
내 답변
처음 개발자 시작했을 땐 파견 업체에서 시작했어. 즉 첫 프로젝트는 '프리'로 나간 셈이야. 거긴 전부 프리인 프로젝트인거지. 나도 웹쪽 지식 자체가 없고, 특히 프론트쪽은 아예 처음 해봤었거든. 그러다보니 PL급의 말을 철썩처럼 따랐지. 공통 라이브러리 형태의 js 함수들을 하나의 파일안에 전부 몰아두는 구조였어. 물론 js가 뭐 객체지향 언어라고 보긴 힘들지만, 그래도 결합도 얘기는 패러다임과 상관없는 얘기라 생각하니 계속 얘기해볼께.
공통 함수가 자주 추가된건 아니지만, 아무튼 여러 개발자가 추가하다보니 꽤 파일 크기가 커지지. 결론적으로 몇만줄 이상의 거대한 무언가가 되었어. 너가 말한 ‘영구 구조물’이라 볼 수 있겠네. 짠 사람도 다 다르고, 특히나 객체지향 언어도 아니니깐 이게 어디까지 영향이 퍼져있는지도 모르지. 게다가 나나 대부분은 (대외적으론 대리인데 실제론) 신입 수준이고, 애초에 그 구조를 PL이 제시한거였잖아. 그러니 아무도 리팩토링 할 생각자체를 안한거지. 물론 나도 변명이긴한데, 솔직히 프론트 자체를 거의 처음해본거라, 감히 손 댈 생각 못했어 ㅋㅋ. 깨진 창문 이론에 따라, 이미 남들이 그렇게 짜고 있는 코드에서 혼자만 바꿔보는건 용기가 많이 필요하다고 생각해.
얼마전에 본 MSA 관련 책에 이런 내용이 나와. ‘MSA 도입을 위해서는 팀원 전체의 상향 평준화가 필요하다’. 물론 PL 역할의 개발자가 충분한 설계 지식이 있다면, 팀이나 프로젝트의 규칙으로 정해서 미리 방지가 가능할꺼야. 그럼에도 해당 개발자도 놓치는 부분이 언제나 있을 수 있지. 결국 팀이나 사내의 문화 자체가 리팩토링에 열려있는 자세가 되는게 제일 중요할 것 같아. 일종의 문화처럼 서로 토론하면서 재밌게 할 수 있도록 말이야. 물론 초기부터 잘 잡는게 제일 좋겠지. 그러려면 PL 급도 중요하지만 팀원이나 사내의 문화 자체가 중요한 것 같아.
물론 일정 압박 같은 외부적 요인도 영구 구조물을 만드는데 기여할 수 있어. 그치만 ‘어느정도의 선’ 까지는 솔직히 역량의 차이라고 봐. 일정같은 외부 요인이 변명이 될 순 없다고 봐. 예를들어 어느정도 역량이 있는 사람이 일정이 급하다고 if, else 루프가 한 100개 있는 함수를 짤까? 본인이 이해하기 힘들어서라도 그렇게 안하겠지. 오히려 그렇게 짜는게 더 시간이 많이 걸린다는걸 이미 알고 있는 사람일테니까. 로버트 C 마틴의 클린코드 책에서 나온 내용을 인용해볼께. “요구사항이 변했다고 불평, 일정이 촉박해 제대로 할 시간이 없었다고 한탄, 멍청한 관리자와 조급한 고객과 쓸모없는 마케팅 부서 탓이라 떠벌인다. 하지만 잘못은 전적으로 우리 프로그래머에게 있다. 우리가 전문가 답지 못했기 때문이다. 인정하기 어려울 것이다.”
어떤 노력을 하고있냐고 하면 그건 대답이 좀 어렵네. 그냥 사내 세미나를 많이 해보려고 노력하고 있어. 예를들어 가장 최근에 진행한 세미나는 ‘왜 이렇게 코드를 짰을까? 제 코드의 선택과 이유들’ 라는 세미나였어. 내가 개발자 초기부터 짰던 코드 예시를 두고, 왜 그땐 그렇게 생각했고, 이런 부분은 반성한다. 또는 최근엔 이렇게 짰는데 이런 생각을 가지고 짜봤다. 이런 세미나였어. 그리고 하나 더 들어보자면 PR 올라온거에 대한 코드 리뷰를 단순 동작보다는 구조를 위주로 평가하는 편이야. 잘못된 동작은 의외로 빠르게 파악 가능한데, 구조적인 문제는 이후 알 수 없는 문제를 만들거든.
최근 코드 리뷰 단 것 중 몇개 보면 다음과 같아. “postFiles() 과 중복된 역할 같습니다. 테스트용으로 쓰고 삭제 안하신 것 같슴다.”, “위에 적은 내용과 동일합니다. 리터럴이 바로 나와서 이후 운영할 때 헷갈릴 것 같습니다.”, “토큰 발행의 책임을 여기에서 가지는게 맞을지 생각해볼 필요가 있슴다.”
Q4. 너도 이제 주니어 개발자에게 영향을 주는 위치에 있는 것 같아. ‘이 챕터를 주니어에게 어떻게 설명할래?’
- 예를 들어, 신입 개발자가뭐라고 설명할 것 같아?
- “리팩터링이 중요한 건 알겠는데, 뭐가 좋은 리팩터링이에요?”라고 물으면
- 혹은 누군가가 실용성을 지나치게 강조하며너는 어떤 식으로 반론을 펼쳐?
- “어차피 잘 돌아가잖아요”라고 말할 때,
내 답변
뭐가 좋은 리팩터링인진 오히려 내가 묻고싶네. 그냥 내 현재 지식 수준에 따라 매번 바뀌는 것 같아. 그러니 어떤게 좋은 리팩터링인가? 보다는 어떻게 리팩터링하는게 좋은가를 먼저 말해줄 것 같아. 보이스카우트 규칙이 기억하기도 좋고, 아주 좋은 규칙이라 생각해. 캠핑장에 처음 왔을 때보다 더 깨끗하게 하고 떠나라는 원칙이야. 코드에 접목해보자면, 코드를 처음 봤을 때보다 조금이라도 더 나은 상태로 남겨두라는 거지.
뭐 이론적으로 따지면 뭐가 좋은 리팩터링인진 어느정도 나와 있긴 하지. 근데 그보다는 마인드가 중요하다고 생각해. 관심을 가지고 지식을 쌓아나간다면 리팩터링 실력은 오를꺼야. 하지만 리팩터링을 할 마음 자체가 없다면, 그건 방법이 없잖아? 그러니 우선 보이스카우트 법칙을 설명해주고, 사소한거 부터 지속적으로 해보라고 말할 것 같아. 뭐 변수명이 이게 더 낫지 않나요?, 인덴테이션 이거 한줄 더 띄우는게 보기 좋아보여요도 하나의 리팩터링이니까.
‘어차피 잘 돌아가잖아요’는 프로젝트의 생명주기에 대해 몰라서 하는 얘기라고 생각해. 초기 개발보다 유지보수 기간이 훨씬 길지. 물론 단건으로 돈받고 원하는걸 짜주는 프리거나, SI만 해왔다면 정말 모를수도 있다고 봐. 그치만 서비스를 만들고 본인 회사에서 계속 운영하는 형태라면, 이후 유지보수팀도 계속 봐야할꺼잖아. 얼굴 들고 다니는데 부끄러움은 없어야지 ㅋㅋ.
뭐 이건 감성적인 측면이고, 반대로 생각해보면 사실 좀 애매하긴 해. 언제나 생각은 상대적인거잖아? 극단적으로 들어가보면 누군간 프레임워크를 쓰는 것 조차 실용성만 강조한거라고 할 수 있어. 모든 결합도는 무조건 인터페이스가 있어야 한다고 할 수 있어. 모든 프로젝트는 MSA로 구성되어야 한다고 할 수 있어. 프로젝트의 테스트 커버리지는 항상 100%를 유지해야 한다고도 할 수 있지.
Q3의 답변에 있던 내 초기 개발자 생활에서, 난 “비즈니스 로직은 DB 프로시져에 있어야 한다” 라는 가르침을 받았어. 그게 배포 없이 프로시져만 갈아끼워도 로직을 바꿀 수 있다고 하면서. 어쨌든 대학교때 배운게 있어서, 아닐꺼라곤 생각했지만 뭐 Q3에서도 말했듯 내가 막 대놓고 반대하긴 쉽지 않았지. 아무튼 거기서 난 어느정도 비즈니스 로직을 프레임워크 안으로 가져왔어. 그들이 보았을 때 난 ‘실용성을 지나치게 강조하며 어차피 잘 돌아가잖아요’ 라고 말하는 사람이었을 수 있어.
결국 답은 없어. 누군가에겐 내가 이전에 나한테 “비즈니스 로직이 프로시져에 있어야한다”라고 했던 사람처럼 보일 수 있다고 생각해. 애초에 내 지식이 완벽한게 아니고, 그건 모두가 마찬가지니까. 그러니 난 적극적으로 내 생각을 피력하는 타입은 아니야. 반대로 그럼 “이 부분에 기능 추가하려면 어떻게 해야할까요?” 이런식으로 역으로 생각하게 할 것 같아. 결국 중요한건 의견이 다르더라도, 명분과 팀의 합의라는 입장에서 그 실용성 논리가 팀 내에서 납득할 수 있느냐야. 여기서 단순히 ‘편하니까요’가 나오면 안되겠지. ‘이게 사실 이후에 A 기능 추가시엔 더 유리해요.’ 이런 설명이 가능하다면 얼마든지 수용 가능해.
읽어주셔서 감사합니다.
'Study > 책, 강의 생각 정리' 카테고리의 다른 글
| 질문은 LLM이, 답은 내가 - LLM을 활용한 사고확장 자기계발 방법 추천 (Prompted by AI, Answered by Me) (9) | 2025.07.22 |
|---|---|
| LLM과 개발 책을 읽으며 든 내 생각 - 개발자식 하브루타 독서 (2) | 2025.07.04 |
| '주니어 백엔드 개발자가 반드시 알아야 할 실무 지식'을 읽고 떠오른 내 생각들 (6) | 2025.05.19 |
댓글