Study with book/토비의 스프링 3.1

3장. 템플릿1


템플릿이란 바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다,

3.1 다시보는 초난감 DAO

3.1.1 예외처리 기능을 갖춘 DAO

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/3.1.1

일반적으로 서버에서는 제한된 개수의 DB 커넥션을 만들어서 재사용 가능한 풀로 관리한다. DB 풀은 명시적으로 close() 메소드를 호출해서 돌려줘야지만 다시 풀에 넣었다가 다음 커넥션 요청이 있을 때 재사용할 수 있다. 스터디에서 try-with-resources로 자원을 반납하는 것에 대해서 이야기 했는데 데코레이션 패턴으로 AutoCloseable 인터페이스를 상속받은 여러 개의 인스턴스가 존재하는 경우 어떤 인스턴스의 close() 메소드를 호출할지 알아오기로 하였다. 추후에 블로그 꼭 포스팅하자!

close() 메소드의 경우 만들어진 순서의 반대로 하는 것이 원칙이다.

 

3.2 변하는 것과 변하지 않는 것

3.2.1 JDBC try/catch/finally 코드의 문제점

 복잡한 try/catch/finally 블록이 2중으로 중첩까지 되어 나오는데다, 모든 메소드마다 반복된다. 복사 붙여넣기 신공으로 코드를 작성한다고 하더라도 개발자 부주의로 인한 실수가 안나온다고 보장할 수 없다. 만약 리소스 반납을 하는 코드를 복사 붙여넣기 하는 과정에 빠트렸다고 하면, 당장은 문제가 없을 수 있겠지만 계속 쌓이다보면 리소스가 꽉 찼다는 에러가 나면서 서비스가 중단되는 상황이 발생할 수 있다. 이런 코드를 효과적으로 다룰 수 있는 방법은 없을까? 개발자라면 당연히 이런 의문을 가져야 한다.

 이 문제의 핵심은 변하지 않는 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자주 변하는 코드를 잘 분리해내는 작업이다.

 

3.2.2 분리와 재사용을 위한 디자인 패턴 적용

1) 메소드 추출

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/3.2.2-1

 먼저 생각해볼 수 있는 방법은 변하는 부분을 메소드로 빼는 것이다. 하지만 분리시킨 메소드를 다른 곳에서 재사용할 수 있어야 하는데, 이건 반대로 분리시키고 남은 메소드가 재사용이 필요한 부분이다.

2) 템플릿메소드 적용
실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/3.2.2-2

 기존 상위 DAO 클래스에 불필요한 변화는 생기지 않도록 할 수 있으니 객체지향 설계의 핵심 원리인 개방 폐쇄 원칙을 그럭저럭 지키는 구조를 만들어 낼 수 있다. 하지만 템플릿 메소드 패턴으로의 접근은 제한이 많다. DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다는 점이다. 또한 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다는 점이다. 유연성이 떨어져 버린다.

3) 전략 패턴의 적용

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/3.2.2-3

 전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔 쓸 수 있다는 것인데 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있어서 뭔가 이상하다.

4) DI 적용을 위한 클라이언트/컨텍스트 분리
실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/3.2.2-4

 전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는 게 일반적이다. 컨텍스트에 해당하는 별도의 메소드로 독립시켜보자. 클라이언트로부터 StatementStrategy 타입의 전략 오브젝트를 제공받고 JDBC try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행한다. 클라이언트의 경우는 deleteAll() 메소드가 클라이언트가 된다. deleteAll()은 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 지고 있다. 비록 클라이언트와 컨텍스트는 클래스를 분리하진 않았지만, 의존관계와 책임으로 볼 때 이상적인 클라이언트/컨텍스트 관계를 갖고 있다.

 

3.3 JDBC 전략패턴의 최적화

3.3.1 전략 클래스의 추가 정보

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/3.3.1 

add() 적용해보자. 생성자를 통해서 User객체를 주입받을 수 있도록 하자.

 

3.3.2 전략과 클라이언트의 동거

현재 만들어진 구조는 DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 점이다. DAO 메소드에서 StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우 이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 번거롭게 만들어야 한다. 이를 해결하기 위해서 아래와 같이 리팩토링 해보자.

1) 로컬 클래스

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/3.3.2-1

여기서 스터디 중에서 왜 로컬 클래스의 경우 메소드의 지역 변수에 접근하기 위해서 final을 붙여야하는지 의논했는데 이것도 나중에 포스팅하자!

2) 익명 내부클래스

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/3.3.2-2

deleteAll()은 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 지고 있다. 비록 클라이언트와 컨텍스트는 클래스를 분리하진 않았지만, 의존관계와 책임으로 볼 때 이상적인 클라이언트/컨텍스트 관계를 갖고 있다.

 

3.4 컨텍스트와 DI

3.4.1 전략 클래스의 추가 정보

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/3.4.1 

전략 패턴의 구조로 보자면 UserDao의 메소드가 클라이언트이고, 익명 내부 클래스로 만들어지는 것이 개별적인 전략이고, jdbcContextWithStatementStrategy() 메소드는 컨텍스트이다. 그런데 JDBC의 일반적인 작업 흐름을 담고 있는 jdbcContextWithStatementStrategy()는 다른 DAO에서도 사용 가능하다.

 

1) 클래스 분리

분리해서 만들 클래스의 이름은 JdbcContext라고 하자. JdbcContext에 UserDao에 있던 컨텍스트 메소드를 workWithStatementStrategy()라는 이름으로 옮겨놓는다.

 

2) 빈 의존관계 변경

UserDao는 이제 JdbcContext에 의존하고 있다. JdbcContext는 인터페이스인 DataSource와는 달리 구체 클래스다. 스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는 게 목적이다. 하지만 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을 뿐이고 구현 방법이 바뀔 가능성은 없다. 따라서 인터페이스를 구현하도록 만들지 않았고, UserDao와 JdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 된다.

 

3.4.2 JdbcContext의 특별한 DI

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/3.4.2 

UserDao는 인터페이스를 거치지 않고 코드에서 바로 JdbcContext 클래스를 사용하고 있다. UserDao와 JdbcContext는 클래스 레벨에서 의존관계가 결정된다. 비록 런타임 시에 DI 방식으로 외부에서 오브젝트를 주입해주는 방식을 사용하긴 했지만 의존 오브젝트의 구현 클래스를 변경할 수는 없다.

 

스프링 빈으로 DI

 의존관계 주입이라는 개념을 충실하게 따르자면, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않게 하고, 런타임 시에 의존할 오브젝트와의 관계를 다이내믹하게 주입해주는 것이 맞다. 따라서 인터페이스를 사용하지 않았다면 엄밀히 말해서 온전한 DI라고 할수 없다. 그러나 스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄한다. 그런 의미에서 JdbcContext를 스프링을 이용해 UserDao 객체에서 사용하게 주입했다는 건 DI의 기본을 따르고 있다고 볼 수 있다.

 인터페이스를 사용해서 클래스를 자유롭게 변경할 수 있게 하지는 않았지만, JdbcContext를 UserDao와 DI구조로 만들어야 할 이유는 뭘까? 첫째는 JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문이다. 비록 내부에 DataSource라는 인터페이스 변수는 있지만 읽기 전용이므로 JdbcContext가 싱글톤이 되는 데 아무런 문제가 없다. 둘째는 JdbcContext가 DI를 통해서 다른 빈에 의존하고 있기 때문이다. DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록돼야 한다. 

 여기서 중요한 것은 인터페이스 사용 여부이다. 왜 인터페이스를 사용하지 않았을까? 인터페이스가 없다는 건 UserDao와 JdbcContext가 매우 긴밀한 관계를 가지고 강하게 결합되어 있다는 의미다. UserDao는 JdbcContext 클래스와 함께 사용되어야 한다. UserDao가 JPA나 ORM을 사용해야 한다면 JdbcContext도 통째로 바뀌어야 한다.

장점 : 오브젝트 사이의 실제 의존관계가 설정파일에 명확하게 드러난다.

단점 : DI의 근본적인 원칙에 부합하지 않은 구체적인 클래스와 관계가 설정에 직접 노출된다.

 

코드를 이용하는 수동 DI

 UserDao 내부에서 직접 DI를 적용하는 방법이다. 이 방법은 JdbcContext를 스프링 빈으로 등록해서 사용했던 이유인 싱글톤으로 만들려는 것은 포기해야 한다. DAO마다 하나의 JdbcContext 오브젝트를 갖고 있게 하는 것이다. 웬만큼 대형 프로젝트라고 하더라도 수백개면 충분하다. JdbcContext를 스프링 빈으로 등록하지 않았으므로 다른 누군가가 JdbcContext의 생성과 초기화를 책임져야 한다. UserDao가 갖는 것이 적당한데, 자신이 사용할 오브젝트를 직접 만들고 초기화하는 전통적인 방법을 사용하는 것이다. 

 문제는 JdbcContext를 스프링 빈으로 등록해서 사용했던 두 번째 이유다. JdbcContext는 다른 빈을 인터페이스를 통해 간접적으로 의존하고 있다. 의존 오브젝트를 DI를 통해 제공받기 위해서라도 자신도 빈으로 등록돼야 한다고 했다. 이런 경우 JdbcCotext에 대한 제어권을 갖고 생성과 관리를 담당하는 UserDao에게 DI까지 맡기는 것이다. UserDao가 임시로 DI 컨테이너처럼 동작하게 만들면 된다. 설정 파일만을 보자면 UserDao가 직접 DataSource를 의존하고 있는 것 같지만, 내부적으로는 JdbcCotnext를 통해 간접적으로 DataSource를 사용하고 있을 뿐이이다.

장점 : 관계를 외부에서는 드러내지 않는다는 장점이 있다.

단점 : JdbcContext를 싱글톤으로 만들 수 없고, DI작업을 위한 부가적인 코드가 필요하다.


[참고자료]

토비 스프링 vol.1 - 이일민

'Study with book > 토비의 스프링 3.1' 카테고리의 다른 글

4장. 예외  (0) 2020.03.29
3장. 템플릿2  (0) 2020.03.22
2장. 테스트  (0) 2020.03.15
1장. 오브젝트 의존관계2  (0) 2020.03.15
1장. 오브젝트 의존관계1  (0) 2020.03.08