refactoring

17 개의 포스트

gitlab3분 읽기큐레이션 요약

GitLab의 Claude Opus 5: 어려운 작업을 위해 설계된 추론

Anthropic의 Claude Opus 5가 GitLab Duo Agent Platform에 출시되어 복잡하고 중요한 개발 작업에 더 깊은 추론 능력을 제공한다. GitLab 내부 평가에서 작업 해결률 93.3%를 기록해 Opus 4.8의 73.0%보다 크게 향상됐으며, 속도도 유지했다. 일상적인 작업에는 Sonnet 계열을, 대규모 리팩터링·장기 디버깅처럼 실패 비용이 큰 작업에는 Opus 5를 선택하는 전략이 권장된다. ## 복잡한 작업에서 강화된 추론과 정확성 - 여러 파일을 수정하는 기능 개발, 대규모 리팩터링, 장기간의 커밋 이력을 추적하는 디버깅에 적합하다. - 내부 평가에서: - Opus 5 작업 해결률: **93.3%** - Opus 4.8 작업 해결률: **73.0%** - 두 모델 모두 시도한 작업의 완료율은 **100%** - 단순히 작업을 끝내는 것을 넘어, 결과가 검증된 올바른 해결책인지에서 큰 차이를 보였다. - 부분적인 패치나 반복적인 재프롬프트가 줄어들어, 결과물을 바로 머지할 가능성이 높아진다. - 예시로 5개 파일을 수정하고 새로운 공개 타입과 설정 필드를 추가해야 하는 CLI SSO 인증 기능을 완전히 구현하고 커밋한 뒤 머지 리퀘스트까지 생성했다. ## 코드 리뷰와 멀티 에이전트 협업 - 실제 버그를 잘 포착하면서도 오탐이 적어, 개발자가 불필요한 경고를 검토하는 시간을 줄인다. - 여러 에이전트를 동시에 실행할 때 각 에이전트가 서로의 작업을 덮어쓰거나 충돌시키는 문제를 줄인다. - Writer-verifier 패턴을 활용할 수 있다. - 한 에이전트가 코드를 작성한다. - 다른 에이전트가 결과를 검증한다. - 검증을 통과한 결과만 최종 작업에 반영한다. - 장시간 자율 실행하거나 여러 에이전트를 병렬로 운영하는 환경에서 특히 효과가 크다. - GitLab Credits 사용량 상한을 설정해 병렬 에이전트 작업의 비용이 예산을 초과하지 않도록 제한할 수 있다. ## 깊은 추론을 유지하면서도 빠른 실행 - GitLab의 고난도 벤치마크에서 95번째 백분위 실행 시간은 다음과 같다. - Opus 5: **768초** - Opus 4.8: **784.98초** - Sonnet 4.6: **982.57초** - Opus 5는 Opus 4.8보다 **2.2% 빠르고**, Sonnet 4.6보다 **21.9% 빠르다**. - 복잡한 장기 작업에서도 실행 시간이 지나치게 늘어지는 상황을 줄여 보다 예측 가능한 결과를 제공한다. ## 작업별 모델 선택 - 모든 개발 작업에 가장 강력한 모델을 적용하기보다 작업의 복잡도에 따라 모델을 선택해야 한다. - **Sonnet 계열** - 일상적인 개발 작업 - 빠른 코드 생성과 수정 - 비용 효율성이 중요한 반복 작업 - **Opus 5** - 대규모 리팩터링 - 어려운 버그 추적 - 여러 파일과 시스템에 걸친 기능 구현 - 잘못된 판단을 다시 되돌리는 비용이 큰 작업 - 모델은 GitLab 인스턴스에서 직접 선택할 수 있다. - 모델이 달라도 GitLab Duo Agent Platform의 컨텍스트 계층, 정책 검사, 감사 추적 인프라는 동일하게 적용된다. ## GitLab Duo Agent Platform에서의 이용 - Claude Opus 5는 GitLab Duo Agent Platform에서 사용할 수 있으며 GitLab Credits로 과금된다. - 신규 사용자는 무료 체험을 시작할 수 있다. - GitLab Premium 또는 Ultimate 구독자는 포함된 GitLab Credits를 활용해 Duo Agent Platform을 활성화할 수 있다. 복잡하고 실패 비용이 큰 작업에는 Opus 5를 우선 적용하고, 반복적인 일상 개발에는 Sonnet 계열을 사용하는 혼합 전략이 가장 실용적이다. 다만 제시된 성능 수치는 GitLab의 내부 평가 결과이므로, 실제 도입 전에는 팀의 코드베이스와 작업 유형을 기준으로 별도 검증하는 것이 좋다.

원문 읽기(새 탭에서 열림)
github4분 읽기큐레이션 요약

전 세계 AI 기회 확장: GitHub

GitHub와 Andela는 AI 도구를 별도 교육 과정이 아니라 실제 프로덕션 개발 업무에 통합하는 방식으로 글로벌 개발자의 AI 활용 역량을 높였다. 2년간 Andela의 550만 명 네트워크를 대상으로 협력했고, AI Academy를 통해 3,000명의 엔지니어가 GitHub Copilot을 교육받았다. 사례에서 AI는 단순한 코드 생성보다 레거시 시스템 파악, 테스트 작성, 리팩터링 준비, 의사결정 지원을 통해 개발자의 자신감과 생산성을 높이는 데 효과적이었다. ## 지역에 따른 AI 학습 기회의 격차 - 아프리카, 남미, 동남아시아 등에는 뛰어난 개발자가 많지만 최신 기술, 멘토링, 학습 경로에 대한 접근성은 지역과 고용주에 따라 크게 다르다. - 불안정한 인터넷 연결, 고성능 컴퓨팅 부족, 클라우드 도구와 데이터 비용 등이 AI 학습을 어렵게 만든다. - 기존 교육 콘텐츠는 안정적인 인터넷과 충분한 자원을 전제로 하며, 지역 언어·문화·업무 환경을 충분히 반영하지 못하는 경우가 많다. - 비정규직이나 계약직 개발자는 재교육에 투자할 시간과 경제적 여유가 부족할 수 있다. - 저렴한 도구 접근성, 지역에 맞는 학습 과정, 커뮤니티 중심 생태계가 없으면 AI 발전이 기존의 기술 격차를 확대할 수 있다. ## 실제 업무 안에서 AI를 학습하는 방식 - 중견 개발자가 프로덕션 업무를 중단하고 AI를 실험하기는 현실적으로 어렵기 때문에, 학습은 기존 업무 흐름 안에서 이루어져야 한다. - 단순히 조직 전체에 AI 도구를 배포하고 자율적인 실험을 맡기는 것만으로는 충분하지 않다. - 어떤 직무가 AI의 효과를 크게 얻는지, 어떤 업무를 대상으로 하는지, 코드 리뷰와 품질 기준을 어떻게 바꿀지 명확히 해야 한다. - Andela는 개발자의 담당 업무와 AI 활용 가능성을 기준으로 참여자를 선정하고, 실제 운영 중인 시스템에 맞춰 직무와 교육 과정을 설계했다. - GitHub Copilot을 IDE, 풀 리퀘스트 리뷰, 리팩터링 작업에 직접 적용해 실제 제약 조건 속에서 효과를 평가했다. - 교육은 이상적인 연습 문제가 아니라 개발자가 실제로 유지보수해야 하는 시스템과 업무를 기반으로 구성됐다. ## 레거시 시스템 이해와 안전한 변경 - AI를 활용했을 때 가장 먼저 나타난 효과는 코드 작성 속도보다 낯선 시스템에 빠르게 적응하는 능력이었다. - 브라질의 시니어 엔지니어 Daniel Nascimento는 변경 전에 프로젝트의 목적, 아키텍처, 강점과 취약점을 파악하는 데 AI를 활용했다. - 테스트 커버리지가 부족한 레거시 코드에서는 리팩터링 전에 AI로 단위 테스트를 생성해 기존 동작을 보호할 경계를 만들었다. - 개발자들이 활용한 방식은 다음과 같다. - 기존 동작을 파악하기 위한 테스트 생성 - 복잡한 제어 흐름을 명확하게 만드는 리팩터링 초안 작성 - 시스템 경계와 구성 요소를 이해하기 위한 다이어그램 구상 - AI가 생성한 결과는 정리되지 않았거나 미묘한 오류를 포함할 수 있으므로, 개발자의 검토와 품질 관리가 여전히 필수적이다. ## 생산성보다 중요한 자신감과 판단력 - 몇 주간 실제 시스템에 AI를 적용한 개발자들은 다음과 같은 변화를 보고했다. - 익숙하지 않은 시스템에 더 빠르게 온보딩 - 모호하고 복잡한 업무를 맡는 데 대한 자신감 향상 - 환경 설정과 반복 작업에 쓰는 시간 감소 - 비즈니스 맥락과 실제 영향에 집중할 수 있는 시간 증가 - Daniel은 GitHub Copilot 사용 후 생산성이 약 50% 향상됐다고 평가했다. - 다만 효과의 핵심은 단순히 더 많은 코드를 빠르게 생성하는 데 있지 않고, 개발자가 시스템을 이해하고 더 나은 결정을 내릴 시간을 확보하는 데 있다. - AI는 개발자의 이해와 책임을 대체하지 않으며, 복잡한 프로덕션 환경에서는 검토·테스트·리팩터링 역량과 함께 사용해야 한다. ## 실무 적용을 위한 시사점 - AI 교육은 일회성 자격증 과정이 아니라 실제 담당 시스템과 업무에 연결해야 한다. - 모든 개발자에게 동일한 방식으로 배포하기보다 AI 효과가 큰 역할과 문제를 먼저 선정하는 것이 좋다. - 코드 생성뿐 아니라 테스트 작성, 레거시 분석, 문서화, 온보딩 등 초기 효과가 큰 영역부터 적용할 수 있다. - AI 도구의 도입 성과는 코드 생산량만이 아니라 시스템 이해 속도, 변경 안정성, 개발자의 판단력과 자신감까지 함께 평가해야 한다. - 지역별 인프라와 경제적 제약을 고려한 접근성 높은 교육·도구 지원이 글로벌 AI 격차를 줄이는 데 중요하다.

원문 읽기(새 탭에서 열림)
line원문

코드 품질 개선 기법 30편: (투명한) 운명의 붉은 실 (새 탭에서 열림)

코드 내에서 서로 다른 함수가 암묵적인 전제 조건을 공유할 때 발생하는 유지보수의 위험성을 경고하고, 이를 해결하기 위한 구체적인 리팩터링 방향을 제시합니다. 특정 함수가 다른 함수의 실행 결과에 의존하는 '암묵적 연관성'은 런타임 에러의 원인이 되므로, 로직을 하나로 통합하거나 의존 관계를 명확히 정의하여 코드의 안전성을 높여야 한다는 것이 핵심입니다. ### 함수 간 암묵적 연관성의 위험성 데이터의 유효성을 검사하는 함수(`isContentValid`)와 데이터를 처리하는 함수(`getMessageText`)가 분리되어 있을 때, 두 함수 사이에는 보이지 않는 의존성이 발생합니다. * **런타임 에러 발생 가능성:** 처리 함수가 유효성 검사 함수에서 `true`를 반환했을 때만 안전하게 호출될 것을 전제로 설계되면, 이 규칙을 어길 경우 컴파일 타임이 아닌 런타임에 에러가 발생합니다. * **일관성 유지의 어려움:** 새로운 데이터 타입이 추가될 때마다 두 함수의 로직을 동시에 업데이트해야 하며, 하나라도 누락할 경우 시스템 전체의 논리적 일관성이 깨지게 됩니다. * **낮은 가독성과 오용 위험:** 호출부의 코드만 봐서는 두 함수가 강하게 결합되어 있다는 사실을 인지하기 어려워, 향후 리팩터링이나 기능 확장 시 함수를 잘못 사용할 가능성이 큽니다. ### 로직 통합을 통한 원자적 처리 필터링(유효성 검사)과 변환(데이터 처리) 기능을 하나의 함수로 합치면 암묵적인 의존성을 제거하고 코드의 안전성을 즉각적으로 향상시킬 수 있습니다. * **Nullable 반환 타입 활용:** `getMessageText` 함수가 유효하지 않은 입력에 대해 에러를 던지는 대신 `null`을 반환하도록 수정함으로써, 호출자가 반환 값을 통해 유효성 여부를 자연스럽게 판단하도록 유도합니다. * **책임의 단일화:** 유효성 검사와 텍스트 추출 로직이 한 곳에 모이게 되어, 데이터 구조가 변경되더라도 한 함수 내의 `when` 절 등에서 모든 처리를 완결할 수 있습니다. ### 연관성을 명시하는 대안적 구현 성격이 다른 두 함수를 반드시 분리해야 하는 상황이라면, 한 함수가 다른 함수를 참조하게 만들어 의존 관계를 겉으로 드러내야 합니다. * **함수 재정의:** `isContentValid` 함수를 독립적인 로직으로 구현하는 대신, `getMessageText(content) != null`과 같이 데이터 처리 함수의 결과를 확인하는 방식으로 재정의합니다. * **단일 진실 공급원(SSOT) 확보:** 이렇게 구현하면 로직의 실질적인 판단 근거가 하나로 집중되어, 함수 간의 동작 불일치 문제를 원천적으로 차단할 수 있습니다. 함수 사이에 '보이지 않는 붉은 실'과 같은 암묵적 규칙이 존재한다면, 이를 코드상에 명확히 드러내거나 하나의 함수로 묶어 관리하는 것이 좋습니다. 이를 통해 동료 개발자가 별도의 사전 지식 없이도 코드를 안전하게 재사용할 수 있는 환경을 만들 수 있습니다.

line원문

코드 품질 개선 기법 29편: 고르디우스 변수 (새 탭에서 열림)

코드 내 데이터의 의존성이 복잡하게 얽혀 로직을 파악하기 어려운 상태를 '고르디우스의 매듭'에 비유하며, 이를 해결하기 위한 설계 기법을 제시합니다. 복잡한 조건문과 데이터 가공이 반복되는 경우, 최종 로직에 필요한 이상적인 중간 데이터 구조를 먼저 정의하고 이를 생성하는 방식으로 코드를 재구성하면 가독성과 유지보수성을 동시에 높일 수 있습니다. **데이터 의존성 과다로 인한 가독성 저하** * 원격과 로컬 데이터를 동기화할 때 추가, 업데이트, 삭제 대상을 구분하는 과정에서 데이터 의존성이 복잡해지기 쉽습니다. * 단순히 ID 목록을 비교해 차집합을 구하는 방식은 실제 데이터를 처리할 때 다시 원본 리스트에서 객체를 찾아야 하거나, 맵(Map)에서 데이터를 꺼낼 때 발생할 수 없는 예외 상황을 처리해야 하는 번거로움을 유발합니다. * 이로 인해 비즈니스 로직의 핵심인 '동기화 액션'보다 데이터를 분류하고 가공하는 '준비 과정'이 코드의 흐름을 방해하게 됩니다. **이상적인 중간 데이터 설계를 통한 역설계** * 복잡한 매듭을 풀기 위해서는 최종적으로 필요한 데이터의 형태를 먼저 상상하고, 그 지점부터 함수의 구성을 역으로 설계하는 것이 효과적입니다. * 이번 사례에서는 추가(`created`), 업데이트(`updated`), 삭제(`deleted`)될 대상들을 명확히 분리한 세 가지 리스트를 중간 데이터로 정의했습니다. * 로컬과 원격의 모든 ID 집합을 기준으로 `Pair<RemoteData?, LocalData?>` 형태의 시퀀스를 만들고, 이를 상태에 따라 분류하는 것이 핵심입니다. **`partitionByNullity`를 활용한 로직 단순화** * `partitionByNullity`라는 유틸리티 함수를 도입하여 데이터의 존재 여부에 따라 세 그룹(Remote만 존재, 둘 다 존재, Local만 존재)으로 깔끔하게 분리합니다. * 이 함수를 사용하면 메인 함수인 `synchronizeWithRemoteEntries`에서는 복잡한 필터링이나 조건문 없이 각각의 리스트에 대해 `forEach`를 돌며 추가, 업데이트, 삭제 로직만 수행하면 됩니다. * 결과적으로 런타임 에러를 방지하기 위한 불필요한 null 체크가 사라지고, 전체적인 실행 흐름이 일관성 있게 정돈됩니다. **실용적인 제언** 코드의 흐름을 따라가기 벅차다면 데이터의 흐름이 꼬여있지 않은지 점검해야 합니다. 구현에 매몰되기보다 "어떤 모양의 데이터가 있으면 이 로직이 가장 깔끔해질까?"를 먼저 고민하고, 그 중간 구조를 만들어내는 로직을 별도로 분리하면 코드 품질을 획기적으로 개선할 수 있습니다.

line원문

코드 품질 개선 기법 27편: 티끌이 모여 태산이 되듯 의존성도 쌓이면 (새 탭에서 열림)

의존성 주입(Dependency Injection)은 코드의 유연성을 높이는 강력한 도구이지만, 명확한 목적 없이 모든 요소를 주입 대상으로 삼는 것은 오히려 코드 복잡도를 높이고 유지보수를 어렵게 만듭니다. 참조 투명한 유틸리티나 단순한 모델 클래스까지 외부에서 주입받기보다는, 복잡도가 낮거나 변경 가능성이 희박한 객체는 내부에서 직접 생성하는 것이 더 효율적일 수 있습니다. 따라서 의존성을 주입할 때는 객체의 라이프사이클 관리, 구현체 전환, 테스트 용이성 등 구체적인 목적이 있는지 먼저 검토해야 합니다. **불필요한 의존성 주입의 사례와 개선** * **과도한 주입의 예시**: 뉴스 기사 스니펫을 생성하는 `LatestNewsSnippetUseCase` 클래스에서 데이터 모델인 `NewsSnippet`의 팩토리 함수나, 단순한 문자열 포매터인 `StringTruncator`까지 생성자로 주입받는 경우입니다. * **개선 방향**: 상태를 갖지 않는 단순한 유틸리티 구현체나 데이터 모델의 생성자는 클래스 내부에서 직접 호출하도록 변경합니다. * **단순화 결과**: 환경에 따라 달라지는 값(Locale)이나 네트워크 통신 등 복잡한 로직을 가진 리포지터리만 주입 대상으로 남겨 코드를 더 간결하게 유지할 수 있습니다. **의존성 주입이 필요한 명확한 목적** * **라이프사이클 및 범위 관리**: 객체의 상태를 공유해야 하거나, 사용하는 객체보다 더 긴 수명을 가진 객체를 활용해야 할 때 주입을 사용합니다. * **의존성 역전(DIP)**: 모듈 간의 순환 의존성을 해결하거나 아키텍처에서 정의한 계층 간 의존 방향을 준수하기 위해 필요합니다. * **구현체 전환 및 분리**: 테스트나 디버깅을 위해 Mock 객체로 교체해야 하는 경우, 혹은 빌드 속도 향상을 위해 독점 라이브러리를 분리해야 할 때 유용합니다. **무분별한 주입이 초래하는 문제점** * **추적의 어려움**: 인터페이스와 주입이 남발되면 특정 로직의 실제 동작을 확인하기 위해 생성자의 호출자를 거꾸로 추적해야 하는 수고가 발생합니다. * **호출자 책임 전가**: 하위 클래스의 모든 의존성을 상위 호출자가 해결해야 하므로, 의존성 해결이 연쇄적으로 전달되어 메인 클래스에 과도한 책임이 집중됩니다. * **연관 데이터의 일관성 파괴**: 예를 들어 동일한 `Locale` 값을 사용해야 하는 여러 객체를 각각 주입받을 경우, 실수로 서로 다른 로케일이 전달되어도 컴파일 타임에 이를 감지하기 어렵고 테스트 작성이 까다로워집니다. 의존성 주입은 '할 수 있기 때문'이 아니라 '필요하기 때문'에 수행해야 합니다. 복잡한 비즈니스 로직이나 외부 시스템 의존성은 주입을 통해 유연성을 확보하되, 단순한 값 객체(Value Object)나 유틸리티는 직접 인스턴스화하여 코드의 명확성을 높이는 것을 권장합니다.

toss원문

레거시 정산 개편기: 신규 시스템 투입 여정부터 대규모 배치 운영 노하우까지 (새 탭에서 열림)

토스페이먼츠는 20년 동안 운영되어 온 레거시 정산 시스템의 한계를 극복하기 위해 대대적인 개편을 진행했습니다. 거대하고 복잡한 단일 쿼리 기반의 로직을 객체지향적인 코드로 분산하고, 데이터 모델링을 집계 중심에서 거래 단위로 전환하여 정산의 정확성과 추적 가능성을 확보했습니다. 이를 통해 폭발적인 거래량 증가에도 대응할 수 있는 고성능·고효율의 현대적인 정산 플랫폼을 구축하는 데 성공했습니다. ### 거대 쿼리 중심 로직의 분할 정복 * **문제점:** 수많은 JOIN과 중첩된 DECODE/CASE WHEN 문으로 이루어진 거대한 공통 쿼리가 모든 비즈니스 로직을 처리하고 있어 유지보수와 테스트가 매우 어려웠습니다. * **도메인 및 기능 분리:** 거대 쿼리를 분석하여 도메인별, 세부 기능별로 카테고리를 나누는 분할 정복 방식을 적용했습니다. * **비즈니스 규칙의 가시화:** 복잡한 SQL 로직을 Kotlin 기반의 명확한 객체와 메서드로 재구성하여, 코드상에서 비즈니스 규칙이 명확히 드러나도록 개선했습니다. * **점진적 전환:** 기능을 단위별로 분할한 덕분에 전체 시스템 개편 전이라도 특정 기능부터 우선적으로 새 시스템으로 전환하며 실질적인 가치를 빠르게 창출했습니다. ### 데이터 모델링 개선을 통한 추적 가능성 확보 * **최소 단위 데이터 관리:** 기존의 집계(Sum) 기반 저장 방식에서 벗어나 모든 거래를 1:1 단위로 관리함으로써, 오류 발생 시 원인 추적을 용이하게 하고 데이터 재활용성을 높였습니다. * **설정 정보 스냅샷 도입:** 계산 결과와 함께 당시의 계약 조건(수수료율 등)을 스냅샷 형태로 저장하여, 시간이 지나도 과거의 정산 맥락을 완벽히 복원할 수 있게 했습니다. * **상태 기반 재처리:** 거래별로 독립적인 상태를 기록하는 설계를 통해, 장애 발생 시 전체 재처리가 아닌 실패한 건만 선별적으로 복구할 수 있도록 효율화했습니다. ### 고해상도 데이터 대응을 위한 DB 최적화 * **파티셔닝 및 인덱스 전략:** 정산일자 기준의 Range 파티셔닝과 복합 인덱스를 활용해 데이터량 증가에 따른 조회 성능 저하를 방지했습니다. * **조회 전용 테이블 및 데이터 플랫폼 활용:** 실시간 조회가 필요한 핵심 기능은 전용 테이블로 대응하고, 복잡한 어드민 통계는 고성능 데이터 플랫폼에 위임하여 시스템 부하를 분산했습니다. ### 배치 시스템 성능 극대화 * **I/O 횟수 최소화:** 배치 실행 시점에 가맹점 설정 정보를 전역 캐시에 로드하여 반복적인 DB 조회를 제거했습니다. * **Bulk 조회 및 처리:** Spring Batch의 `ItemProcessor`에서 개별 건별로 I/O가 발생하지 않도록 Wrapper 구조를 도입하여 묶음 단위(Bulk)로 조회하도록 개선했습니다. * **멀티 스레드 활용:** `AsyncItemProcessor`와 `AsyncItemWriter`를 도입하여 단일 스레드 제약을 극복하고 처리 속도를 비약적으로 향상시켰습니다. 이번 개편은 단순히 기술적인 스택을 바꾸는 것을 넘어, 레거시 시스템에 숨겨진 복잡한 비즈니스 맥락을 명확한 도메인 모델로 추출해냈다는 점에서 큰 의미가 있습니다. 대규모 트래픽과 복잡한 정산 규칙을 다루는 시스템이라면, 데이터를 최소 단위로 관리하고 I/O 최적화와 캐싱을 적극적으로 활용하는 아키텍처를 검토해볼 것을 추천합니다.

woowahan원문

우리는 코드처럼 문화도 리팩토링한다 (새 탭에서 열림)

배달의민족 커머스웹프론트개발팀은 조직 규모 확대에 따른 복잡도와 비효율을 해결하기 위해 문화를 코드처럼 리팩토링하며 '경계 없는 파트' 구조를 도입했습니다. 특정 도메인이나 서비스에 갇히지 않고 책임을 확장하는 R&E(Responsibility & Expandability) 원칙을 통해 기술적 통합과 조직의 유연성을 동시에 확보했습니다. 이러한 시도는 서비스 간 장벽을 허물고 구성원들이 커머스 전반을 조망하는 엔지니어로 성장하며, 비즈니스 요구에 기민하게 대응하는 결과로 이어졌습니다. ### 경계 없는 파트와 R&E 중심의 조직 구성 * **전통적 분할 방식의 탈피**: 프로젝트, 페이지, 서비스(B마트/배민스토어) 단위로 조직을 나눌 경우 발생하는 리소스 불균형과 도메인 파편화 문제를 해결하기 위해 고정된 경계를 제거했습니다. * **R&E(Responsibility & Expandability) 도입**: 단순히 주어진 역할만 수행하는 R&R을 넘어, 문제 해결을 위해 업무 영역을 스스로 확장하고 동료를 돕는 'Own It' 정신을 조직 구조에 이식했습니다. * **유연한 리소스 배분**: 약 20명의 프론트엔드 개발자를 3개 파트로 나누되, 특정 도메인에 종속시키지 않고 팀 상황에 따라 업무를 배분하여 병목 현상을 최소화했습니다. ### 기술적 통합을 통한 도메인 확장성 확보 * **통합 아키텍처 구축**: B마트와 배민스토어의 상품 카드 및 상세 화면 등 유사한 UI/UX를 공통 모듈로 추상화하고 API 구조를 맞춤으로써 코드 베이스의 일관성을 확보했습니다. * **엔지니어링 역량 강화**: 개발자들이 고객 서비스의 UX부터 어드민의 데이터 흐름까지 전방위적인 도메인을 학습하게 하여, 특정 기능 담당자가 아닌 커머스 전체를 이해하는 전문가로 성장하도록 유도했습니다. * **리스크 관리(Bus Factor 개선)**: 특정 인원이 부재하더라도 다른 팀원이 맥락을 즉시 이어받을 수 있는 구조를 만들어 프로젝트 중단 위험인 '버스 팩터'를 획기적으로 낮췄습니다. ### 지속적인 개선을 위한 소통과 기록의 리팩토링 * **의사결정 자산화(ADR)**: 단순한 기획 공유인 1Pager 방식에서 나아가, 기술적 결정의 배경과 맥락을 기록하는 ADR(Architecture Decision Record)을 도입해 팀의 지식을 체계적으로 관리합니다. * **루틴의 재설계와 자동화**: 반복적인 업무나 귀찮은 과정을 레거시로 남기지 않고, 자동화와 프로세스 개선을 통해 개발 효율성을 지속적으로 높입니다. * **심리적 안전감 기반의 협업**: '불판'과 같은 자유로운 논의 문화를 통해 실패를 과정으로 수용하고, 질문이 스터디로 이어지는 선순환 구조를 구축했습니다. 성장하는 조직에서 발생하는 비효율을 방치하지 않고, 코드 리팩토링과 같은 관점에서 구조와 문화를 끊임없이 개선하는 태도가 중요합니다. 특히 도메인 간 경계를 허무는 시도는 대규모 서비스 통합이라는 복잡한 비즈니스 과제를 해결하는 데 매우 강력한 전략이 될 수 있습니다.

line원문

코드 품질 개선 기법 25편: 요컨대... 무슨 말이죠? (새 탭에서 열림)

효과적인 코드 리뷰를 위해서는 리뷰 코멘트를 작성할 때 결론인 제안이나 요청 사항을 가장 먼저 제시하고, 그에 따른 근거와 이유는 뒤에 덧붙이는 구조를 취해야 합니다. 이러한 방식은 리뷰 요청자가 코멘트의 핵심을 즉각적으로 파악하게 하여 전체적인 리뷰 프로세스의 효율성을 높여줍니다. 명확한 구조로 작성된 코멘트는 불필요한 재독을 줄이고 제안된 의견의 타당성을 더 빠르게 검증할 수 있게 돕습니다. **불명확한 리뷰 코멘트의 예시와 문제점** * **가변 객체 사용의 위험성**: Kotlin의 `data class`에서 속성을 `var`로 선언하면 외부에서 객체의 상태를 직접 변경할 수 있어, 의도치 않은 시점에 데이터가 수정되는 버그를 유발할 수 있습니다. * **불필요한 인스턴스 공유**: 상태를 업데이트할 때 새로운 불변 인스턴스를 생성하는 대신 동일한 가변 객체를 공유하면 시스템의 견고함이 떨어집니다. * **정보 전달의 지연**: 제안 사항(모든 속성을 `val`로 변경하고 클래스를 분리할 것)이 코멘트의 마지막에 위치하면, 작성자는 긴 설명을 다 읽은 후에야 무엇을 고쳐야 하는지 알게 되어 인지적 부담이 커집니다. **제안 사항 우선 방식의 코멘트 구조화** * **핵심 제안 선행**: 코멘트의 첫머리에 "데이터 업데이트 빈도에 따라 클래스를 분리하고 속성을 `val`로 선언하세요"와 같이 구체적인 액션을 명시합니다. * **근거의 범주화**: 제안 뒤에 붙는 이유는 '객체의 불변성'과 '값의 라이프사이클'처럼 논리적인 항목으로 나누어 설명합니다. * **가독성 향상 기법**: 설명해야 할 항목이 몇 개인지 미리 밝히고(예: "다음 두 가지 측면에 기반합니다"), 각 항목에 제목을 붙여 구조화하면 전달력이 극대화됩니다. **데이터 모델링의 기술적 개선 방향** * **불변성 유지**: `data class`에서는 `var` 대신 `val`을 사용하여 `copy` 함수를 통한 예측 가능한 상태 업데이트를 지향해야 합니다. * **라이프사이클에 따른 분리**: 사용자 ID와 같이 거의 변하지 않는 속성과, 온라인 상태나 상태 메시지처럼 자주 변하는 속성을 별도의 클래스(예: `UserModel`과 `UserStatus`)로 분리하면 잘못된 업데이트를 방지하기 쉬워집니다. 리뷰 코멘트를 작성할 때는 '빠른 이해'를 목표로 결론부터 쓰는 것이 기본입니다. 다만, 상대방이 스스로 답을 찾아보게 하거나 깊은 고민을 유도하고 싶을 때는 의도적으로 중요한 부분을 뒤에 배치하는 전략을 취할 수도 있습니다. 상황에 맞는 적절한 설명 순서가 코드 품질과 팀의 개발 문화를 결정짓는 중요한 요소가 됩니다.

line원문

코드 품질 개선 기법 24편: 유산의 가치 (새 탭에서 열림)

코드에서 로직은 동일하고 단순히 값만 달라지는 경우, 인터페이스와 상속을 사용하는 대신 데이터 클래스와 인스턴스 생성을 활용하는 것이 더 효율적입니다. 상속은 동적 디스패치나 의존성 역전과 같은 특정한 목적이 있을 때 강력한 도구가 되지만, 단순한 값의 차이를 표현하기 위해 사용하면 불필요한 복잡성을 초래할 수 있습니다. 따라서 객체의 속성 값만 변경되는 상황이라면 상속 불가능한 클래스를 정의하고 값만 다른 인스턴스를 만들어 사용하는 것이 코드의 가독성과 예측 가능성을 높이는 지름길입니다. **상속이 과용되는 사례와 문제점** * UI 테마(색상, 아이콘 등)를 적용할 때 인터페이스를 정의하고 각 테마(Light, Dark)별로 클래스를 상속받아 구현하는 방식은 흔히 발견되는 과잉 설계의 예시입니다. * 바인딩 로직이 모든 테마에서 동일함에도 불구하고 상속을 사용하면, 각 하위 클래스가 자신만의 고유한 로직을 가질 수 있다는 가능성을 열어두게 되어 코드 추적을 어렵게 만듭니다. * 특히 단순히 속성 값을 반환하는 목적일 때는 인터페이스를 통한 동적 디스패치가 성능상 미미한 오버헤드를 발생시킬 뿐만 아니라 구조를 복잡하게 만듭니다. **상속을 사용해야 하는 정당한 경우** * **로직의 동적 변경:** 상황에 따라 실행 시점에 로직 자체를 갈아끼워야 하는 동적 디스패치가 필요할 때 사용합니다. * **합 타입(Sum Types) 구현:** Kotlin의 `sealed class`처럼 정해진 하위 타입들의 집합을 정의해야 할 때 유용합니다. * **추상화와 구현의 분리:** 프레임워크의 제약 조건 대응, 의존성 주입(DI), 또는 빌드 속도 향상을 위해 인터페이스가 필요할 때 활용합니다. * **의존성 역전 법칙(DIP) 적용:** 순환 의존성을 해결하거나 의존성 방향을 단방향으로 유지해야 할 때 상속과 인터페이스를 사용합니다. **인스턴스 기반 모델링의 이점** * 인터페이스 대신 모든 속성을 포함하는 단일 클래스(Model)를 정의하고, 각 테마를 이 클래스의 인스턴스로 생성하면 구조가 훨씬 간결해집니다. * Kotlin의 기본 클래스처럼 상속이 불가능한(`final`) 클래스로 정의할 경우, 해당 인스턴스에 고유한 로직이 포함되지 않았음을 보장할 수 있습니다. * 속성 값이 변하지 않는다면 각 인스턴스는 데이터의 묶음으로서만 존재하게 되어, 상태 변화나 로직의 부수 효과를 걱정할 필요 없이 안전하게 재사용할 수 있습니다. 단순히 값만 다른 여러 타입을 구현해야 한다면 먼저 "상속이 반드시 필요한 로직의 차이가 있는가?"를 자문해 보시기 바랍니다. 로직이 같다면 클래스 상속보다는 데이터 모델의 인스턴스를 생성하는 방식이 유지보수하기 훨씬 쉬운 코드를 만들어줍니다.

line원문

코드 품질 개선 기법 23편: 반환의 끝이 에지 케이스의 끝 (새 탭에서 열림)

조기 반환(Early Return)은 에러 케이스를 미리 배제하여 함수의 주요 로직에 집중하게 돕는 훌륭한 기법이지만, 모든 상황에서 정답은 아닙니다. 만약 에러 케이스와 정상 케이스의 처리 방식이 본질적으로 같다면, 이를 분리하기보다 하나의 흐름으로 통합하는 것이 코드의 복잡성을 낮추는 데 더욱 효과적입니다. 무분별한 조기 반환 대신 언어의 특성과 라이브러리 기능을 활용해 에지 케이스를 정상 흐름에 포함시키는 것이 코드 품질 개선의 핵심입니다. ### 조기 반환 대신 정상 케이스로 통합하기 * **빈 컬렉션 순회 활용**: `map`, `filter`, `sum`과 같은 고차 함수는 컬렉션이 비어 있어도 오류 없이 자연스럽게 동작하므로, `isEmpty()`를 통한 별도의 조기 반환 처리가 불필요한 경우가 많습니다. * **Safe Call과 엘비스 연산자**: `null`을 체크하여 조기 반환하는 대신, `?.`(세이프 콜)이나 `?:`(엘비스 연산자)를 사용하면 `null`을 정상적인 데이터 흐름의 일부로 처리할 수 있어 코드가 간결해집니다. * **인덱스 범위 체크의 추상화**: 리스트 인덱스를 직접 조사하기보다 `getOrNull`이나 `getOrElse` 같은 함수를 사용하면, 범위를 벗어난 경우를 `null` 처리 흐름에 통합하여 조건문을 줄일 수 있습니다. ### 속성 의존성 및 예외 처리의 최적화 * **무의미한 대입 배제 지양**: UI 요소의 가시성(`isVisible`)에 따라 텍스트 대입 여부를 결정할 때, 조기 반환으로 대입을 막기보다는 가시성 여부와 상관없이 값을 대입하도록 로직을 통합하는 것이 상태 관리에 더 유리할 수 있습니다. * **flatMap을 이용한 연쇄 함수 호출**: 여러 단계에서 발생하는 예외를 각각 `try-catch`와 조기 반환으로 처리하면 흐름이 복잡해집니다. 이때 `Result` 객체와 `flatMap`을 활용하면 성공과 실패 케이스를 동일한 파이프라인에서 처리할 수 있습니다. * **성능과 가독성의 균형**: 로직을 통합하는 과정에서 인스턴스 생성 등으로 인한 미세한 성능 저하가 발생할 수 있으나, 대부분의 경우 코드의 명확성과 유지보수성이 주는 이점이 더 큽니다. 조기 반환을 작성하기 전, 현재 다루고 있는 에지 케이스가 정말로 '별도의 처리'가 필요한 예외 상황인지, 아니면 '일반적인 처리' 과정에 자연스럽게 녹여낼 수 있는 데이터의 한 형태인지 고민해보는 것이 좋습니다. 에러 케이스와 정상 케이스의 경계를 허물 때 코드는 더욱 단순하고 견고해집니다.

line원문

코드 품질 개선 기법 18편: 함수만 보고 관계는 보지 못한다 (새 탭에서 열림)

코드를 리팩토링할 때 단순히 중첩된 내부 루프를 별도의 함수로 분리하는 것만으로는 가독성을 근본적으로 개선할 수 없습니다. 진정한 코드 품질 향상은 기술적인 구조를 따라가는 것이 아니라, '코드가 무엇을 하는지'라는 의미 단위에 맞춰 함수의 경계를 재설정할 때 이루어집니다. 이를 위해 데이터 조회와 처리 로직을 분리하여 중첩된 구조를 평탄화하는 접근 방식이 필요합니다. ## 단순한 함수 추출의 한계와 문제점 페이지나 청크 단위로 분할된 데이터를 처리할 때 흔히 `while`과 `for`가 중첩된 루프 구조가 나타납니다. 이를 개선하기 위해 내부 루프만 별도 함수로 추출하는 방식은 다음과 같은 한계를 가집니다. * **의미 단위의 파편화**: '모든 아이템 조회'라는 하나의 논리적 흐름이 여러 함수에 걸쳐 분산되어 코드의 전체적인 의도를 파악하기 더 어려워집니다. * **가독성 개선 미비**: 함수의 경계와 의미 단위의 경계가 일치하지 않으면, 호출부의 복잡도는 여전히 높게 유지됩니다. * **구조적 종속성**: 단순히 기존의 중첩 구조를 유지한 채 함수를 나누는 것은 데이터가 가진 물리적 구조(페이지, 청크 등)에 로직이 강하게 결합되는 결과를 초래합니다. ## 의미 단위를 반영한 숲 보기 리팩토링 단순 추출에서 벗어나 코드의 의미를 재구성하는 리팩토링은 로직의 복잡도를 획기적으로 낮춥니다. '모든 아이템 조회'와 '메타데이터 저장'이라는 두 가지 핵심 역할에 집중하여 코드를 재설계해야 합니다. * **추상화된 열(Sequence) 활용**: Kotlin의 `Sequence`나 `Iterator`를 사용하여 중첩된 페이지 구조를 하나의 연속된 데이터 흐름으로 변환합니다. * **중첩 루프의 평탄화**: 데이터를 가져오는 복잡한 로직(페이징 처리 등)을 별도의 생성 함수로 캡슐화하고, 이를 사용하는 쪽에서는 단일 `for` 루프만 사용하도록 단순화합니다. * **yieldAll을 이용한 지연 계산**: `sequence { ... }` 블록 내에서 `yieldAll`을 사용하면 다음 페이지가 필요한 시점에만 데이터를 요청하면서도, 외부에는 단일 리스트처럼 보이게 할 수 있습니다. ## 실용적인 결론 리팩토링 시 단순히 추출하기 쉬운 부분을 떼어내는 것이 아니라, 기존 구조를 유지할지 혹은 의미에 맞게 재구성할지 먼저 고민해야 합니다. 루프 중첩뿐만 아니라 조건 분기나 데이터 구조가 복잡하게 얽혀 있을 때도 '의미 단위'를 기준으로 경계를 나누면 훨씬 읽기 쉽고 관리하기 편한 코드를 작성할 수 있습니다.

line원문

코드 품질 개선 기법 17편: 사상누각 (새 탭에서 열림)

무분별한 빌더 패턴의 사용은 필수 인자의 누락을 런타임 시점에야 발견하게 만들어 코드의 안정성을 해칠 수 있습니다. 견고한 소프트웨어를 구축하기 위해서는 런타임 에러 대신 컴파일 타임에 결함을 발견할 수 있는 생성자나 팩토리 함수를 우선적으로 고려해야 합니다. 특별한 제약 사항이 있는 경우가 아니라면, 프로그래밍 언어의 기능을 활용해 불완전한 객체 생성을 원천 차단하는 것이 코드 품질 개선의 핵심입니다. **빌더 패턴의 한계와 위험성** * 전통적인 빌더 패턴은 필수 인자가 누락되어도 컴파일 단계에서 이를 감지하지 못하며, `build()` 호출 시점에 `IllegalStateException` 등의 런타임 에러를 발생시킨다. * 이는 '사상누각'처럼 기초가 불안정한 코드를 양산하는 결과를 초래하므로, 컴파일러가 인자 누락을 체크할 수 있는 생성자 기반 설계를 지향해야 한다. **기본값이 있는 인자가 많은 경우의 대안** * Kotlin과 같이 기본 인수를 지원하는 언어에서는 빌더 대신 생성자에 기본값을 설정함으로써 인자 전달의 유연성을 확보하고 가독성을 높일 수 있다. * 만약 환경상 빌더 패턴을 반드시 사용해야 한다면, 필수 인자만큼은 빌더의 생성자 인수로 직접 전달받도록 설계하여 누락 가능성을 구조적으로 방지한다. **생성 중인 상태의 처리와 타입 구분** * 빌더 객체를 다른 함수에 인자로 전달해 값을 채우는 방식(출력 인수)은 가독성을 떨어뜨리므로, 값을 반환받아 생성자나 팩토리 함수에 전달하는 방식으로 개선하는 것이 바람직하다. * 객체 생성 로직이 복잡한 파이프라인 형태라면 각 단계마다 서로 다른 타입을 정의함으로써, 유효하지 않은 중간 상태의 객체가 사용되는 것을 방지할 수 있다. **빌더 패턴이 효과적인 상황: 마지막 작업 정의** * 0회 이상 임의의 순서로 적용되는 작업이 있고, 특정 '마지막 작업(terminal operation)'을 통해 최종 결과를 산출해야 하는 경우에는 빌더 패턴과 유사한 구조가 유용하다. * 예를 들어 이미지 편집 과정(crop, filter 등)에서 데코레이터 패턴을 사용할 때, 빌더 형식을 도입하면 순수 데코레이터 패턴보다 중첩 구조가 단순해져 가독성이 크게 향상된다. 객체 생성 시 발생할 수 있는 결함을 런타임이 아닌 컴파일 타임에 검출할 수 있도록, 가장 먼저 생성자나 팩토리 함수 사용을 검토하세요. 빌더 패턴은 언어적 제약이 있거나 특수한 파이프라인 설계가 필요한 경우에만 선택적으로 활용하는 것이 좋습니다.

line원문

코드 품질 개선 기법 15편: 문법은 이름을 나타낸다 (새 탭에서 열림)

개발 시 식별자의 이름을 지을 때는 선언부의 시각적 통일성보다 호출부에서 문법적으로 자연스럽게 읽히는지를 우선해야 합니다. 수식어를 뒤에 붙이는 방식은 목록화했을 때 보기 좋을 수 있으나, 실제 코드에서 사용될 때는 문법적 오해를 불러일으켜 가독성을 저해할 위험이 크기 때문입니다. 따라서 명명법의 기준을 ‘작성자의 편의’가 아닌 ‘사용자의 이해도’에 두는 것이 코드 품질 개선의 핵심입니다. **수식어 위치에 따른 가독성 차이** - 클래스를 분할할 때 `SettingRepositorySecurity`와 같이 수식어를 뒤에 붙이면(Postfix), 정의부에서는 일관성이 있어 보이지만 호출부에서는 문법적 오해를 낳을 수 있습니다. - 예를 들어 해당 클래스를 사용하는 측에서는 이를 'SettingRepository의 보안 모듈'로 잘못 해석할 수 있으며, 이는 코드를 읽는 속도를 늦추는 원인이 됩니다. - 반면 `SecuritySettingRepository`와 같이 수식어를 앞에 붙이는 방식(Prefix)은 영문법에 부합하여 클래스의 정체성을 훨씬 명확하게 전달합니다. **복합 수식어가 필요한 경우의 처리** - 수식어가 여러 개여서 단순히 앞에 나열할 경우 의미가 왜곡될 때는 전치사(of, for, at 등)를 활용하여 의미를 명확히 해야 합니다. - '세로 모드에서의 전송 버튼 높이'를 명명할 때 `portraitSendButtonHeight`는 '세로 이미지를 보내는 버튼'으로 오해받을 수 있으므로, `sendButtonHeightForPortrait`와 같이 표현하는 것이 바람직합니다. - 다만, 클래스나 구조체 같은 타입 이름에는 전치사 사용을 피하는 것이 좋은데, 이는 인스턴스 생성 시 타입 이름의 마지막 단어를 주로 사용하게 되는 관례 때문입니다. **언어 및 플랫폼 관습의 존중** - 문법적 정확성만큼이나 해당 언어 공동체의 관습을 따르는 것도 중요합니다. - Java나 Kotlin의 `currentTimeMillis`처럼 전치사가 생략되어도 충분히 의미가 전달되는 표준 API 사례가 있다면, 이를 참고하여 해당 플랫폼의 스타일과 일관성을 유지해야 합니다. 명확한 이름을 짓기 위해서는 항상 "이 이름을 처음 본 개발자가 문법적으로 오해할 여지가 없는가?"를 자문해 보아야 합니다. 선언부의 정렬된 모습보다는 코드가 실제로 쓰이는 맥락에서의 직관성을 최우선으로 고려할 것을 권장합니다.

line원문

코드 품질 개선 기법 14편: 책임을 부여하는 오직 하나의 책임 (새 탭에서 열림)

단일 책임 원칙(SRP)을 기계적으로 적용하여 클래스를 과도하게 분리하면, 오히려 시스템 전체의 복잡도가 증가하고 사양의 제약 조건을 파악하기 어려워질 수 있습니다. 코드 품질을 높이기 위해서는 개별 클래스의 응집도뿐만 아니라, 분리된 클래스들이 맺는 의존 관계와 호출자가 짊어져야 할 관리 부담을 종합적으로 고려해야 합니다. 결국 핵심적인 제약 조건을 한곳에서 관리할 수 있다면, 약간의 책임이 섞여 있더라도 초기 구현의 단순함을 유지하는 것이 더 나은 선택일 수 있습니다. **과도한 책임 분리가 초래하는 문제** * 동적으로 실행 로직이 변하는 '론치 버튼'을 구현할 때, 버튼 바인딩 책임과 로직 선택 책임을 별도 클래스로 분리하면 각 클래스는 단순해지지만 시스템 구조는 복잡해집니다. * 로직별로 별도의 바인더 인스턴스를 생성하고 `isEnabled` 상태를 통해 실행 여부를 제어하게 되면, 버튼 하나에 여러 개의 리스너가 등록되는 등 내부 동작을 추적하기 어려워집니다. * 결과적으로 "단 하나의 로직만 실행되어야 한다"는 비즈니스 제약 조건을 확인하기 위해 여러 클래스와 루프 문을 모두 훑어야 하는 비용이 발생합니다. **제약 조건의 분산과 상태 중복** * 책임을 분리하면 특정 사양이 코드 전체로 흩어지는 '책임 떠넘기기' 현상이 발생할 수 있습니다. * 예를 들어 어떤 로직이 활성화되었는지 나타내는 상태를 상위 클래스(Selector)에 추가하면, 하위 클래스(Binder)의 `isEnabled` 속성과 데이터가 중복되어 상태 불일치 문제가 생길 위험이 있습니다. * 이러한 중복은 코드의 신뢰성을 떨어뜨리며, 사양 변경 시 수정해야 할 포인트가 늘어나는 결과를 초래합니다. **의존성 비대화와 라비올리 코드(Ravioli Code)** * 세부 사항을 은닉하기 위해 의존 관계를 더 잘게 쪼개면, 이를 조합해야 하는 호출자(Caller)의 코드가 비대해지는 '갓 클래스(God Class)' 현상이 나타날 수 있습니다. * 너무 작은 단위로 쪼개진 클래스들이 서로 얽히면 전체 흐름을 파악하기 위해 수많은 파일을 오가야 하는 '라비올리 코드'가 되어 유지보수성이 저하됩니다. * 객체 지향의 핵심은 캡슐화인데, 제약 조건을 보장하는 로직을 분리해버리면 오히려 캡슐화가 깨지고 외부 의존성만 강해지는 부작용이 생깁니다. **실용적인 설계를 위한 제언** 클래스를 분할할 때는 응집도라는 단일 지표에만 매몰되지 말고, 분할 후의 의존성 그래프와 호출자의 편의성을 반드시 확인해야 합니다. 만약 특정 클래스가 내부에서 핵심 제약 조건을 깔끔하게 관리하고 있다면, 억지로 책임을 나누기보다 그 응집된 구조를 유지하는 것이 시스템 전체의 결합도를 낮추고 코드의 가독성을 높이는 길입니다.

line원문

코드 품질 개선 기법 13편: 클론 가족 (새 탭에서 열림)

두 개의 상속 트리가 서로 암묵적으로 대응하며 발생하는 '클론 가족' 문제는 코드의 타입 안정성을 해치고 유지보수를 어렵게 만듭니다. 이 글은 코드 공통화를 위한 부적절한 상속 사용을 경계하고, 대신 컴포지션이나 제네릭을 활용하여 데이터 모델 간의 관계를 명확히 정의함으로써 런타임 오류 가능성을 줄이는 방법을 제시합니다. ### 클론 가족 현상과 타입 안정성 문제 데이터를 공급하는 클래스 계층과 데이터 모델 계층이 서로 일대일로 대응하지만, 이 관계가 코드상에서 명시되지 않을 때 문제가 발생합니다. * **다운캐스팅의 필요성**: 부모 공급자 클래스가 공통 데이터 모델 인터페이스를 반환할 경우, 실제 사용 시점에서는 구체적인 타입으로 변환하는 다운캐스팅(`as`)이 강제됩니다. * **암묵적 제약 조건**: '특정 공급자는 특정 모델만 반환한다'는 규칙이 컴파일러가 아닌 개발자의 머릿속에만 존재하게 되어, 코드 변경 시 실수로 인한 오류가 발생하기 쉽습니다. * **유연성 부족**: 하나의 공급자가 여러 모델을 반환하거나 구조가 복잡해질 때, 타입 검사만으로는 시스템의 안전성을 보장하기 어렵습니다. ### 상속 대신 애그리게이션과 컴포지션 활용 단순히 로직의 공통화가 목적이라면 상속보다는 기능을 분리하여 포함하는 방식이 더 효과적입니다. * **로직 추출**: 공통으로 사용하는 데이터 획득 로직을 별도의 클래스(예: `OriginalDataProvider`)로 분리합니다. * **의존성 주입**: 각 공급자 클래스가 분리된 로직 클래스를 속성으로 가지도록 설계하면, 부모 클래스 없이도 코드 중복을 피할 수 있습니다. * **타입 명확성**: 각 공급자가 처음부터 구체적인 데이터 타입을 반환하므로 다운캐스팅이 아예 필요 없어집니다. ### 제네릭을 이용한 매개변수적 다형성 적용 여러 공급자를 하나의 컬렉션으로 관리해야 하는 등 상속 구조가 반드시 필요한 경우에는 제네릭을 통해 타입을 명시해야 합니다. * **타입 파라미터 지정**: 부모 클래스에 타입 파라미터 `T`를 도입하여, 자식 클래스가 어떤 타입의 데이터를 반환하는지 컴파일 시점에 명시하도록 합니다. * **상한 제한(Upper Bound)**: 필요한 경우 `T : CommonDataModel`과 같이 제약 조건을 추가하여 최소한의 공통 인터페이스를 보장할 수 있습니다. * **업캐스팅 지원**: 제네릭을 사용하면 부모 타입으로 관리하면서도 각 인스턴스가 반환하는 타입의 안전성을 유지할 수 있어 활용도가 높습니다. 상속은 강력한 도구이지만 단순히 코드를 재사용하기 위한 목적으로 사용하면 의도치 않은 타입 문제를 야기할 수 있습니다. 클래스 간의 관계가 암묵적인 제약에 의존하고 있다면, 이를 컴포지션으로 분리하거나 제네릭을 통해 명시적인 관계로 전환하는 것이 견고한 코드를 만드는 핵심입니다.