라인 / code-quality

29 개의 포스트

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

코드 품질 개선 기법 28편: 제약 조건에도 상속세가 발생한다 (새 탭에서 열림)

코드의 불변성을 보장하기 위해 설계된 클래스가 상속을 허용할 경우, 자식 클래스에서 해당 제약을 위반함으로써 시스템 전체의 안정성을 해칠 수 있습니다. 특히 `Immutable`이라는 이름을 가진 클래스가 가변적인 자식 클래스를 가질 수 있게 되면 개발자의 의도와 다른 런타임 동작이 발생할 위험이 큽니다. 따라서 특정 제약 조건을 강제하고 싶다면 클래스를 상속 불가능하게 설계하거나, 공통의 '읽기 전용' 인터페이스를 활용하는 구조적 접근이 필요합니다. ### 불변성 보장을 방해하는 상속 구조 * Kotlin의 `IntArray`를 래핑하여 성능과 불변성을 동시에 잡으려는 `ImmutableIntList` 예시를 통해 상속의 위험성을 설명합니다. * 클래스를 상속 가능(`open`)하게 설정하면, `Immutable`이라는 명칭에도 불구하고 이를 상속받아 내부 상태를 변경하는 `MutableIntList`와 같은 자식 클래스가 생성될 수 있습니다. * 외부에서는 `ImmutableIntList` 타입으로 참조하더라도 실제 인덱스 값이 변할 수 있는 객체를 다루게 되어, 불변성을 전제로 한 로직에서 오류가 발생합니다. ### 멤버 오버라이딩을 통한 제약 조건 우회 * 내부 데이터 구조를 `private`이나 `protected`로 보호하더라도, 메서드 오버라이딩을 통해 불변성 제약을 우회할 수 있습니다. * 예를 들어 `get` 연산자를 오버라이딩하여 내부 배열이 아닌 가변적인 외부 필드 값을 반환하도록 재정의하면, 클래스의 핵심 규약인 '불변 데이터 제공'이 깨지게 됩니다. * 범용적인 클래스일수록 예상치 못한 곳에서 잘못된 상속이 발생할 가능성이 높으므로, 어떤 멤버를 노출하고 오버라이딩을 허용할지 엄격하게 제한해야 합니다. ### 가변·불변 객체의 올바른 상속 관계 * 가변 객체가 불변 객체를 상속하면 불변성 제약이 깨지고, 불변 객체가 가변 객체를 상속하면 불필요한 변경 메서드(`add`, `set`)로 인해 런타임 에러가 발생할 수 있습니다. * 가장 이상적인 구조는 가변 객체와 불변 객체가 모두 '읽기 전용(Read-only)' 인터페이스나 클래스를 상속받는 형태입니다. * 가변 객체는 읽기 전용 부모의 메서드 집합을 확장하고, 불변 객체는 읽기 전용 부모의 제약 조건을 확장하는 방식(예: Kotlin의 `List` 구조)이 안전합니다. 특정 제약 조건(불변성 등)이 핵심인 클래스를 설계할 때는 기본적으로 상속을 금지(`final`)하고, 확장이 필요하다면 상속 대신 독립된 타입을 정의하거나 읽기 전용 인터페이스를 통한 계층 분리를 권장합니다.

line

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

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

line

코드 품질 개선 기법 26편: 설명의 핵심은 첫 문장에 있다 (새 탭에서 열림)

코드의 가독성과 유지보수성을 높이기 위해서는 주석의 첫 번째 문장에 가장 핵심적인 정보를 담는 것이 중요합니다. 단순히 코드의 동작 과정을 나열하기보다 높은 추상화 수준에서 코드의 목적을 먼저 설명해야 하며, 이를 통해 읽는 사람이 전체 맥락을 빠르게 파악할 수 있도록 유도해야 합니다. 주석의 유형에 따라 가장 중요한 요소(반환값, 존재 이유, 이상적인 상태 등)를 선별하여 서두에 배치하는 것이 문서화의 핵심입니다. **효율적인 문서화 주석 작성법** - **첫 문장에서 개요 전달**: 문서화 주석은 전체를 다 읽지 않고 첫 번째 문장만 읽어도 해당 함수나 클래스의 역할을 이해할 수 있도록 작성해야 합니다. - **추상화 수준 높이기**: "마침표로 분할한다"와 같은 구현 디테일보다는 "문장 단위로 분할한다"처럼 코드보다 높은 수준의 용어를 사용하여 의도를 명확히 합니다. - **중요 정보 우선 배치**: 함수의 경우 '무엇을 반환하는지'가 가장 중요하므로, 반환값의 의미를 첫 문장에 기술하고 구체적인 구분자나 예외 처리 조건은 뒷부분에 보충합니다. - **구체적인 예시 활용**: 경계 조건이나 복잡한 입력값이 있을 때는 주석 하단에 `input -> output` 형태의 예시를 추가하여 이해를 돕습니다. **임시 방편 및 인라인 주석의 구성** - **존재 이유 명시**: 특정 버그를 회피하기 위한 코드나 임시 방편적인 코드에서는 '무엇을 하는지'보다 '왜 이 코드가 필요한지'를 먼저 설명해야 합니다. - **맥락 제공**: 예를 들어 "기기 X의 버그를 해결하기 위함"이라는 목적을 서두에 두면, 나중에 코드를 읽는 작업자가 해당 로직의 삭제 여부를 더 쉽게 판단할 수 있습니다. **효과적인 TODO 주석 작성** - **목표 상태 우선 기술**: TODO 주석에서는 앞으로 수행해야 할 리팩토링의 방향이나 '이상적인 상태'를 가장 먼저 작성합니다. - **제약 사항 후순위 배치**: 현재 상태의 문제점이나 즉시 수정하지 못하는 기술적 부채에 대한 설명은 목표를 명시한 뒤에 덧붙이는 것이 가독성에 유리합니다. 코드의 의미는 코드가 스스로 말해야 하지만, 그 너머의 의도와 비즈니스 로직의 맥락은 주석의 첫 문장이 결정합니다. 읽는 이의 시간을 아끼기 위해 가장 중요한 핵심부터 말하는 습관을 들이는 것이 좋습니다.

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

코드 품질 개선 기법 22편: To equal, or not to equal (새 탭에서 열림)

Java와 Kotlin에서 객체의 등가성을 정의하는 `equals` 메서드는 반드시 객체의 동일성(Identity)이나 모든 속성이 일치하는 등가성(Equivalence) 중 하나를 명확히 표현해야 합니다. 식별자(ID)와 같은 일부 속성만 비교하도록 `equals`를 잘못 구현하면, 상태 변경을 감지하는 옵저버블 패턴에서 데이터 업데이트가 무시되는 심각한 버그를 초래할 수 있습니다. 따라서 특정 속성만 비교해야 하는 상황이라면 `equals`를 오버라이딩하는 대신 별도의 명시적인 함수를 정의하여 사용하는 것이 안전합니다. ### 부분 비교 `equals` 구현의 위험성 * 객체의 식별자(ID) 등 일부 필드만 사용하여 `equals`를 구현하면, 객체가 논리적으로는 변경되었음에도 기술적으로는 '같은 객체'로 판정되는 모순이 발생합니다. * `StateFlow`, `LiveData`, `Observable` 등의 프레임워크는 이전 데이터와 새 데이터를 `equals`로 비교하여 변경 사항이 있을 때만 UI를 업데이트합니다. * 만약 사용자의 식별자는 같지만 닉네임이나 상태 메시지가 변경된 경우, 부분 비교 `equals`는 `true`를 반환하므로 화면에 변경 사항이 반영되지 않는 버그가 발생합니다. ### 올바른 등가성 정의와 대안 * **동일성(Identity):** 두 객체의 참조가 같은지를 의미하며, 특별한 구현이 필요 없다면 Java/Kotlin의 기본 `equals`를 그대로 사용합니다. * **등가성(Equivalence):** 모든 속성과 필드가 같을 때 `true`를 반환하도록 설계해야 합니다. Kotlin에서는 `data class`를 사용하면 생성자에 선언된 모든 필드를 비교하는 `equals`가 자동으로 생성됩니다. * **명시적 비교 함수:** 특정 식별자만 비교해야 하는 로직이 필요하다면 `hasSameIdWith(other)`와 같이 의도가 명확히 드러나는 별도의 함수를 정의하여 사용하는 것이 좋습니다. ### 구현 시 주의해야 할 예외와 맥락 * **Kotlin data class의 제약:** `data class`는 생성자 파라미터에 정의된 속성만 `equals` 비교에 사용합니다. 클래스 본문에 선언된 변수(`var`)는 비교 대상에서 제외되므로 주의가 필요합니다. * **캐시 필드의 제외:** 계산 결과의 캐시값처럼 객체의 논리적 상태에 영향을 주지 않고 성능 최적화를 위해 존재하는 필드는 등가성 비교에서 제외해도 무방합니다. * **도메인 맥락에 따른 설계:** 유리수(1/2과 2/4)의 예시처럼, 모델이 단순한 '표시용'인지 '수학적 계산용'인지에 따라 등가성의 기준이 달라질 수 있으므로 개발 목적에 맞는 신중한 정의가 필요합니다. 객체의 등가성을 설계할 때는 해당 객체가 시스템 내에서 어떻게 관찰되고 비교될지를 먼저 고려해야 합니다. 특히 데이터 바인딩이나 상태 관리를 사용하는 환경에서는 `equals`가 객체의 전체 상태를 대변하도록 엄격하게 구현하고, 식별자 비교는 명시적인 명칭의 메서드로 분리하는 것이 코드의 예측 가능성을 높이는 방법입니다.

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`을 사용하면 다음 페이지가 필요한 시점에만 데이터를 요청하면서도, 외부에는 단일 리스트처럼 보이게 할 수 있습니다. ## 실용적인 결론 리팩토링 시 단순히 추출하기 쉬운 부분을 떼어내는 것이 아니라, 기존 구조를 유지할지 혹은 의미에 맞게 재구성할지 먼저 고민해야 합니다. 루프 중첩뿐만 아니라 조건 분기나 데이터 구조가 복잡하게 얽혀 있을 때도 '의미 단위'를 기준으로 경계를 나누면 훨씬 읽기 쉽고 관리하기 편한 코드를 작성할 수 있습니다.

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`을 활용하여 타입 시스템의 보호를 받아야 합니다.