분리와 확장을 위해 자기 참조 빈으로 시작하기리소스 추상화리소스 로더인터페이스 상속을 통한 안전한 기능확장DI와 기능의 확장DI로서 인터페이스를 사용해야 하는 이유인터페이스 상속새로운 요구사항
7장에서는 스프링 DI와 서비스 추상화 등을 응용해 새로운 SQL 서비스 기능을 설계하고 개발한 뒤에 이를 점진적으로 확장 발전시킨다. 스프링 사용자라면 객체지향적인 설계와 DI를 효과적으로 활용하는 방법에 익숙해야 한다.
스프링이 제공해주는 내장 기능에만 만족하지 말고 스프링의 기반기술을 자유자재로 활용할 수 있도록 다양한 시도를 해야 한다.
스프링이 제공해주지 않는 기능을 직접 구현할 때도 적극적으로 DI와 서비스 추상화, AOP 등을 활용할 수 있어야 한다.
그것이 스프링이 프레임워크로서 제공하는 가치를 제대로 누리는 방법이다.
분리와 확장을 위해 자기 참조 빈으로 시작하기

public class XmlSqlService implements SqlService, SqlRegistry, SqlReader { ... }
<bean id="sqlService" class="springbook.user.sqlservice.XmlSqlService"> <property name="sqlReader" ref="sqlService" /> <property name="sqlRegistry" ref="sqlService" /> <property name="sqlmapFile" value="sqlmap.xml" /> </bean>
- 자기 자신을 참조하는 빈은 사실 흔히 쓰이는 방법은 아니다. 책임이 다르다면 클래스를 구분하고 각기 다른 오브젝트로 만들어지는 것이 자연스럽다. 다만 자기 참조 빈을 만들어 보는 것은, 책임과 관심사가 복잡하게 얽혀 있어서 확장이 힘들고 변경에 취약한 구조의 클래스를 유연한 구조로 만들려고 할 때 처음 시도해 볼 수 있는 방법이다.
- 이를 통해 기존의 복잡하게 얽혀 있던 코드를 책임을 가진 단위로 구분해 낼 수 있다.
- 당장 확장 구조를 이용해 구현을 바꿔 사용하지 않더라도 확장구조를 만들어두는 게 좋다고 생각될 때 가장 간단히 접근할 수 있는 방법이기도 하다.
- 실제로 스프링이 제공하는 클래스 중에는 비슷하게 자신의 의존 오브젝트 인터페이스를 스스로 구현하고 자신을 참조하거나 DI 하는 코드가 제법 있음.
리소스 추상화
- 클래스패스 루트 경로에 존재하는 파일을 읽으려고 할 때, 파일 시스템, http, ftp 프로토콜로 접근할 수 있는 리소스 파일 가져오고 싶을 때, 리소스 추상화가 필요. 자바에는 이런 것이 없음
- ⇒ 스프링은 자바에 존재하는 일관성 없는 리소스 접근 API를 추상화해서
Resource
라는 추상화 인터페이스를 정의했다.
Resource
는 빈으로 등록하지 않기에 외부에서 지정할때 <property>의 value 애트리뷰트에 넣는 방법밖에 없는데, 여기에는 문자열 밖에 못넣음 → 스프링에는 URL 클래스와 유사하게 접두어를 이용해 Resource 오브젝트를 선언할 수 있음- 문자열 안에 리소스의 종류와 리소스 위치를 함께 표현
- file:/C:/temp/file.txt
- classpath:file.txt
- http://www.myserver.com/test.data
- 접두어가 없는 경우 ResourceLoader의 구현에 따라 리소스의 위치가 결정됨
리소스 로더
ResourceLoader
: 문자열 안에 리소스의 종류와 위치를 함께 표현하게 하여 문자열로 정의된 리소스를 실제 Resource 타입 오브젝트로 변환해줌- 대표적 예시 : Spring Application Context. 스프링 설정정보 불러올 때도 리소스 로더를 이용해 Resource 형태로 읽음
- 이외에도 Application Context가 읽어오는 모든 정보는 리소스 로더를 사용함
- 접두어를 붙여주면 리소스 로더의 종류와 상관없이 접두어가 의미하는 위치와 방법을 이용해 리소스를 읽음
- 접두어를 붙이지 않으면 리소스 로더의 구현 방식에 따라 리소스를 가져오는 방식이 달라짐
- 스프링이 제공하는 빈으로 등록 가능한 클래스에 파일을 지정해주는 프로퍼티가 존재한다면 거의 모두 Resource 타입임
private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class);
<property name="sqlmap" value="classpath:springbook/user/dao/sqlmap.xml" /> <property name="sqlmap" value="file:/opt/resources/sqlmap.xml" /> <property name="sqlmap" value="http://www.epril.com/resources/sqlmap.xml" />
- 이렇게 스프링의 리소스 추상화를 이용하면 리소스의 위치나 접근 방법에 독립적인 코드를 쉽게 만들 수 있음
인터페이스 상속을 통한 안전한 기능확장
잘 적용된 DI는 결국 잘 설계된 오브젝트 의존관계에 달려 있다.
인터페이스를 적절하게 분리하고 확장하는 방법을 통해 오브젝트 사이의 의존관계를 명확하게 해주고, 기존 의존관계에 영향을 주지 않으면서 유연한 확장성을 얻는 방법이 무엇인지 항상 고민해야 한다.
다시 말하지만, DI와 객체지향 설계는 서로 밀접한 관계를 맺고 있다.
DI와 기능의 확장
- DI는 특별한 기술 이라기보다는 일종의 디자인 패턴 또는 프로그래밍 모델이라는 관점에서 이해하는 것이 더 자연스럽다.
- 스프링과 같은 DI 프레임워크를 적용하고 빈 설정파일을 이용해 애플리케이션을 구성했다고 해서 DI를 바르게 활용하고 있다고 볼 수는 없다. DI의 가치를 제대로 얻으려면 먼저 DI에 적합한 오브젝트 설계가 필요하다.
- 유연하고 확장 가능한 좋은 오브젝트 설계와 DI 프로그래밍 모델은 서로 상승작용을 한다.
- 객체지향 설계를 잘하는 방법은 다양하겠지만, 그중에서 추천하고 싶은 한 가지가 있다면 바로 DI를 의식하면서 설계하는 방식이다.
- DI를 적용하려면 커다란 오브젝트 하나만 존재해서는 안 된다.
- 최소한 두 개 이상의 의존관계를 가지고 서로 협력해서 일하는 오브젝트가 필요하다.
- 그리고 항상 의존 오브젝트는 자유롭게 확장될 수 있다는 점을 염두에 둬야 한다.
- 확장은 항상 미래에 일어나는 일이다. 지금 당장 기능이 동작하는 데 아무런 문제가 없으면 된다고 생각하면 오늘을 위한 설계 밖에 나오지 않는다.
DI로서 인터페이스를 사용해야 하는 이유
- 다형성을 편리하게 적용할 수 있음(=구현 클래스를 바꾸고 의존 오브젝트를 변경해서 자유롭게 확장 가능. 같은 타입으로 존재하지만 다른 구현을 가진 오브젝트를 만들수 있음)
- 인터페이스 분리 원칙을 통해 클라이언트와 의존 오브젝트 사이의 관계를 명확하게 해줄 수 있음
- DI를 적용할 때는 가능한 한 인터페이스를 사용하게 해야 한다. 물론 인터페이스 사용하지 않고도 DI가 가능하지만 DI를 DI답게 만들려면 두 개의 오브젝트가 인터페이스를 통해 느슨하게 연결돼야 함
- DI의 목적이 다형성을 편리하게 적용하는 것 때문만이라면 제약이 많고 불편한 점이 있다고 해도 클래스를 사용할 수도 있다. 상속이 불가능한 final 클래스만 아니라면 상속을 통해서도 여러 가지 방식으로 구현을 확장할 수 있기 때문이다.
- 그럼에도 인터페이스를 사용해야 하는 이유가 또 있다면 그것은 인터페이스 분리 원칙을 통해 클라이언트와 의존 오브젝트 사이의 관계를 명확하게 해줄 수 있기 때문이다.
- A 오브젝트가 B 오브젝트를 사용한다고 했을 때, A를 사용하는 입장이니 클라이언트, B를 의존 오브젝트라 하면
- A와 B가 인터페이스로 연결되어 있다는 의미는 다르게 해석하는 A가 B를 바라볼 때 해당 인터페이스라는 창을 통해서만 본다는 뜻이다.
- 만약 B1이라는 인터페이스를 B가 구현하고 있고 A는 B1 인터페이스를 통해서만 B를 사용한다면, 그래서 DI 받을때도 B1 인터페이스를 통해 받는다면 A에게 B는 B1 이라는 관심사를 구현한 임의의 오브젝트에 불과하다.
- 즉 인터페이스는 하나의 오브젝트가 여러 개를 구현할 수 있으므로 하나의 오브젝트를 바라보는 창이 여러 가지일 수 있다는 뜻이다. 각기 다른 관심과 목적을 가지고 어떤 오브젝트에 의존하고 있을 수 있다는 의미다.
- 오브젝트가 그 자체로 충분히 응집도가 높은 작은 단위로 설계됐더라도, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 이를 적절하게 분리해줄 필요가 있고 이를 객체지향 설계 원칙에서는 인터페이스 분리 원칙(ISP)라고 부른다.
- 클라이언트는 자신이 필요로 하는 기능을 가진 인터페이스를 통해 의존 오브젝트를 DI 해야 함!!
- 예를 들어 EmbeddedDatabase(DataSource 하위 인터페이스)와 DataSource 인터페이스가 있을 때, 클라이언트가 JDBC를 이용해 DB에 접근할 수만 있으면 되기에 DataSource 인터페이스를 이용하는게 적합함.
인터페이스 상속
- 하나의 오브젝트가 구현하는 인터페이스를 여러 개 만들어서 구분하는 이유 중의 하나는 오브젝트의 기능이 발전하는 과정에서 다른 종류의 클라이언트가 등장하기 때문이다.
- 때로는 인터페이스를 여러 개 만드는 대신 기존 인터페이스를 상속을 통해 확장하는 방법도 사용된다.

- 위의 그림에서 BaseSqlService는 SqlRegistry 인터페이스에 의존하기 때문에 그의 구현 클래스인 MySqlRegistry를 받을 수 있고, MySqlRegistry 오브젝트는 또 다른 제 3의 클라이언트를 위한 인터페이스를 가질 수 있음
- 현재는 MySqlRegistry의 유일한 클라이언트는 BaseSqlService 이지만 기본 기능이 확장되면서 새로운 클라이언트가 나타날 수도 있다는 것
- 이를 위해 SqlRegistry와는 다른 인터페이스가 필요하지만(새로운 클라이언트를 위한 새로운 인터페이스) 꼭 별개의 인터페이스를 만들 필요 없이 SqlRegistry를 확장하는 인터페이스를 이용할 수도 있음
새로운 요구사항
public interface SqlRegistry { void registerSql(String key, String sql); String findSql(String key) throws SqlNotFoundException; }
- 위의 SqlRegistry에 더해 이미 등록된 SQL을 변경할 수 있는 기능을 넣어서 확장하고 싶다고 할 때,
- 이때는 이미
SqlRegistry
인터페이스를 이용해 접근하는 클라이언트인BaseSqlService
클래스와 그 서브 클래스가 존재하기 때문에SqlRegistry
인터페이스 자체를 수정하는 건 바람직한 방법이 아님. - BaseSqlService는 SqlRegistry 인터페이스가 제공하는 기능이면 충분하기 때문
- 따라서 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공한다는 인터페이스 분리 원칙을 지키기 위해서라도 이미 적용한 SqlRegistry 는 건드리면 안된다.
- SQL 저장소에 담긴 SQL 정보를 변경하는 기능을 추가하기로 했다는 건, 그런 기능을 사용할 클라이언트가 존재해야 한다는 의미. 아마 관리자가 사용할 SQL 관리 기능을 맡은 오브젝트가 새로운 클라이언트가 될 것
- SQL 관리 기능이라 하면 단순히 업데이트 뿐 아니라 SQL 등록이나 검색 같은 기본적인 기능도 필요할 테니 기존 SqlRegistry 인터페이스에 정의된 메소드도 사용할 필요가 있음
⇒ 새롭게 추가할 기능을 사용하는 클라이언트를 위해 새로운 인터페이스를 정의하거나 기존 인터페이스를 확장하는 게 바람직하다.
public interface UpdateableSqlRegistry extends SqlRegistry { public void updateSql(String key, String sql) throws SqlUpdateFailureException; public void updateSql(Map<String, String> sqlmap) throws SqlUpdateFailureException; }
UpdateableSqlRegistry
인터페이스를 BaseSqlService
클래스는 사용할 필요 없음. 거기에서는 이 기능이 필요하지 않기 때문에UpdateableSqlRegistry
인터페이스를 통해 SQL 레지스트리 오브젝트에 접근하도록 만들어야 함
BaseSqlService
는SqlRegistry
인터페이스에 의존
SqlAdminService
는UpdateableSqlRegistry
인터페이스에 의존
- 그러나, 둘 다
MyUpdateableSqlRegistry
오브젝트를 DI 받음
BaseSqlService
와SqlAdminService
는 동일한 오브젝트에 의존하고 있지만 각자의 관심과 필요에 따라 다른 인터페이스를 통해 접근함. 인터페이스를 사용하는 DI 이기 때문에 가능한 일
- 이렇게 인터페이스를 추가하거나 상속을 통해 확장하는 방식을 잘 활용하면 이미 기존의 인터페이스를 사용하는 클라이언트가 있는 경우에도 유연한 확장이 가능함
- 만약 SQL 수정 기능만을 처리하는 클라이언트가 필요했다면 기존 SqlRegistry 인터페이스를 상속하지 않고 새로운 인터페이스를 만들었을 수도 있음. 의존관계와 목적에 따라 적절한 방식 택하기