이제 아래의 설정만 고쳐도 DB 연결 기술, 데이터 액세스 기술, 트랜잭션 기술을 자유롭게 바꿔서 사용할 수 있게 되었다. 어떻게 이런 것들이 가능했는지 되돌아보자.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="username" value="postgres" />
<property name="password" value="iwaz123!@#" />
<property name="driverClass" value="org.postgresql.Driver" />
<property name="url" value="jdbc:postgresql://localhost/toby_spring" />
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userDao" class="toby_spring.user.dao.UserDaoJdbc">
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>
<bean id="userLevelUpgradePolicy" class="toby_spring.user.service.user_upgrade_policy.OrdinaryUserLevelUpgradePolicy">
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userService" class="toby_spring.user.service.UserService">
<property name="transactionManager" ref="transactionManager"/>
<property name="userDao" ref="userDao" />
<property name="userLevelUpgradePolicy" ref="userLevelUpgradePolicy" />
</bean>
</beans>
UserDao
와 UserService
는 각각 담당하는 코드의 기능적인 관심에 따라 분리되었다. 사실 둘은 같은 애플리케이션 로직을 담은 코드이지만 내용에 따라 분리하여, 수평적으로 분리했다고 볼 수 있다.
트랜잭션의 추상화는 이와는 좀 다르다. 애플리케이션의 비즈니스 로직과 그 하위에서 동작하는 로우레벨의 트랜잭션 기술이라는 아예 다른 계층의 특성을 갖는 코드를 분리한 것이다.
위 그림은 지금까지 만들어진 사용자 관리 모듈의 의존관계이다.
UserService
, UserDao
는 애플리케이션의 로직을 담으므로 애플리케이션 계층이다.UserDao
는 데이터 등록, 조회 등 데이터 액세스에 대한 로직을 담는다.UserServce
는 사용자 관리 업무의 비즈니스 로직을 담는다.UserDao
와 UserService
는 인터페이스와 DI를 통해 연결됨으로써 결합도가 낮아졌다.결합도가 낮다는 건 데이터 액세스 로직이 바뀌거나, 데이터 액세스 기술이 바뀐다고 할지라도 UserService
의 코드에는 영향을 주지 않는다는 것을 의미한다. 서로 독립적으로 확장될 수 있는 부분이라는 뜻이다.
UserDao
는 DB 연결을 생성하는 방법에 대해서도 독립적이다.DataSource
인터페이스와 DI를 통해 추상화된 방식으로 로우레벨의 DB 연결 기술을 사용한다.UserService
는 트랜잭션 기술에 독립적이다.UserService
는 영향을 받지 않는다.UserDao
와 DB연결 기술, UserService
와 트랜잭션 기술의 결합도가 낮은 분리는 애플리케이션 코드를 로우레벨 기술 서비스와 환경에서 독립시켜준다.
애플리케이션 내부의 로직 종류에 따른 수평적 구분이든, 로직과 기술이라는 수직적인 구분이든 모두 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들 수 있는 데는 스프링의 DI가 중요한 역할을 하고 있다.
DI의 가치는 관심, 책임, 성격이 다른 코드를 깔끔하게 분리하는 데에 있다.
적절한 분리가 가져오는 특징은 객체지향 설계의 원칙 중 하나인 단일 책임 원칙(Single Responsibility Principle)
으로 설명할 수 있다.
단일 책임 원칙이란, 하나의 모듈은 한가지 책임을 가져야 한다는 의미다. 다른 말로 풀면 하나의 모듈이 바뀌는 이유는 한가지여야 한다고 설명할 수도 있다.
트랜잭션을 구현하기 위해 UserService
에 JDBC 코드가 들어가있을 때는 UserService
의 책임은 두가지였다.
책임이 두가지라는 것은 코드가 수정되는 이유도 두가지라는 뜻이다.
UserService
를 수정해야 한다.UserService
를 수정해야 한다.결국 위와 같이 2가지 이상의 책임을 가지는 순간 단일 책임 원칙
은 깨지는 것이다.
하지만, 우리는 개선을 통해 트랜잭션 기술에 대한 부분은 PlatformTransactionManager
라는 인터페이스를 두고 해당 기술에 맞는 xxxTransactionManager
를 주입받도록 바꾸었다.
이제 '어떻게 트랜잭션을 관리할 것인가'는 더이상 UserService
의 책임이 아니게 됐으며, '어떻게 사용자 레벨을 관리할 것인가'만 UserService
의 책임이 되어 단일 책임 원칙을 잘 지키게 되었다.
이제는 사용자 관리 로직이 바뀌거나 추가되지 않는 한 UserService
의 코드에 손댈 일이 없어졌다. 따라서 이제는 단일 책임 원칙을 훌륭하게 지키고 있다고 말할 수 있다.
좋은 설계 원칙이라기에 단일 책임 원칙을 지키긴 했는데 그렇다면 장점은 무엇일까?
단일 책임 원칙을 잘 지키고 있다면, 어떤 변경이 필요할 때 수정 대상이 명확해진다. 기술이 바뀌면 기술 계층과의 연동을 담당하는 기술 추상화 계층의 설정만 바꿔주면 된다. 데이터를 가져오는 테이블의 이름이 바뀌었다면 데이터 액세스 로직을 담고 있는 UserDao
를 바꾸면 된다. 비즈니스 로직도 마찬가지다.
지금은 User
라는 단순한 하나의 모듈만 있어서 장점을 체험하기 힘들 수도 있찌만 DAO가 각각 수백개가 되고 서비스 클래스도 그만큼 많다면 달라진다. 단일 책임 원칙을 지키지 않은 경우 클래스, 메소드 하나하나에 달린 의존 관계가 매우 복잡해진다. DAO를 수정할 때마다 그에 딸린 서비스 클래스를 같이 수정해야만 한다면? 수백개의 클래스를 같이 수정해야 할 뿐만 아니라, 테스트코드까지 수정해야 할지도 모르는 판이다.
기술적인 수정사항도 마찬가지다. 애플리케이션 계층의 코드가 특정 기술에 종속돼있다면? 이를테면 트랜잭션을 100군데에서 사용했는데, 데이터 액세스 기술이 JDBC에서 JPA로 바뀌었다면? 100군데 모두 일일이 찾아서 트랜잭션을 사용하는 부분을 수정해주어야 할 것이다.
단지 작업량만의 문제가 아니라, 많은 코드를 작업하면서 그만큼 실수가 일어날 확률도 증대한다. 운영중인 코드에 이런 수정이 필요하다면, 엄청난 부담이 될 것이다.
적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 다양한 추상화 기법을 도입하고, 애플리케이션 로직과 기술/환경을 분리하는 등의 작업은 갈수록 복잡해지는 엔터프라이즈 애플리케이션에는 반드시 필요하다. 이를 위한 핵심적인 도구가 바로 스프링이 제공하는 DI다.
DI가 없었다면? 나름 추상화를 했더라도 적지 않은 코드 사이 결합이 남게 된다. PlatformTransactionManager
인터페이스를 적용했지만, 코드 내부에 구체적인 타입이 new DataSourceTransactionManager()
라는 것이 노출될 것이다. 이를테면 서비스가 100개면 100개의 서비스 모두에 new DataSourceTransactionManager()
라는 구체적인 코드가 생기게 되고 변화가 생길 때마다 100개를 수정해야 한다. 물론 인터페이스로 추상화를 안했을 때보다는 훨씬 적지만, 로우레벨 기술의 변화가 있을 때마다 비즈니스 로직을 담은 코드의 수정이 발생한다.
객체지향 설계와 프로그래밍 원칙은 서로 긴밀하게 관련이 있다. 단일 책임 원칙을 잘 지키는 코드를 만들려면 인터페이스를 도입하고 이를 DI로 연결해주어야 한다. 그 결과로 단일 책임 원칙
뿐만 아니라 개방 폐쇄 원칙
도 잘 지키고 모듈간에 결합도도 낮아져
서로의 변경이 영향을 주지 않고, 같은 이유로 변경이 단일 책임에 집중되는 응집도 높은 코드가 나온다.
이런 코드 개선 과정에서 전략 패턴, 어댑터 패턴, 브리지 패턴, 미디에이터 패턴 등 많은 디자인 패턴이 자연스럽게 적용되기도 한다. 객체지향 설계 원칙을 잘 지켜서 만든 코드는 테스트하기도 편하다. 스프링이 지원하는 DI와 싱글톤 레지스트리 덕분에 더욱 편리하게 자동화된 테스트를 만들 수 있다.
자연스레 좋은 설계가 나오기 까지는 몇달정도 공부하는 것으로는 되지 않는다. 개발하며 꾸준한 노력이 필요하다. 그저 기능이 동작한다고해서 코드에 쉽게 만족하지말고 계속 다듬고 개선하려는 자세도 필요하다.
지금까지 코드를 개선하고 발전시켜온 과정에는 DI가 항상 쓰였다.
UserDao
와 DataSource
를 분리했을 때스프링 의존관계 주입 기술인 DI는 모든 스프링 기술의 기반이 되는 핵심 엔진이자 원리이며 스프링이 지지하고 지원하는 좋은 설계와 코드를 만드는 모든 과정에서 사용되는 가장 중요한 도구이다.
객체지향 기술이나 패턴을 익히고 적용하는 일이 어렵고 지루하게 느껴지면, 스프링에서 DI가 어떻게 적용되고 있는지 살펴보며 이를 따라하는 것도 좋은 방법이다. 그러면서 좋은 코드의 특징이 무엇이고 가치가 있는지 살펴보는 것이다. 변경사유가 생겼을 때 코드의 어디를 어떻게 수정해야 하는지 주의 깊게 살펴보자.
DI의 원리를 잘 활용해서 스프링을 열심히 사용하면, 어느날 자신의 코드에 객체지향 원칙과 디자인 패턴의 장점이 잘 녹아있따는 사실을 발견할 수 있다.