Software Design

디미터의 법칙(Law of Demeter), 메서드 체이닝이 나쁜 코드가 되는 순간

코드 리뷰를 하다 보면 이런 지적을 한 번쯤 받게 됩니다.

이석우iOS Developer4분 읽기
디미터의 법칙(Law of Demeter), 메서드 체이닝이 나쁜 코드가 되는 순간 대표 이미지

코드 리뷰를 하다 보면 이런 지적을 한 번쯤 받게 됩니다.

“이 줄, 점(.)이 너무 많은데요?”

한 줄에 쭉 이어 쓰면 오히려 깔끔해 보이는데, 뭐가 문제일까요?

이 이야기의 핵심이 바로 디미터의 법칙입니다.

오늘은 디미터의 법칙이 뭔지, 그리고 왜 어떤 메서드 체이닝은 괜찮고 어떤 건 나쁜 코드가 되는지를 예시와 함께 풀어볼게요.

디미터의 법칙, 나쁜 체이닝과 좋은 체이닝의 차이부터 봅니다
디미터의 법칙, 나쁜 체이닝과 좋은 체이닝의 차이부터 봅니다

먼저 결론부터 말씀드릴게요.

디미터의 법칙은 “낯선 객체에게 말 걸지 말라”는 규칙입니다.

메서드 체이닝 자체가 나쁜 게 아니라, 체이닝으로 남의 내부까지 파고들 때 나쁜 코드가 됩니다.

이 한 문장만 기억해도 오늘 글의 절반은 챙기신 거예요.


디미터의 법칙이란

디미터의 법칙(Law of Demeter)은 ’최소 지식 원칙’이라고도 불립니다.

쉽게 말하면 이래요.

어떤 객체는 자기가 직접 아는 ’가까운 친구’하고만 대화해야 한다는 겁니다.

친구의 친구, 그 친구의 또 다른 친구까지 끌어다 쓰기 시작하면 문제가 생겨요.

메서드 안에서 호출해도 되는 대상은 보통 이렇게 정리됩니다.

  • 자기 자신(self)의 메서드
  • 메서드의 매개변수로 넘어온 객체
  • 메서드 안에서 직접 생성한 객체
  • 자신이 들고 있는 프로퍼티(멤버) 객체

딱 이 범위 안에서만 대화하자는 거예요.

여기서 ‘한 줄에 점을 하나만’ 이라는 유명한 경험칙도 나왔습니다.

물론 점 개수는 어디까지나 힌트일 뿐, 절대 규칙은 아니에요.


메서드 체이닝은 왜 나쁜 코드가 될까?

제가 실제로 겪었던 코드로 보여드릴게요.

주문에서 고객의 도시 이름을 꺼내는 상황이었습니다.

// 주문 → 고객 → 주소 → 도시까지 줄줄이 파고든다
let city = order.customer
                .address
                .city
                .name

얼핏 깔끔해 보이죠.

하지만 이 한 줄은 Order가 Customer의 내부, 그 안의 Address, 또 그 안의 City까지 전부 알고 있다는 뜻이에요.

문제는 여기서 터집니다.

만약 Address 구조가 바뀌거나 City가 사라지면, 이 코드도 같이 무너져요.

남의 집 서랍 속까지 손을 넣고 있으니, 그 집 구조가 바뀌면 내 코드가 깨지는 거죠.

이렇게 객체 그래프를 타고 내려가는 체이닝을 흔히 ’기차 사고(train wreck)’라고 부릅니다.

객차처럼 줄줄이 엮인 기차 사고 체이닝
객차처럼 줄줄이 엮인 기차 사고 체이닝

객차가 줄줄이 이어진 모양이라서요.

점이 줄줄이 이어진 이 한 줄이 바로 기차 사고예요
점이 줄줄이 이어진 이 한 줄이 바로 기차 사고예요

그럼 좋은 체이닝과 나쁜 체이닝은 어떻게 구분하나요?

여기가 오늘 글에서 가장 중요한 부분이에요.

메서드 체이닝이라고 다 나쁜 게 아니거든요.

판단 기준은 하나입니다.

“체이닝하면서 남의 내부 객체를 계속 꺼내 쓰는가?”

앞의 예시처럼 customer → address → city로 서로 다른 남의 객체를 계속 파고들면 위반이에요.

반대로 아래처럼 같은 종류의 대상을 다루며 자기 자신을 계속 돌려주는 체이닝은 괜찮습니다.

// filter·map 체이닝은 매번 '같은 맥락'을 돌려주므로 위반이 아니다
let names = users
    .filter { $0.isActive }
    .map(\.name)

이런 고차 함수 체이닝이나 빌더 패턴은 점이 아무리 많아도 남의 내부를 헤집지 않아요.

매번 자기 자신(같은 흐름)을 돌려주며 이어질 뿐이죠.

두 코드를 표로 비교하면 이렇게 정리됩니다.

구분 나쁜 체이닝 좋은 체이닝
대상 남의 내부 객체를 계속 꺼냄 같은 흐름/타입을 반환
예시 order.customer.address filter { }.map { }
결합도 높음(구조 변경에 취약) 낮음
판단 디미터 위반 위반 아님

위반 코드, 이렇게 고칩니다

해결법은 생각보다 간단합니다.

“묻지 말고 시켜라(Tell, Don’t Ask)“를 떠올리면 돼요.

내부를 꺼내 오지 말고, 그 일을 해달라고 부탁하는 겁니다.

앞의 도시 예시라면 이렇게 바꿀 수 있어요.

// Order에게 필요한 결과만 물어본다
let city = order.shippingCity
묻지 말고 시켜라, 이렇게 한 줄이면 충분하더라고요
묻지 말고 시켜라, 이렇게 한 줄이면 충분하더라고요

Order 내부에서 알아서 Customer와 Address를 거쳐 도시를 돌려주도록 만드는 거죠.

이렇게 하면 Address 구조가 바뀌어도 바깥 코드는 멀쩡합니다.

수정 범위가 Order 안쪽으로 딱 갇히거든요.

제가 직접 이런 식으로 리팩터링해 보니, 나중에 구조를 바꿀 때 손댈 곳이 확 줄더라고요.


마무리로 정리하면 이래요.

디미터의 법칙은 점 개수를 세라는 규칙이 아니라, 남의 내부에 함부로 손대지 말라는 이야기입니다.

다음에 긴 체이닝을 만나면 딱 하나만 물어보세요.

“이거, 남의 서랍을 열고 있는 건가?”

이 질문 하나만 던져 봐도 코드가 한결 깔끔해집니다.