관심사 분리제어의 역전(Inversion Of Control)오브젝트 팩토리Spring IoC 용어 정리오브젝트 팩토리가 아닌 어플리케이션 컨텍스트를 사용했을 때 얻을 수 있는 장점싱글톤 레지스트리로서의 애플리케이션 컨텍스트의존관계 주입(DI)의존관계의존관계 검색의존관계 주입의 응용부가기능 추가기능 구현의 교환
스프링이란 어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크 라는 사실을 꼭 기억하기. 스프링의 관심은 오브젝트와 그 관계
하지만 오브젝트를 어떻게 설계하고, 분리하고, 개선하고, 어떤 의존관계를 가질지 결정하는 일은 스프링이 아니라 개발자의 역할이며 책임
관심사 분리
- 관심사의 분리. 관심이 같은 것 끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지않도록 분리
extract method방식으로 분리
— 문제점 : 클라이언트가 해당 기능을 이용하고자 할 때 소스코드를 제공하지 않고 이용 하게끔 하고 싶지만 중요 기능이 메서드로만 구현되어 있으면 소스코드를 제공하지 않고서야 확장이 불가능함 → 상속을 통한 템플릿 메서드 패턴 이용상하위 클래스로 분리(상속)
— 문제점 : 상속이라는 방법은 여러가지 단점이 많음. 슈퍼클래스가 바뀌면 모든 서브클래스를 함께 수정하거나 다시 개발해야 할 수 있음(두 가지 다른 관심사에 대해 긴밀한 결합 허용)- 이 때 구체 클래스로 composition을 한다면 그 클래스에 대해 너무 구체적으로 알고 있게 되고 구체적 방법에 종속되게 됨(확장이 힘듦) → 인터페이스 도입(어떤 일을 하겠다는 기능만 정의해 놓았고, 구현 방법은 나타나 있지 않은 것)
- 그러나 이렇게 되었을때 그 인터페이스를 사용하는 클래스의 생성자에서 구체 클래스로 인터페이스를 초기화 해야 되는 문제가 발생! →
관계 설정 책임의 분리가 필요
- 어떤 구체 클래스를 사용해야 될지를 명시해주는 관심사가 포함되어 있는 것 ⇒ UserDao의 클라이언트 오브젝트가 바로 제 3의 관심사항인 UserDao와 ConnectionMaker 구현 클래스의 관계를 결정해주는 기능을 분리해서 두기에 적절한곳!!
- 클래스 사이에 관계가 만들어진다는 것은 한 클래스가 인터페이스 없이 다른 클래스를 직접 사용한다는 뜻
클래스 사이의 관계
는 코드에 다른 클래스 이름이 나타나기 때문에 만들어지는 것- 따라서
클래스 사이의 관계
아닌오브젝트 사이의 관계
를 설정해주어야 함(오브젝트 사이의 관계는 런타임 시에 한쪽이 다른 오브젝트의 레퍼런스를 갖는 방식으로 만들어짐) connectionMaker = new DConnectionMaker();
→ DConnectionMaker 오브젝트의 레퍼런스를 UserDao의 connectionMaker 변수에 사용하게 함으로써 이 두개의 오브젝트가 ‘사용'이라는 관계를 맺게 해줌- 오브젝트 사이의 관계는 코드에서는 특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면, 그 클래스의 오브젝트를 인터페이스 타입으로 받아서 사용할 수 있음
- 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다. UserDao는 DB연결 방법이라는 기능을 확장하는 데는 열려 있음
- 동시에 UserDao 자신의 핵심 기능을 구현한 코드는 그런 변화에 영향을 받지 않고 유지할 수 있으므로 변경에는 닫혀 있다고 말할 수 있음
- 인터페이스를 사용해 확장 기능을 정의한 대부분의 API는 바로 이 개방 폐쇄 원칙을 따른다고 볼 수 있음
- 개방 폐쇄 원칙은 높은 응집도와 낮은 결합도 라는 소프트웨어 개발의 고전적인 원리로도 설명이 가능함
- 응집도가 높다는 건 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻
- UserDao 클래스에는 사용자의 데이터를 처리하는 기능이 그 안에 이해하기 쉽고 깔끔하게 모여있음
- 불필요하거나 직접 관련이 없는 외부의 관심과 책임이 얽혀 있지 않고
- 낮은 결합도 : 책임과 관심사가 다른 오브젝트 또는 모듈과는 낮은 결합도, 즉 느슨하게 연결된 형태를 유지하느느 것이 바람직하다. → 하나의 변경이 발생할 때 다른 모듈과 객체로 변경에 대한 요구가 전파되지 않는 상태
- ConnectionMaker 인터페이스의 도입으로 DB 연결 기능을 구현한 클래스가 바뀌더라도 UserDao는 변경 x
- ConnectionMaker 클래스를 결정하는 책임을 DAO의 클라이언트로 분리 → 구현 클래스 바뀌어도 UserDao 변경 x
완전히 독립적인 클래스로 만들기(Composition)


개방 폐쇄 원칙
높은 응집도와 낮은 결합도
제어의 역전(Inversion Of Control)
- 일반적인 프로그램 구조에서는 모든 오브젝트가 능동적으로 자신이 사용할 클래스를 결정하고, 언제 어떻게 그 오브젝트를 만들지를 스스로 관장. 모든 종류의 작업을 사용하는 쪽에서 제어하는 구조
- 이런 제어 흐름의 개념을 거꾸로 뒤집는 것
- 프로그램의 시작을 담당하는 main()과 같은 엔트리 포인트를 제외하면 모든 오브젝트는 이렇게 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어짐
- 제어의 역전 개념은 이미 폭넓게 적용되어 있음. 서블릿을 생각해보면, 서블릿에 대한 제어 권한을 가진 컨테이너가 적절한 시점에 서블릿 클래스의 오브젝트를 만들고 그 안의 메서드를 호출함
- 템플릿 메서드 패턴 또한도 제어의 역전이라는 개념을 활용해 문제를 해결하는 디자인 패턴임
- 프레임워크도 제어의 역전 개념이 적용된 대표적인 기술. 프레임워크는 애플리케이션 코드가 프레임워크에 의해 사용됨. 보통 프레임워크 위에 개발한 클래스를 등록해두고, 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 만드는 방식.
오브젝트 팩토리
실질적인 로직
을 담당하는 컴포넌트(ConnectionMaker, UserDao)와, 애플리케이션을 구성하는컴포넌트의 구조와 관계를 정의하는 설계도 같은 역할
(간단히, 어떤 오브젝트가 어떤 오브젝트를 사용하는 지를 정의해놓은 코드)을 담당하는 컴포넌트(DaoFactory)가 있음.
- 위에서는 UserDaoTest에서 ConnectionMaker 클래스를 생성해서 넘겨주었는데 이는 그 관계를 정의하는 책임까지 UserDaoTest가 갖고 있는 것임. 이를 오브젝트 팩토리로 분리하기
public class DaoFactory{ public UserDao userDao(){ ConnectionMaker connectionMaker = new DConnectionMaker(); UserDao userDao = new UserDao(connectionMaker); return userDao; } }
- 장점 : 에플리케이션의 컴포넌트 역할을 하는 오브젝트와(UserDao, ConnectionMaker)
애플리케이션의 구조를 결정하는 오브젝트를 분리
했다는 의미가 큼
- 어떤 ConnectionMaker 구현 클래스를 만들고 사용할지를 결정할 권한을 DaoFactory에 넘김으로 UserDao는 이제 능동적이 아닌 수동적인 존재가 됨. DaoFactory를 도입하는 과정이 바로 IoC를 적용하는 작업
- 제어의 역전에서는 프레임워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요함.
Spring IoC 용어 정리
- Bean : 스프링이 IoC 방식으로 관리하는 오브젝트
- Bean Factory : 스프링의 IoC를 담당하는 핵심 컨테이너를 가리킴. 빈을 등록하고, 생성하고 조회하고 돌려주고 그 외에 부가적인 빈을 관리하는 기능을 담당
- ApplicationContext(~= BeanFactory) : 빈 팩토리를 확장한 IoC 컨테이너. IoC 방식을 따라 만들어진 일종의 빈 팩토리 + 스프링이 제공하는 각종 부가 서비스를 추가로 제공함
- 컨테이너 or IoC Container: IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고 함
- 애플리케이션 컨텍스트는 그 자체로 ApplicationContext 인터페이스를 구현한 오브젝트를 가리키기도 하는데, 애플리케이션 컨텍스트 오브젝트는 하나의 애플리케이션에서 보통 여러 개가 만들어져 사용 되기에 이를 통틀어 스프링 컨테이너라고 부를 수 있음
- 앞에서는 DaoFactory 자체가 설정정보까지 담고 있는 IoC 엔진 이었음. 이 설정정보가 설계도 역할을 하는 것이고 애플리케이션 컨텍스트와 그 설정정보를 말한다고 보면 됨.
오브젝트 팩토리가 아닌 어플리케이션 컨텍스트를 사용했을 때 얻을 수 있는 장점
- 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없음
애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해줌
- 단지 오브젝트 생성과 오브젝트와의 관계설정만이 전부가 아닌, 오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수도 있고, 이에 부가적으로 자동생성, 오브젝트에 대한 후처리, 정보의 조합, 설정방식의 다변화, 인터셉팅 등 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공함.
- 빈이 사용할 수 있는 기반기술 서비스나 외부 시스템과의 연동 등을 컨테이너 차원에서 제공해주기도 함
- 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공함
- 빈이 싱글톤으로 관리됨. 팩토리로 관리하면 매번 호출할 때마다 새로운 객체를 만듦.
싱글톤 레지스트리로서의 애플리케이션 컨텍스트
오브젝트의 동일성(identity)과 동등성(equality)
동일성(identity) 비교 : 두개의 오브젝트가 완전히 같은 동일한 오브젝트라는 것
동등성(equality) 비교 : 두개의 오브젝트가 동일한 정보를 담고 있는 오브젝트
평범한 자바 클래스라도 싱글톤으로 활용할 수 있게 해준다는 장점이 있음
- 애플리케이션 컨텍스트는 오브젝트 팩토리와 비슷한 방식으로 동작하지만, 싱글톤으로 빈들을 관리함
- 기본으로 별다른 설정 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만듦
- 싱글톤으로 빈을 만드는 이유 ?? → 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문(요청 당 오브젝트가 계속 만들어진다고 생각하면, 초당 500 개 요청 들어오면 초당 500개 오브젝트가 생성되는 것..)
- 그래서, 엔터프라이즈 분야에서는 서비스 오브젝트라는 개념을 일찍부터 사용해옴
- 서블릿은 자바 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할 수 있음
- 서블릿은 대부분 멀티스레드 환경에서 싱글톤으로 동작함
- 싱글톤 레지스트리 덕분에 싱글톤 방식으로 사용될 애플리케이션 클래스라도 public 생성자를 가질 수 있음
싱글톤과 오브젝트의 상태
싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있음. 따라서 상태 관리에 주의를 기울여야 함
기본적으로 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는 상태정보를 내부에 갖고 있지 않은 무상태 방식으로 만들어야 함.
의존관계 주입(DI)
DI 받는다
DI의 동작방식은 이름 그대로 외부로부터의 주입이다. 하지만 단지 외부에서 파라미터로 오브젝트를 넘겨줬다고 해서, 즉 주입해줬다고 해서 다 DI가 아니라는 점을 주의해야 한다. 주입받는 메소드 파라미터가 이미 특정 클래스 타입으로 고정되어 있다면 DI가 일어날 수 없다
DI에서 말하는 주입은 다이내믹하게 구현 클래스를 결정해서 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이루어져야 한다.
해당 책에서는 DI 원리를 지키며 외부에서 오브젝트를 제공받는 방법을 단순히 ‘
주입받는다
’ 라고 하는 대신 ‘DI 받는다’ 라고 표현하기도 할 것 ⇒ 단순한 오브젝트 주입이 아닌 DI 개념을 따르는 주입임을 강조하는 것- IoC는 소프트웨어에서 자주 발견할 수 있는 일반적인 개념. 객체지향적인 설계나 디자인 패턴, 컨테이너에서 동작하는 서버 기술을 사용한다면 자연스럽게 IoC를 적용하거나 그 원리로 동작하는 기술을 사용하게 될 것임
- 한 가지 짚고 넘어갈 것은 IoC 가 매우 느슨하게 정의돼서 폭넓게 사용되는 용어이기에 스프링을 IoC 컨테이너라고만 해서는 스프링이 제공하는 기능의 특징을 명확하게 설명하지 못함 ⇒ 스프링이 제공하는 IoC 방식의 핵심을 짚어주는
의존관계 주입
이라는 좀 더 의도가 명확히 드러나는 이름을 사용하기 시작함. - 의존(종속) 오브젝트 주입이라고도 불리지만 이는 엄밀히 말하면 틀림. 오브젝트의 참조를 전달할 뿐이기에.
- DI에서 말하는 주입은 다이나믹하게 구현 클래스를 결정해서 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이루어져야 함
의존 관계 주입의 세 가지 조건
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 함
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해 줌으로써 만들어짐.
- 의존 관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제 3의 존재가 있다는 것임.
의존관계
- 두개의 클래스 또는 모듈이 의존관계에 있다고 말할 때는 항상 방향성을 부여해주어야 함
A가 B에 의존한다 의 의미?
- B가 변하면 A에게로 그 영향이 전달된다는 것. B의 기능이 추가되거나 변경되거나 형식이 바뀐다거나 하면 그 영향이 A로 전달된다는 것
- 예를 들어 A에서 B에 정의된 메소드를 호출해서 사용하는 경우 →
사용에 대한 의존관계
- 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계 말고, 런타임 시에 오브젝트 사이에서 만들어지는 의존관계도 있음. 런타임 의존관계 또는 오브젝트 의존관계인데, 설계 시점의 의존관계가 실체화된 것이라고 볼 수 있음
- 런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트라고 말함.
- 의존관계 주입은 이렇게 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말함.
public class UserDao{ private ConnectionMaker connectionMaker; public UserDao(ConnectionMaker connectionMaker){ this.connectionMaker = connectionMaker; } }
의존관계 검색
public UserDao{ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class); }
- 의존관계 주입 쪽이 훨씬 단순하고 깔끔
- 반면 의존관계 주입에서는 UserDao와 ConnectionMaker사이에 DI가 적용되려면 UserDao도 반드시 컨테이너가 만드는 빈 오브젝트여야 함
- 의존관계 검색 방식을 사용해야 할 때가 있음
- 스태틱 메서드인 main() 에서는 DI를 이용해 오브젝트를 주입받을 방법이 없
- 의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없음
- 위의 UserDao 에서 ConnectionMaker만 스프링의 빈이기만 하면되는데 반해
- 의존관계 주입에서는 UserDao와 ConnectionMaker 사이에 DI가 적용되려면 UserDao도 반드시 컨테이너가 만드는 빈 오브젝트여야 함
- 컨테이너가 UserDao에 ConnectionMaker 오브젝트를 주입해주려면 UserDao에 대한 생성과 초기화 권한을 갖고 있어야 하기에.
의존관계 주입의 응용
부가기능 추가
- 데코레이터 패턴을 활용하여 가능함
- 기존에 UserDao가 ConnectionMaker를 의존관계를 맺고 있었는데 그 사이에 CountingConnectionMaker를 하나 끼워넣으면 됨
public class CountingConnectionMaker implements ConnectionMaker { int counter=0; private ConnectionMaker realConnectionMaker; public CountingConnectionMaker(ConnectionMaker realConnectionMaker){ this.realConnectionMaker = realConnectionMaker; } public Connection makeConnection() throws ClassNotFoundException{ this.counter++; return realConnectionMaker.makeConnection(); } }
기능 구현의 교환
- DI 방식을 적용해서 만들면 바꾸고 싶은 기능에 해당하는 Bean의 생성 부분만 다른 클래스로 교체해주면 쉽게 적용이 됨
@Bean public ConnectionMaker connectionMaker() { return new LocalDBConnectionMaker(); } @Bean public ConnectionMaker connectionMaker() { return new ProductionDBConnectionMaker(); }