오브젝트와 의존관계
분리와 확장을 고려한 설계
관심사의 분리(Separation of Concerns)
관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이다.
관심의 종류에 따라 코드를 구분해놓으면 한 가지 관심에 대한 변경이 일어날 경우 그 고나심이 집중되는 부분의 코드만 수정하면 된다. 관심이 다른 코드가 있는 메소드에는 영향을 주지도 않을뿐더러, 관심 내용이 독립적으로 존재 하므로 수정도 간단하다.
변화의 성격이 다르다는 것은 변화의 이유와 시기, 주기 등이 다르다는 뜻이다.
확장
템플릿 메소드 패턴
슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법
팩토리 메소드 패턴
서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것
위의 두 방법은 상속을 사용했다는 단점이 있음, 상속 자체는 간단해 보이고 사용하기도 편리하게 느껴지지만 많은 한계점이 있다.
한계점
대상 클래스가 이미 상속을 사용하고 있다면 자바에선 다중 상속을 허용하지 않으므로 서로 다른 목적으로 상속을 사용할 수 없다.
상속을 통한 상하위 클래스의 관계는 생각보다 밀접하다. 서브클래스는 슈퍼클래스의 기능을 직접 사용할 수 있고, 그래서 슈퍼클래스 내부의 변경이 있을 때 모든 서브클래스를 함께 수정하거나 다시 개발해야 할 수도 있다.
인터페이스
두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이다.
추상화란 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이다. 자바가 추상화를 위해 제공하는 가장 유용한 도구는 바로 인터페이스다.
인터페이스는 자신을 구현한 클래스에 대한 구체적인 정보는 모두 감춰버린다. 결국 오브젝트를 만들려면 구체적인 클래스 하나를 선택해야겠지만 인터페이스로 추상화 해놓은 최소한의 통로를 통해 접근하는 쪽에서는 오브젝트를 만들 때 사용할 클래스가 무엇인지 몰라도 된다.
관계 설정 책임의 분리
new ConcreteService() 라는 코드는 매우 짧고 간단하지만 그 자체로 충분히 독립적인 관심사를 담고 있다. 바로 Client가 어떤 Service 구현 클래스의 오브젝트를 이용할지를 결정하는 것이다. 이 관심사를 담은 코드를 Client 에서 분리하지 않으면 Client는 결코 독립적으로 확장 가능한 클래스가 될 수 없다.
클래스 사이에 관계가 만들어진다는 것은 한 클래스가 인터페이스 없이 다른 클래스를 직접 사용한다는 뜻이다. 따라서 클래스가 아니라 오브젝트와 오브젝트 사이의 관계를 설정해줘야 한다. 오비젝트 사이의 관계는 런타임 시에 한쪽이 다른 오브젝트의 레퍼런스를 갖고 있는 방식으로 만들어진다. 오브젝트 사이의 관계가 만들어지려면 일단 만들어진 오브젝트가 있어야 하는데, 이처럼 직접 생성자를 호출해서 직접 오브젝트를 만드는 방법도 있지만 외부에서 만들어준 것을 가져오는 방법도 있다. 외부에서 만든 오브젝트를 전달받으려면 메소드 파라미터나 생성자 파라미터를 이용하면 된다.
Client 오브젝트가 동작하려면 특정 클래스의 오브젝트와 관계를 맺어야 한다. 인터페이스 자체에는 기능이 없으니 이를 사용할 수도 없다. 결국 특정 클래스의 오브젝트와 관계를 맺게 된다. 하지만 클래스 사이에 관계가 만들어진 것은 아니고, 단지 오브젝트 사이에 다이나믹한 관계가 만들어지는 것이다. 이 차이를 잘 구분할 수 있어야 한다. 클래스 사이의 관계는 코드에 다른 클래스 이름이 나타나기 때문에 만들어지는 것이다. 하지만 오브젝트 사이의 관계는 그렇지 않다. 코드에서는 특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면, 그 클래스의 오브젝트를 인터페이스 타입으로 받아서 사용할 수 있다. 바로 객체지향 프로그램에는 다형성이라는 특징이 있는 덕분이다.
제어의 역전(Inversion of Control)
팩토리
객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것으로, 이런 일을 하는 오브젝트를 흔히 팩토리라고 부른다.
추상 팩토리 패턴이나 팩토리 메소드 패턴과는 다르니 혼동하지 말자. 단지 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 깔끔하게 분리하려는 목적으로 사용하는 것이다.
Factory로 분리했을 때 얻을 수 있는 장점은 다양한데, 그중에서도 애플리케이션의 컴포넌트 역할을 하는 오브젝트와 애플리케이션의 구조를 결정하는 오브젝트를 분리했다는 데 가장 의미가 있다.
제어의 역전
제어의 역전이라는 건, 간단히 프로그램의 제어 흐름 구조가 뒤바뀌는 것이라고 설명할 수 있다.
일반적으로 프로그램의 흐름은 main()메소드와 같이 프로그램이 시작되는 지점에서 다음에 사용할 오브젝트를 결정하고 결정한 오브젝트를 생성하고, 만들어진 오브젝트에 있는 메소드를 호출하고, 그 오브젝트 메소드 안에서 다음에 사용할 것을 결정하고 호출하는 식의 작업이 반복된다.
제어의 역전이란 이런 제어 흐름의 개념을 거꾸로 뒤집는 것이다. 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 당연히 생성하지도 않는다. 또 자신도 어떻게 만들어지고 어디서 사용되는지를 알 수 없다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문이다. 프로그램의 시작을 담당하는 main()과 같은 엔트리 포인트를 제외하면 모든 오브젝트는 이렇게 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다.
프레임워크가 어떤 것인지 이해하려면 라이브러리와 프레임워크가 어떻게 다른지 알아야 한다. 라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어한다. 단지 동작하는 중에 필요한 기능이 있을 때 능동적으로 라이브러리를 사용할 뿐이다. 반면에 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다. 프레임워크에는 분명한 제어의 역전 개념이 적용되어 있어야 한다.
의존관계 주입(DI)
두 개의 클래스 또는 모듈이 의존관계에 있다고 말할 때는 항상 방향성을 부여해줘야 한다.
의존한다는 건 의존대상인 서비스 클래스가 변하면 클라이언트 클래스에 영향을 미친다는 뜻이다. 서비스에 기능이 추가되거나 변경되거나, 형식이 바뀌거나 하면 그 영향이 클라이언트로 전달된다는 것이다.
인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태가 된다. 결합도가 낮다고 설명할 수 잇다. 의존 관계란 한쪽의 변화가 다른 쪽에 영향을 주는 것이라고 했으니, 인터페이스를 통해 의존관계를 제한해주면 그만큼 변경에서 자유로워지는 셈이다.
UML에서 말하는 의존관계란 이렇게 설계 모델의 관점에서 이야기하는 것이다. 그런데 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계 말고, 런타임 시에 오브젝트 사이에서 만들어지는 의존관계도 있다. 런타임 의존관계 또는 오브젝트 의존관계인데, 설계 시점의 의존관계가 실체화된 것이라고 볼 수 있다. 런타임 의존관계는 모델링 시점의 의존관계와는 성격이 분명히 다르다.
런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트라고 말한다.
의존관계 주입이란 다음과 같은 세 가지 조건을 충족하는 작업을 말한다.
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.
의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다는 것이다.
DI에서 말하는 주입은 다이나믹하게 구현 클래스를 결정해서 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이뤄져야 한다.
DI의 장점은 관심사의 분리를 통해 얻어지는 높은 응집도에서 나온다.