지금까지 만든 DAO에 트랜잭션을 적용해 보면서 스프링이 어떻게 성격이 비슷한 여러 종류의 기술을 추상화하고 이를 일관된 방법으로 사용할 수 있도록 지원하는지를 살펴볼 것이다.
작성된 코드를 살펴볼 때는 다음과 같은 질문을 해 볼 필요가 있다.
각자 자기 책임에 충실한 작업만 하고 있으니 코드를 이해 하기도 쉽다. 또, 변경이 필요할 때 어디를 수정해야 할지도 쉽게 알 수 있다. 잘못된 요청 이나 작업을 시도했을 때 이를 확인하고 예외를 던져줄 준비도 다 되어 있다. 각각을 독립적으로 테스트하도록 만들면 테스트 코드도 단순해진다.
일반적으로 서비스 추상화라고 하면 트랜잭션과 같이 기능은 유사하나 사용 방법이 다른 로우레벨의 다양한 기술에 대해 추상 인터페이스와 일관성 있는 접근 방법을 제공해주는 것을 말한다. 반면에 JavaMail의 경우처럼 테스트를 어렵게 만드는 건전하지 않은 방식으로 설계된 API를 사용할 때도 유용하게 쓰일 수 있다.
트랜잭션이란 더 이상 나눌 수 없는 단위 작업을 말한다.(원자성) 중간에 예외가 발생해서 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 초기 상태로 돌려놔야 한다.
트랜잭션을 시작을 선언하고 commit 또는 rollback으로 트랜잭션을 종료하는 작업을 트랜잭션의 경계설정이라고 한다. 트랜잭션의 경계는 하나의 Connection이 만들어지고 닫히는 범위 안에 존재한다는 점도 기억하자.
DAO 메소드에서 DB 커넥션을 매번 만들기 때문에 어쩔 수 없이 나타나는 결과이다. 결국 DAO를 사용하면 비즈니스 로직을 담고 있는 UserService 내에서 진행되는 여러 가지 작업을 하나의 트랜잭션으로 묶는 일이 불가능해진다.UserService에서 Connection을 관리하지 않는데 이를 어떻게 해결할까?
스프링에서는 트랜잭션 동기화라는 방법을 통해 이러한 문제를 해결한다. UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져와서 사용하게 하는 것이다.
참고로 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스레드 환경에서도 충돌이 날 염려는 없다.
미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate이 직접 DB 커넥션을 만들고 트랜잭션을 시작해서 JDBC 작업을 시작한다. 반면에 트랜잭션 동기화를 시작해놓았다면 그때부터 실행되는 JdbcTemplate 메소드에서는 직접 DB 커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어 있는 DB 커넥션을 가져와서 사용한다.
현재의 상황
로컬 트랜잭션이 아닌 여러 DB를 사용하는 경우 어떻게 대처할 수 있을까? 글로벌 트랜잭션을 사용하여 해결할 수 있다. Java Transaction API(JTA)를 통해서 아래의 그림의 형태로
분산 트랜잭션에 관한 내용은 추후에 작성할 것이다. 일단은 하나 이상의 DB가 참여하는 트랜잭션을 만들려면 JTA를 사용해야 한다는 것만 기억하자.
JDBC API, JTA, Hibernate Transaction API 어떤 것을 사용하건 간에 우리는 확장가능한 서비스를 제공해야 한다. 현재는 사용하는 기술에 따라서 UserService가 변경되는 형태를 갖는다. 이를 추상화해보자.
UserService의 코드가 특정 트랙잭션 방법에 의존적이지 않고 독립적일 수 있게 만 들려면 어떻게 해야 할까? UserService의 메소드 안에서 트랜잭션 경계설정 코드를 제거할 수는 없다. 하지만 특정 기술에 의존적인 Connection, UserTransaction, Session/ Transaction API 등에 종속되지 않게 할 수 있는 방법은 있다. 추상화란 하위 시스템의 공통점 뽑아내서 분리시키는 것을 말한다. 그렇게 하면 하위 시스템 이 어떤 것인지 알지 못해도, 또는 하위 시스템이 바뀌더라도 일관된 방법으로 접근할 수 가 있다.
결국 하위 시스템의 공통적인 부분을 추상화하여 인터페이스의 형태로 제공하고 이를 DI로 주입 받는 방법을 통해 구체적인 클래스에 대한 정보 없이 독립적으로 UserService를 구성할 수 있을 것이다.
스프링의 트랜잭션 서비스 추상화
스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다. 즉 구현체와 관계없이 일관된 방법으로 트랜잭션을 제어하는 경계설정이 가능해진다.
위와 같이 추상화 되어 있기 때문에 우리의 어플리케이션에서는 이를 외부에서 DI받아서 사용하기만 한다면 UserService가 외부 API에 종속적이지 않게 설계할 수 있다. (JTATransactionManager, HibernateTransactionManager 등등)
위와 같이 기술과 서비스에 대한 추상화 기법을 이용하면 특정 기술 환경에 종속되지 않는 포터블한 코드를 만들 수 있다. UserDao와 UserService는 각각 담당하는 코드의 기능적인 관심에 따라 분리되고, 서로 불필요한 영향을 주지 않으면서 독자적으로 확장이 가능하도록 만든 것이다. 같은 어플리케이션 로직이지만 내용에 따라 분리한 것을 수평적 분리라고 한다.
어플리케이션의 비즈니스 로직과 그 하위에서 동작하는 로우레벨의 트랜잭션 기술이라는 아예 다른 계층의 특성을 갖는 코드를 분리하는 수직적 분리도 존재한다.
애플리케이션 로직의 종류에 따른 수평적인 구분이든, 로직과 기술이라는 수직적인 구 분이든 모두 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만 들 수 있는 데는 스프링의 DI가 중요한 역할을 하고 있다. DI의 가치는 이렇게 관심, 책 임, 성격이 다른 코드를 깔끔하게 분리하는 데 있다.
적절한 분리가 가져오는 특징은 객체지향 설계의 원칙 중의 하나인 단일 책임 원칙으로 설명할 수 있다. 단일 책임 원칙은 하나의 모듈은 한 가지 책임만 가지고, 하나의 이유로만 변경되어야 한다는 의미이다.
스프링의 의존관계 주입 기술인 DI는 모든 스프링 기술의 기반이 되는 핵심 엔진이자 원리이며 스프링이 지지하고 지원하는 좋은 설계와 코드를 만드는 모든 과정에서 사용되는 가장 중요한 도구다. 스프링을 단순히 DI 프레임워크라고 부르는 이유는 외부 설정 정보를 통한 런타임 오브젝트 DI라는 단순한 기능을 제공하기 때문이 아니다. 오히려 스프링이 DI에 담긴 원칙과 이를 응용하는 프로그래밍 모델을 자바 엔터프라이즈 기술의 많은 문제를 해결하는 데 적극적으로 활용하고 있기 때문이다. 또 스프링과 마찬가지로 스프링을 사용하는 개발자가 만드는 어플리케이션 코드 또한 이런 DI를 활용해서 깔끔하고 유연한 코드와 설계를 만들어낼 수 있도록 지원하고 지지해주기 때문이다.
DI의 원리를 잘 활용해서 스프링을 열심히 사용하다 보면, 어느 날 자신이 만든 코드에 객체지향 원칙과 디자인 패턴의 장점이 잘 녹아 있다는 사실을 발견하게 될 것이다. 그것이 스프링을 사용함으로써 얻을 수 있는 가장 큰 장점이다.
DAO에서 특정 DataSource 구현 클래스를 new 로 직접 만들어서 사용하지 않는 편이 낫다고 했다. 운영 중에는 절대 바뀌지 않더라도 테스트 때는 바꿀 수밖에 없기 때문이다. 그래서 DataSource라는 인터페이스를 사용하고, 어떤 클래스의 오브젝트를 사용할지 외부에서 주입해주도록 스프링의 DI를 적용해야 한다. - 테스트 Mocking Stubbing에서도 DI는 유용하게 사용될 수 있다. 의존 구체 클래스가 하나여도 인터페이스 분리 및 약한 결합도를 만드는 것은 의미가 있는 것이다.
비즈니스 로직을 담은 코드는 데이터 액세스 로직을 담은 코드와 깔끔하게 분리되는 것이 바
람직하다. 비즈니스 로직 코드 또한 내부적으로 책임과 역할에 따라서 깔끔하게 메소드로 정
리돼야 한다.
이를 위해서는 DAO의 기술 변화에 서비스 계층의 코드가 영향을 받지 않도록 인터페이스와
DI를 잘 활용해서 결합도를 낮춰줘야 한다.
DAO를 사용하는 비즈니스 로직에는 단위 작업을 보장해주는 트랜잭션이 필요하다.
트랜잭션의 시작과 종료를 지정하는 일을 트랜잭션 경계설정이라고 한다. 트랜잭션 경계설
정은 주로 비즈니스 로직 안에서 일어나는 경우가 많다.
시작된 트랜잭션 정보를 담은 오브젝트를 파라미터로 DAO에 전달하는 방법은 매우 비효율
적이기 때문에 스프링이 제공하는 트랜잭션 동기화 기법을 활용하는 것이 편리하다.
자바에서 사용 되는 트랜잭션 API의 종류와 방법은 다양하다. 환경과 서버에 따라서 트랜잭
션 방법이 변경되면 경계설정 코드도 함께 변경돼야 한다.
트랜잭션 방법에 따라 비즈니스 로직을 담은 코드가 함께 변경되면 단일책임원칙에 위배되
며, DAO가 사용하는 특정 기술에 대해 강한 결합을 만들어낸다.
트랜잭션 경계설정 코드가 비즈니스 로직 코드에 영향을 주지 않게 하려면 스프링이 제공하
는 트랜잭션 서비스 추상화를 이용하면 된다.
서비스 추상화는 로우레벨의 트랜잭션 기술과 API의 변화에 상관없이 일관된 API를 가진
추상화 계층을 도입한다.
서비스 추상화는 테스트하기 어려운 JavaMail 같은 기술에도 적용할 수 있다. 테스트를 편
리하게 작성하도록 도와주는 것만으로도 서비스 추상화는 가치가 있다.
테스트 대상이 사용하는 의존 오브젝트를 대체할 수 있도록 만든 오브젝트를 테스트 대역이
라고 한다.
테스트 대역은 테스트 대상 오브젝트가 원활하게 동작할 수 있도록 도우면서 테스트를 위해
간접적인 정보를 제공해주기도 한다.
테스트 대역 중에서 테스트 대상으로부터 전달받은 정보를 검증할 수 있도록 설계된 것을 목
오브젝트라고 한다.
결국 인터페이스를 통한 약한 결합도와 DI를 활용하여 스프링은 변화하는 환경에서도 일관성 있는 추상 API 계층을 제공한다. 이를 PSA(Portable Service Abstraction) 이라고 말한다. 휴대성 추상 서비스 즉 변경에 있어서 유연하게 대처할 수 있는 서비스를 제공하는 것이다. 스프링이 DI 프레임워크라고 불리는 이유는 스프링 스스로 DI를 활용하여 다양한 종단/횡단 관심사들을 추상화하여 제공하고 있으며 스프링을 사용하는 사람에게도 그것을 사용하는 형태로 지지하기 때문이다. - 횡단(Service, Dao)이거나 종단(Transaction) 관심사거나 관계 없이 변화하는 기준이 달라진다면 이를 추상화하고 DI를 통해 약한 결합 관계를 가져가는 것이 바람직한 방법이다.
추가적으로 단 하나의 구현체만 존재하는 경우에도 DI 및 인터페이스의 활용은 의미가 있다. 테스트 객체에서 Stubbing을 통해 의존 오브젝트가 아닌 테스트 오브젝트에만 집중할 수 있는 구조를 쉽게 만들 수 있기 때문이다. 뿐만 아니라 변하지 않는 것은 없으니까.