루비로 배우는 객체지향 디자인
3. 의존성 관리하기
의존성 관리에 대해서 이야기하기 전에 먼저 한가지 알아두고 가야 할 것이 있습니다.
객체지향에서 어떤 행동 (우리가 원하는 결과라고도 볼 수 있겠네요.) 을 유발 할 때는 객체가 그 행동을 이미 알고 있거나 상속 받았거나 또는 그 행동을 알고 있는 다른 객체에 대해 알아야만 합니다.
2장에서는 그 행동을 알고 있을 때, 즉 자체적으로 구현된 행동에 대해서 이야기를 했다면 상속에 대해서는 6장에서 살펴볼 것이고 이번 장에서는 다른 객체에 의해 구현된 행동에 접근하는 법에 대해서 이야기를 하려고 합니다.
의존성은 곧 다른 객체에게 의존적일 경우 의존성이 있다고 하는데 이러한 의존성은 다음과 같은 상황에서 발생합니다. 하나의 객체를 수정 했을 때 다른 객체들을 뒤따라 수정해야 할 경우, 후자는 전자에게 의존적이라고 할 수 있습니다.
객체간의 메세저 전송을 통해 행동하는 객체지향 에서 이러한 의존성은 어쩔 수 없이 생깁니다. 왜냐면 메세지 전송을 위해서는 받는쪽도 보내는쪽도 서로에 대해 알아야 할 필요성이 있기 때문입니다. 다만 이러한 의존성을 관리한다면 좀 더 변경에 유연한 코드를 작성 할 수 있고 하나의 객체가 변경 되었더라도 최소한의 변경으로 코드를 관리 할 수 있게 됩니다.
결합이라는 용어에 대해 간단하게 이야기를 하고 넘어가겠습니다. 서로 다른 객체가 있는데 이 두 객체가 서로에 대해 알아야 하는 부분이 많으면 많아질수록 객체들은 서로 떨어질 수 없게 되고 이를 결합이라고 합니다.
다음은 불필요한 의존성으로 불리는 4가지 체크사항 입니다.
- 다른 클래스의 이름
- 자기 자신을 제외한 다른 객체에 전송할 메세지의 이름
- 메세지가 필요로 하는 인자들
- 인자들을 전달하는 순서
만약 4가지 중에 해당하는 부분이 있다면 불필요한 의존성, 객체 하나를 수정 했을때 결합된 객체를 수정해야만 하는 의존성 을 가지고 있게 됩니다.
이러한 의존성을 관리하는 방법은 아래와 같습니다.
각 클래스가 자신이 해야하는 일을 하기 위한 최소한의 지식만을 알고 그 외에는 아무것도 모르도록 하는 것
다음은 베드 케이스로 소개된 사례입니다.
- 하나의 객체가 다른 객체에 대해 알고 있는데 이 객체가 무언가를 알고 있는 다른 객체를 알고 있는 경우
- 테스트 코드에 대한 의존성
이 두가지 케이스는 첫째는 하나의 변경점이 생겼을 때 모든 객체를 수정 해야할 필요가 생기고 이러한 행동은 흔히 자주 보는 하나를 수정했더니 다른곳에서 문제가 터져버렸다!!! 할 때 자주 쓰이는 것 같네요.
두번째는 테스트 코드가 실제 코드와 너무 결합되어 있으면 테스트 코드 역시도 계속 수정을 해줘야 하는 문제를 낳게 된다는 이야기 입니다. 이 부분에 대해서는 저도 어느정도 공감하고 있었는데 9장, 비용-효율적인 테스트 디자인하기 에서 알려준다고 하네요.
의존성을 관리하는 방법에는 크게 2가지가 있지만 세세하게는 여러가지가 더 있습니다.
- 약하게 결합된 코드 작성하기
- 의존성 주입하기
- 의존성 격리시키기
- 인자 순서에 대한 의존성 제거하기
- 의존성 방향 관리하기
- 의존성의 방향 바꾸기
- 의존성의 방향 결정하기
의존성 주입하기는 한가지 중요한 점을 기억하면 좋습니다.
해당 행동을 할 때 객체의 클래스가 무엇인지 보다 우리가 전송하는 메세지가 무엇인지에 대해서 아는 것, 이것이 중요합니다.
이전의 Gear 클래스를 가져오겠습니다.
class Gear
attr_reader :chainring, :cog, :rim, :tire
def def initialize(chainring, cog, rim, tire)
@chainring = chainring
@cog = cog
@rim = rim
@tire = tire
end
def ratio
chainring / cog.to_f
end
def gear_inches
ratio * Wheel.new(rime, tire).diameter
end
end
이 코드의 가장 큰 문제점은 gear_inches 가 오직 Wheel 인스턴스의 기어인치만을 계산하게 되어 있다는 점입니다. 이 부분 때문에 gear 와 wheel 은 강하게 결합되어 있다고 이야기 할 수 있습니다.
여기서 gear 는 사실 diameter 만 알고 있으면 기어 인치를 계산 할 수 있습니다.
gear 가 사실상 알고 있어야 하는게 뭔지 알았으니 Wheel 에 대한 의존성을 줄이는 방식으로 코드를 다시 작성해보겠습니다.
class Gear
attr_reader :chainring, :wheel
def def initialize(chainring, cog, wheel)
@chainring = chainring
@cog = cog
@wheel = wheel
end
def ratio
chainring / cog.to_f
end
def gear_inches
ratio * wheel.diameter
end
end
이렇게 되면 gear 에서는 wheel 의 diameter 가 있다는 사실만 알고 있으면 되고 diameter 을 가지고 있는 객체 @wheel 만 전달되면 어떤 것이든 기어 인치를 구할 수 있게 됩니다.
이런 기술을 의존성 주입 (dependency injection) DI 라고 부릅니다.
의존성 격리시키기는 기술적으로 불필요한 모든 의존성을 현실적으로 제거하지 못할 경우에 사용하면 좋습니다.
이번에는 이전의 단일 책임 원칙을 따르는 클래스 디자인하기 에서 적당한 시점이 왔을 때 어디를 수정하고 무엇을 제거 해야하는지 쉽게 알 수 있도록 추가적인 책임을 고립 시키는 방법에서 착안해 나눠놓는 방법을 이야기 합니다.
- 인스턴스 생성을 격리시키기
- 외부로 전송하는 메세지 중 위험한 것들을 격리시키기
gear 객체에 의존성 주입을 할 수 없을 때, Wheel 객체를 생성하는 과정을 gear 내부에 격리시켜보도록 하겠습니다.
class Gear
attr_reader :chainring, :cog, :rim, :tire
def def initialize(chainring, cog, rim, tire)
@chainring = chainring
@cog = cog
@wheel = Wheel.new(rime, tire)
end
def ratio
chainring / cog.to_f
end
def gear_inches
ratio * wheel.diameter
end
end
class Gear
attr_reader :chainring, :cog, :rim, :tire
def def initialize(chainring, cog, rim, tire)
@chainring = chainring
@cog = cog
@rim = rim
@tire = tire
end
def ratio
chainring / cog.to_f
end
def gear_inches
ratio * wheel.diameter
end
def wheel
@wheel ||= Wheel.new(rime, tire)
end
end
첫번째 코드는 생성자 내부에 wheel 인스턴스 생성을 함으로 gear 인스턴스를 생성 할때 무족건 wheel 인스턴스를 생성한다는 의존성을 뚜렷하게 나타낼 수 있습니다. 두번째 코드는 wheel 인스턴스가 필요한 순간이 왔을 때 wheel 객체를 생성하도록 구현되어 있습니다.
완전히 의존성이 줄어들지는 않았지만 그래도 wheel 에 의존하고 있다는 사실을 좀 더 뚜렷하게 나타 낼 수 있고 오히려 이런 부분을 보임으로 재사용과 추후 코드 수정이 용이하게 되었습니다.
먼저 외부로 전송되는 메세지는 ‘나 자신이 아닌 객체에게 보내는 메세지’ 입니다.
gear_inches 메서드의 diameter 는 wheel 객체에 보내는 메세지 입니다. 지금은 gear_inches 메서드가 간단하지만 중간 과정에서 많은 연산이 필요해진다면? gear 가 wheel 에게 의존한다는 사실이 쉽게 보이지 않게 될 수 있습니다.
따라서 wheel.diameter 를 캡슐화 했습니다.
def gear_inches
ratio * diameter
end
def diameter
wheel.diameter
end
물론 처음부터 코드를 DRY 하게 유지하기 위해 diameter 메서드를 만들 수도 있었지만 타이밍이 다릅니다.
모든 외부 메서드 호출에 대해 꼭 이렇게 대응할 필요는 없지만 코드를 꼼꼼히 살펴보고 가장 위태로운 의존성을 찾아내 내부 메서드로 감싸는 작업은 시도해 볼만 합니다.
그런데 이런 문제를 제거하는 방법에는 시작 지점으로 돌아가 의존성의 방향을 반대로 돌려버리는 방법이 있습니다.
하지만 그전에 인자 순서에 대한 의존성을 제거하는 방법을 간단하게 설명하겠습니다.
기존의 생성 메서드를 보면 순서를 맞춰야만 가능한데 이를 해시로 변경하는겁니다.
def def initialize(args)
@chainring = args[:chainring]
@cog = args[:cog]
@rim = args[:rim]
@tire = args[:tire]
end
코드가 길어진것처럼 보이지만 나중에 코드를 관리하는 측면에서는 훨씬 이득인 부분이 많습니다.
여기에 기본 값을 설정하는 방법을 취할텐데 해당 해쉬 키 값이 없을 경우에도 사용 할 수 있는 fetch 라는 루비 메서드를 사용하도록 하겠습니다.
def initialize(args)
@chainring = args.fetch(:chainring, 40)
@cog = args.fetch(:cog, 18)
@rim = args.fetch(:rim, 11)
@tire = args.fetch(:tire, 21)
end
마지막으로 메서드를 직접 수정 할 수 없는 고정된 인자값을 가진 메서드를 사용할 때의 방법으로 Wrapper 모듈을 사용하는 방법 입니다.
외부에 있어 코드를 변경 할 수 없는 객체를 생성해야 할 때가 생겼다고 가정하겠습니다.
module SomeFramework
class Gear
attr_reader :chainring, :cog, :wheel
def def initialize(chainring, cog, wheel=nil)
@chainring = chainring
@cog = cog
@wheel = wheel
end
def ratio
chainring / cog.to_f
end
def gear_inches
ratio * wheel.diameter
end
end
end
gear 객체의 생성 부분만 감싸주는 Wrapper 모듈을 작성해보겠습니다.
module GearWrapper
def self.gear(args)
SomeFramework::Gear.new(args[:chainring], args[:cog], args[:wheel])
end
end
이런 기술은 변경할 수 없는 외부 인터페이스에 의존 해야하는 경우에 쓰기 좋은 방법입니다.
이제 의존성의 방향을 관리하는 방법을 알아볼텐데 그전에 의존성의 방향이 중요한 이유에 대해서 알아보려고 합니다.
왜 의존성의 방향이 중요한가 생각을 해보면 어떤 행동을 할 때, 코드를 작성할 때 수정은 불가피합니다. 다만 수정이 많은 클래스와 덜 수정하는 클래스가 있을 때 어느쪽을 의존할까 방향을 정할때는 덜 수정하는 클래스쪽으로 의존하는게 좋습니다. 이에 대한 내용을 좀 더 다듬으면 다음과 같습니다.
- 어떤 클래스는 다른 클래스에 비해 요구사항이 더 자주 바뀐다.
- 구현 클래스는 추상 클래스보다 수정해야하는 경우가 번번히 발생한다.
- 의존성이 높은 클래스를 변경하는 것은 코드의 여러 곳에 영향을 미친다.
그렇다면 변경 가능성이 높다는건 어떻게 판단하는게 좋을지 정리해보겠습니다.
어떤 코드를 작성할 때 이 코드는 루비로 만들어져 있으며 특정 루비 프레임워크를 사용한 로직이 담겨있습니다. 가장 변경 가능성이 낮은 부분부터 순위를 매겨보겠습니다.
- 루비 베이스 클래스
- 프레임워크 클래스
- 우리가 작성하는 클래스
수정은 이루어지지만 비교적 적은 수정이 이루어진다는 점을 참고해서 보시면 이해 하실 수 있습니다. 다만 프레임워크의 경우에는 조금 다를 수 있는게 생긴지 얼마 안된 프레임워크의 경우에는 수정이 번번하게 일어날 수 있습니다.
다음으로 고민할 것은 구현 클래스와 추상 클래스로 나뉘는 부분을 어떻게 고려할지 입니다.
의존성 주입으로 만들어진 gear 클래스는 diameter 메세지에 반응하는 객체를 필요로 하게 됩니다. 이 객체는 어떤 객체인지 모르지만 diameter 을 가지고 있어야 한다는 생각을 하고 구현되어 있습니다. 이러한 어떤 객체인지 모르겠지만에 해당 하는 부분이 추상적인 생각을 코드로 구현한 부분입니다.
책에는 의존성이 높은 클래스를 만들었을때의 높은 대가 지불에 대한 것과 문제가 되는 의존성을 찾는 방법에 대해서 더 설명하고 있지만 내용 정리는 여기서 마무리 해도 될 것 같습니다.