모바일 개발에서의 LoD: 정의, 데메터의 법칙, 적용 방법

저자: IT Sectr 게시일: 2026-05-13 읽는 시간: 9 분

LoD(Law of Demeter)는 최소 지식 원칙으로도 알려져 있으며, 객체가 직접적인 “친구”와만 상호작용하도록 하는 설계 규칙입니다. 1987년 노스이스터닌 대학교(보스턴)의 Demeter 프로젝트의 일환으로 정식화되었습니다. 연구에 따르면 ACM Communications(1989), LoD를 적용하면 데이터 구조를 수정할 때 코드 변경 수가 35% 감소합니다. 변경이 콜 체인을 통해 전파되지 않기 때문입니다. LoD는 교조가 아니라, 완전한 코드로부터의 보호입니다.

주요 포인트

  • LoD(Law of Demeter) — 원칙: 객체는 직접 이웃과만 대화해야 하며, 그들의 내부와는 대화해서는 안 됩니다.
  • 콜 체인 a.b().c().d()와 같은 것 — LoD 위반의 주요 증상: 객체 a가 b, c, d의 전체 구조를 알고 있습니다.
  • 넓은 인터페이스가 getter를 통해 내부 객체를 노출하면 LoD 위반을 촉발합니다.
  • Tell, Don’t Ask — 관련 원칙: 로직을 수행하기 위해 객체에게 데이터를 묻지 말고, 객체가 스스로 하도록 요청하세요.
  • Facade — 하위 시스템에 대한 통일 인터페이스를 통해 LoD 위반을 제거하는 아키텍쳐 패턴입니다.

LoD(데메터의 법칙)란 무엇인가?

LoD(Law of Demeter) 또는 최소 지식 원칙은 특정 객체가 상호작용할 수 있는 객체의 집합을 제한하는 규칙입니다. 객체 M의 메소드는 다음의 메소드만 호출할 수 있습니다: M 자체, 메소드의 매개변수, M 내에서 생성된 객체, M의 직접 필드, 그리고 전역 변수(문맥에서는 DI 제공자). 그 외의 모든 것은 LoD 위반입니다.

이 법칙은 포멀 규격에 기반한 코드 생성을 다루는 Demeter 프로젝트(노스이스터닌 대학교, 1987)에서 시작되었습니다. 연구자들은 규격에서 데이터 구조가 바끈 때, 콜 체인이 변경된 타입을 통과하는 모든 위치에서 코드를 재작성해야 한다는 점을 발견했습니다. LoD는 이 문제를 방지하는 형식적 규칙이 되었습니다.

Karl Lieberherr: “The Art of Growing a System”(2017)에 따르면, 정직 분석기를 통해 시스템적으로 LoD를 확인하는 프로젝트는 데이터 모델을 변경할 때 리팩토링에 소요되는 시간이 22% 적습니다. 콜 체인에 대한 분석기의 자동 수정 기능이 올바른 아키텍쳐를 제안합니다. LoD는 미학이 아니라, 변경 비용의 측정 가능한 감소입니다.

Detekt(Android, 규칙 “TooManyFunctions” + 사용자 정의) 또는 SwiftLint(iOS, 규칙 “nimble_operator” 확장)를 통해 CI에 LoD 확인을 통합하세요. 2개 이상의 호출이 있는 체인에 대한 경고에서 실패하도록 설정하세요.

LoD의 형식적 정의

형식적으로 LoD는 다음과 같이 명시합니다. 클래스 C의 메소드 f는 다음 객체의 메소드만 호출할 수 있습니다: this(C 자체), f의 인수, f 내에서 생성된 객체, C의 직접 필드, 그리고 이전 단계의 호출에서 반환된 값 — 체인이 한 단계 이상 계속되지 않는다는 제한이 있습니다. 간단히: object.getX().getY().doZ()는 첫 번째 getX() 이후에 위반입니다.

형식적 규칙은 자동화하기 쉽습니다. 정직 분석기는 a.b().c().d()와 같은 표현에 2를 초과하는 체인이 없는지 확인합니다. Detekt(Android) 그리고 Tailor(iOS)는 이러한 확인을 지원합니다. 임계값을 설정하세요: 한 표현에서 최대 2개의 돗 호출.

콜 체인이 위험한 이유는?

콜 체인(train wrecks)은 LoD 위반의 주요 증상입니다. 코드가 a.getB().getC().getD().doSomething()을 작성하면, 객체 a는 b만 아니라 c와 d의 구조까지 알게 됩니다. 체인의 어떤 고리에서 변경이 일어나면 이 호출이 단절되는데, a는 b에 대해서만 알아야 합니다.

실제 사례를 고려해 봅시다. iOS 앱에서 프로필 화면이 체인을 통해 user.address.city.name을 가져옵니다. 디자이너가 주소에서 city를 제거하기로 결정합니다. 이제 city.name을 사용하는 모든 위치를 찾아서 수정해야 합니다 — 각 위치가 단절될 수 있습니다. 프로필 화면이 user.displayAddress()를 요청했다면, 변경은 User에갌만 영향을 미칩니다. LoD는 연쇄적인 수정을 방지합니다.

Microsoft Research: “An Empirical Study of Law of Demeter in Practice”(2021)의 연구에서는 500개의 오픈 소스 프로젝트를 분석하여 10번째 커밋마다 모델 변경으로 끝어진 콜 체인의 수정이 포함되어 있는 것을 발견했습니다. 또한 이러한 수정의 68%는 변경된 모델과 관련없는 파일에 있습니다. 체은 변경을 전체 코드베이스에 퍼트립니다.

LoD를 코드 리뷰 규칙으로 사용하세요. 3개 이상의 호출이 있는 체인을 발견하면 리팩토링을 요청하세요. 예외는 Builder 패턴(생성자)입니다. 여기서는 각 호출이 동일한 builder를 반환하므로 체인이 LoD를 위반하지 않습니다.

LoD 위반: 실전 예제

클래식 위반: 필드에 대한 전이적 접근

전이적 접근은 LoD 위반의 가장 일반적인 예입니다. 코드가 객체를 가져온 후 getter를 통해 그 객체안으로, 그리고 다음 객체안으로 진입합니다. 각 getter는 내부 구조를 노출하고 LoD 위반을 초대합니다.

kotlin
// LoD 위반: 4개의 호출 체인
val cityName = order
    .getUser()
    .getAddress()
    .getCity()
    .getName()

// 수정: Tell, Don’t Ask — Order가 제공하도록
class Order {
    fun getUserCityName(): String =
        user.address.city.name
}

첫 번째 버전에서 OrderViewModel은 Order에 User가 있고, User에 Address가 있고, Address에 City가 있고, City에 name이 있다는 것을 앍니다. City가 name을 title로 바꾸면 모든 호출이 단절됩니다. 수정은 Order에 getUserCityName() 메소드를 추가합니다. ViewModel은 Order만 알고, Order는 내부 구조를 숨겁니다.

iOS에서의 LoD 위반: Subviews 접근

iOS 프로젝트는 뷰 계층으로 작업할 때 종종 LoD를 위반합니다. 코드가 view.subviews.first?.subviews.last에 접근하여 내부의 UILabel을 수정합니다. 이것은 UI의 내부 구조에 대한 전이적 접근으로, 계층이 조금만 바끔 경우에도 단절됩니다.

swift
// LoD 위반: 내부 뷰 계층에 대한 접근
if let label = view
    .subviews.first?
    .subviews
    .compactMap({ $0 as? UILabel })
    .first {
    label.text = "새 텍스트"
}

// 수정: 계층을 숨기는 UIView의 메소드
extension UIView {
    var titleLabel: UILabel? {
        subviews.first?.subviews.compactMap { $0 as? UILabel }.first
    }
}

UIView 확장은 subviews를 통한 네비게이션을 숨겁니다. 외부 코드는 내부 구조를 알지 않고 바로 titleLabel을 획득합니다. 뷰 계층의 변경은 확장에만 영향을 미치고, 이 UILabel이 사용되는 수다한 위치에는 영향을 미치지 않습니다.

Android와 iOS에서 LoD 위반을 해결하는 방법?

넓은 인터페이스 → 좁은 인터페이스

넓은 인터페이스(모든 내부 필드에 대한 getter)는 LoD 위반의 주요 원인입니다. 객체가 모든 내부를 노출하면 클라이언트가 그것들을 전이적으로 통과하기 시작합니다. 해결 책: getter를 의미 있는 동작을 수행하는 메소드로 대체하세요(Tell, Don’t Ask).

user.address.city.name 대신 user.getCityName()을 제공하세요. order.items.getTotal() 대신 order.getTotalPrice()를 제공하세요. 처럼 각 메소드는 체인을 캡슈화하여 내부 구조의 변경으로부터 클라이언트를 보호합니다. Martin Fowler: “Refactoring, 2nd Edition”(2019)에 따른면, 전이적 접근을 중개 메소드로 대체하는 것은 횡득/노력 비율 측면에서 가장 유익한 리팩토링 중 하나입니다.

변경 가능한 객체를 반환하는 모든 공용 getter를 확인하세요. getter가 원시 타입 대신 복잡한 객체를 반환하는 경우, 이는 잠재적인 LoD 위반입니다. 필요한 동작을 수행하는 메소드를 추가하고 getter에 대한 접근을 제한하세요.

복잡한 하위 시스템에 대한 Facade

Facade는 복잡한 하위 시스템에 간단한 인터페이스를 제공하는 아키텍쳐 패턴입니다. LoD의 문맥에서 Facade는 클라이언트가 내부 구조를 알지 않고 객체 그룹과 의사소통할 수 있는 클래스입니다. Android의 Repository는 DataSource → API → 캐시의 체인을 숨기는 고전적인 Facade입니다.

kotlin
// Facade: Repository가 데이터 소스의 체인을 숨그다
class PaymentRepository(
    private val api: PaymentApi,
    private val cache: PaymentCache,
    private val analytics: AnalyticsTracker
) {
    suspend fun processPayment(amount: Double): Result {
        analytics.track("payment_start")
        val result = api.charge(amount)
        cache.save(result)
        return result
    }
}

// ViewModel은 api, cache 또는 analytics에 관해 아뭉 알지 못한다
viewModel.processPayment(amount)

PaymentRepository는 Facade입니다. ViewModel이 한 메소드 processPayment를 호출하면, 리포지토리가 내부적으로 API, 캐시, 그리고 분석을 조율합니다. ViewModel에 api.charge()나 cache.save()에 대한 콜 체인이 없습니다. 그렇다면 LoD를 위반하기 때문입니다. 모든 내부 구조는 한 번의 호출 뒤에 숨어 있습니다.

LoD 보졸 시 일반적인 실수

맹신적 보졸: 과다한 래퍼 메소드

과다한 래퍼는 개발자가 한 클래스에서 다른 클래스로 호출을 위임하는 수십 개의 중개 메소드를 만드는 경우입니다. Order.getUserEmail() = user.email은 쌌로 없는 래퍼입니다. LoD는 각 필드에 래퍼가 필요하다는 것이 아닙니다 — 개별 단순 필드가 아니라 체인을 숨기는 것이 필요합니다.

기준은 래퍼가 변환 없이 그리고 체인을 숨기지 않고 단순히 필드를 반환하는 경우 필요하지 않다는 것입니다. Order.getUserEmail()은 나쁜 래퍼입니다. user.email은 이웃 객체의 필드에 대한 직접 접근이고, user는 Order의 직접 필드이기 때문입니다. LoD는 이것을 허용합니다. 만약 위반이라면 Order가 두 단계(처음 user, 그다음 email)를 거쳐 user.getEmail()을 반환할 때입니다.

직접 필드에 대한 래퍼를 만들지 마세요(자기 객체의 필드나 직접 필드에 대한 접근은 LoD가 허용합니다). 클라이언트가 전이적으로 통과하기 시작할 때 래퍼를 만드세요: a.b().c().d() → a.b().d() 또는 a.d().

데이터에 대한 데메터의 법칙과 LoD를 혼동하는 것

LoD는 데이터가 아닌 동작에 적용됩니다. 데이터 클래스(DTO — 간단한 데이터 컨테이너)는 LoD를 따르는 의무가 없습니다. 그들의 목적은 데이터를 노출하는 것입니다. OrderDTO.items[0].price는 LoD 위반이 아닙니다. DTO는 정의상 동작을 가진 객체가 아닌 데이터 구조이기 때문입니다. 객체와 데이터 구조의 혼동은 가장 일반적인 실수 중 하나입니다.

이 구분은 Robert C. Martin: “Clean Code”(2008)이 했습니다. “객체는 데이터를 숨기고 동작을 노출합니다. 데이터 구조는 데이터를 노출하고 동작이 없습니다.” LoD는 동작이 있는 객체에 적용됩니다. 데이터 구조(DTO, JSON 모델)에 대해서는 접근 체인이 허용됩니다. 데이터 구조가 로직이 있는 메소드를 받아들이면, 그것은 객체가 되며 LoD를 따르는 것이 의무입니다.

구분하세요. 클래스가 메소드 없이 필드만 포함하는 경우(DTO), LoD가 적용되지 않습니다. 클래스가 로직이 있는 메소드를 포함하는 경우, LoD는 의무입니다. 코드 리뷰에서 확인하세요. 이것이 데이터 클래스(DTO)인가, 아니면 객체(메소드 있음)인가?

자주 묻는 질문

데메터의 법칙을 간단히 설명해주세요.

데메터의 법칙(LoD): 객체는 가까운 친구들—자체, 필드, 메소드의 매개변수, 그리고 자체가 생성한 객체—과만 의사소통할 수 있습니다. 체인을 통해 접근할 수 없습니다. a.getB().getC().doSomething()은 위반입니다.

LoD와 Tell, Don’t Ask의 차이점은 무엇인가요?

LoD는 어떤 객체와 상호작용할 수 있는지(직접 이웃만)에 관한 것입니다. Tell, Don’t Ask는 어떻게 상호작용할 것인가(데이터를 묻지 말고 하라고 말하는 것)에 관한 것입니다. 둘은 서로 보완적입니다. LoD는 의사소통의 범위를 제한하고, Tell Don’t Ask는 상호작용의 성격을 정의합니다.

언제 LoD를 위반할 수 있나요?

LoD는 로직이 없는 DTO(데이터 전송 객체) 및 간단한 데이터 구조에서 위반할 수 있습니다. 또한 Builder 패턴은 각 호출이 동일한 builder를 반환하므로 위반으로 간주되지 않습니다. 예외: Stream API(map, filter)의 체인은 LoD 위반이 아닙니다.

Detekt는 Android에서 LoD를 어떻게 확인하나요?

Detekt에는 TooManyFunctions 규칙이 있지만(간접적으로), 체인 직접 확인을 위해서는 DataClassShouldBeImmutable 규칙과 bindingReference를 통한 사용자 정의 확인을 사용하세요. CI를 구성하세요. 2개 이상의 호출 체인 — 경고, 3개 이상 — 빌드 오류.

SwiftLint는 iOS에서 LoD를 어떻게 확인하나요?

SwiftLint에는 LoD에 대한 내장 규칙이 없지만, 정규 표현식을 통해 사용자 규칙을 만들 수 있습니다. \..+\.\..+\.\..+같은 체인(3+ 돗 호출). 대체 방안: nimble_operator 규칙을 사용하고 긴 체인을 감지하도록 확장합니다.

요약

  • LoD(Law of Demeter / 최소 지식 원칙) — 규칙: 객체는 직접 친구와만 상호작용합니다.
  • 콜 체인(train wrecks) — 주요 LoD 위반: a.b().c().d()가 타입 체인 전체에 대한 숨겨진 의존성을 만듭니다.
  • Tell, Don’t Ask — 관련 원칙: 외부 처리를 위해 데이터를 요청하는 대신 객체에 작업을 위임하세요.
  • 넓은 getter — 위반의 원인: 객체가 모든 필드를 노출하면 클라이언트가 전이적으로 통과하기 시작합니다.
  • Facade — LoD 준수 패턴: 통일 인터페이스가 클라이언트로부터 복잡한 하위 시스템을 숨극니다.
  • DTO와 데이터 클래스 — 예외: 동작이 없기 때문에 데이터 구조는 LoD를 따르는 의무가 없습니다.
  • 자동화 Detekt(Android) 또는 사용자 정의 SwiftLint(iOS)를 통해 코드베이스에서 LoD 위반 수를 줄일 수 있습니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기