💡 리뷰 활동을 하면서 디미터의 법칙을 상기시켜보라는 리뷰를 받게 되었습니다. 객체 지향 프로그래밍에서 중요하게 적용되는 디미터의 법칙에 대하여 학습하였습니다.
디미터의 법칙?
- Law of Demeter
- 어떤 객체가 다른 객체에 대해 지나치게 많이 알다보니, 결합도가 높아지고 좋지 못한 설계가 발생합니다.
- 이를 개선하고자 객체에게 내부 값을 숨기는 대신 함수를 공개하도록 하는 것이 디미터의 법칙입니다.
- 다른 객체가 어떠한 내부 값을 가지고 있는지 속사정을 몰라아 햔다는 뜻으로, 아래와 같이 불리기도 합니다.
- Don’t Talk to Stranger (낯선 이에게 말하지 마라)
- Principle of lesat knowledge (최소 지식 원칙)
- 또한 직관적인 이해를 위해서 여러 개의 .(도트)를 사용하지 않도록 권장하고 있습니다.
디미터의 법칙의 장점
- 디미터의 법칙을 준수함으로써 캡슐화를 높혀 객체의 자율성과 응집도를 높일 수 있습니다.
- 낮은 결합도
- 객체들 간에 낮은 결합도를 유지하게 되어, 시스템이 유연해지고 유지보수가 쉬워집니다.
- 향상된 모듈성
- 각 객체가 자신의 책임에 충실하고 다른 객체의 내부 구조를 알 필요가 없으므로, 모듈성이 향상됩니다.
- 이로인해 코드의 재사용성이 높아지고, 시스템을 더 쉽게 확장할 수 있습니다.
- 테스트 용이성
- 객체 간의 상호작용이 단순해지고 예측 가능해지므로, 단위 테스트가 쉬워집니다.
- 이로인해 테스트 작성 시 모킹(mocking)을 간단하게 할 수 있습니다.
- 가독성 향상
- 객체 간의 상호작용이 단순하고 명확해지므로, 코드의 가독성이 향상됩니다.
- 변경에 대한 탄력성
- 내부 구조가 바뀌더라도, 외부에서 접근하는 방식이 일관성을 유지하기 때문에 변경에 대한 탄력성이 높아집니다.
- 이로인해 시스템의 안정성을 높일 수 있습니다.
객체 지향 프로그래밍과의 관계
- 객체 지향 프로그래밍에서 가장 중요한 것은 “객체가 어떤 데이터를 가지고 있는가?” 보단, “객체가 어떤 메세지를 주고 받는가?”이다.
- 디미터의 법칙이 위배된다는 것은 올바른 객체 지향 프로그래밍을 하지 못한다고 할 수 있다.
OOP
- OOP는 소프트웨어 설계와 개발에서 중요한 패러다임으로 아래와 같은 개념을 가지고 있습니다.
- 추상화 : 복잡한 시스템을 이해하기 쉽게 단순화
- 캡슐화 : 데이터와 메서드를 하나의 단위로 묶고, 내부 ****구현을 숨김
- 상속 : 클래스 간의 계층 구조를 형성하여 코드의 재사용성을 높임
- 다형성 : 동일한 인터페이스를 사용하여 다른 구현을 제공
- 디미터의 법칙은 객체가 자신이 직접 소유하거나 생성한 객체와만 상호작용을 제한합니다.
- 이를 활용해서 OOP에 중요한 설계를 할 수 있도록 도움을 줄 수 있습니다.
- OOP의 핵심 개념과 디미터의 법칙은 상호 보완적인 관계에 있으며, 이를 잘 적용하면 견고하고 유연한 소프트웨어 시스템을 설계할 수 있습니다.
디미터의 법칙 활용하기
- 우선 디미터의 법칙을 잘 지키지 못한 경우를 확인 해보겠습니다.
class Engine {
fun start() {
print("Engine Started")
}
}
class Car(val endgine: Engine)
class Driver(private val car: Car) {
fun drive() {
car.engine.start()
// driver가 car의 engine에 직접 접근하여 start() 메서드 실행
}
}
fun main() {
val engine = Engine()
val car = Car(engine = engine)
val driver = Driver(car)
driver.drive()
// Driver가 Car의 연산에 직접 접근
// 이는 결합도가 높다고 할 수 있다.
}
- 위는 디미터의 법칙을 지키지 않았으며, 결합도가 높은 설계라고 할 수 있습니다.
class Engine {
fun start() {
print("Engine Started")
}
}
class Car(val endgine: Engine) {
fun start() {
engine.start()
}
}
class Driver(private val car: Car) {
fun drive() {
car.start()
}
}
fun main() {
val engine = Engine()
val car = Car(engine = engine)
val driver = Driver(car)
driver.drive()
// Driver가 Car의 start()만 호출하므로
// Engine의 내부 구조를 알 수 없습니다.
}
- Cart에 start() 함수를 생성해야 하는 번거로움이 생겼지만, 확실하게 아래와 같은 이점을 얻었습니다.
- 캡슐화를 통해서 내부 구현을 숨김
- 다중 도트 활용을 방지
- 결합도를 낮출 수 있음