7장 스프링 핵심 기술의 응용 - 7.4 인터페이스 상속을 통한 안전한 기능확장
7.4 인터페이스 상속을 통한 안전한 기능확장
서버가 운영 중인 상태에서 서버를 재시작하지 않고 긴급하게 애플리케이션이 사용 중인 SQL 을 변경해야 할 수도 있습니다.
지금까지의 SqlService 구현 클래스는 SQL 정보를 초기에 읽어서 메모리에 두고 사용합니다.
SQL 정보를 실시간 반영하는 기능을 통해 기존에 설계하고 개발했던 기능이 발전되어야 할 경우, 스프링답게 접근하는 방법이 무엇인지 알아봅니다.
7.4.1 DI와 기능의 확장
지금까지 적용해왔던 DI 는 특별한 기술이라기보다 일종의 디자인 패턴 또는 프로그래밍 모델이라는 관점으로 이해됩니다.
DI 의 가치를 제대로 얻으려면 먼저 DI 에 적합한 오브젝스 설계가 필요합니다.
DI를 의식하는 설계
초기부터 SqlService 의 내부 기능을 적절한 책임과 역할에 따라 분리하고, 인터페이스를 정의해 느슨하게 연결해주고, DI 를 통해 유연하게 의존관계를 지정하도록 설계하였기 때문에 그 뒤의 작업이 매우 쉬워졌습니다.
결국 유연하고 확장 가능한 좋은 오브젝트 설계와 DI 프로그래밍 모델은 서로 상승작용을 합니다.
객체지향 설계를 잘하는 방법은 다양하겠지만, 그 중에서 추천하고 싶은 한가지가 있다면 바로 DI 를 의식하면서 설계하는 방식입니다.
DI 를 적용하려면 적절한 책임에 따라 분리된 오브젝트가 서로 의존관계를 가지고 협력하는 구조가 필요합니다.
또한 DI 는 런타임 시에 의존 오브젝트를 다이내믹하게 연결해줘서 유연한 확장을 꾀하는 것이 목적입니다.
항상 확장에 염두를 두고 오브젝트 관계를 생각해야 합니다.
확장은 항상 미래에 일어난다.
지금 당장 기능이 동작하는 데 아무런 문제가 없으면 된다고 생각하면 오늘을 위한 설계밖에 나오지 않는다.
DI 는 확장을 위해 필요한 것이므로 항상 미래에 일어날 변화를 예상하고 고민해야 적합한 설계가 가능해진다.
DI 란 결국 미래를 프로그래밍하는 것이다.
7장_ 스프링 핵심 기술의 응용, 618.
DI와 인터페이스 프로그래밍
DI 를 적용할 때는 가능한 인터페이스를 사용해야 합니다.
인터페이스를 사용하는 첫 번째 이유는 다형성을 얻기 위해서입니다.
하나의 인터페이스를 통해 여러 개의 구현을 바꿔가면서 사용할 수 있게 하는 것이 DI 가 추구하는 첫 번째 목적입니다.
인터페이스를 사용하는 다른 이유는 인터페이스 분리 원칙을 통해 클라이언트와 의존 오브젝트 사이의 관계를 명확하게 해줄 수 있기 때문입니다.
A 가 B 의 인터페이스를 사용한다는 말은 A 가 B 를 바라볼 때 해당 인터페이스라는 창을 통해서 본다는 뜻입니다.
인터페이스는 하나의 오브젝트가 여러 개를 구현할 수 있기 때문에 하나의 오브젝트를 바라보는 창이 여러 개일 수 있습니다.
오브젝트가 그 자체로 충분히 응집도가 높은 작은 단위로 설계되었더라도, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 이를 적절하게 분리해주어야 합니다.
이를 객체지향 설계 원칙에서는 인터페이스 분리 원칙 Interface Segregation Principle 이라고 합니다.
DI 는 특별한 이유가 없는 한 항상 인터페이스를 사용한다고 생각해야 합니다.
Note:
7장_ 스프링 핵심 기술의 응용, 620.
단지 인터페이스를 추가하기 귀찮아서 약간의 게으름을 부리고자 인터페이스를 생략했다면 이후의 개발, 디버깅, 테스트, 기능의 추가, 변화 등에서 적지 않은 부담을 안게 될 것이다.
7.4.2 인터페이스 상속
하나의 오브젝트가 여러 개의 인터페이스를 만드는 이유 중의 하나는 오브젝트에게 다른 종류의 클라이언트가 등장하기 때문입니다.
인터페이스 분리 원칙이 주는 장점은 모든 클라이언트가 자신의 관심에 따른 접근 방식을 불필요한 간섭없이 유지할 수 있다는 점입니다.
BaseSqlService 와 그 서브클래스는 SqlReader 와 SqlRegistry 라는 두 개의 인터페이스를 통해 의존 오브젝트들을 DI 하도록 되어 있습니다.
SqlRegistry 의 구현클래스인 MySqlRegistry 의 오브젝트는 SqlRegistry 인터페이스 외에 또 다른 클라이언트를 위한 인터페이스를 가질 수 있습니다.
BaseSqlService 는 이 SqlRegistry 인터페이스를 구현하는 오브젝트에 의존하고 있습니다.
여기에 이미 등록된 SQL 을 변경할 수 있는 기능을 넣어서 확장하고 싶다고 생각해 봅니다.
기존의 SqlRegistry 인터페이스를 이용하는 클라이언트가 있기 때문에 SqlRegistry 인터페이스 자체를 수정하는 것은 바람직한 방법이 아닙니다.
클라이언트의 목적과 용도에 적합한 인터페이스만을 제공한다는 인터페이스 분리 원칙을 지키기 위해서라도 새로운 기능을 위해 이미 적용한 SqlRegistry 를 건드리는 것은 안됩니다.
이런 경우에는 추가할 기능을 이용하는 클라이언트를 위한 새로운 인터페이스를 정의하거나 기존 인터페이스를 확장해야 합니다.
새로운 클라이언트가 필요로 하는 인터페이스는 SQL 에 대한 수정을 요청할 수 있는 메소드를 갖고 있어야 합니다.
그리고 SQL 등록 및 검색 같은 기능이 있는 기존의 SqlRegistry 인터페이스에 정의된 메소드도 사용할 수 있어야 합니다.
따라서 기존의 SqlRegistry 인터페이스를 상속하고 메소드를 추가한 새로운 인터페이스를 정의해야 합니다.
// highlight-next-line
public interface UpdatableSqlRegistry extends SqlRegistry {
public void updateSql(String key, String sql) throws SqlUpdateFailureException;
public void updateSql(Map<String, String> sqlmap) throws SqlUpdateFailureException;
}
SQL 업데이트 작업이 필요한 새로운 클라이언트는 UpdatableSqlRegistry 인터페이스를 통해 SQL 레지스트리 오브젝트에 접근하도록 합니다.
새로운 클라이언트의 이름은 SqlAdminService 이라고 합니다.
UpdatableSqlRegistry 오브젝트는 BaseSqlService 와 SqlAdminService 에 DI 됩니다.
BaseSqlService 와 SqlAdminService 는 동일한 오브젝트에 의존하고 있지만 각자의 관심과 필요에 따라서 다른 인터페이스를 통해 접근합니다.
클라이언트가 정말 필요한 기능을 가진 인터페이스를 통해 오브젝트에 접근하도록 만들었는지가 중요합니다.
잘 적용된 DI 는 결국 잘 설계된 오브젝트 의존관계에 달려 있습니다.
이렇게 DI 와 객체지향 설계는 서로 밀접한 관계를 맺고 있습니다.