7.4) 인터페이스 상속을 통한 안전한 기능확장
DI와 기능의 확장
- 지금까지 만든 SqlService 구현 클래스들은 초기에 리소스로부터 SQL 정보를 읽어오면 이를 메모리에 두고 그대로 사용한다.
- 중간에 SQL 매핑정보 파일을 변경했다고 해서 메모리상의 SQL 정보가 갱신되진 않는다.
- 굳이 방법이 있다면 서버를 재시작하거나 웹 애플리케이션을 리로딩해서 다시 SqlService 구현 빈을 초기화하는 것이다.
- 하지만 애플리케이션을 다시 시작하지 않고 특정 SQL의 내용만을 변경하고 싶다면 어떻게 해야 할지 생각해보자.
- 또한 기존에 설계하고 개발했던 기능이 발전돼야 할 경우에 스프링답게 접근하는 방법이 무엇인지 살펴보자.
- DI의 가치를 제대로 얻으려면 먼저 DI에 적합한 오브젝트 설계가 필요하다.
- 단지 스프링과 같은 DI 프레임워크를 적용하고 빈 설정파일을 이용해 애플리케이션을 구성했다고 해서
DI를 바르게 활용하고 있다고 볼 수는 없다. - 모든 기능을 클래스 하나 안에 마구 섞어서 SqlService 구현 클래스를 만들었다면,
지금까지 진행했던 다양한 기능의 확장을 위해 클래스 전부를 뜯어고쳐야 하는
매우 비효율적이고 골치 아픈 작업에 시달려야 했을 것이다. - 하지만 초기부터 SqlService의 내부 기능을 적절한 책임과 역할에 따라 분리하고, 인터페이스를 정의해
느슨하게 연결해주고, DI를 통해 유연하게 의존관계를 지정하도록 설계해뒀기 때문에 그 뒤의 작업은 매우 쉬워졌다. - 오브젝트들이 서로 세부적인 구현에 얽매이지 않고
유연한 방식으로 의존관계를 맺으며 독립적으로 발전할 수 있게 해주는 DI 덕분이다.
결국 유연하고 확장 가능한 좋은 오브젝트 설계와 DI 프로그래밍 모델은 서로 상승작용을 한다.
- 단지 스프링과 같은 DI 프레임워크를 적용하고 빈 설정파일을 이용해 애플리케이션을 구성했다고 해서
- 하지만 DI의 가치를 제대로 누리기가 쉽진 않다.
- DI에 필요한 유연하고 확장성이 뛰어난 오브젝트 설계를 하려면 많은 고민과 학습, 훈련, 경험이 필요하다.
- DI를 적용하려면 의존관계를 가지고 서로 협력해서 일하는 오브젝트가 필요하므로
적절한 책임에 따라 오브젝트를 분리해주어 항상 의존 오브젝트가 자유롭게 확장할 수 있도록 해야 한다. - 이렇게 DI를 잘 활용할 수 있는 방법을 의식하면서 설계한다면
객체지향 기술이 약속하는 유연한 확장과 재사용이 가능한 설계를 만드는데 많은 도움이 될 것이다. - 확장은 항상 미래에 일어나는 일이므로
지금 당장 기능이 동작하는데 아무런 문제가 없으면 된다고 생각하면 오늘을 위한 설계밖에 나오지 않는다.
DI는 확장을 위해 필요한 것이므로 항상 미래에 일어날 변화를 예상하고 고민해야 적합한 설계가 가능해진다.
즉, DI란 결국 미래를 프로그래밍하는 것이다.
- DI를 적용할 때는 가능한 한 인터페이스를 사용하게 해야 한다.
- 인터페이스를 사용하는 첫 번째 이유는 다형성을 얻기 위해서다.
하나의 인터페이스를 통해 여러 개의 구현을 바꿔가면서 사용할 수 있게 하는 것이 DI가 추구하는 첫 번째 목적이다.
의존 오브젝트가 가진 핵심 로직을 바꿔서 적용하는 것 외에도
프록시, 데코레이터, 어댑터, 테스트 대역 등의 다양한 목적을 위해 인터페이스를 통한 다형성이 활용된다. - 인터페이스를 사용하는 또다른 이유는
인터페이스 분리 원칙을 통해 클라이언트와 의존 오브젝트 사이의 관계를 명확하게 해줄 수 있기 때문이다.
인터페이스는 하나의 오브젝트가 여러 개를 구현할 수 있으므로, 하나의 오브젝트를 바라보는 창이 여러 개일 수 있어
각기 다른 관심과 목적을 가지고 어떤 오브젝트에 의존하고 있을 수 있다.
그러면 이렇게 클라이언트의 종류에 따라 적절하게 분리해서 오브젝트가 구현하게 하면 매우 유용하다. - 또한 오브젝트가 그 자체로 충분히 응집도가 높은 작은 단위로 설계됐더라도, 목적과 관심이 각기 다른 클라이언트가 있다면
인터페이스를 통해 이를 적절하게 분리해줄 필요가 있고, 이를 객체지행 원칙에서는 인터페이스 분리 원칙이라고 부른다.
만약 인터페이스를 사용하지 않고 클래스를 직접 참조하는 방식으로 DI를 했다면,
인터페이스 분리 원칙과 같은 클라이언트에 특화된 의존관계를 만들어낼 방법 자체가 없는 것이다. - 다형성은 물론이고 클라이언트별 다중 인터페이스 구현과 같은
유연하고 확장성 높은 설계가 가능함에도 인터페이스를 피할 이유는 없다.
DI는 특별한 이유가 없는 한 항상 인터페이스를 사용한다고 기억해두자.
- 인터페이스를 사용하는 첫 번째 이유는 다형성을 얻기 위해서다.
인터페이스 상속
- 하나의 오브젝트가 구현하는 인터페이스를 여러 개 만들어서 구분하는 이유 중의 하나는
오브젝트의 기능이 발전하는 과정에서 다른 종류의 클라이언트가 등장하기 때문이다.- 때로는 인터페이스를 여러 개 만드는 대신 기존 인터페이스를 상속을 통해 확장하는 방법도 사용된다.
- 이러한 인터페이스 분리 원칙은 모든 클라이언트가 자신의 관점에 따른 접근 방식을 불필요한 간섭 없이 유지할 수 있다.
그래서 기존 클라이언트에 영향을 주지 않은 채로 오브젝트의 기능을 확장하거나 수정할 수 있다.
- SqlService의 기본 구현인 BaseSqlService 클래스의 설계구조를 살펴보자.
- BaseSqlService와 그 서브클래스는
SqlReader와 SqlRegistry라는 두 개의 인터페이스를 통해 의존 오브젝트들을 DI 하도록 되어 있다. - BaseSqlService는 SqlRegistry를 구현한 클래스의 오브젝트에 접근하기 때문에
오브젝트의 구현 내용이 변경을 통해 확장될지라도 BaseSqlService 클래스는 변경 없이 유지될 수 있다. - 인터페이스를 사용해 DI 관계를 만들었기 때문에
SqlRegistry의 구현 클래스의 오브젝트는 확장을 위해 또 다른 제3의 클라이언트를 위한 인터페이스를 가질 수 있다.
이처럼 새로운 클라이언트의 성격에 따라서 기존 SqlRegistry를 확장한 인터페이스를 이용할 수도 있다.
- BaseSqlService와 그 서브클래스는

- 기존의 SqlRegistry에 이미 등록된 SQL을 변경할 수 있는 기능을 넣어서 확장하고 싶다면?
- 이미 SqlRegistry 인터페이스를 이용해 접근하는 클라이언트인 BaseSqlService 클래스와
그 서브클래스가 존재하기 때문에 SqlRegistry 인터페이스 자체를 수정하는 건 바람직한 방법이 아니다. - 대신 새롭게 추가할 기능을 사용하는 클라이언트를 위해
새로운 인터페이스를 정의하거나 기존 인터페이스를 확장하는 게 바람직하다. - 이때 이와 같은 SQL 관리 기능은 기존의 SQL 등록이나 검색 같은 기본적인 기능도 필요할테니
기존 SqlRegistry 인터페이스에 정의된 메소드도 사용할 필요가 있다.
그러므로 기존의 SqlRegistry 인터페이스를 상속하고 메소드를 추가해서 새로운 인터페이스를 정의하도록 한다. - 이를 위해 SQL의 등록과 조회만 가능한 SqlRegistry에 SQL 업데이트를 위한 기능을 추가한
서브 인터페이스인 UpdatableSqlRegistry를 생성할 수 있다. - 이후 기존의 BaseSqlService는 초기화를 통한 SQL 등록과 조회만을 목적으로
SQL 레지스트리 오브젝트를 사용할 것이므로, 기존의 SqlRegistry 인터페이스를 통해 접근하면 충분하고,
반면에 SQL 업데이트 작업이 필요한 새로운 클라이언트 오브젝트인 SqlAdminService는
UpdatableSqlRegistry 인터페이스를 통해 SQL 레지스트리 오브젝트인 MyUpdatableSqlRegistry에 접근하도록 한다. - 실제 오브젝트 사이에 일어나는 DI의 결과만 보자면 BaseSqlService와 SqlAdminService 오브젝트는
동일한 MyUpdatableSqlRegistry 오브젝트를 DI 받아서 사용하게 되지만,
설계와 코드에서는 각자의 관심과 필요에 따라 각각 SqlRegistry와 UpdatableSqlRegistry 인터페이스에 의존하게 된다. - 이렇게 인터페이스를 추가하거나 상속을 통해 확장하는 방식을 잘 활용하면
이미 기존의 인터페이스를 사용하는 클라이언트가 있는 경우에도 유연한 확장이 가능해진다.
- 이미 SqlRegistry 인터페이스를 이용해 접근하는 클라이언트인 BaseSqlService 클래스와

<bean id="sqlService" class="com.gaga.springtoby.user.sqlService.BaseSqlService">
<property name="unmarshaller" ref="unmarshaller"/>
<property name="sqlRegistry" ref="sqlRegistry"/>
</bean>
<bean id="sqlRegistry" class="com.gaga.springtoby.user.sqlService.MyUpdatableSqlRegistry"/>
<bean id="sqlAdminService" class="com.gaga.springtoby.user.sqlService.SqlAdminService">
<property name="unmarshaller" ref="unmarshaller"/>
<property name="updatableSqlRegistry" ref="sqlRegistry"/>
</bean>
<bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="contextPath" value="com.gaga.springtoby.user.sqlService.jaxb"/>
</bean>
- 잘 적용된 DI는 결국 잘 설계된 오브젝트 의존관계에 달려 있다.
- 인터페이스를 적절하게 분리하고 확장하는 방법을 통해 오브젝트 사이의 의존관계를 명확하게 해주고,
기존 의존관계에 영향을 주지 않으면서 유연한 확장성을 얻는 방법이 무엇인지 항상 고민해야 한다. - 다시 말하자면, DI와 객체지향 설계는 서로 밀접한 관계를 맺고 있다.
- 인터페이스를 적절하게 분리하고 확장하는 방법을 통해 오브젝트 사이의 의존관계를 명확하게 해주고,
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (6) (0) | 2024.04.06 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (5) (0) | 2024.02.17 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (3) (0) | 2024.02.05 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (2) (0) | 2024.02.01 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (1) (0) | 2024.01.24 |
7.4) 인터페이스 상속을 통한 안전한 기능확장
DI와 기능의 확장
- 지금까지 만든 SqlService 구현 클래스들은 초기에 리소스로부터 SQL 정보를 읽어오면 이를 메모리에 두고 그대로 사용한다.
- 중간에 SQL 매핑정보 파일을 변경했다고 해서 메모리상의 SQL 정보가 갱신되진 않는다.
- 굳이 방법이 있다면 서버를 재시작하거나 웹 애플리케이션을 리로딩해서 다시 SqlService 구현 빈을 초기화하는 것이다.
- 하지만 애플리케이션을 다시 시작하지 않고 특정 SQL의 내용만을 변경하고 싶다면 어떻게 해야 할지 생각해보자.
- 또한 기존에 설계하고 개발했던 기능이 발전돼야 할 경우에 스프링답게 접근하는 방법이 무엇인지 살펴보자.
- DI의 가치를 제대로 얻으려면 먼저 DI에 적합한 오브젝트 설계가 필요하다.
- 단지 스프링과 같은 DI 프레임워크를 적용하고 빈 설정파일을 이용해 애플리케이션을 구성했다고 해서
DI를 바르게 활용하고 있다고 볼 수는 없다. - 모든 기능을 클래스 하나 안에 마구 섞어서 SqlService 구현 클래스를 만들었다면,
지금까지 진행했던 다양한 기능의 확장을 위해 클래스 전부를 뜯어고쳐야 하는
매우 비효율적이고 골치 아픈 작업에 시달려야 했을 것이다. - 하지만 초기부터 SqlService의 내부 기능을 적절한 책임과 역할에 따라 분리하고, 인터페이스를 정의해
느슨하게 연결해주고, DI를 통해 유연하게 의존관계를 지정하도록 설계해뒀기 때문에 그 뒤의 작업은 매우 쉬워졌다. - 오브젝트들이 서로 세부적인 구현에 얽매이지 않고
유연한 방식으로 의존관계를 맺으며 독립적으로 발전할 수 있게 해주는 DI 덕분이다.
결국 유연하고 확장 가능한 좋은 오브젝트 설계와 DI 프로그래밍 모델은 서로 상승작용을 한다.
- 단지 스프링과 같은 DI 프레임워크를 적용하고 빈 설정파일을 이용해 애플리케이션을 구성했다고 해서
- 하지만 DI의 가치를 제대로 누리기가 쉽진 않다.
- DI에 필요한 유연하고 확장성이 뛰어난 오브젝트 설계를 하려면 많은 고민과 학습, 훈련, 경험이 필요하다.
- DI를 적용하려면 의존관계를 가지고 서로 협력해서 일하는 오브젝트가 필요하므로
적절한 책임에 따라 오브젝트를 분리해주어 항상 의존 오브젝트가 자유롭게 확장할 수 있도록 해야 한다. - 이렇게 DI를 잘 활용할 수 있는 방법을 의식하면서 설계한다면
객체지향 기술이 약속하는 유연한 확장과 재사용이 가능한 설계를 만드는데 많은 도움이 될 것이다. - 확장은 항상 미래에 일어나는 일이므로
지금 당장 기능이 동작하는데 아무런 문제가 없으면 된다고 생각하면 오늘을 위한 설계밖에 나오지 않는다.
DI는 확장을 위해 필요한 것이므로 항상 미래에 일어날 변화를 예상하고 고민해야 적합한 설계가 가능해진다.
즉, DI란 결국 미래를 프로그래밍하는 것이다.
- DI를 적용할 때는 가능한 한 인터페이스를 사용하게 해야 한다.
- 인터페이스를 사용하는 첫 번째 이유는 다형성을 얻기 위해서다.
하나의 인터페이스를 통해 여러 개의 구현을 바꿔가면서 사용할 수 있게 하는 것이 DI가 추구하는 첫 번째 목적이다.
의존 오브젝트가 가진 핵심 로직을 바꿔서 적용하는 것 외에도
프록시, 데코레이터, 어댑터, 테스트 대역 등의 다양한 목적을 위해 인터페이스를 통한 다형성이 활용된다. - 인터페이스를 사용하는 또다른 이유는
인터페이스 분리 원칙을 통해 클라이언트와 의존 오브젝트 사이의 관계를 명확하게 해줄 수 있기 때문이다.
인터페이스는 하나의 오브젝트가 여러 개를 구현할 수 있으므로, 하나의 오브젝트를 바라보는 창이 여러 개일 수 있어
각기 다른 관심과 목적을 가지고 어떤 오브젝트에 의존하고 있을 수 있다.
그러면 이렇게 클라이언트의 종류에 따라 적절하게 분리해서 오브젝트가 구현하게 하면 매우 유용하다. - 또한 오브젝트가 그 자체로 충분히 응집도가 높은 작은 단위로 설계됐더라도, 목적과 관심이 각기 다른 클라이언트가 있다면
인터페이스를 통해 이를 적절하게 분리해줄 필요가 있고, 이를 객체지행 원칙에서는 인터페이스 분리 원칙이라고 부른다.
만약 인터페이스를 사용하지 않고 클래스를 직접 참조하는 방식으로 DI를 했다면,
인터페이스 분리 원칙과 같은 클라이언트에 특화된 의존관계를 만들어낼 방법 자체가 없는 것이다. - 다형성은 물론이고 클라이언트별 다중 인터페이스 구현과 같은
유연하고 확장성 높은 설계가 가능함에도 인터페이스를 피할 이유는 없다.
DI는 특별한 이유가 없는 한 항상 인터페이스를 사용한다고 기억해두자.
- 인터페이스를 사용하는 첫 번째 이유는 다형성을 얻기 위해서다.
인터페이스 상속
- 하나의 오브젝트가 구현하는 인터페이스를 여러 개 만들어서 구분하는 이유 중의 하나는
오브젝트의 기능이 발전하는 과정에서 다른 종류의 클라이언트가 등장하기 때문이다.- 때로는 인터페이스를 여러 개 만드는 대신 기존 인터페이스를 상속을 통해 확장하는 방법도 사용된다.
- 이러한 인터페이스 분리 원칙은 모든 클라이언트가 자신의 관점에 따른 접근 방식을 불필요한 간섭 없이 유지할 수 있다.
그래서 기존 클라이언트에 영향을 주지 않은 채로 오브젝트의 기능을 확장하거나 수정할 수 있다.
- SqlService의 기본 구현인 BaseSqlService 클래스의 설계구조를 살펴보자.
- BaseSqlService와 그 서브클래스는
SqlReader와 SqlRegistry라는 두 개의 인터페이스를 통해 의존 오브젝트들을 DI 하도록 되어 있다. - BaseSqlService는 SqlRegistry를 구현한 클래스의 오브젝트에 접근하기 때문에
오브젝트의 구현 내용이 변경을 통해 확장될지라도 BaseSqlService 클래스는 변경 없이 유지될 수 있다. - 인터페이스를 사용해 DI 관계를 만들었기 때문에
SqlRegistry의 구현 클래스의 오브젝트는 확장을 위해 또 다른 제3의 클라이언트를 위한 인터페이스를 가질 수 있다.
이처럼 새로운 클라이언트의 성격에 따라서 기존 SqlRegistry를 확장한 인터페이스를 이용할 수도 있다.
- BaseSqlService와 그 서브클래스는

- 기존의 SqlRegistry에 이미 등록된 SQL을 변경할 수 있는 기능을 넣어서 확장하고 싶다면?
- 이미 SqlRegistry 인터페이스를 이용해 접근하는 클라이언트인 BaseSqlService 클래스와
그 서브클래스가 존재하기 때문에 SqlRegistry 인터페이스 자체를 수정하는 건 바람직한 방법이 아니다. - 대신 새롭게 추가할 기능을 사용하는 클라이언트를 위해
새로운 인터페이스를 정의하거나 기존 인터페이스를 확장하는 게 바람직하다. - 이때 이와 같은 SQL 관리 기능은 기존의 SQL 등록이나 검색 같은 기본적인 기능도 필요할테니
기존 SqlRegistry 인터페이스에 정의된 메소드도 사용할 필요가 있다.
그러므로 기존의 SqlRegistry 인터페이스를 상속하고 메소드를 추가해서 새로운 인터페이스를 정의하도록 한다. - 이를 위해 SQL의 등록과 조회만 가능한 SqlRegistry에 SQL 업데이트를 위한 기능을 추가한
서브 인터페이스인 UpdatableSqlRegistry를 생성할 수 있다. - 이후 기존의 BaseSqlService는 초기화를 통한 SQL 등록과 조회만을 목적으로
SQL 레지스트리 오브젝트를 사용할 것이므로, 기존의 SqlRegistry 인터페이스를 통해 접근하면 충분하고,
반면에 SQL 업데이트 작업이 필요한 새로운 클라이언트 오브젝트인 SqlAdminService는
UpdatableSqlRegistry 인터페이스를 통해 SQL 레지스트리 오브젝트인 MyUpdatableSqlRegistry에 접근하도록 한다. - 실제 오브젝트 사이에 일어나는 DI의 결과만 보자면 BaseSqlService와 SqlAdminService 오브젝트는
동일한 MyUpdatableSqlRegistry 오브젝트를 DI 받아서 사용하게 되지만,
설계와 코드에서는 각자의 관심과 필요에 따라 각각 SqlRegistry와 UpdatableSqlRegistry 인터페이스에 의존하게 된다. - 이렇게 인터페이스를 추가하거나 상속을 통해 확장하는 방식을 잘 활용하면
이미 기존의 인터페이스를 사용하는 클라이언트가 있는 경우에도 유연한 확장이 가능해진다.
- 이미 SqlRegistry 인터페이스를 이용해 접근하는 클라이언트인 BaseSqlService 클래스와

<bean id="sqlService" class="com.gaga.springtoby.user.sqlService.BaseSqlService">
<property name="unmarshaller" ref="unmarshaller"/>
<property name="sqlRegistry" ref="sqlRegistry"/>
</bean>
<bean id="sqlRegistry" class="com.gaga.springtoby.user.sqlService.MyUpdatableSqlRegistry"/>
<bean id="sqlAdminService" class="com.gaga.springtoby.user.sqlService.SqlAdminService">
<property name="unmarshaller" ref="unmarshaller"/>
<property name="updatableSqlRegistry" ref="sqlRegistry"/>
</bean>
<bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="contextPath" value="com.gaga.springtoby.user.sqlService.jaxb"/>
</bean>
- 잘 적용된 DI는 결국 잘 설계된 오브젝트 의존관계에 달려 있다.
- 인터페이스를 적절하게 분리하고 확장하는 방법을 통해 오브젝트 사이의 의존관계를 명확하게 해주고,
기존 의존관계에 영향을 주지 않으면서 유연한 확장성을 얻는 방법이 무엇인지 항상 고민해야 한다. - 다시 말하자면, DI와 객체지향 설계는 서로 밀접한 관계를 맺고 있다.
- 인터페이스를 적절하게 분리하고 확장하는 방법을 통해 오브젝트 사이의 의존관계를 명확하게 해주고,
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (6) (0) | 2024.04.06 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (5) (0) | 2024.02.17 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (3) (0) | 2024.02.05 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (2) (0) | 2024.02.01 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 스프링 핵심 기술의 응용 (1) (0) | 2024.01.24 |