Kotlin

30 개의 포스트

airbnb원문

LLM 및 @generateMock (새 탭에서 열림)

에어비앤비는 LLM과 제품 컨텍스트를 결합한 `@generateMock` 지시어를 도입하여, 수동으로 작성하던 GraphQL 모의 데이터 생성 과정을 자동화하고 혁신했습니다. 이 시스템은 단순한 랜덤 값 생성을 넘어 쿼리 정의, 스키마 주석, 그리고 디자인 목업 이미지까지 컨텍스트로 활용해 실제 서비스 환경과 매우 흡사한 타입 안정적(Type-safe) 데이터를 생성합니다. 이를 통해 개발자는 백엔드 구현을 기다리지 않고도 고품질의 데모와 테스트를 수행할 수 있으며, 쿼리 변경에 따른 모킹 데이터의 관리 부담을 획기적으로 줄였습니다. ### 기존 모킹 방식의 한계와 도전 과제 * **수동 작업의 비효율성:** 수백 줄에 달하는 GraphQL 쿼리에 대응하는 JSON 데이터를 직접 작성하고 수정하는 과정은 매우 번거롭고 실수에 취약합니다. * **병렬 개발의 병목:** 서버 스키마가 확정된 후에도 실제 API가 구현될 때까지 클라이언트 개발자는 UI 테스트를 진행하기 어려워 임시방편(하드코딩, 로컬 프록시 등)에 의존하게 됩니다. * **데이터의 동기화 문제:** 쿼리나 스키마가 진화함에 따라 수동으로 작성된 모의 데이터는 점차 실제 프로덕션 환경과 괴리가 생기며, 이는 테스트 신뢰도 저하로 이어집니다. ### @generateMock 지시어를 통한 선언적 모킹 * **지시어 기반 워크플로우:** 개발자는 `.graphql` 파일의 연산, 프래그먼트, 또는 특정 필드에 `@generateMock` 지시어를 추가하는 것만으로 모의 데이터를 정의할 수 있습니다. * **주요 파라미터 활용:** * `id`: 여러 버전의 모의 데이터를 생성할 때 식별자로 사용하며, 생성된 헬퍼 함수의 이름에 반영됩니다. * `hints`: "파리, 교토로 가는 여행 일정을 포함해달라"와 같이 LLM에게 구체적인 데이터 생성을 지시하는 자연어 가이드를 제공합니다. * `designURL`: 디자인 도구(Figma 등)의 URL을 입력하면 LLM이 실제 디자인 화면의 텍스트와 레이아웃에 부합하는 데이터를 생성합니다. * **로컬 개발 도구 통합:** 에어비앤비의 코드 생성 도구인 'Niobe'와 결합되어, 코드 생성 시 JSON 데이터와 이를 로딩하는 소스 코드(TypeScript, Swift, Kotlin)가 자동으로 빌드 아티팩트에 포함됩니다. ### LLM을 활용한 컨텍스트 중심의 데이터 생성 * **스키마 최적화 주입:** 전체 스키마를 LLM에 전달하는 대신, 해당 쿼리와 연관된 타입 및 인라인 문서 주석만을 추출하여 컨텍스트 윈도우 내에서 효율적으로 처리합니다. * **디자인 시각 정보 반영:** 내부 API를 통해 `designURL`의 스냅샷 이미지를 생성하고 이를 LLM에 전달함으로써, 실제 UI 디자인에 명시된 이름, 주소 등의 콘텐츠와 일치하는 현실적인 데이터를 얻습니다. * **수동 수정 및 보존:** 생성된 JSON 데이터는 개발자가 직접 수정할 수 있으며, 이후 다시 코드를 생성하더라도 Niobe는 사용자가 직접 수정한 내용을 지우지 않고 보존하는 지능적인 병합 기능을 제공합니다. 이러한 접근 방식은 단순히 더 나은 가짜 데이터를 만드는 것을 넘어, 프론트엔드와 백엔드 간의 의존성을 분리하고 개발 생산성을 극대화하는 데 목적이 있습니다. 대규모 GraphQL 환경을 운영하는 조직이라면 스키마 메타데이터와 LLM을 결합하여 테스트 자동화 수준을 한 단계 높이는 이 모델을 참고할 가치가 있습니다.

line원문

코드 품질 개선 기법 21편: 생성자를 두드려 보고 건너라 (새 탭에서 열림)

객체의 상태에 따라 특정 메서드 호출이 제한되는 설계는 런타임 에러를 유발하는 주요 원인이 됩니다. 개발자는 주석이나 문서에 의존하기보다, 언어의 문법적 특성을 활용해 '애초에 잘못 사용할 수 없는 구조'를 설계해야 합니다. 이를 위해 생성 시점에 초기화를 완료하거나, 지연 초기화 또는 상태를 분리한 타입을 활용해 객체의 안전성을 보장하는 것이 핵심입니다. **초기화 시점에 로직 실행과 팩토리 함수 활용** 객체를 생성하는 즉시 필요한 준비 작업을 마치는 방식입니다. * **생성자 및 init 블록 사용**: 모든 속성을 생성 시점에 결정하여 읽기 전용(`val`)으로 선언할 수 있어 객체의 불변성을 유지하기 좋습니다. * **생성자의 제약 사항**: 생성자 내에서는 `suspend` 함수 호출이 불가능하며, 복잡한 로직이나 부작용이 큰 코드를 작성할 경우 초기화되지 않은 속성에 접근하는 버그가 발생할 수 있습니다. * **정적 팩토리 함수**: 생성자를 `private`으로 숨기고 별도의 `createInstance` 같은 함수를 제공하면, 복잡한 준비 로직을 안전하게 처리한 뒤 완전한 상태의 인스턴스만 반환할 수 있습니다. **호출 시점에 실행되는 지연 초기화** 준비 작업의 비용이 크지만 실제로 사용되지 않을 가능성이 있을 때 유용한 방식입니다. * **최초 접근 시 실행**: 메서드 내부에서 준비 상태를 확인하고, 필요한 경우에만 로직을 실행하여 리소스를 효율적으로 관리합니다. * **Kotlin의 lazy 위임**: 수동으로 상태 체크 코드를 작성하는 대신 `by lazy`를 활용하면, 스레드 안전성을 확보하면서도 코드를 더 깔끔하게 유지할 수 있습니다. * **가변성 제어**: 수동 구현 시 속성을 가변(`var`)으로 선언해야 하는 단점이 있지만, `lazy`를 사용하면 이를 완화할 수 있습니다. **정적 타입을 활용한 상태 분리** 준비 전과 후의 상태를 별개의 클래스로 정의하여 컴파일 단계에서 오류를 방지하는 방식입니다. * **타입에 따른 권한 부여**: 준비 전 클래스에는 `prepare()`만 정의하고, 이 함수가 준비 완료된 새로운 타입의 객체를 반환하게 하여 `play()`와 같은 핵심 기능을 준비된 객체만 가질 수 있도록 제한합니다. * **컴파일 타임 안전성**: 호출자가 준비 과정을 거치지 않으면 기능을 아예 호출할 수 없으므로, 런타임 예외 발생 가능성을 원천적으로 차단합니다. * **세밀한 제어**: 호출자가 준비 시점을 직접 결정해야 하거나, 준비된 상태의 인스턴스를 캐싱하여 재사용해야 할 때 특히 효과적입니다. **실용적인 제언** 가장 좋은 설계는 사용자에게 주의를 요구하는 대신, 구조적으로 실수를 방지하는 설계입니다. 초기화 비용이 낮다면 생성자나 팩토리 함수를 통한 **즉시 초기화**를 권장하며, 실행 시점을 제어해야 하거나 안전성을 극대화해야 한다면 **상태별 클래스 분리**를 검토하는 것이 좋습니다.

line원문

코드 품질 개선 기법 20편: 이례적인 예외 과대 포장 (새 탭에서 열림)

리소스를 안전하게 해제하기 위해 사용하는 `use` 패턴이나 커스텀 예외 처리 구현 시, 발생한 여러 예외를 하나의 커스텀 예외로 감싸서(wrapping) 던지는 것은 주의해야 합니다. 이러한 '과대 포장'은 호출자가 기대하는 특정 예외 유형을 가려버려 예외 처리 로직을 무력화시키고 디버깅을 어렵게 만듭니다. 따라서 여러 예외가 동시에 발생할 때는 원인이 되는 주요 예외를 우선시하고, 부수적인 예외는 `addSuppressed`를 통해 전달하는 것이 올바른 품질 개선 방향입니다. ### 예외 과대 포장의 부작용 * 리소스 해제 과정에서 발생하는 예외까지 관리하기 위해 `DisposableException` 같은 별도의 예외 클래스로 감싸게 되면, 원래 발생한 구체적인 예외 정보(예: `IOException`)가 추상화되어 버립니다. * 이 경우 호출부에서 특정 예외를 잡기 위해 작성한 `catch(e: IOException)` 문이 작동하지 않게 되어, 의도치 않은 런타임 오류로 이어질 수 있습니다. * 특히 유틸리티 함수나 보조 함수 내부에서 이러한 포장이 일어날 경우, 호출자는 내부 구현을 상세히 알기 전까지는 예외 처리 실패의 원인을 파악하기 매우 어렵습니다. ### `addSuppressed`를 활용한 예외 우선순위 설정 * 한 코드 블록에서 비즈니스 로직과 리소스 해제(dispose) 로직 모두 예외가 발생할 수 있다면, 어떤 예외가 더 중요한지 판단하여 우선순위를 정해야 합니다. * 일반적으로 비즈니스 로직이 실행되는 `block`에서 발생한 예외가 핵심적인 정보를 담고 있으므로 이를 우선적으로 `throw`해야 합니다. * 리소스 해제 시 발생하는 보조적인 예외는 버리지 않고, 주요 예외의 `addSuppressed` 메서드에 추가함으로써 전체적인 예외 맥락을 보존하면서도 타입 시스템을 해치지 않을 수 있습니다. ### 언어별 예외 처리 시 주의사항 * **Kotlin:** `Closeable.use` 확장 함수는 이미 `addSuppressed`를 활용하여 주요 예외를 우선하는 방식으로 구현되어 있으므로, 커스텀 리소스 클래스 제작 시에도 이와 유사한 패턴을 따르는 것이 좋습니다. * **Java:** Checked Exception이 존재하는 Java에서는 예외를 다른 타입으로 감쌀 때 상속 관계를 신중히 고려해야 합니다. * 복구가 불가능한 경우가 아니라면 Checked Exception을 `RuntimeException`으로 함부로 변환하여 던지지 않아야 하며, 부모 예외 타입으로 뭉뚱그려 잡는 과정에서 예외 처리 누락이 발생하지 않도록 주의가 필요합니다. 리소스 해제와 같은 부수적인 작업에서 발생하는 예외가 본래의 실행 목적을 가진 코드의 예외를 덮어쓰지 않도록 설계해야 합니다. 항상 "어떤 예외가 개발자나 시스템에게 더 중요한 정보인가"를 고민하고, 언어에서 제공하는 예외 억제(suppression) 기능을 활용해 예외의 층위를 명확히 관리할 것을 권장합니다.

line원문

코드 품질 개선 기법 19편: 차일드 록 (새 탭에서 열림)

상속 구조에서 자식 클래스가 부모 클래스의 함수를 오버라이딩할 때 발생할 수 있는 결함을 방지하기 위해, 오버라이딩 가능한 범위를 최대한 제한해야 합니다. 부모 클래스의 공통 로직과 자식 클래스의 확장 로직을 분리하지 않으면 `super` 호출 누락이나 책임 범위의 혼선과 같은 버그가 발생하기 쉽습니다. 따라서 부모 클래스에서 전체적인 흐름을 제어하고 자식 클래스는 특정 지점의 로직만 구현하도록 설계하는 '차일드 록(child lock)' 기법이 필요합니다. **기존 오버라이딩 방식의 문제점** * **super 호출 누락의 위험:** 자식 클래스에서 부모의 기능을 실행하기 위해 `super.bind()`를 명시적으로 호출해야 하는 구조는 실수를 유발하기 쉽습니다. 호출을 잊더라도 컴파일 에러가 발생하지 않아 헤더나 푸터가 업데이트되지 않는 등의 버그가 방치될 수 있습니다. * **구현 강제성 부족:** 오버라이딩이 필수적인 상황임에도 불구하고 주석으로만 안내되어 있다면, 개발자가 실수로 구현을 누락할 가능성이 큽니다. * **책임 범위의 모호함:** 하나의 함수(`bind`)가 공통 로직과 개별 로직을 모두 포함하고 있으면 오버라이딩의 책임 범위를 오해하기 쉽고, 결과적으로 자식 클래스에 부적절한 코드가 포함될 수 있습니다. **차일드 록을 통한 구조 개선** * **공통 흐름의 고정:** 부모 클래스의 메인 함수(예: `bind`)에서 `open` 키워드를 제거하여 자식 클래스가 전체 흐름을 수정할 수 없도록 '록'을 겁니다. * **추상 메서드 분리:** 자식 클래스마다 달라져야 하는 로직만 별도의 `abstract` 메서드(예: `updateMessageList`)로 추출합니다. * **템플릿 메서드 패턴 적용:** 부모 클래스의 `bind` 함수에서 공통 로직(헤더/푸터 업데이트)을 실행한 후, 자식 클래스가 구현한 추상 메서드를 호출하는 방식으로 설계합니다. 이를 통해 자식 클래스는 부모의 로직을 신경 쓰지 않고 자신의 역할에만 집중할 수 있습니다. **견고한 상속 설계를 위한 가이드라인** * **super 호출 지양:** 라이프사이클 관리나 플랫폼 API의 제약이 있는 특수한 경우를 제외하고는, 자식 클래스에서 `super`를 호출해야만 기능이 완성되는 구조를 피해야 합니다. * **제어 흐름의 중앙 집중화:** 자식 클래스들이 공통으로 사용하는 함수의 실행 순서나 흐름은 반드시 부모 클래스에서 정의하고 관리해야 합니다. * **캡슐화 강화:** C++의 `private virtual` 기법처럼, 부모 클래스에서만 호출 가능하면서 자식 클래스에서 동작을 재정의할 수 있는 구조를 활용하여 오버라이딩 범위를 엄격하게 제한하는 것이 좋습니다. 상속을 설계할 때는 자식 클래스에게 과도한 자유를 주기보다, 필요한 부분만 안전하게 확장할 수 있도록 제약 장치를 마련하는 것이 시스템의 안정성을 높이는 핵심입니다. 이는 코드 리뷰 과정에서 발견하기 어려운 논리적 오류를 컴파일 단계나 구조적 제약으로 사전에 차단하는 효과를 줍니다.

line원문

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

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

airbnb원문

에어비앤비 (새 탭에서 열림)

에어비앤비는 4.5년에 걸쳐 수천만 라인의 Java, Kotlin, Scala 코드로 구성된 대규모 JVM 모노레포를 Gradle에서 Bazel로 성공적으로 이전했습니다. 이번 마이그레이션을 통해 빌드 속도는 3~5배, IDE 동기화 및 배포 속도는 2~3배 향상되었으며, 개발자 만족도(CSAT)가 38%에서 68%로 크게 올랐습니다. Bazel의 밀폐성(Hermeticity)과 원격 실행 기능을 활용하여 대규모 코드베이스에서도 안정적이고 확장 가능한 빌드 시스템을 구축한 것이 핵심 성과입니다. **Gradle에서 Bazel로 전환한 이유** * **빌드 속도의 혁신:** Bazel의 원격 빌드 실행(RBE)을 통해 수천 개의 작업을 병렬로 처리하며, 'Build without the Bytes' 기능을 도입하여 필요한 아티팩트만 다운로드함으로써 대역폭 소모를 줄였습니다. * **빌드 안정성 및 밀폐성:** Gradle과 달리 샌드박스 환경을 제공하여 빌드 작업이 지정된 입력 외의 파일 시스템(예: /tmp 디렉토리)에 접근하는 것을 차단하고, 환경 차이로 인한 빌드 실패를 방지했습니다. * **통일된 인프라 구축:** 에어비앤비 내의 웹, iOS, Python, Go 등 다양한 언어의 레포지토리를 Bazel로 단일화하여 원격 캐싱, 로깅, 변경된 타겟 계산 로직을 공유할 수 있게 되었습니다. **단계적 마이그레이션과 개념 증명(PoC)** * **Viaduct 플랫폼 선정:** 에어비앤비에서 가장 크고 복잡한 서비스 중 하나인 GraphQL 모놀리스 'Viaduct'를 첫 타겟으로 선정하여, 가장 까다로운 케이스에서 성능 이점을 증명했습니다. * **공존 전략:** 초기에는 개발자가 Gradle과 Bazel 중 선택해서 사용할 수 있도록 두 시스템을 병렬로 운영하여 서비스 중단 위험을 최소화했습니다. * **개발자 설득:** 단순한 성능 향상을 넘어, 초기 단계에서 발생한 버그와 통합 문제를 해결하여 개발자들이 자발적으로 Bazel을 선택하도록 유도했습니다. **자동 빌드 파일 생성 및 유지보수** * **커스텀 생성기 개발:** Bazel 빌드 파일(BUILD)을 수동으로 관리하는 불편을 줄이기 위해 소스 코드의 패키지와 임포트 구문을 분석하여 의존성 그래프를 그리는 자동 생성기를 구축했습니다. * **Gazelle의 영감:** Go 언어의 Gazelle 도구에서 아이디어를 얻었으나, JVM 언어의 특성과 성능 요구사항을 맞추기 위해 캐싱 기능을 포함한 자체 도구로 발전시켰습니다. * **CI 통합:** 모든 커밋 전에 자동 생성기를 실행하여 Gradle과 Bazel의 빌드 그래프가 항상 일치하도록 유지했습니다. **IDE 사용자 경험 개선** * **IntelliJ 동기화 최적화:** 대규모 모노레포에서 Gradle 동기화가 최대 40분까지 소요되던 문제를 Bazel의 병렬 분석과 'Query Sync(실험적 기능)' 도입을 통해 3~10분 수준으로 단축했습니다. * **IntelliJ Aspect 활용:** Bazel의 Aspect 기능을 사용하여 프로젝트 구조 정보를 추출함으로써 IDE가 소스 코드와 의존성을 더 효율적으로 이해하도록 돕습니다. **성공적인 전환을 위한 교훈** 대규모 마이그레이션에서 가장 중요한 것은 **성능에 대한 집착**과 **개발자 경험(DevEx)에 대한 투자**입니다. 빌드 속도가 빨라지면 개발자들은 자연스럽게 새로운 도구를 수용하게 되며, 특히 IntelliJ와 같은 IDE와의 매끄러운 통합이 프로젝트의 성패를 좌우합니다. 또한 빌드 파일 생성과 같은 반복적인 작업을 자동화하여 개발자가 시스템 환경 설정이 아닌 코드 작성에만 집중할 수 있는 환경을 조성하는 것이 필수적입니다.

line원문

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

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

line원문

코드 품질 개선 기법 16편: 불이 'null'인 굴뚝에 연기가 'null'이 아닐 수 없다 (새 탭에서 열림)

널 객체(null object) 패턴은 `null` 대신 '비어 있음'을 나타내는 객체를 사용하여 호출부의 코드를 단순화하고 예외 처리를 줄이는 유용한 디자인 패턴입니다. 그러나 일반적인 상태와 오류 상태를 명확히 구분해야 하는 상황에서 이 패턴을 무분별하게 사용하면, 컴파일러의 정적 검증을 우회하게 되어 오히려 버그를 발견하기 어렵게 만듭니다. 따라서 오류 처리가 필수적인 로직에서는 널 객체 대신 언어 차원의 `null`이나 `Optional` 타입을 사용하여 타입 안정성을 확보하는 것이 권장됩니다. ### 널 객체 패턴의 활용과 장점 널 객체 패턴은 유효하지 않은 값이나 비어 있는 상태를 특정 객체로 정의하여 프로그램의 흐름을 끊지 않도록 돕습니다. - **코드 단순화**: 컬렉션의 경우 `null` 대신 빈 리스트(`.orEmpty()`)를 반환하면 호출 측에서 별도의 널 체크 없이 즉시 순회(iteration) 로직을 수행할 수 있습니다. - **폴백 데이터 제공**: UI 표시를 위한 데이터 모델에서 '알 수 없는 사용자'와 같은 기본 객체를 정의하면, 데이터가 없는 경우에도 화면 레이아웃을 깨뜨리지 않고 기본 정보를 안전하게 보여줄 수 있습니다. - **로직 통합**: 경계 조건이나 오류 상황을 일반적인 비즈니스 로직에 자연스럽게 통합시켜 코드의 가독성을 높입니다. ### 널 객체 패턴이 유발하는 타입 안정성 문제 오류 상태를 일반 객체처럼 취급하게 되면 개발자가 의도적으로 해당 상태를 확인해야 하는 로직을 누락했을 때 이를 잡아낼 방법이 부족해집니다. - **컴파일 타임 검증 부재**: `isInvalid`와 같은 속성으로 오류를 확인해야 하는 널 객체를 사용하면, 확인 로직을 잊더라도 컴파일러는 이를 정상적인 코드로 인식합니다. - **런타임 버그 발생**: 유효하지 않은 널 객체가 시스템 내부에서 계속 전달되다가 예상치 못한 지점에서 오작동을 일으킬 수 있으며, 이는 즉시 런타임 오류가 발생하는 것보다 원인 파악이 더 어렵습니다. - **대안으로서의 정적 타입**: Kotlin의 널 가능 타입(`?`)이나 Swift의 `Optional`을 사용하면 컴파일러가 강제로 널 처리를 요구하므로, 오류 조건과 일반 조건을 명확히 분리하여 처리할 수 있습니다. ### 널 객체 패턴 사용 시 주의할 점: 동일성과 동등성 널 객체를 정의하고 비교할 때는 객체의 비교 방식에 각별히 유의해야 합니다. - **동일성(Identity) 문제**: `UserModel.INVALID`와 같은 정적 인스턴스를 `==` 연산자로 비교할 때, 해당 클래스에 `equals`가 적절히 구현되어 있지 않으면 내용이 같더라도 다른 객체로 판별될 위험이 있습니다. - **값 기반 비교의 한계**: 단순히 기본값(ID 0, 빈 문자열 등)을 채워 넣은 새 객체를 생성해 비교할 경우, 실제 '무효한 상태'를 나타내는 싱글톤 객체와 일치하지 않아 로직 오류가 발생할 수 있습니다. ### 상황에 맞는 도구 선택 제안 널 객체 패턴은 만능 해결책이 아니며, 상황에 따라 적절한 도구를 선택하는 것이 중요합니다. - **널 객체를 권장하는 경우**: 일반적인 경우와 오류/경계 상황을 굳이 구분할 필요가 없거나, '오류'를 나타내는 후보가 너무 많아 정적 검증이 오히려 복잡해질 때 사용합니다. - **정적 타입을 권장하는 경우**: 비즈니스 로직상 오류 상태를 반드시 인지하고 별도의 처리(예: 에러 다이얼로그 표시)를 수행해야 한다면 널 객체 대신 언어에서 제공하는 `null`이나 `Optional`을 활용하여 타입 시스템의 보호를 받아야 합니다.

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`과 같이 제약 조건을 추가하여 최소한의 공통 인터페이스를 보장할 수 있습니다. * **업캐스팅 지원**: 제네릭을 사용하면 부모 타입으로 관리하면서도 각 인스턴스가 반환하는 타입의 안전성을 유지할 수 있어 활용도가 높습니다. 상속은 강력한 도구이지만 단순히 코드를 재사용하기 위한 목적으로 사용하면 의도치 않은 타입 문제를 야기할 수 있습니다. 클래스 간의 관계가 암묵적인 제약에 의존하고 있다면, 이를 컴포지션으로 분리하거나 제네릭을 통해 명시적인 관계로 전환하는 것이 견고한 코드를 만드는 핵심입니다.

line원문

코드 품질 개선 기법 12편: 세트 할인 (새 탭에서 열림)

여러 속성을 개별적으로 변경할 수 있게 허용하는 구조는 상태 간의 불일치를 초래하고 예기치 못한 버그를 유발할 수 있습니다. 이를 해결하기 위해 서로 연관된 상태들을 하나의 객체로 묶어 한 번에 업데이트하도록 인터페이스를 제한하면 시스템의 예측 가능성을 높일 수 있습니다. 결과적으로 코드의 의도가 명확해지고 스레드 안전성 확보와 디버깅이 훨씬 용이해집니다. **개별 속성 변경의 위험성** * **실행 순서 의존성:** 활성화 상태(`isActive`)와 세부 설정(`minImportanceToRecord` 등)이 분리되어 있으면, 설정을 변경하기 전에 활성화를 먼저 시도할 경우 의도치 않게 이전 설정값이 적용되는 문제가 발생합니다. * **상태 초기화의 불일치:** 특정 속성이 변경될 때 내부 카운터가 초기화되어야 함에도 불구하고, 어떤 속성은 초기화를 수행하고 어떤 속성은 누락하는 등의 관리 포인트가 파편화되어 로직이 복잡해집니다. * **경쟁 상태(Race Condition):** 비동기 환경에서 여러 속성을 순차적으로 변경하면, 변경 중간에 다른 로직이 개입하여 불완전한 상태의 데이터를 읽게 될 위험이 있습니다. **데이터 묶음과 인터페이스 제한을 통한 개선** * **객체 캡슐화:** 연관된 설정값들을 `SamplingPolicy`와 같은 별도의 불변(Immutable) 클래스로 묶어 관리함으로써 속성들이 항상 한 세트로 업데이트되도록 강제합니다. * **상태 표현의 최적화:** 별도의 불리언 플래그 대신 상태 객체의 `null` 여부로 활성화 상태를 표현하여, 활성 상태일 때는 반드시 유효한 설정값이 존재함을 보장합니다. * **원자적 업데이트:** `startRecording`과 같이 명시적인 메서드를 통해서만 상태를 변경하게 함으로써 내부 카운터 초기화와 설정 변경이 한 번에(Atomic) 이루어지도록 제어합니다. **실용적인 결론** 단순히 모든 필드에 세터(setter)를 열어두는 것보다, 비즈니스 로직상 함께 움직여야 하는 데이터는 하나의 '상태 객체'로 정의하는 것이 좋습니다. 특히 한 속성의 변화가 다른 속성의 의미나 동작에 영향을 주는 경우에는 인터페이스를 엄격하게 제한하여 잘못된 상태 조합이 발생하는 것을 원천적으로 차단해야 합니다.

line원문

코드 품질 개선 기법 11편: 반복되는 호출에 함수도 지친다 (새 탭에서 열림)

객체의 상태를 확인하고 그 결과에 따라 상태를 변경하는 로직은 호출자가 아닌 해당 객체 내부로 캡슐화하는 것이 코드 품질을 높이는 핵심입니다. 이를 통해 외부로 드러나는 상태 전이 로직을 단순화하고, 조건 확인 누락으로 인해 발생할 수 있는 잠재적인 버그를 효과적으로 방지할 수 있습니다. 특히 상태 변경 여부에 따른 후속 작업이 필요할 때는 복잡한 콜백보다 명확한 반환값을 활용하는 것이 코드의 가독성과 유지보수 측면에서 유리합니다. **상태 확인 로직의 내재화** * `if (receiver.a()) { receiver.b() }`와 같이 외부에서 객체의 상태를 묻고 동작을 결정하는 구조는 중복 호출의 번거로움과 확인 누락의 위험을 수반합니다. * 상태를 변경하는 함수(예: `markAsFriend`) 내부에서 직접 조건을 검사(예: `isFriend`)하도록 설계하면, 호출자는 객체의 내부 상태를 일일이 신경 쓰지 않고도 안전하게 기능을 수행할 수 있습니다. * 이러한 방식은 객체 내부의 상태 전이를 단순화하며, '이미 해당 상태인 경우 아무것도 하지 않는다'는 동작을 자연스럽게 보장합니다. * 만약 조건부 동작임을 명시적으로 드러내야 한다면 `markAsFriendIfNotYet`과 같이 함수 이름을 명확하게 짓거나 주석으로 보완하는 방법이 권장됩니다. **콜백 대신 반환값으로 결과 전달** * 상태 변경 성공 여부에 따라 팝업 노출과 같은 후속 작업이 필요할 때, 고차 함수를 통한 콜백(onSucceeded) 방식은 피하는 것이 좋습니다. * 콜백 방식은 의존성 순환을 일으킬 수 있고, 해당 로직이 동기적으로 실행되는지 비동기적으로 실행되는지 호출부에서 파악하기 어렵게 만듭니다. * 대신 `Boolean` 등의 반환값을 활용하면 호출자가 결과에 따라 후속 로직을 직접 제어할 수 있어 코드의 실행 흐름이 명확해집니다. * 이때 함수 이름에서 반환값의 의미가 명확히 드러나지 않는다면 문서화를 통해 보완하고, 호출자가 반환값을 반드시 확인하도록 강제하는 기법을 함께 사용할 수 있습니다. 객체 설계 시 "묻지 말고 시키라(Tell, Don't Ask)"는 원칙을 적용해 보시기 바랍니다. 객체 외부에서 상태를 묻고 판단하기보다, 객체가 스스로 자신의 상태를 확인하고 동작하게 함으로써 더 견고하고 읽기 쉬운 코드를 작성할 수 있습니다.

line원문

코드 품질 개선 기법 10편: 적절한 거리 유지에 신경 쓰자 (새 탭에서 열림)

코드 품질을 높이기 위해서는 각 레이어나 컴포넌트가 서로의 세부 구현을 알지 못하도록 적절한 거리를 유지하는 것이 중요합니다. 특히 UI와 데이터 레이어가 암묵적인 규칙을 공유하며 의존할 경우, 사양 변경 시 예측하지 못한 버그가 발생하기 쉬우므로 명확한 상태 값과 인터페이스를 통해 책임을 분리해야 합니다. **암묵적 정보 공유의 문제점** * 리포지터리 레이어에서 UI의 표시 형식을 고려해 '최대 개수 + 1'의 데이터를 조회하는 식의 구현은 레이어 간의 경계를 무너뜨립니다. * UI 레이어가 리포지터리의 특정 동작(예: 100개 초과 시 리스트 크기가 101임)에 의존해 비즈니스 로직을 판단하면 코드의 가독성과 유지보수성이 떨어집니다. * 이러한 방식은 주석으로만 의도를 설명할 수 있을 뿐, 코드 구조 자체로는 데이터의 의미를 명확히 전달하지 못하는 한계가 있습니다. **명시적인 속성을 활용한 책임 분리** * 모델 클래스에 `hasMoreItems`와 같은 명시적인 불리언 속성을 추가하여 데이터의 상태를 직접적으로 표현하는 것이 좋습니다. * 리포지터리는 모델 인스턴스를 생성할 때 추가 데이터 존재 여부를 판단하는 로직을 수행하고, UI에는 정제된 데이터만 전달합니다. * UI 레이어는 더 이상 특정 상수값이나 리포지터리의 조회 규칙을 알 필요 없이, 모델이 제공하는 속성에만 기반하여 화면을 구성할 수 있게 됩니다. **로직과 상수의 적절한 위치 선정** * 데이터 개수를 제한하는 상수(`ITEM_LIST_MAX_COUNT`)는 서비스의 성격에 따라 비즈니스 로직 레이어(도메인, 유스 케이스 등)에서 정의하는 것이 이상적입니다. * 비즈니스 레이어를 별도로 두기 어려운 규모라면 모델 클래스 내부에 정의할 수도 있으나, 이때는 데이터 구조와 알고리즘 간의 의존 방향이 모호해지지 않도록 주의해야 합니다. * 특정 기능에 국한된 로직이 범용적인 데이터 모델에 포함되어 재사용성을 해치지 않는지 검토하는 과정이 필요합니다. **실용적인 제언** 코드 작성 시 "이 컴포넌트가 다른 컴포넌트의 내부 사정을 너무 자세히 알고 있지는 않은가?"를 자문해 보시기 바랍니다. 다른 레이어의 세부 동작에 암묵적으로 의존하는 코드를 피하고, 인터페이스를 통해 명확한 정보를 주고받도록 설계하는 것이 변경에 유연한 소프트웨어를 만드는 핵심입니다.

figma3분 읽기큐레이션 요약

왜 우리는 코드가 범

AI가 디자인을 코드로 변환하고 반복적인 구현 작업을 자동화하더라도, 엔지니어의 역할 자체가 사라지는 것은 아니다. 코드 작성은 엔지니어링의 일부일 뿐이며, 사용자의 문제를 정의하고 제약 조건 속에서 적절한 추상화와 시스템을 설계하는 능력은 여전히 사람의 핵심 역량이다. 따라서 코드를 상품화의 대상으로 두려워하기보다, 자동화를 통해 창의성·문제 해결·제품 의사결정에 더 집중해야 한다는 것이 글의 결론이다. ## 디자인과 코드의 경계가 흐려지는 이유 - AI는 프로그래밍 언어 간 변환뿐 아니라 디자인 시안을 실제 코드로 변환하는 작업도 자동화할 수 있다. - Figma 같은 현대적인 디자인 도구는 내부적으로 디자인을 일종의 구조화된 코드 형태로 저장한다. - 따라서 디자인을 TypeScript·React 또는 Kotlin·Jetpack 코드로 바꾸는 일은 본질적으로 한 표현 방식에서 다른 표현 방식으로 번역하는 작업에 가깝다. - 그러나 목업을 코드로 바꾸는 능력이 곧 좋은 제품이나 시스템을 만드는 능력을 의미하지는 않는다. ## 코드 작성보다 중요한 엔지니어링의 본질 - 엔지니어링은 단순히 코드를 출력하는 일이 아니라 다음을 포함한다. - 어떤 문제를 해결해야 하는지 판단하기 - 문제를 해결할 적절한 방법 선택하기 - 복잡한 시스템을 이해하기 쉬운 추상화로 구성하기 - 정확하고 단순하며 유지보수 가능한 해법 만들기 - 뛰어난 엔지니어는 특정 언어·프레임워크·플랫폼의 세부 문법에만 의존하지 않고, 여러 기술에 공통된 원리를 바탕으로 사고한다. - React나 Jetpack 같은 특정 기술의 전문성은 시간이 지나면 가치가 감소할 수 있지만, 사용자의 요구와 시스템 제약을 해석하는 능력은 오래 지속된다. - AI가 부족한 영역은 사용자의 실제 필요, 제품 맥락, 기술적 제약을 종합해 우아하고 직관적인 시스템으로 설계하는 일이다. ## 반복 작업 자동화와 엔지니어의 창의성 - AI는 반복적이고 정형화된 작업을 줄여 엔지니어가 더 창의적인 문제에 집중하도록 도울 수 있다. - 하나의 기능을 구현하는 방법은 여러 가지일 수 있으며, AI는 기존에 고려하지 않았던 대안을 빠르게 제시할 수 있다. - 최종적으로 어떤 방식을 선택할지는 성능, 복잡도, 유지보수성, 사용자 경험 등의 트레이드오프를 평가하는 사람의 판단에 달려 있다. - 엔지니어의 창의성은 제약을 제거하는 데만 있지 않고, 제약을 활용해 더 나은 기술적·제품적 선택을 만들어내는 데 있다. - 기술 시스템의 구현은 단순한 코딩 작업이 아니라 팀의 제품 결정을 개선하는 창의적 활동이다. ## 구현자에서 문제 정의자·조정자로의 역할 변화 - Figma의 엔지니어들은 출근 후 곧바로 코드를 작성하기보다 팀원들과 사용자 문제를 논의하고 해결 우선순위를 정한다. - 무엇을 만들지, 왜 만들어야 하는지, 어떤 방식이 적절한지를 합의한 뒤에야 구현에 들어간다. - AI가 하위 수준의 기술 작업을 자동화할수록 엔지니어가 직접 신경 써야 할 구현 세부사항은 줄어든다. - 그 결과 엔지니어의 업무는 다음 영역으로 더 이동한다. - 사용자 문제의 발견과 해석 - 문제의 우선순위 설정 - 팀 간 정렬과 의사결정 - 시스템 구조와 품질 기준 설계 - 즉, 엔지니어의 추상화 수준이 높아지고 구현 자체가 전체 업무에서 차지하는 비중은 작아진다. ## 생산성보다 중요한 개발 속도와 가치 전달 - AI 도입의 효과를 단순히 “더 많은 코드를 더 빨리 작성하는 것”으로 측정해서는 안 된다. - 중요한 것은 코드 출력량이 아니라, 올바른 문제를 선택하고 사용자에게 가치 있는 결과를 얼마나 빠르게 전달하는가이다. - 잘못 정의된 문제를 AI로 빠르게 구현하면 오히려 불필요한 기능과 기술 부채가 빠르게 늘어날 수 있다. - 따라서 AI는 구현 자동화 도구이면서 동시에 여러 해결책을 탐색하고 제품 실험을 가속하는 도구로 활용해야 한다. AI 시대에 엔지니어는 특정 프레임워크의 코드 작성 능력보다 문제 정의, 시스템 사고, 트레이드오프 판단, 사용자 맥락 이해를 강화하는 것이 좋다. 반복 구현은 AI에 맡기되, 무엇을 만들지와 어떤 구조가 장기적으로 좋은지는 사람이 책임지는 방식이 바람직하다.

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