Enjoy My Posts

토비의 스프링 1장

Posted on By Geunwon Lim

객체 지향 설계의 가치

객체 지향의 세계에서는 모든 것이 변한다. 구체적으로 객체에 대한 설계와 이를 구현한 코드가 변한다. 사용자의 비즈니스 프로세스와 그에 따른 요구사항은 끊임없이 바뀌고, 기술도 바뀌고, 운영 환경도 바뀐다. 어플리케이션이 망하지 않고서는 계속 변화가 지속된다. 그래서 객체 설계를 할 때 가장 염두에 둬야 할 사항이 ‘미래 변화에 어떻게 대비할 것인가’이다. 객체지향 프로그래밍이 절차적 프로그래밍 패러다임에 비해 초기에 좀 더 번거로운 작업을 해야하는 이유는 객체 지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 특징 때문이다. 객체 지향 기술은 실세계를 최대한 가깝게 모델링 할 수 있기 때문에 가치가 있다고 여겨지는데, 그보다는 객체 지향 기술이 추상세계 자체를 효과적으로 구성할 수 있고, 자유롭고 편리하게 변경, 발전, 확장시킬 수 있다는 가치가 크다. 변경이 일어날 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 하려면 분리와 확장을 고려한 설계를 해야 한다. 분리에 대해 먼저 생각해보자. 모든 변경과 발전은 한 번에 한 가지 관심사항에 집중해서 일어난다. 문제는 변화는 대체로 집중된 한 가지 관심에 대해 일어나지만 그에 따른 작업은 한 곳에 집중되지 않는 경우가 많다는 것이다. 변화가 한 번에 한 가지 관심에 집중돼서 일어난다면, 우리가 준비해야 할 일은 한 가지 관심이 한 군데에 집중되게 하는 것이다. 즉 관심이 같은 것끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하는 것이다. 이를 관심사의 분리라고 한다. 부연설명하자면, 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이다. 관심사가 같은 것끼리 모으고 다른 것은 분리해줌으로써 같은 관심에 효과적으로 집중할 수 있다. 관심사가 방만하게 중복되어 있고, 여기저기 흩어져 있어서 다른 관심의 대상과 얽혀 있으면 변경이 일어날 때 엄청난 고통을 일으키는 원인이 된다. 보통 이를 스파게티 코드가 된다고 한다. 관심의 종류에 따라 코드를 구분해놓으면 한 가지 관심에 대한 변경이 일어날 경우 그 관심이 집중되는 부분의 코드만 수정하면 된다. 관심이 다른 코드가 있는 메소드에는 영향을 주지도 않을뿐더러, 관심 내용이 독립적으로 존재하므로 수정도 간단해진다.

리팩토링

리팩토링은 기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술을 말한다. 토비의 스프링은 처음에 문제가 있는 코드를 보여주고, 그걸 리팩토링 하는 과정을 보여준다.

상속을 통한 확장

변화에 대처하기 위해 추상메소드를 두고 각 상황에 맞춰 추상메소드를 구현할 수 있다. 즉 특정 관심을 추상메소드로 분리하면 변화에 보다 쉽게 대처할 수 있다. 단순히 변경이 용이한 것을 넘어 손쉽게 확장할 수도 있다. 새로운 기술을 적용하고 싶으면 추상메소드를 새로 구현하는 것으로도 대처할 수 있기 때문이다. 이렇게 추상메소드를 두고 서브클래스에서 이 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법을 템플릿 메소드 패턴이라고 한다. 한편, 추상메소드를 ‘오브젝트를 어떻게 생성할 것인지를 결정하는 방법’이라고도 볼 수 있는데, 이렇게 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메소드 패턴이라고 한다. 즉, 관점에 따라 템플릿 메소드 패턴이 될 수도, 팩토리 메소드 패턴이 될 수도 있다. 중요한 건 상속구조를 통해 성격이 다른 관심사항을 분리한 코드를 만들어내고 서로 영향을 덜 주도록 했다는 것이다.

하지만 상속을 사용하는 것은 단점이 존재한다. 첫째로, 만약 하위클래스가 이미 다른 클래스를 상속하고 있다면, 내가 원하는 수퍼클래스를 상속하기 어려울 수 있다. 둘째로 상속을 하면 상하위 클래스의 관계가 너무 밀접해진다는 것이다. 서브클래스는 수퍼클래스의 기능을 직접 사용할 수 있다. 그래서 수퍼클래스 내부의 변경이 있을때 모든 서브클래스를 함께 수정하거나 다시 개발해야 할 수도 있다. 반대로 그런 변화에 따른 불편을 주지 않기 위해 수퍼클래스가 더 이상 변화하지 않도록 제약을 가해야 할지도 모른다. 캡슐화가 깨졌다고 할 수 있겠다. 셋째로 수퍼클래스를 상속하지 않는, 서브클래스와 유사한 성격의 코드에는 잘 만들어놓은 메소드를 재활용할 수 없다는 것도 문제다.

모든 오브젝트는 변하지만 다 동일한 방식으로 변하는 건 아니다. 관심사에 따라서 분리한 오브젝트들은 제각기 독특한 변화의 특징이 있다. 변화의 성격이 다르다는 건 변화의 이유와 시기, 주기 등이 다르다는 뜻이다. 추상 클래스를 만들고 이를 상속한 서브클래스에서 변화가 필요한 부분을 바꿔서 쓸 수 있게 만든 이유는 바로 이렇게 변화의 성격이 다른 것을 분리해서, 서로 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 하기 위해서다. 그러나 여러가지 단점이 많은, 상속이라는 방법을 사용했다는 사실이 불편하게 느껴진다.

클래스의 분리 - 인터페이스 활용

관심사가 다른 부분을 아예 다른 클래스로 만들고, 이 클래스가 필요한 곳에서 이걸 이용하게 하자. 그런데 이렇게 하니 상속했을 때의 장점이 사라진다. 클라이언트 클래스가 서비스 클래스에 완전히 종속되기 때문에, 변화에 대처하기 어려워졌다. 변경을 하려면 클라이언트 코드를 직접 수정해야 하기 때문에, 변경이 어렵다. 클래스 분리를 하면서 자유롭게 확장하려면 두 가지 문제를 해결해야 한다. 첫째로 함수명을 표준화해야한다. 변화할 때마다 같은 기능을 하는 함수가 명칭이 다르면 함수명 수정이라는 불필요한 대응을 해야한다. 둘째로 클라이언트 클래스가 서비스 클래스를 구체적으로 알지 못하게 해야한다. 구체적으로 알면 서비스 클래스를 바꾸려면 반드시 클라이언트 클래스도 수정해야한다. 근본적으로 클라이언트 클래스가 서비스 클래스를 너무 많이 알고 있기 때문에, 특정 서비스 클래스에 종속되기 때문에 생기는 문제다.

이를 개선하기 위해 두 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이다. 추상화란 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이다. 자바가 추상화를 위해 제공하는 가장 유용한 도구는 바로 인터페이스다. 인터페이스는 어떤 일을 하겠다는 기능만 정의해놓고, 자신을 구현한 클래스에 대한 구체적인 정보는 모두 감춰 버린다. 결국 오브젝트를 만들려면 구체적인 클래스 하나를 선택해야겠지만 인터페이스로 추상화해놓은 최소한의 통로를 통해 접근하는 클라이언트 쪽에서는 오브젝트를 만들 때 사용할 클래스가 무엇인지 몰라도 된다. 즉 실제 구현 클래스를 바꿔도 신경 쓸 일이 없다.

인터페이스를 활용하면 인터페이스를 구현한 클래스들의 메소드명은 통일되기 때문에 첫번째 문제를 해결한다. 하지만 인터페이스를 활용했다고 해서 두 번째 문제를 해결하는 것은 아니다. 인터페이스를 썼어도 초기에 한 번 어떤 클래스의 오브젝트를 사용할지를 결정하는 생성자의 코드는 제거를 제거하지 않으면, 클라이언트 클래스가 구체적인 서비스 클래스를 알게되고, 서비스 클래스를 변경하기 위해 클라이언트 클래스를 변경해야 한다. 이를 개선하기 위해 필요한 것은 관계설정 책임을 분리하는 것이다. 클라이언트 코드에는 어떤 서비스 클래스를 사용할 지 결정하는 ‘생성자 호출’ 코드가 있다. ‘생성자 호출’은 그 자체로 충분히 독립적인 관심사를 담고 있다. 바로 클라이언트 클래스가 어떤 서비스 클래스의 오브젝트를 이용하게 할지를 결정하는 것이다. 즉, 클라이언트 클래스와 클라이언트 클래스가 사용할 특정 서비스 구현 클래스 사이의 관계를 설정해주는 것에 관한 관심이다. 이 때문에 클라이언트는 서비스 클래스로부터 독립하여 확장하기 어렵다. 그러면 이 관계를 정해주는 관심은 누구에게 맡길 것인가? 현재 우리가 말하는 클라이언트 클래스를 사용하는 클래스, 즉 클라이언트 클래스의 클라이언트 클래스에 맡길 수 있다. 오브젝트 사이의 관계는 런타임 시에 한쪽이 다른 오브젝트의 레퍼런스를 갖고 있는 방식으로 만들어진다. 오브젝트 사이의 관계가 만들어지려면 일단 만들어진 오브젝트가 있어야 하는데, 이처럼 직접 생성자를 호출해서 직접 오브젝트를 만드는 방법도 있지만 외부에서 만들어준 것을 가져오는 방법도 있다. 클라이언트 클래스가 다른 클래스와 관계를 맺으려면 관계를 맺을 클래스가 필요한데, 이 클래스를 꼭 클라이언트 클래스에서 만들 필요는 없다. 오브젝트는 얼마든지 메소드 파라미터 등을 이용해 전달할 수 있으니 외부에서 만든 걸 가져올 수도 있다. 외부에서 만든 오브젝트를 전달받으려면 메소드 파라미터나 생성자 파라미터를 이용하면 된다. 이때 파라미터의 타입을 전달받을 오브젝트의 인터페이스로 선언해뒀다고 해보자. 이런 경우 파라미터로 전달되는 오브젝트의 클래스는 해당 인터페이스를 구현하기만 했다면 어떤 것이든지 상관없다. 단지 해당 인터페이스 타입의 오브젝트라면 파라미터로 전달 가능하고, 파라미터로 제공받은 오브젝트는 인터페이스에 정의된 메소드만 이용한다면 그 오브젝트가 어떤 클래스로부터 만들어졌는지 신경 쓰지 않아도 된다. 기존에 클라이언트 클래스에서 인터페이스를 적용했을 때 인터페이스를 사용한 덕분에 클라이언트 클래스와 서비스 구현 클래스들은 직접 연결되어 있지 않는 것처럼 보인다. 하지만 잘 보면 인터페이스를 썼어도 클라이언트 클래스와 서비스 구현 클래스 사이에 직접적인 관계가 있다는 걸 알 수 있다. 생성자 호출 때문이다. 그래서 서비스 클래스를 바꾸려면 클라이언트 클래스를 뜯어 고쳐 다른 서비스 구현 클래스와 관계를 맺도록 해야 한다. 따라서 클라이언트 클래스는 서비스 인터페이스만 알아야 하고, 서비스 구현 클래스와는 직접적인 관계를 갖지 말아야 한다. 그래야 클라이언트 클래스 수정 없이 서비스 구현 클래스를 바꿀 수 있다. 물론 클라이언트 클래스가 동작하려면 특정 구현 클래스와 관계를 맺어야 한다. 하지만 인터페이스에만 의존하게 하면 클래스 사이에 관계가 만들어진 것은 아니고, 단지 오브젝트 사이에 다이내믹한 관계가 만들어지는 것이다. 코드에서는 특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면, 그 클래스의 오브젝트를 인터페이스 타입으로 받아서 사용할 수 있다. 바로 객체지향 프로그램에는 다형성이라는 특징이 있는 덕분이다. 클라이언트 클래스가 특정 서비스 구현 클래스를 사용하게 하려면 두 클래스 사이에 런타임 시용관계 또는 링크 또는 의존관계라고 불리는 관계를 맺어주면 된다. 런타임 오브젝트 관계를 갖는 구조로 만들어주는 게 바로 클라이언트를 사용하는 클라이언트(즉, 클라이언트의 클라이언트 클래스. 다시 얘기하자면, 계속 얘기하던 클라이언트 클래스는 서비스 클래스가 되는 거고 그 서비스 클래스를 활용하는 클래스가 클라이언트 클래스가 된다.)의 책임이다. 관계 설정이라는 책임을 다른 클래스에 넘겼기 때문에, 클라이언트 클래스는 구체적인 서비스 구현 클래스를 몰라도 관계를 맺을 수 있다. 덕분에 클라이언트 클래스의 수정 없이 서비스 구현 클래스를 쉽게 바꿀 수 있게 된다. 즉, 클라이언트 클래스는 자신의 관심사에만 집중할 수 있게 된다.

이렇게 인터페이스를 활용한 것은 상속을 활용했을 때보다 훨씬 유연하다. 인터페이스를 활용하면 비슷한 성격의 다른 클라이언트 클래스에도 인터페이스의 구현 클래스들을 그대로 적용할 수 있기 때문이다.

원칙과 패턴

1. 개방 폐쇄 원칙

개방폐쇄원칙을 간단히 정의하면 ‘클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다’라고 할 수 있다. 클라이언트 클래스 입장에서 서비스 클래스가 제공하는 기능을 확장하는 데 열려있다. 클라이언트 클래스 코드에 영향을 주지 않아도 기능을 확장할 수 있다. 동시에 클라이언트 클래스는 그런 변화에 영향을 받지 않고 자신의 핵심 기능을 구현한 코드를 유지할 수 있으므로 변경에는 닫혀 있다고 말할 수 있다. 인터페이스를 통해 제공되는 확장 포인트는 활짝 개방되어 있다. 반면 인터페이스를 이용하는 클래스는 자신의 변화가 불필요하게 일어나지 않도록 굳게 폐쇄되어 있다.

디자인 패턴 vs 객체지향 설계 원칙

디자인 패턴은 특별한 상황에서 발생하는 문제에 대한 좀 더 구체적인 솔루션이라고 한다면, 객체지향 설계 원칙은 좀 더 일반적인 상황에서 적용 가능한 설계 기준이라고 불 수 있다. 당연하겠지만 객체지향 디자인 패턴은 대부분 객체지향 설계 원칙을 잘 지켜서 만들어져 있다.

2. 높은 응집도

개방 패쇄 원칙은 높은 응집도와 낮은 결합도라는 소프트웨어 개발의 고전적인 원리로도 설명이 가능하다. 응집도가 높다는 건 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻이다. 불필요하거나 직접 관련이 없는 외부의 관심과 책임이 얽혀 있지 않으며, 하나의 공통 관심사는 한 클래스에 모여있다. 높은 응집도는 클래스 레벨뿐 아니라, 패키지, 컴포넌트, 모듈에 이 르기까지 그 대상의 크기가 달라도 통일한 원리로 적용될 수 있다. 응집도가 높다는 것은 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것으로 설명할 수도 있다. 즉 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 말할 수 있다. 만약 모듈의 일부분에만 변경이 일어나도 된다면, 모듈 전체에서 어떤 부분이 바뀌어야 하는지 파악해야 하고 또 그 변경으로 인해 바뀌지 않는 부분에는 다른 영향을 미치지는 않는지 확인해야 하는 이중의 부담이 생긴다. 여러 관심사와 책임이 얽혀 있는 복잡한 코드에서는 변경이 필요한 부분을 찾아내는 것도 번거로운 일일뿐더러, 그렇게 변경한 것이 혹시 클라이언트 클래스의 다른 기능에 영향을 줘서 오류를 발생시키지는 않는지도 일일이 확인해야 한다. 반면에 관심사를 분리하여 응집도를 높인다면, 변경이 필요할 때 서비스 클래스만 새로 구현해서 바꿔주면 된다. 아예 바꾸지 않고 기존 서비스 클래스를 조금 수정한다고 해도 마찬가지다. 작업은 항상 전체적으로 일어나고 무엇을 변경할 지 명확하며, 그것이 클라이언트 클래스 등 다른 클래스의 수정을 요구하지 않을뿐더러 기능에 영향을주지 않는다는 사실을 손쉽게 확인할 수 있다. 서비스 클래스의 기능에 변경이 일어난 경우에 이를 검증하려고 한다면, 변경한 서비스 구현 클래스를 직접 테스트해보는 것만으로도 충분하기 때문이다.

3. 낮은 결합도

낮은 결합도는 높은 응집도보다 더 민감한 원칙이다. 책임과 관심사가 다른 오브젝트 또는 모듈과는 낮은 결합도, 즉 느슨하게 연결된 형태를 유지하는 것이 바람직하다. 느슨한 연결은 관계를 유지히는 데 꼭 필요한 최소한의 방법만 간접적인 형태로 제공하고, 나머지는 서로 독립적이고 알 필요도 없게 만들어주는 것이다. 결합도가 낮아지면 변화에 대응하는 속도가 높아지고 구성이 깔끔해진다. 또한 확장하기에도 매우 편리하다. 여기서 결합도란 ‘하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도’라고 설명할 수 있다. 낮은 결합도란 결국, 하나의 변경이 발생할 때 마치 파문이 이는 것처럼 여타 모듈과 객체로 변경에 대한 요구가 전파되지 않는 상태를 말한다. 서비스 기능을 인터페이스로 만들면서 구현 클래스가 바뀌더라도 클라이언트 클래스는 바꿀 필요가 없게 됐다. 결합도가 높아지면 변경에 따르는 작업량이 많아지고, 변경으로 인해 버그가 발생할 가능성이 높아진다.

4. 전략 패턴

전략 패턴은 자신의 기능 맥락(context)에서, 필요에 따라 변경이 필요한 알고리즘(독립적인 책임으로 분리가 가능한 기능)을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다. 클라이언트 클래스는 전략패턴의 컨텍스트에 해당한다. 컨텍스트는 자신의 기능을 수행하는 데 필요한 기능 중에서 변경 가능한 로직을 인터페이스로 정의하고, 이를 구현한 클래스, 즉 전략을 바꿔가면서 사용할 수 있게 분리했다. 전략패턴은 클라이언트의 클라이언트 클래스의 필요성도 잘 설명한다. 클라이언트의 클라이언트는 클라이언트(컨텍스트)가 사용할 전략을 컨텍스트의 생성자 등을 통해 제공해주는 게 일반적이다.

제어의 역전

이제부터 기존에 클라이언트라고 부르던 클래스를 컨텍스트로, 클라이언트의 클라이언트라고 부르던 클래스를 클라이언트로 부르겠다. 컨텍스트와 서비스 클래스의 관계를 맺어주는 역할은 누가 할까? 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 역할을 하는 객체를 팩토리(디자인 패턴에서 쓰는 용어와 다른 의미)라고 부른다. 이 팩토리는 컨텍스트를 어떻게 만들 지, 어떻게 준비시킬 지를 결정한다. 즉 오브젝트들을 구성하고 그 관계를 정의한다. 이런 작업이 애플리케이션 전체에 걸쳐 일어난다면 컴포넌트의 의존관계에 대한 설계도와 같은 역할을 할 것이다. 팩토리 클래스의 가치 중 으뜸은 애플리케이션의 컴포넌트 역할을 하는 오브젝트와 애플리케이션의 구조를 결정하는 오브젝트를 분리했다는 것이다.

그런데 한 가지 이슈가 또 있다. 만약 여러 개의 유사하지만 다른 컨텍스트를 만들고자 하면 어떻게 될까? 컨텍스트와 서비스 클래스의 관계를 맺어주기 위해 서비스 클래스를 생성하는 코드가 반복될것이다. 이러한 중복을 없애려면 분리가 좋은 방법이다. 메소드 분리를 통해 중복 문제를 해결하자.

제어의 역전이란 프로그램의 제어 흐름 구조가 뒤바뀌는 것이다. 제어가 역전되지 않는다면, 모든 오브젝트가 능동적으로 자신이 사용할 클래스를 결정하고, 언제 어떻게 그 오브젝트를 만들 지 스스로 관장한다. 모든 종류의 작업은 사용하는 쪽에서 제어하는 구조다. 반대로 제어의 역전은 이러한 제어 흐름을 거꾸로 뒤집는다. 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 생성하지도 않는다. 또 자신이 어떻게 만들어지고 어디서 사용되는지를 알 수 없다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문이다. main() 을 제외하면 모든 오브젝트는 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다. 템플릿 메소드 패턴도 제어의 역전이 적용된 예다. 서브클래스는 메소드를 구현하지만, 그 메소드가 언제 어떻게 사용될 지 자신은 모른다. 서브클래스에서 결정되는 것이 아니라, 기능만 구현해놓으면 슈퍼클래스의 템플릿 메소드에서 서브클래스의 함수를 호출한다. 즉 서브클래스는 제어권을 상위 템플릿 메소드에 넘기고 자신은 필요할 때 호출되어 사용되도록 한다는 제어의 역전이 적용됐다. 프레임워크도 제어의 역전이 적용된 기술이다. 프레임워크는 단지 미리 만들어둔 반제품이나, 확장해서 사용할 수 있도록 준비된 추상 라이브러리 집합이 아니다. 라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어한다. 단지 동작하는 중 필요한 기능이 있을 때 능동적으로 라이브러리를 사용할 뿐이다. 반면 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다. 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하는 방식이다. 프레임워크는 분명한 제어의 역전 개념이 적용되어 있어야 한다. 예제 코드도 마찬가지다. 원래 서비스 인터페이스의 구현 클래스를 결정하고 오브젝트를 만드는 제어권은 컨텍스트에게 있었다. 그런데 개선 후에는 클라이언트에게 있다. 즉 컨텍스트는 자신이 어떤 클래스를 사용할 지도 모르는, 수동적인 존재가 됐다. 또한 컨텍스트의 생성과 서비스 클래스의 생성도 클라이언트가 맡는다.

제어의 역전에서는 프레임워크, 컨테이너, 예제 코드에서의 팩토리 클래스와 같이 애플리케이션 컴포넌트의 생성과 관계설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요하다.

스프링의 IoC

스프링의 핵심을 담당하는 건 빈 팩토리 또는 애플리케이션 컨텍스트라고 불리는 것이다. 이 두 가지는 우리가 만든 팩토리가 하는 일을 좀 더 일반화한 것이다.

스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 객체를 빈이라고 부른다. 즉 스프링 빈은 스프링 컨테이너가 생성과 관게설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말이다. 스프링에서는 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리, 애플리케이션 컨텍스트라고 부른다. 애플리케이션 컨텍스트는 별도의 설정 정보를 참고해 빈을 관리(생성, 관계설정, 제어)한다.애플리케이션 컨텍스트 그 자체로는 애플리케이션 로직을 담당하지는 않지만 IoC 방식을 이용해 애플리케이션 컴포넌트를 생성하고, 사용할 관계를 맺어주는 등의 책임을 담당하여 설계도와 같은 역할을 한다.

예제로 만든 팩토리 클래스가 컨텍스트를 비롯한 다양한 유사 컨텍스트를 생성하고, 서비스 클래스와 관계를 맺어주는 제한적인 역할을 하는 데 반해, 애플리케이션 컨텍스트는 애플리케이션 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성과 관계설정을 담당한다. 애플리케이션 컨텍스트에는 팩토리 클래스와 달리 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 없고, 그런 생성정보와 연관관계 정보를 별도의 설정정보를 통해 얻는다. 예제 팩토리 클래스에서 사용한 IoC원리를 그래도 적용하는데 애플리케이션 컨텍스트를 사용하는 이유는 범용적이고 유연한 방법으로 IoC 기능을 확장하기 위해서다. 오브젝트 팩토리를 직접 사용했을 때와 비교해 애플리케이션 컨텍스트를 사용했을 때 얻는 장점은 다음과 같다.

  1. 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.

    애플리케이션이 발전할수록 팩토리 클래스도 계속 추가될 것이다. 클라이언트가 필요한 오브젝트를 가져오기 위해 팩토리 클래스를 사용해야하면, 필요할 때마다 팩토리 오브젝트를 생성해야 하는 번거로움이 있다. 애플리케이션 컨텍스트를 사용하면 팩토리 클래스가 아무리 많아져도 이를 알아야 하거나 직접 사용할 필요가 없다. 애플리케이션 컨텍스트를 이용하면 일관된 방식으로 원하는 오브젝트를 가져올 수 있다. 또 자바 코드 대신 XML처럼 단순한 방법을 사용해 IoC 설정정보를 만들 수도 있다.

  2. 애플리케이션 컨텍스트는 종합 IoC서비스를 제공해준다.

    애플리케이션 컨텍스트의 역할은 단지 오브젝트 생성과 다른 오브젝트와의 관계설정이 전부가 아니다. 오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수 있고, 이에 부가적으로 자동생성, 오브젝트에 대한 후처리, 정보의 조합, 설정 방식의 다변화, 인터셉팅 등 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공한다. 또 빈이 사용할 수 있는 기반기술 서비스나 외부 시스템과의 연동 등을 컨테이너 차원에서 제공해주기도 한다.

  3. 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제고한다.

    빈의 이름을 이용해 찾을수도, 타입으로 빈을 찾을수도, 애노테이션 설정으로 빈을 찾을 수도 있다.

스프링 IoC의 용어 정리

  • 빈: 스프링이 IoC 방식으로 관리하는 오브젝트라는 뜻이다. 주의할 점은 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 다 빈은 아니라는 사실이다. 그 중 스프링이 직접 생성과 제어를 담당하는 오브젝트만을 빈이라고 부른다.
  • 빈 팩토리: 스프링의 IoC를 담당하는 핵심 컨테이너를 말한다. 빈을 등록하고, 생성하고, 조회하고 돌려주고, 그 외 부가적인 빈을 관리하는 기능을 담당한다. 보통 빈 팩토리를 바로 쓰지 않고 이를 확장한 애플리케이션 컨텍스트를 이용한다.
  • 애플리케이션 컨텍스트: 빈 팩토리를 확장한 IoC 컨테이너다. 빈을 등록하고 관리하는 기본적인 기능은 빈 팩토리와 동일하다. 여기에 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다. 빈 팩토리라고 부를 때는 주로 빈의 생성과 제어의 관점에서 이야기하는 것이고, 애플리케이션 컨텍스트라고 할 때는 스프링이 제공하는 애플리케이션 지원 기능을 모두 포함해서 이야기하는 것이라고 보면 된다.
  • 설정정보/설정 메타정보: 스프링의 설정정보란 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다. 스프링의 설정정보는 컨테이너에 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만, 그보다는 IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용된다.
  • 컨테이너 또는 IoC컨테이너: IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고도 한다. 후지는 주로 빈 팩토리의 관점에서 이야기하는 것이고, 그냥 컨테이너 또는 스프링 컨테이너라고 할 때는 애플리케이션 컨텍스트를 가리키는 것이라고 보면 된다.

싱글톤

어플리케이션 컨텍스트로 예제 팩토리를 쓰는것과 직접 예제 팩토리 클래스를 쓰는 것은 중요한 차이점이 있다. 어플리케이션 컨텍스트는 빈을 싱글톤으로 관리한다는 것이다. 즉 스프링은 여러 번에 걸쳐 빈을 요청받아도 매번 동일한 오브젝트를 돌려준다. 빈을 요청받을 때마다 팩토리 클래스의 메소드를 호출하여 ‘new 컨텍스트 오브젝트’를 하지 않는다는 의미다. 애플리케이션 컨텍스트가 싱글톤을 저장하고 관리하는 싱글톤 레지스트라는 것을 알 수 있다. 여기서 말하는 싱글톤은 디자인 패턴의 싱글톤 패턴과 비슷하지만 구현 방법은 확연히 다르다.

스프링이 싱글톤으로 빈을 만드는 이유는 스프링이 주로 자바 엔터프라이즈 기술을 사용하는 서버환경에서 활용되기 때문이다. 로컬 프로그램을 개발할 때도 스프링을 활용할 수 있지만 드물다. 태생적으로 스프링은 엔터프라이즈 시스템을 위해 고안된 기술이기 때문에 서버 환경에서 사용될 때 그 가치가 있다.

스프링이 처음 설계됐던 대규모의 엔터프라이즈 서버환경은 서버 하나당 최대로 초당 수십에서 수백 번씩 브라우저나 여타 시스템으로부터의 요청을 받아 처리할 수 있는 높은 성능이 요구되는 환경이었다. 또 하나의 요청을 처리하기 위해 데이 터 액세스 로직, 서비스 로직, 비즈니스 로직, 프레젠태이션 로직 등의 다양한 기능을 담당하는 오브젝트들이 참여히는 계층형 구조로 이뤄진 경우가 대부분이다. 비즈니스 로직도 복잡한 경우가 많다. 그런데 매 번 클라이언트에서 요청이 올 때마다 각 로직을 담당하는 오브젝트를 새로 만들어서 사용한다고 생각해보자. 아무리 자바의 오브젝트 생성과 가비지 컬렉션의 성능이 좋아졌다고 한들 이 정도로 부하가 걸리면 서버가 감당하기 힘들다. 그래서 엔터프라이즈 분야에서는 서비스 오브젝트라는 개념을 일찍부터 사용해왔다. 서블릿은 자바 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할 수 있다. 스펙에서 강제하진 않지만, 서블릿은 대부분 멀티스레드 환경에서 싱글톤으로 동작한다. 서블릿 클래스당 하나의 오브젝트만 만들어두고 사용자의 요청을 담당하는 여러 스레드에 서 하나의 오브젝트를 공유해 동시에 사용한다. 이렇게 애플리케이션 안에 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용하는 것이 싱글톤 패턴의 원리다. 따라서 서버환경에서는 서비스 싱글톤의 사용이 권장된다.

디자인 패턴에서의 싱글톤 패턴

디자인 패턴에 소개된 싱글톤 패턴은 시용하기가 까다롭고 여러 가지 문제점이 있다. 즉 위에서 말하는 싱글톤은 디자인 패턴에서 말하는 싱글톤 패턴과 다르다. 싱글톤 패턴은 어떤 클래스를 애플리케이션 내에서 제한된 인스턴스 개수, 이름처럼 주로 하나만 존재하도록 강제하는 패턴이다 이렇게 하나만 만들어지는 클래스의 오브젝트는 애플리케이션 내에서 전역적으로 접근이 가능하다. 단일 오브젝트만 존재해야 하고 이를 애플리케이션의 여러 곳에서 공유하는 경우에 주로 사용한다.

싱글톤 패턴을 적용하는 방법

• 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 pnvate으로 만든다.

• 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.

• 스태틱 팩토리 메소드인 getlnstance( )를 만들고 이 메소드가 최초로 호출되는 시점에서 한번만 오브젝트가 만들어지게 한다. 생성된 오브젝트는 스태틱 필드에 저장된다. 또는 스태틱 필드의 초기값으로 오브젝트를 미리 만들어둘 수도 있다.

• 한번 오브젝트(싱글톤)가 만들어지고 난 후에는 getlnstance( ) 메소드를 통해 이미 만들어져 스태틱 필드에 저장해둔 오브젝트를 넘겨준다.

싱글톤 패턴의 한계

이렇게 짜면 싱글톤으로 짜지 않았을 때에 비해 지저분하다는 느낌이 든다. 게다가 private으로 바뀐 생성자는 외부에서 호출할 수가 없기 때문에 팩토리 클래스에서 컨텍스트 클래스를 생성하며 서비스 클래스와의 관계를 맺어주는 게 불가능해졌다.

  1. private 생성자를 갖고 있기 때문에 상속받을 수 없다.

    싱글톤 패턴은 생성자를 private으로 두어 싱글톤 클래스 자신만이 자기 오브젝트를 만들도록 제한한다. 문제는 private 생성자를 가진 클래스는 다른 생성자가 없다면 상속이 불가능하다는 점이다. 객체지향의 장점인 상속과 이를 이용한 다형성을 적용할 수 없다. 기술적인 서비스만 제공하는 경우라면 상관없겠지만, 애플리케이션의 로직을 담고 있는 일반 오브젝트의 경우 싱글톤으로 만들었을 때 객체지향적인 설계의 장점을 적용하기가 어렵다는 점은 심각한 문제다. 또한 상속과 다형성 같은 객체지향의 특징이 적용되지 않는 스태틱 필드와 메소드를 사용하는 것도 역시 동일한 문제를 발생시킨다.

  2. 싱글톤은 테스트하기가 힘들다

    싱글톤은 만들어지는 방식이 제한적이기 때문에 테스트에서 사용될 때 목 오브젝트 등으로 대체하기가 힘들다. 싱글톤은 초기화 과정에서 생성자 등을 통해 사용할 오브젝트를 다이내믹하게 주입하기도 힘들기 때문에 필요한 오브젝트는 직접 오브젝트를 만들어 사용할 수밖에 없다. 이런 경우 테스트용 오브젝트로 대체하기가 힘들다.

  3. 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.

    서버에서 클래스 로더를 어떻게 구성하고 있느냐에 따라서 싱글톤 클래스임에도 하나 이상의 오브젝트가 만들어질 수 있다. 따라서 자바 언어를 이용한 싱글톤 패턴 기법은 서버환경에서는 싱글톤이 꼭 보장된다고 볼 수 없다. 여러 개의 JVM에 분산돼서 설치가 되는 경우에도 각각 독립적으로 오브젝트가 생기기 때문에 싱글톤으로서의 가치가 떨어진다.

  4. 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.

    싱글톤은 사용하는 클라이언트가 정해져 있지 않다. 싱글톤의 스태틱 메소드를 이용해 언제든지 싱글톤에 쉽게 접근할 수 있기 때문에 애플리케이션 어디서든지 사용될 수 있고, 그러다 보면 자연스럽게 전역 상태로 사용되기 쉽다. 아무 객체나 자유롭게 접근하고 수정하고 공유할 수 있는 전역 상태를 갖는 것은 객체지향 프로그래밍에서는 권장되지 않는 프로그래밍 모델이다. 그럼에도 싱글톤을 사용하면 그런 유혹에 빠지기 쉽다. 그럴 바에는 아예 스태틱 필드와 메소드로만 구성된 클래스를 사용하는 편이 낫다.

싱글톤 레지스트리

스프링은 서버환경에서 싱글톤이 만들어져서 서비스 오브젝트 방식으로 사용되는 것은 적극 지지한다. 하지만 자바의 기본적인 싱글톤 패턴의 구현 방식은 여러 가지 단점이 있기 때문에, 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 그것이 바로 싱글톤 레지스트리다. 스프링 컨테이너는 싱글톤을 생성하고, 관리하고, 공급하는 싱글톤 관리 컨테이너이기도 하다. 싱글톤 레지스트리의 장점은 스태틱 메소드와 private 생성자를 사용해야 하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해준다는 점이다. 평범한 자바 클래스라도 IoC방식의 컨테이너를 사용해서 생성과 관계설정, 사용 등에 대한 제어권을 컨테이너에게 넘기면 손쉽게 싱글톤 방식으로 만들어져 관리되게 할 수 있다. 오브젝트 생성에 관한 모든 권한은 IoC 기능을 제공하는 애플리케이션 컨텍스트에게 있기 때문이다. 스프링의 싱글톤 레지스트리 덕분에 싱글톤 방식으로 사용될 애플리케이션 클래스라도 public 생성자를 가질 수 있다. 싱글톤으로 사용돼야 하는 환경이 아니라면 간단히 오브젝트를 생성해서 사용할 수 있다. 따라서 테스트 환경에서 자유롭게 오브젝트를 만들 수 있고, 테스트를 위한 목 오브젝트로 대체하는 것도 간단하다. 생성자 파라미터를 이용해서 사용할 오브젝트를 넣어주게 할 수도 있다. 가장 중요한 것은 싱글톤 패턴과 달리 스프링이 지지하는 객체지향적인 설계 방식과 원칙, 디자인 패턴(싱글톤 패턴은 제외) 등을 적용하는 데 아무런 제약이 없다는 점이다. 스프링은 IoC 컨테이너일 뿐만 아니라 고전적인 싱글톤 패턴을 대신해서 싱글톤을 만들고 관리해주는 싱글톤 레지스트리라는 점을 기억해두자. 스프링이 빈을 싱글톤으로 만드는 것은 결국 오브젝트의 생성 방법을 제어하는 IoC 컨테이너로서의 역할이다. 만약 스프링 없이 팩토리 클래스만 사용한다면 싱글톤 방식으로 컨텍스트 클래스를 한 번만 만들어두고 매번 같은 오브젝트를 리턴하게 하려면 팩토리 클래스가 상당히 지저분해질 것이다.

빈이 싱글톤으로 만들어지기 때문에 주의해야할 점

싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있다. 따라서 상태 관리에 주의를 기울여야 한다. 기본적으로 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는 상태정보를 내부에 갖고 있지 않은 무상태(stateless)방식으로 만들어져야 한다. 다중 사용자의 요청을 한꺼번에 처리하는 스레드들이 동시에 싱글톤 오브젝트의 인스턴스 변수를 수정하는 것은 매우 위험하다. 저장할 공간이 하나뿐이니 서로 값을 덮어쓰고 자신이 저장하지 않은 값을 읽어올 수 있기 때문이다. 따라서 싱글톤은 기본적으로 인스턴스 필드의 값을 변경하고, 유지하는 상태유지(stateful)방식으로 만들지 않는다. 이를 지키지 않으면 개발자 혼자서 개발하고 테스트할 때는 아무런 문제가 없겠지만, 서버에 배포되고 여러 시용자가 동시에 접속하면 데이터가 엉망이 돼버리는 등의 심각한 문제가 발생할 것이다. 물론 읽기전용의 값이라면 초기화 시점에서 인스턴스 변수에 저장해두고 공유하는 것은 아무 문제가 없다. 상태가 없는 방식으로 클래스를 만드는 경우에 각 요청에 대한 정보나, DB나 서버의 리소스로부터 생성한 정보는 어떻게 다뤄야 할까? 이때는 파라미터와 로컬 변수, 리턴 값 등을 이용하면 된다. 메소드 파라미터나 메소드 안에서 생성되는 로컬 변수는 매번 새로운값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라고 해도 여러 스레드가 변수의 값을 덮어쓸 일은 없다. 그런데 기존 예제 컨텍스트 클래스에서 인스턴스 변수로 갖는 게(connectionMaker) 있다. 이것은 인스턴스 변수를 사용해도 상관없다. 왜냐하면 읽기전용의 정보이기 때문이다. 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도라면 인스턴스 변수를 사용해도 좋다. 스프링이 한 번 초기화해주고 나면 이후에는 수정되지 않기 때문에 멀티스레드 환경에서 사용해도 아무런 문제가 없다.

스프링 빈의 스코프

스프링이 관리하는 오브젝트의 스코프(빈이 생성되고, 존재하고,적용되는 범위)에 대해 알아보자. 스프링 빈의 기본 스코프는 싱글톤이다. 싱글톤 스코프는 컨테이너 내에 한 개의 오브젝트만 만들어져서, 강제로 제거하지 않는 한 스프링 컨테이너가 존재하는 동안 계속 유지된다. 스프링에서 만들어지는 대부분의 빈은 싱글톤 스코프를 갖는다. 경우에 따라서는 싱글톤 외의 스코프를 가질 수 있다. 대표적으로 프로토타입 스코프가 있다. 프로토타입은 싱글톤과 달리 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어준다. 그 외에도 웹을 통해 새로운 HTTP 요청이 생길 때마다 생성되는 요청 스코프가 있고 웹의 세션과 스코프가 유사한 세션 스코프도 있다.

의존관계 주입

의존하고 있다는 건 무슨 의미일까? A가 B에 의존한다는 건, B가 변하면 그것이 A에 영향을 미친다는 뜻이다. 이제까지 공부한 IoC라는 용어는 매우 느슨하게 정의돼서 폭넓게 사용된다. 때문에 스프링을 IoC 컨테이너라고만 해서는 스프링이 제공하는 기능의 특징을 명확하게 설명하지 못한다. 그래서 새로운 용어를 만드는 데 탁월한 재능이 있는 몇몇 사람의 제안으로 스프링이 제공하는 IoC 방식을 의존관계 주입이라는, 스프링 IoC의 핵심을 짚어주며 좀 더 의도가 명확히 드러나는 이름을 사용하기 시작했다. 스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입이라고 불린다. 물론 스프링이 컨테이너이고 프레임워크이니 기본적인 동작원리가 모두 IoC 방식이라고 할 수 있지만, 스프링이 여타 프레임워크와 차별화돼서 제공해주는 기능은 의존관계 주입이라는 새로운 용어를 사용할 때 분명하게 드러난다.

Dependency Injection은 여러개의 말로 번역되는데, 그 중 가장 흔히 사용되는 용어가 의존성 주입이다. 하지만 의존성이라는 말은 DI의 의미가 무엇인지 잘 드러내주지 못한다. 또한 의존 오브젝트 주입이라고도 부르기도 하는데. 이때는 DI가 얼어나는 방법에 초점을 맞춘 것이다. 엄밀히 말해서 오브젝트는 다른 오브젝트에 주입할 수 있는 게 아니다. 오브젝트의 레퍼런스가 전달될 뿐이다. DI는 오브젝트 레퍼런스를 외부로부터 제공(주입)받고 이를 통해 여타 오브젝트와 다이내믹하게 의존관계가 만들어지는 것이 핵심이다. 이러한 면에서 의존관계 설정이라는 번역도 나쁘지 않다.

모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계 말고, 런타임 시에 오브젝트 사이에서 만들어지는 의존관계도 있다. 런타임 의존관계 또는 오브젝트 의존관계인데, 설계 시점의 의존관계가 실체화된 것이라고 볼 수 있다. 런타임 의존관계는 모델링 시점의 의존관계와는 성격이 분명히 다르다. 인터페이스를 통해 설계 시점에 느슨한 의존관계를 갖는 경우에는 클라이언트 오브젝트가 런타임 시에 사용할 서비스 오브젝트가 어떤 클래스로 만든 것인지 미리 알 수가 없다. 개발자나 운영자가 사전에 어떤클래스의 오브젝트를 쓸 지 미리 정해놓을 수는 있지만 그것이 클라이언트 클래스나 서비스 인터페이스 등의 설계와 코드 속에서는 드러나지 않는다는 말이다. 프로그램이 시작되고 런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트라고 말한다. 의존관계 주입은 이렇게 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다.

정리하면 의존관계 주입이란 다음과 같은 세 가지 조건을 충족히는 작업을 말한다.

  1. 클래스 모댈이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
  2. 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.
  3. 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.

의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도외주는 제 3의 존재가 있다는 것이다. 제3의 존재는 바로 관계설정 책임을 가진 코드를 분리해서 만들어진 오브젝트라고 볼 수 있다.

DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC 의 개념에 잘 들어맞는다. 스프링 컨테이너의 IoC는 주로 의존관계 주입 또는 DI라는 데 초점이 맞춰져 있다. 그래서 스프링을 IoC 컨테이너 외에도 DI 컨테이너 또는 DI 프레임워크라고 부르는 것이다.

질문: 그러면 DI를 하지 않으면서도 IoC라고 부를 수 있는 게 있다는 것인가?

DI의 동작방식은 이름 그대로 외부로부터의 주입이다. 하지만 단지 외부에서 파라미터로 오브젝트를 넘겨줬다고 해서, 즉 주입해줬다고 해서 다 DI가 아니라는 점을 주의해야 한다. 주입받는 메소드 파라미터가 이미 특정 클래스 타입으로 고정되어 있다면 DI가 일어날 수 없다. DI에서 말하는 주입은 다이내믹하게 구현 클래스를 결정해서 제공받올 수 있도록 인터페이스 타입의 파라미터를 통해 이뤄져야 한다.

지금까지 기술한 DI 기술의 장점을 요약하면, 코드에는 런타임 클래스에 대한 의존관계가 나타나지 않고, 인터페이스를 통해 결합도가 낮은 코드를 만드므로, 다른 책임을 가진 사용 의존관계에 있는 대상이 바뀌거나 변경되더라도 자신은 영향을 받지 않으며, 변경을 통한 다양한 확장 방법에는 자유롭는 것이다.

DI 응용

  1. 기능 구현의 교환

    DAO에 DI를 적용하지 않았다고 가정하자. 개발 중에는 로컬 DB를 사용하도록 해야 하니 로컬 DB 에 대한 연결 기능이 있는 LocalDBConnectionMaker라는 클래스를 만들고, 모든 DAO에서 이 클래스의 오브젝트를 매번 생성해서 사용하게 했을 것이다. 그런데 서버에 배포할 때는 다시 서버가 제공하는 특별한 DB 연결 클래스를 사용해야 한다. DI를 안 했으니 모든 DAO는 코드에서 이미 클래스인 LocalDBConnectionMaker에 의존하고 있다. new LocalDBConnectionMaker() 라는 코드가 모든 DAO 에 들어 있을 것이다. 이를 서버에 배치하는 시점에서 운영서버에서 DB에서 연결할 때 필요한 ProductionDBConnectionMaker 라는 클래스로 변경해줘야 한다. 끔찍한 일이다. 반면에 DI 방식을 적용해서 만들었다고 해보자. 모든 DAO는 생성 시점에 ConnectionMaker 타입의 오브젝트를 컨테이너로부터 제공받는다. 구체적인 사용 클래스 이름은 컨테이너가 사용할 설정정보에 들어 있다. 이를 서버에 배포할 때는 어떤 DAO 클래스와 코드도 수정할 필요가 없다. 단지 서버에서 사용할 DaoFactory를 한 곳에서 수정하기만 하면 된다. 개발환경과 운영환경에서 DI의 설정정보에 해당하는 DaoFactory만 다르게 만들어두면 나머지 코드에는 전혀 손대지 않고 개발 시와 운영 시에 각각 다른 런타임 오브젝트에 의존관계를 갖게 해줘서 문제를 해결할 수 있다.

  2. 부가기능 추가

    DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶다고 해보자. 모든 DAO의 makeConnection() 메소드를 호출하는 부분에 새로 추가한 카운터를 증가시키는 코드를 넣어야 할까? 그리고 분석 작업이 끝나면 모두 제거하고? 그것은 엄청난 낭비이고 노가다다. 게다가 DAO 코드를 수정한다는 건 지금까지 그렇게 피하려고 했던 일이 아닌가. 또한 DB 연결횟수를 세는 일은 DAO의 관심사항이 아니다. 어떻게든 분리돼야 할 책임이기도 하다. DI 컨테이너에서라면 아주 간단한 방법으로 가능하다. DAO와 DB 커넥션을 만드는 오브젝트 사에에 연결횟수를 카운팅히는 오브젝트를 하나 더 추가하는 것이다. 뭔가 새로운 기능을 호출 과정에 추가하려고 한다면 분명 그 앞뒤의 코드를 같이 수정해야 하는 것이 아닐까? 꼭 그럴 필요는 없다. DI 의 개념을 응용히는 것으로 충분하다. DI를 이용한다고 했으니 당연히 기존 코드는 수정하지 않아도 된다. 그리고 컨테이너가 사용하는 설정정보만 수정해서 런타임 의존관계만 새롭게 정의해주면 된다. DI 의 장점은 관심사의 분리를 통해 얻어지는 높은 응집도에서 나온다.

스프링은 DI를 편하게 사용할 수 있도록 도와주는 도구이면서 그 자체로 DI를 적극 활용한 프레임워크이기도 하다. 그래서 스프링을 공부하는 건 DI를 어떻게 활용해야 할지를 공부하는 것이기도 하다. 1장을 통해 스프링이란 ‘어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크라는 걸 알 수 있다. 하지만 오브젝트를 어떻게 설계하고, 분리하고, 개선하고, 어떤 의존관계를 가질지 결정하는 일은 스프링이 아니라 개발자의 역할이며 책임이다. 스프링은 단지 원칙을 잘 따르는 설계를 적용하려고 할 때 필연적으로 등장하는 번거로운 작업을 편하게 할 수 있도록 도와주는 도구일 뿐임을 잊지 말자.