스프링 핵심 원리 - 기본편 / 객체 지향 설계 / DI와 정적, 동적 의존 관계
스프링 핵심 원리의 기본편을 수강중입니다. 이론에 해당되는 내용 중 일부를 재해석해서 정리해보았습니다.
용어 정리
의존 관계
이 글에서는 일단 "A가 B에 의존한다"를 "B가 변경될 경우 A가 변경되어야 한다"라고 정의하겠습니다.
예를 들어 어떤 프로젝트 내에서 데이터 저장소를
ARepository
에서BRepository
로 변경할 때CService
의 코드 내에 해당 부분을 변경해야 할 경우, "CService
클래스 구현 코드는ARepository
클래스에 의존한다"고 표현하겠습니다.
정적 / 동적
이 글에서만 관심 있는 영역의 코드가 컴파일되기 전에 이미 고정된 것을 정적인 것으로 정의하고, 코드 상으로는 고정되어 있지 않고, 컴파일 후 실행될 때에야 정해지는 것을 동적인 것으로 정의하겠습니다.
다른 글들을 보았을 때 맥락에 따라 "컴파일" 대신 다른 단어가 들어오는 경우도 있는 것 같습니다.
OCP (개방-폐쇄 원칙)
"코드는 기능 확장에는 열려 있으면서, 수정에는 닫혀 있어야 한다."는 원칙입니다.
다시 말해 코드 자체를 수정하지 않고도 기능을 확장할 수 있어야 한다는 원칙입니다.
SRP (단일 책임 원칙)
모듈은 한 가지 책임만 지녀야 한다는 원칙입니다.
여기서 책임이란 모듈의 변화에 대한 이유입니다.
이 글의 맥락에서는 지엽적인 내용이지만, 솔직히 말씀드리면 위 정의를 보긴 했지만 적용이 가능할 정도로 명확히 알지 못해 다음 자료들을 참고하였습니다. 제가 일단 이해한 내용을 공유드리겠습니다.
일단 이해한 내용
소프트웨어를 개발할 때 사용하는 자료 구조나 알고리즘의 종류와 같이 변화가 예상되는 부분들이 있다. 소프트웨어를 모듈로 나타낼 때, 실행되는 순서로 모듈을 구성하기보다는 변화하는 부분들을 다른 모듈들로부터 격리시키는 방식으로 모듈화를 한다면 특정 요소가 변하더라도 그 변화에 대한 한 모듈만 수정할 수 있다.
비즈니스 소프트웨어에서는 변화를 요구하는 대상이 특정 비즈니스 로직과 관련된 특정 부서이기 때문에, 모듈을 설계할 때 그 모듈에게 변화를 요청하는 대상이 한 부서가 되도록 구성하는 것이 이상적이다.
참고 자료
DI (의존성 주입)
해결하고자 하는 문제
어떤 기능을 실행 시에 다양한 구현체들을 사용하는 방식으로 확장하고 싶습니다.
예시: 데이터 저장을 위한 Repository를 운영 시에는 DatabaseRepository를 사용해서, 개발 시에는 InMemoryRepository를 사용하여 실행하고 싶습니다.
그러나 Repository 구현체를 변경하려고 보니 Repository를 사용하는 모든 곳에서 구현체를 수정해야 하는 문제가 발생했습니다.
예시
class Service { private final Repository repository = new InMemoryRepository(); // 코드 수정 필요 // Repository 사용 로직 ... }
DI를 사용한 해결 방안
Repository를 사용하는 클래스들이 DatabaseRepository와 같은 구체적인 구현체들을 사용하지 않게 지워버린 후 Repository 인스턴스를 외부에서 주입받도록 합니다. 여기서는 생성자를 통해 주입받았습니다.
예시
class Service { private final Repository repository; public Service(Repository repository) { this.repository = repository; } // Repository 사용 로직 ... }
Config 클래스를 따로 두어 인스턴스를 생성하는 코드들을 모두 이 클래스에 둡니다. 인스턴스가 필요한 경우 이 클래스에서 사용하면 됩니다.
예시
class Config { repository() { return new InMemoryRepository(); } service() { return new Service(repository()); } }
OCP 관점에서의 해석
원래 문제에서는 실행 시점에 다른 구현체를 사용하는 기능 확장을 하고 싶었지만, 이를 위해 구현체를 사용하는 코드를 변경해야 했습니다. 따라서 OCP를 위반하는 설계였습니다.
이 때 OCP의 각 부분을 이 맥락에서 다음과 같이 해석할 수 있습니다.
기능 확장의 대상은 실행 시점에 실행되는 코드로, 이는 Repository 클래스의 인스턴스입니다.
반면 소스 코드 수정과 관련된 부분은 컴파일 시점에 결정되는 Service 클래스의 코드 자체입니다.
따라서 OCP를 만족하기 위해서는 "Service 인스턴스의 실행 시점에는 서로 다른 Repository 구현체 인스턴스를 사용하되, 컴파일 시점에 같은 Service
클래스 코드를 사용하고 싶다"는 문제를 해결해야 합니다.
다시 말하면 각 Service 인스턴스는 서로 다른 Repository 구현체 인스턴스에 의존하게 하되, Service 클래스는 구체적인 Repository 구현체 클래스에 의존하지 않도록 해야 합니다.
그래서 DI를 사용해서 Service와 Repository 간의 동적인 의존관계와 정적인 의존관계를 서로 다르게 만듦으로써 OCP를 만족하는 설계를 구성하였다고 해석할 수 있습니다.
SRP 관점에서의 해석
기존의 Service 코드에서는 Repository의 인스턴스를 생성하는 책임과 Repository를 사용하는 책임이 같이 있었습니다. 따라서 SRP를 위반하는 설계였습니다.
이 때 Repository를 사용하는 로직은 Repository 인터페이스만 알고 구현체 클래스를 몰라도 문제가 없습니다.
반면 Repository 인스턴스를 생성하려면 구체적인 Repository 구현체 클래스의 정보를 알아야 합니다.
여기서 Repository 구현체를 변경하면, 원래는 구현체를 몰라도 되는 Repository 사용 로직이 인스턴스 생성 로직과 같은 클래스 안에 담겨 있어 수정의 대상이 되었습니다.
DI를 사용하여 수정된 설계에서는 객체들의 인스턴스를 생성하는 책임을 Config 클래스에 몰아줌으로써 Repository 구현체를 변경할 때에도 Service 코드는 영향을 받지 않고 Config 클래스만 수정하면 되도록 바뀌었습니다.
댓글을 작성해보세요.