5장에선 지금까지 만든 DAO에 트랜잭션을 적용해보면서 스프링이 어떻게 성격이 비슷한 여러 종류의 기술을 추상화하고 이를 일관된 방법으로 사용할 수 있도록 지원하는지 살펴볼 것이다.
현재까지의 DAO
구현해야 할 비즈니스 로직
Level 이늄(enum)User 클래스에 사용자의 레벨을 저장할 필드를 추가하자.
USER 테이블에서의 타입, 이에 매핑되는 자바의 User 클래스에서의 타입은?varchar 타입으로 선언하고 문자열을 넣는다.Level 타입이 int면 다른 종류의 정보를 넣는 실수를 해도 컴파일러가 체크해주지 못한다.int 타입의 값을 갖고 있지만, 겉으로는 Level 타입의 오브젝트이기 때문에 안전하게 사용할 수 있다. 

UserDao 필드 추가로그인 횟수와 추천수를 추가하자.
int 타입으로 만들어도 좋다.

DB의 USER 테이블에도 필드를 추가한다.

UserDaoTest 테스트 수정UserDaoJdbc와 테스트에도 필드를 추가하자.
UserDaoJdbc는 테스트까지 갖추고 있는 안정된 코드이기 때문에,
기존 코드에 새로운 기능을 추가하려면 테스트를 먼저 만드는 것이 안전하다.
User 클래스 생성자의 파라미터도 추가해준다. User 오브젝트 필드 값이 모두 같은지 비교하는 checkSameUser() 메서드를 수정한다. 검증용 필드를 추가해서 기존 DAO 테스트 기능을 보완하는 것이다.
필드도 늘어났고, 앞으로 추가되거나 변경되도 User 오브젝트를 비교하는 assertThat 검증 로직을 일정하게 유지할 수 있도록 따로 메서드로 분리하는 것이다.




UserDaoJdbc 수정미리 준비된 테스트가 성공하도록 UserDaoJdbc 클래스를 수정하자.
INSERT 문장이 들어 있는 add() 메서드의 SQL과 각종 조회 작업에 사용되는 User 오브젝트 매핑용 콜백인 userMapper에 추가된 필드를 넣는다. 📌
Level타입의 level 필드를 사용할 때
- 저장할 때
Levelenum은 오브젝트이므로 DB에 저장될 수 있는 SQL 타입이 아니다. 따라서 DB에 저장 가능한 정수형 값으로 변환해줘야 한다. 각Level이늄의 DB 저장용 값을 얻기 위해서Level에 미리 만들어둔intValue()메서드를 사용한다.
- 조회할 때
ResultSet에서는 DB의 타입인int로level정보를 가져온다. 이 값을User의setLevel()메서드에 전달하면 타입이 일치하지 않는다는 에러가 발생할 것이다. 이때는Level의 스태틱 메서드인valueOf()를 이용해int타입의 값을Level타입의 enum 오브젝트로 만들어서setLevel()메서드에 넣어줘야 한다.


id를 제외한 나머지 필드는 수정될 가능성이 있다. User 오브젝트를 전달하면 id를 참고해서 사용자를 찾아 필드 정보를 UPDATE 문을 이용해 모두 변경해주는 메서드를 하나 만들자. id를 제외한 필드의 내용을 바꾼 뒤 update()를 호출한다. id로 조회해서 가져온 User 오브젝트와 수정한 픽스처 오브젝트를 비교한다. 📌 user1이라는 텍스트 픽스처는 인스턴스 변수인데 직접 변경해도 될까?
상관없다. 테스트 메서드가 실행될 때마다UserDaoTest오브젝트는 새로 만들어지고,setUp()메서드도 다시 불러와 초기화되기 때문이다.

UserDao와 UserDaoJdbc 수정UserDao 인터페이스에 update() 메서드를 추가한다.UserDaoJdbc에서 update() 메서드를 add()와 비슷한 방식으로 구현한다.
JDBC 개발에서 리소스 반환과 같은 기본 작업을 제외하면 가장 많은 실수가 일어나는 곳은 바로 SQL 문장이다. 필드 이름이나 SQL 키워드를 잘못 넣은 거라면 테스트에서 에러가 나니 쉽게 확인할 수 있지만, UPDATE 문장에서 WHERE 절은 빼먹는 경우는 테스트로는 검증하지 못하는 오류가 있을 수 있다. UPDATE는 WHERE가 없어도 아무런 경고 없이 정상적으로 동작하는 것처럼 보인다. 현재 update() 테스트는 수정할 로우의 내용이 바뀐 것만 확인할 뿐이지, 수정하지 않아야 할 로우의 내용이 그대로 남아 있는지는 확인해주지 못한다는 문제가 있다.
JdbcTemplate의 update()가 돌려주는 리턴 값을 확인한다.
JdbcTemplate의 update()는 UPDATE나 DELETE 같이 테이블의 내용에 영향을 주는 SQL을 실행하면 영향받은 로우의 개수를 돌려준다. update() 메서드의 SQL에 문제가 있다는 사실을 알 수 있다. (선택) 테스트를 보강해서 원하는 사용자 외의 정보는 변경되지 않았음을 직접 확인한다.
UserDao update() 메서드의 SQL문장에서WHERE 부분을 빼보자.
그래도 기존 update() 테스트는 성공할 것이다. 테스트에 결함이 있다는 증거다. 테스트를 수정해서 테스트가 실패하도록 만들어야 한다.

UserService.updgradeLevels()레벨 관리 기능을 구현해보자.
UserDao의 getAll() 메서드로 사용자를 다 가져와서 사용자별로 레벨 업그레이드 작업을 진행하면서 UserDao의 update()를 호출해 DB에 결과를 넣어주면 된다.UserService라는 클래스를 추가하자.UserService는 UserDao 인터페이스 타입으로 userDao 빈을 DI 받아 사용하게 만들자.UserService는 UserDao의 구현 클래스가 바뀌어도 영향받지 않도록 해야한다. 즉, 데이터 액세스 로직이 바뀌었다고 비즈니스 로직 코드를 수정하는 일이 있어서는 안 된다. UserService도 당연히 스프링의 빈으로 등록돼야 한다.UserService를 위한 테스트 클래스인 UserServiceTest를 추가한다.UserService의 클래스 레벨 의존관계
UserService 클래스와 빈 등록UserService 클래스를 만들고 사용할 UserDao 오브젝트를 저장해둘 인스턴스 변수를 선언한다.UserDao 오브젝트의 DI가 가능하도록 수정자 메서드도 추가한다.userService 아이디로 빈을 추가한다. 그리고 userDao 빈을 DI 받도록 프로퍼티를 추가해준다.

UserServiceTest 테스트 클래스UserServiceTest 클래스를 추가하고 테스트 대상인 UserService 빈을 제공받을 수 있도록 @Autowired가 붙은 인스턴스를 선언해준다.UserService는 컨테이너가 관리하는 스프링 빈이므로 스프링 테스트 컨텍스트를 통해 주입받을 수 있다.userService 빈이 생성되서 userService 변수에 주입되는지만 확인하는 테스트 메서드를 추가한다. 이후에 삭제하자.


upgradeLevels() 메서드사용자 레벨 관리 기능을 먼저 만들고 테스트를 만들자.

upgradeLevels() 테스트1. 가능한 모든 조건을 하나씩 확인해보자.

upgradeLevels() 메서드를 실행한다. 
UserService.add()처음 가입하는 사용자는 기본적으로 BASIC 레벨이어야 한다는 로직을 추가하자.
1.1 UserDaoJdbc의 add()는 적합하지 않다.
UserDaoJdbc는 주어진 User 오브젝트를 DB에 정보를 넣고 읽는 방법에만 관심을 가져야지, 비즈니스적인 의미를 지닌 정보를 설정하는 책임을 지는 것은 바람직하지 않다.
1.2 User 클래스에서 level 필드를 Level.BASIC으로 초기화하는 것
처음 가입할 때를 제외하면 무의미한 정보인데 단지 이 로직을 담기 위해 클래스에서 직접 초기화하는 것은 좀 문제가 있다.
1.3 사용자 관리에 대한 비즈니스 로직을 담고 있는 UserService
UserDao의 add() 메서드는 사용자 정보를 담은 User 오브젝트를 받아서 DB에 넣어주는 역할을 한다면, UserService에 add()를 만들고 사용자가 등록될 때 적용할 만한 비즈니스 로직을 담당하게 하면 된다.
2.1 검증할 기능 : UserService의 add()를 호출하면 레벨이 BASIC으로 설정되는 것
❓
UserService의add()에 전달되는User오브젝트에level필드에 값이 미리 설정되어 있는 경우에는 어떻게 할까?
- 이건 정하기 나름이다.
- 여기서는
add()를 호출할 때level값이 비어 있으면 로직을 따라 BASIC을 부여해주고, 만약 특별한 이유가 있어 미리 설정된 레벨을 가진User오브젝트인 경우에는 그대로 두자.
2.2 테스트 케이스는 두 종류를 만든다.
레벨이 미리 정해진 경우, 레벨이 비어 있는 두 가지 경우에 각각 add() 메서드를 호출하고 결과를 확인하도록 만들자.
User 오브젝트의 레벨이 변경됐는지 확인하는 두 가지 방법
UserService의 add() 메서드를 호출할 때 파라미터로 넘긴 User 오브젝트에 level 필드를 확인해보는 것UserDao의 get() 메서드를 이용해 DB에 저장된 User 정보를 가져와 확인하는 것UserService가 UserDao를 제대로 사용하는지 함께 검증할 수 있고, 디폴트 레벨 설정 후에 UserDao를 호출하는지도 검증된다.
null인 사용자 오브젝트 두 개를 준비한다.UserService의 add() 메서드를 통해 초기화한 뒤에 DB에 저장되도록 만든다. 
📌 작성된 코드를 살펴볼 때 체크해야 될 것
- 코드에 중복된 부분을 없는가?
- 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가?
- 코드가 자신이 있어야 할 자리에 있는가?
- 앞으로 변경이 일어난다면 어떤 것이 있을 수 있고, 그 변화에 쉽게 대응할 수 있게 작성되어 있는가?
upgradeLevels() 메서드 코드의 문제점for 루프에 있는 if/elseif/else 블록들이 읽기 불편하다.레벨의 변화 단계와 업그레이드 조건, 조건이 충족됐을 때 해야 할 작업같이 성격이 다른 여러 가지 로직이 한데 섞여 있어 로직을 이해하기 쉽지 않다.
if 블록 하나를 보면

if 조건 블록이 레벨 개수만큼 반복된다.만약 새로운 레벨이 추가된다면 Level 이늄도 수정해야 하고, upgradeLevels()의 레벨 업그레이드 로직을 담은 코드에 if 조건식과 블록을 추가해줘야 되서 코드가 지저분해진다.
upgradeLevels() 리팩토링1. 추상적인 레벨에서 로직을 작성해보자.
레벨을 업그레이드하는 작업의 기본 흐름만 먼저 만들어보자. 구체적인 구현에서 외부에 노출할 인터페이스를 분리하는 것과 마찬가지 작업이라고 생각하면 된다.
모든 사용자 정보를 가져와 한 명씩 업그레이드가 가능한지 확인하고, 가능하면 업그레이드를 한다.


2. 업그레이드가 가능한지를 알려주는 메서드인 canUpgradeLevel() 메서드를 만들자.
user에 대해 업그레이드가 가능하면 true, 가능하지 않으면 false를 리턴한다.
false를 반환한다.3. 업그레이드 조건을 만족했을 경우 구체적으로 무엇을 할 것인가를 담고 있는 upgradeLevel() 메서드를 만들자.

upgradeLevel() 메서드를 수정하자.upgradeLevel() 의 문제점level 필드를 변경해준다는 로직이 함께 있는데다, 너무 노골적으로 드러나 있다. if문이 점점 늘어날 것이다. 또한 레벨 변경 시 변경할 작업이 추가된다면 if 조건 뒤에 붙는 내용도 점점 길어질 것이다.레벨의 순서와 다음 단계 레벨이 무엇인지를 결정하는 일 즉, 레벨의 업그레이드 순서는 Level에게 맡기자.
Level enum에 next라는 다음 단계 레벨 정보를 담을 수 있도록 필드를 추가한다.Level enum을 정의할 때 DB에 저장될 값과 다음 레벨이 무엇인지를 함께 넣어줄 수 있다.nextLevel() 메서드를 호출한다.

사용자 정보가 바뀌는 부분을 UserService 메서드에서 User로 옮기자.
User의 내부 정보가 변경되는 것은 UserService보다 User가 스스로 다루는 게 적절하다. User는 사용자 정보를 담고 있는 단순한 자바빈이긴 하지만 User도 엄연히 자바오브젝트이고 내부 정보를 다루는 기능이 있을 수 있다. User에 업그레이드 작업을 담당하는 독립적인 메서드를 두고 사용할 경우, 업그레이드 시 기타 정보도 변경이 필요해질 때 유용하다.
User는 Level의 nextLevel() 기능을 이용해 Level 타입의 level 필드에게 다음 레벨이 무엇인지 확인한 후 현재 레벨을 변경해준다. UserService의 canUpgradeLevel() 메서드에서 업그레이드 가능 여부를 미리 판단해주긴 하지만, User 오브젝트를 UserService만 사용하는 건 아니므로 스스로 예외상황에 대한 검증 기능을 갖고 있는 편이 안전한다.nextLevel()에서 다음 레벨이 없는 경우엔 null을 리턴하는데, 이 경우에는 User의 레벨 업그레이드 작업이 진행돼서는 안되므로 예외를 던져야 한다. 

지금 개선한 코드를 살펴보면 UserService, User, Level 각 오브젝트와 메서드가 각각 자기 몫의 책임을 맡아 일을 하고, 필요가 생기면 이런 작업을 수행해달라고 서로 요청하는 구조다. 잘못된 요청이나 작업을 시도했을 때 이를 확인하고 예외를 던져줄 준비도 다 되어 있다.
객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다. 오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리이기도 하다.
User 테스트User에 추가한 upgradeLevel() 메서드에 대한 테스트를 만들자.
User 오브젝트는 스프링이 IoC로 관리해주는 오브젝트가 아니기 때문에, 클래스의 테스트는 굳이 스프링의 테스트 컨텍스트를 사용하지 않아도 된다. @Autowired로 가져오는 대신 생성자를 호출해서 테스트할 User 오브젝트를 만들면 된다. upgradeLevel() 테스트는 Level enum에 정의된 모든 레벨을 가져와서 User에 설정해두고 User의 upgradeLevel()을 실행해 다음 레벨로 바뀌는지 확인하는 것이다. 단, 다음 단계가 null인 경우는 제외한다.nextLevel()이 null인 경우를 제외하고 업그레이드를 진행한다. nextLevel()이 null인 경우에 강제로 upgradeLevel()을 호출한다.assertThrows를 사용해 설정한 예외가 발생하면 테스트가 성공이다. 

UserServiceTest 개선checkLevel() 메서드의 중복 개선Level이 갖고 있어야 할 다음 레벨이 무엇인가 하는 정보를 테스트에 직접 넣어둘 이유가 없다.

upgradeLevels() 테스트는 각 사용자에 대해 업그레이드를 확인하려는 것인지 아닌지 좀 더 이해하기 쉽게 true, false로 나타나 있어서 보기 좋다. UserService와 UserServiceTest 중복 제거업그레이드 조건인 로그인 횟수와 추천 횟수가 애플리케이션 코드와 테스트 코드에 중복돼서 타나난다.
case BASIC: return (user.getLogin() >= 50); // UserService
new User("joytouch", "강명성", "p2", Level.BASIC, 50, 0) // UserServiceTest
❓ 테스트와 애플리케이션 코드에 나타난 이런 숫자의 중복도 제거해줘야 할까?
당연하다. 한 가지 변경 이유가 발생했을 때 여러 군데를 고치게 만든다면 중복이기 때문이다. 이런 상수 값을 중복하는 건 바람직하지 못하다.
여기서 가장 좋은 방법은 정수형 상수로 변경하는 것이다.



연말 이벤트나 새로운 서비스 홍보기간 중에는 레벨 업그레이드 정책을 다르게 적용할 필요가 있을 수도 있다. 그럴 때마다 중요한 사용자 관리 로직을 담은 UserService의 코드를 직접 수정했다가 이벤트 기간이 끝나면 다시 이전 코드로 수정한다는 것은 상당히 번거롭고 위험한 방법이다.
이런 경우 사용자 업그레이드 정책을 UserService에서 분리하는 방법을 고려해볼 수 있다. 업그레이드 정책을 담은 인터페이스를 만들어두고 분리된 업그레이드 정책을 담은 오브젝트는 DI를 통해 UserService에 주입한다. 스프링 설정을 통해 평상시 정책을 구현한 클래스를 UserService에서 사용하게 하다가, 이벤트 때는 새로운 업그레이드 정책을 구현한 클래스를 따로 만들어서 DI 해주면 된다.

정기 사용자 레벨 관리 작업을 수행하는 도중에 네트워크가 끊기거나 서버에 장애가 생겨서 작업을 완료할 수 없다면, 그때까지 변경된 사용자의 레벨은 그대로 둬야할까 아니면 모두 초기 상태로 되돌여 놓아야 할까?
이 시스템에선 중간에 문제가 발생해서 작업이 중단된다면 그때까지 진행된 변경 작업도 모두 취소시키도록 결정했다고 하자.
지금까지 만든 사용자 레벨 업그레이드 코드는 모든 사용자에 대해 업그레이드 작업을 진행하다가 중간에 예외가 발생해서 작업이 중단된다면 어떻게 될까?
사용자 레벨 업그레이드를 시도하다가 중간에 예외가 발생했을 경우, 그 전에 업그레이드했던 사용자도 다시 원래 상태로 돌아가는지 확인하는 것이다.
UserService 대역UserService의 대역을 사용하는 것UserService를 대신해 테스트의 목적에 맞게 동작하는 클래스를 만들어 사용하는 것이다.UserService 확장 클래스를 만드는 방법UserService 코드를 복사해서 사용하는 것보다, 간단히 UserService를 상속해서 테스트에 필요한 기능을 추가하도록 일부 메서드를 오버라이딩하는 방법이 좋다. 현재 5개의 테스트용 사용자 정보 중 두 번째와 네 번째가 업그레이드 대상이다. 네 번째 사용자를 처리하는 중 예외를 발생시키고, 그 전에 처리한 두 번째 사용자의 정보가 취소됐는지 여부를 확인하자.
테스트용으로 UserService를 상속한 클래스를 만든다.
테스트에서만 사용할 클래스라면 테스트 클래스 내부에 스태틱 클래스로 만드는 것이 간편하다. 즉, UserServiceTest 안에 추가한다.
테스트용 UserService의 서브클래스를 UserSerive 기능의 일부를 오버라이딩해서 특정 시점에서 강제로 예외가 발생하도록 만든다.
UserService의 메서드 대부분은 현재 private 접근제한이 걸려있어 오버라이딩이 불가능하다.
테스트 코드는 테스트 대상 클래스의 내부의 구현 내용을 고려해 밀접하게 접근해야 하지만, private은 제약이 강한 접근제한자이기 때문에 테스트를 위해 애플리케이션 코드를 수정하자. 하지만 테스트를 위해 애플리케이션 코드를 직접 수정하는 일은 가능한 한 피하자.
UserService 서브클래스에서 upgradeLevel() 메서드를 오버라이딩할 것이다.User 오브젝트를 확인해서 네 번째 User 오브젝트가 전달됐을 경우에 강제로 예외를 발생시킨다. UserService의 upgradeLevel() 메서드 접근권한을 protected로 수정해서 상속을 통해 오버라이딩이 가능하게 하자.upgradeLevel() 메서드가 UserService 메서드의 기능을 그대로 수행하지만 미리 지정된 id를 가진 사용자가 발견되면 강제로 예외를 던지도록 만든다. 
TestUserService와 마찬가지로 최상위 클래스로 정의하는 대신 테스트 클래스 내에 스태틱 멤버 클래스로 만들어도 된다. 
이제 테스트를 만들어보자.


테스트용으로 만들어둔 TestUserService의 오브젝트를 만든다.
id를 넣어준다.userDao를 수동으로 DI 해준다. TestUserService는 upgradeAllOrNothing 테스트 메서드에서만 특별한 목적으로 사용되는 것이라, 번거롭게 스프링 빈으로 등록할 필요는 없다. 5개의 사용자 정보를 등록해준다. 이후 testUserService의 업그레이드 메서드를 실행한다.
upgradeLevels()에서는 DB에서 5개의 User를 가져와 차례로 업그레이드 하다가 지정해둔 4번째 사용자 오브젝트 차례가 되면 TestUserServiceException을 발생시킬 것이다. upgradeLevels()가 정상적으로 종료되면 fail() 메서드 때문에 테스트가 실패할 것이다. fail()은 테스트가 의도한 대로 동작하는지를 확인하기 위해 넣은 것이다. TestUserServiceException을 잡은 후에는 checkLevelUpgraded() 메서드를 이용해 두 번째 사용자 레벨이 변경됐는지 확인한다.
테스트는 실패한다. 네 번째 사용자 처리 중 예외가 발생했지만 두 번째 사용자의 변경된 레벨이 그대로 유지되고 있다.
모든 사용자의 레벨을 업그레이드하는 작업인 upgradeLevels() 메서드가 하나의 트랜잭션 안에서 동작하지 않았기 때문이다.
트랜잭션이란 더 이상 나눌 수 없는 단위 작업을 말한다. 즉, 모든 사용자에 대한 레벨 업그레이드 작업은 전체가 다 성공하든지 아니면 전체가 다 실패하든지 해야 한다. 따라서 중간에 예외가 발생해서 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 초기 상태로 돌려놔야 한다.
DB는 그 자체로 완벽한 트랜잭션을 지원한다. 하나의 SQL 명령을 처리하는 경우는 DB가 트랜잭션을 보장해준다고 믿을 수 있다.
하지만 여러 개의 SQL이 사용되는 작업을 하나의 트랜잭션으로 취급해야 하는 경우도 있다. 문제는 첫 번째 SQL을 성공적으로 실행했지만 두 번째 SQL이 성공하기 전에 장애가 생겨서 작업이 중단되는 경우다. 이때 두 가지 작업이 하나의 트랜잭션이 되려면, 두 번째 SQL이 성공적으로 DB에서 수행되기 전에 문제가 발생한 경우에는 앞에서 처리한 SQL 작업도 취소시켜야 한다. 이런 취소 작업을 트랜잭션 롤백이라고 한다.
반대로 여러 개의 SQL을 하나의 트랜잭션으로 처리하는 경우에 모든 SQL 수행 작업이 다 성공적으로 마무리됐다고 DB에 알려줘서 작업을 확정시켜야 한다. 이것을 트랜잭션 커밋이라고 한다.
모든 트랜잭션은 시작하는 지점과 끝나는 지점이 있다. 시작하는 방법은 한 가지이지만 끝나는 방법은 두 가지다. 모든 작업을 무효화하는 롤백과 모든 작업을 다 확정하는 커밋이다. 애플리케이션 내에서 트랜잭션이 시작되고 끝나는 위치를 트랜잭션의 경계라고 부른다.
Connection, PreparedStatement를 처리하는 일부분은 생략되어 있다. 

false로 만들어주면 된다. 그러면 새로운 트랜잭션이 시작되게 만들 수 있다. commit() 또는 rollback() 메서드가 호출될 때까지의 작업이 하나의 트랜잭션으로 묶인다. commit() 또는 rollback()이 호출되면 그에 따라 작업 결과가 DB에 반영되거나 취소되고 트랜잭션이 종료된다. setAutoCommit(false)로 트랜잭션의 시작을 선언하고 commit() 또는 rollback()으로 트랜잭션을 종료하는 작업을 트랜잭션의 경계설정이라고 한다. Connection이 만들어지고 닫히는 범위 안에 존재한다. 이렇게 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 로컬 트랜잭션이라고도 한다. Connection을 가져와 사용하다가 닫는 사이에서 일어난다. 트랜잭션의 시작과 종료는 Connection 오브젝트를 통해 이뤄지기 때문이다.UserService와 UserDao의 트랜잭션 문제UserService의 upgradeLevels()에는 트랜잭션이 적용되지 않았을까?만든 코드 어디에도 트랜잭션을 시작하고, 커밋하고, 롤백하는 트랜잭션 경계설정 코드가 존재하지 않는다.
JdbcTemplate을 사용하기 때문에 Connection 오브젝트가 코드에서 없어졌다.
JdbcTemplate은 하나의 템플릿 메서드 안에서 DataSource의 getCommection() 메서드를 호출해서 Connection 오브젝트를 가져오고, 작업을 마치면 Connection을 확실하게 닫아주고 템플릿 메서드를 빠져나온다. 결국 템플릿 메서드 호출 한 번에 한 개의 DB 커넥션이 만들어지고 닫히는 것이다. 일반적으로 트랜잭션은 커넥션보다도 존재 범위가 짧다. 따라서 템플릿 메서드가 호출될 때마다 트랜잭션이 새로 만들어지고 메서드를 빠져나오기 전에 종료된다. 결국 JdbcTemplate의 메서드를 사용하는 UserDao는 각 메서드마다 하나씩의 독립적인 트랜잭션으로 실행될 수밖에 없다.
upgradeAllOrNothing() 테스트 메서드에서 2번째 사용자 오브젝트에서 UserDao의 update() 메서드를 호출한다. JdbcTemplate을 사용하는 update() 메서드는 자동으로 UPDATE 작업의 트랜잭션을 종료시킬 것이고, 수정 결과는 영구적으로 DB에 반영된다. 트랜잭션 작업은 내구성을 보장받기 때문에 일단 커밋되고 나면 DB 서버가 다운되더라도 그 결과는 DB에 그대로 남는다.
UserService와 UserDao를 통해 트랜잭션이 일어나는 과정
UserDao는 JdbcTemplate을 통해 매번 새로운 DB 커넥션과 트랜잭션을 만들어 사용한다. update()를 호출할 때 작업이 성공했다면 그 결과는 이미 트랜잭션이 종료되면서 커밋됐기 때문에 두 번째 update()를 호출하는 시점에서 오류가 발생해서 작업이 중단된다고 해도 첫 번째 커밋한 트랜잭션의 결과는 DB에 그대로 남는다. 데이터 액세스 코드를 DAO로 만들어서 분리해놓았을 경우엔 이처럼 DAO 메서드를 호출할 때마다 하나의 새로운 트랜잭션이 만들어지는 구조가 될 수밖에 없다. DAO 메서드에서 DB 커넥션을 매번 만들기 때문이다. 결국 DAO를 사용하면 비즈니스 로직을 담고 있는 UserService 내에서 진행되는 여러 가지 작업을 하나의 트랜잭션으로 묶는 일이 불가능해진다.
❗️ 일련의 작업이 하나의 트랜잭션으로 묶이려면 그 작업이 진행되는 동안 DB 커넥션도 하나만 사용돼야 한다. 트랜잭션은 Connection 오브젝트 안에서 만들어지기 때문이다. 즉, 같은 Connection을 사용해야 같은 트랜잭션 안에서 동작한다.
(비효율) DAO 메서드 안으로 upgradeLevels() 메서드의 내용을 옮기자.
UserDao가 가진 SQL이나 JDBC API를 이용한 데이터 액세스 코드는 최대한 그대로 남겨둔 채로, UserService에는 트랜잭션 시작과 종료를 담당하는 최소한의 코드만 가져오게 하자.
UserService와 UserDao를 그대로 둔 채로 트랜잭션을 적용하려면 결국 트랜잭션의 경계설정 작업을 UserService 쪽으로 가져와야 한다. 프로그램의 흐름을 볼 때 upgradeLevels() 메서드의 시작과 함께 트랜잭션이 시작하고 메서드를 빠져나올 때 트랜잭션이 종료돼야 하기 때문이다.
트랜잭션 경계를 upgradeLevels() 메서드 안에 두려면 DB 커넥션도 이 메서드 안에서 만들고, 종료시킬 필요가 있다.

Connection 오브젝트를 가지고 데이터 액세스 작업을 진행하는 코드는 UserDao의 update() 메서드 안에 있어야 한다. 순수한 데이터 액세스 로직은 UserDao에 둬야 하기 때문이다. UserDao의 update() 메서드는 반드시 upgradeLevels() 메서드에서 만든 Connection을 사용해야 한다. 그럴려면 DAO 메서드를 호출할 때마다 Connection 오브젝트를 파라미터로 전달해줘야 한다.
UserService에서 UserDao의 update()를 직접 호출하는 건 upgradeLevels()가 아닌 upgradeLevel() 메서드이다. 결국 UserService의 메서드 사이에도 같은 Connection 오브젝트를 사용하도록 파라미터로 전달해줘야 한다.
upgradeLevels() 메서드 안에서 트랜잭션의 경계설정 작업이 일어나야 하고, 그 트랜잭션을 갖고 있는 DB 커넥션을 이용하도록 해야만 별도의 클래스에 만들어둔 DAO 내의 코드도 트랜잭션이 적용될 테니 이 방법을 사용할 수밖에 없다.
UserService 트랜잭션 경계설정의 문제점UserService와 UserDao를 이런 식으로 수정하면 트랜잭션 문제는 해결할 수 있지만 다른 문제가 생긴다.
DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더 이상 활용할 수 없다.
결국 JDBC API를 직접 사용해야 한다. try/catch/finally 블록은 이제 UserService 내에 존재하고, UserService의 코드는 JDBC 작업 코드의 전형적인 문제점을 그대로 가질 수밖에 없다.
DAO의 메서드와 비즈니스 로직을 담고 있는 UserService의 메서드에 Connection 파라미터가 추가돼야 한다.
UserService는 스프링 빈으로 선언해서 싱글톤으로 되어 있으니 Connection을 인스턴스 변수에 저장해뒀다 다른 메서드에서 사용하게 할 수도 없다. 멀티스레드 환경에서는 공유하는 인스턴스 변수에 스레드별로 생성하는 정보를 저장하다가는 서로 덮어쓰는 일이 발생하기 때문이다.
Connection 파라미터가 UserDao 인터페이스 메서드에 추가되면 UserDao는 더 이상 데이터 액세스 기술에 독립적일 수가 없다.
JPA나 Hibernate로 UserDao의 구현 방식을 변경하려고 하면 Connection 대신 EntityManager나 Session 오브젝트를 UserDao 메서드가 전달받도록 해야 한다. 결국 UserDao 인터페이스는 바뀔 것이고, 그에 따라 UserService 코드도 함께 수정돼야 한다. 인터페이스를 사용해 DAO를 분리라고 DI를 적용했던 게 소용없어진다.
DAO 메서드에 Connection 파라미터를 받게 하면 테스트 코드에도 영향을 미친다.
테스트 코드에서 직접 Connection 오브젝트를 일일이 만들어서 DAO 메서드를 호출하도록 모두 변경해야 한다. 테스트에서 UserDao를 사용할 때 DB 커넥션까지 신경써야 한다.
비즈니스 로직을 담고 있는 UserService 메서드 안에서 트랜잭션의 경계를 설정해 관리하기 위해서, 스프링은 위 문제들을 해결할 수 있는 방법을 제공한다.
Connection 파라미터 제거upgradeLevels() 메서드가 트랜잭션 경계설정을 해야 한다. 하지만 여기서 생성된 Connection 오브젝트를 계속 메서드의 파라미터로 전달하다가 DAO를 호출할 때 사용하게 하는 것을 피하고 싶다.
이를 위해 스프링은 독립적인 트랜잭션 동기화 방식을 제안한다. 트랜잭션 동기화란 UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, DAO의 메서드에서는 저장된 Connection을 가져다가 사용하게 하는 것이다. 정확히는 DAO가 사용하는 JdbcTemplate이 트랜잭션 동기화 방식을 이용하도록 하는 것이다. 그리고 트랜잭션이 모두 종료되면, 그때는 동기화를 마치면 된다.

(1) UserService는 Connection을 생성하고 (2) 이를 트랜잭션 동기화 저장소에 저장해두고 Connection의 setAutoCommit(false)를 호출해 트랜잭션을 시작시킨 후에 본격적으로 DAO의 기능을 이용하기 시작한다.
(3) 첫 번째 update() 메서드가 호출되고, update() 메서드 내부에서 이용하는 JdbcTemplate 메서드에서는 가장 먼저 (4) 트랜잭션 동기화 저장소에 현재 시작된 트랜잭션을 가진 Connection 오브젝트가 존재하는지 확인한다.
(2) upgradeLevels() 메서드 시작 부분에서 저장해둔 Connection을 발견하고 이를 가져온다. 가져온 (5) Connection을 이용해 PreparedStatement를 만들어 수정 SQL을 실행한다. 트랜잭션 동기화 저장소에서 DB 커넥션을 가져왔을 때는 JdbcTemplate은 Connection을 닫지 않은 채로 작업을 마친다. 이렇게 해서 트랜잭션 안에서 첫 번째 DB 작업을 마쳤다. 여전히 Connection은 열려 있고 트랜잭션은 진행 중인 채로 트랜잭션 동기화 저장소에 저장되어 있다.
(6) 두 번째 update()가 호출되면 이때도 마찬가지로 (7) 트랜잭션 동기화 저장소에서 Connection을 가져와 (8) 사용한다.
(9) 마지막 update()도 (10) 같은 트랜잭션을 가진 Connection을 가져와 (11) 사용한다.
트랜잭션 내의 모든 작업이 정상적으로 끝났으면 UserService는 이제 (12) Connection의 commit()을 호출해서 트랜잭션을 완료시킨다.
마지막으로 (13) 트랜잭션 저장소가 더 이상 Connection 오브젝트를 저장해두지 않도록 이를 제거한다. 어느 작업 중에라도 예외상황이 발생하면 UserService는 즉시 Conneciton의 rollback()을 호출하고 트랜잭션을 종료할 수 있다. 물론 이때도 트랜잭션 저장소에 저장된 동기화된 Connection 오브젝트는 제거해줘야 한다.
이렇게 트랜잭션 동기화 기법을 사용하면 트랜잭션의 경계설정이 필요한 upgradeLevels()에서만 Connection을 다루게 하고, 여기서 생성된 Connection과 트랜잭션을 DAO의 JdbcTemplate이 사용할 수 있도록 별도의 저장소에 동기화하는 방법을 적용하기만 하면 된다.
문제는 멀티스레드 환경에서도 안전한 트랜잭션 동기화 방법을 구현하는 일이 기술적으로 간단하지 않다.
스프링은 JdbcTemplate과 더불어 이런 트랜잭션 동기화 기능을 지원하는 간단한 유틸리티 메서드를 제공하고 있다.


UserService에서 DB 커넥션을 직접 다룰 때 DataSource가 필요하므로 DataSource 빈에 대한 DI 설정을 해둬야 한다.
TransactionSynchronizationManager : 스프링이 제공하는 트랜잭션 동기화 관리 클래스이다.
트랜잭션 동기화 적용 과정
TransactionSynchronizationManager 클래스를 이용해 먼저 트랜잭션 동기화 작업을 초기화하도록 요청한다. DataSourceUtils에서 제공하는 getConnection() 메서드를 통해 DB 커넥션을 생성한다.DataSource에서 Connection을 직접 가져오지 않고, 스프링이 제공하는 유틸리티 메서드를 쓰는 이유는 이 DataSourceUtils의 getConnection() 메서드는 Connection 오브젝트를 생성해줄 뿐만 아니라 트랜잭션 동기화에 사용하도록 저장소에 바인딩해주기 때문이다. JdbcTemplate을 사용하면 JdbcTemplate의 작업에서 동기화시킨 DB 커넥션을 사용하게 된다. 결국 UserDao를 통해 진행되는 모든 JDBC 작업은 upgradeLevels() 메서드에서 만든 Connection 오브젝트를 사용하고 같은 트랜잭션에 참여하게 된다. JDBC의 트랜잭션 경계설정 메서드를 사용해 트랜잭션을 이용하는 전형적인 코드에 간단한 트랜잭션 동기화 작업을 붙여 Connection 파라미터 문제를 해결했다.
트랜잭션이 적용됐는지 테스트를 해보자.
UserServiceTest에 upgradeAllOrNothing() 테스트 메서드에 dataSource 빈을 가져와 주입해주는 코드를 추가한다.TestUserService는 UserService의 서브클래스이므로 UserService와 마찬가지로 트랜잭션 동기화에 필요한 DataSource를 DI 해줘야 하기 때문이다. 
UserService의 dataSource 프로퍼티 설정을 설정파일에 추가해줘야 한다.TestUserService를 직접 구성하는 upgradeAllOrNothing() 테스트와는 달리, upgradeLevels() 테스트는 스프링 컨테이너가 초기화한 userService를 사용해야 하기 때문이다. 
JdbcTemplate은 update()나 query() 같은 JDBC 작업의 템플릿 메서드를 호출하면 직접 Connection을 생성하고 종료하는 일을 모두 담당한다. 테스트에서 특별한 준비 없이 DAO의 메서드를 직접 사용했을 때도 제대로 동작하는 것을 보면 스스로 Connection을 생성해서 사용한다는 사실을 알 수 있다.
만약 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate이 직접 DB 커넥션을 만들고 트랜잭션을 시작해서 JDBC 작업을 진행한다. 반면에 트랜잭션 동기화를 시작해놓았다면 그때부터 실행되는 JdbcTemplate의 메서드에서는 직접 DB 커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어 있는 DB 커넥션을 가져와서 사용한다. 이를 통해 이미 시작된 트랜잭션에 참여하는 것이다.
따라서 DAO를 사용할 때 트랜잭션이 굳이 필요 없다면 바로 호출해서 사용해도 되고, DAO 외부에서 트랜잭션을 만들고 이를 관리할 필요가 있다면 미리 DB 커넥션을 생성한 다음 트랜잭션 동기화를 해주고 사용하면 된다. 트랜잭션 동기화를 해주고 나면 DAO에서 사용하는 JdbcTemplate은 자동으로 트랜잭션 안에서 동작할 것이다.
지금까지 만든 코드는 DB 연결 방법이 바뀌어도 UserDao나 UserService 코드는 수정하지 않아도 된다. DataSource 인터페이스와 DI를 적용한 덕분이다.
Connection을 이용한 트랜잭션 방식인 로컬 트랜잭션으로는 불가능하다. 왜냐하면 로컬 트랜잭션은 하나의 DB Connection에 종속되기 때문이다.👉 JTA(Java Transaction API) : 자바는 JDBC 외에 이런 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위해 제공하는 API이다.

애플리케이션은 기존의 방법대로 DB는 JDBC, 메시징 서버라면 JMS 같은 API를 사용해서 필요한 작업을 수행한다.
단, 트랜잭션은 JDBC나 JMS API를 사용해서 직접 제어하지 않고 JTA를 통해 트랜잭션 매니저가 관리하도록 위임한다.
트랜잭션 매니저는 DB와 메시징 서버를 제어하고 관리하는 각각의 리소스 매니저와 XA 프로토콜을 통해 연결된다.
이를 통해 트랜잭션 매니저가 실제 DB와 메시징 서버의 트랜잭션을 종합적으로 제어할 수 있게 되는 것이다.
이렇게 JTA를 이용해 트랜잭션 매니저를 활용하면 여러 개의 DB나 메시징 서버에 대한 작업을 하나의 트랜잭션으로 통합하는 분산 트랜잭션 또는 글로벌 트랜잭션이 가능해진다.
JTA를 이용한 트랜잭션 처리 코드의 전형적인 구조

트랜잭션 경계설정을 위한 코드의 구조는 JDBC를 사용했을 때와 비슷하다.
JDBC 로컬 트랜잭션을 JTA를 이용하는 글로벌 트랜잭션으로 바꾸려면 UserService의 코드를 수정해야 한다. 로컬 트랜잭션과 글로벌 트랜잭션 중에 필요로 하는 것에 따라 트랜잭션 관리 코드를 적용해야 한다. UserService는 자신의 로직이 바뀌지 않았음에도 기술환경에 따라서 코드가 바뀐다.
Y사에서 Hibernate를 이용해 UserDao를 직접 구현했다고 알려왔다. 그런데 문제는 Hiberenate를 이용한 트랜잭션 관리 코드는 JDBC나 JTA의 코드와는 또 다르다는 것이다. Hibernate는 Connection을 직접 사용하지 않고 Session을 사용하고, 독자적인 트랜잭션 관리 API를 사용한다. 그렇다면 이번엔 UserService를 Hibernate의 Session과 Transaction 오브젝트를 사용하는 트랜잭션 경계설정 코드로 변경할 수 밖에 없다.
UserDao가 DAO 패턴을 사용해 구현 데이터 액세스 기술을 유연하게 바꿔서 사용할 수 있게 했지만 UserService에서 트랜잭션의 경계설정을 해야 할 필요가 생기면서 다시 특정 데이터 액세스 기술에 종속되는 구조가 되고 말았다.
UserService에 트랜잭션 경계설정 코드를 도입한 후에 클래스의 의존관계
UserService는 UserDao 인터페이스에만 의존하는 구조였다. 하지만 JDBC에 종속적인 Connection을 이용한 트랜잭션 코드가 UserService에 등장하면서부터 UserDaoJdbc에 간접적으로 의존하는 코드가 됐다. UserService이 코드가 특정 트랜잭션 방법에 의존적이지 않고 독립적일 수 있게 할 방법은?
UserService의 메서드 안에서 트랜잭션 경계설정 코드를 제거할 수는 없다. 하지만 특정 기술에 의존적인 Connection, UserTransaction, Session/Transaction API 등에 종속되지 않게 할 수 있는 방법은 있다.
트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조라 추상화를 생각해볼 수 있다.
JDBC, JTA, Hibernate, JPA, JDO, JMS 모두 트랜잭션 개념을 갖고 있으니 모두 트랜잭션 경계설정 방법에서 공통점이 있을 것이다. 이 공통적인 특징을 모아 추상화된 트랜잭션 관리 계층을 만들 수 있다.
그러면 애플리케이션 코드에서는 트랜잭션 추상 계층이 제공하는 API를 이용해 트랜잭션을 이용하게 만들어준다면 특정 기술에 종속되지 않는 트랜잭션 경계설정 코드를 만들 수 있다.
📌 추상화 : 하위 시스템의 공통점을 뽑아내서 분리시키는 것을 말한다.
- 추상화하면 하위 시스템이 어떤 것인지 알지 못해도, 또는 하위 시스템이 바뀌더라도 일관된 방법으로 접근할 수 있다.
- DB에서 제공하는 DB 클라이언트 라이브러리와 API는 서로 전혀 호환이 되지 않는 독자적인 방식으로 만들어져 있다. 하지만 모두 SQL을 이용하는 방식이라는 공통점이 있다. 이 공통점을 뽑아내 추상화한 것이 JDBC다. JDBC라는 추상화 기술이 있기 때문에 자바의 DB 프로그램 개발자는 DB의 종류에 상관없이 일관된 방법으로 데이터 액세스 코드를 작성할 수가 있다.
스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다. 이를 이용하면 애플리케이션에서 직접 각 기술의 트랜잭션 API를 이용하지 않고도, 일관된 방식으로 트랜잭션을 제어하는 트랜잭션 경계설정 작업이 가능해진다.


PlatformTransactionManager을 생성한다.
PlatformTransactionManager : 스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스 PlatformTransactionManager를 구현한 DataSourceTransactionManager를 사용하면 된다. 사용할 DB의 DataSource를 생성자 파라미터로 넣으면서 DataSourceTransactionManager의 오브젝트를 만든다. 트랜잭션을 시작한다.
Connection을 생성하고 나서 트랜잭션을 시작했지만, DataSourceTransactionManager은 트랜잭션을 가져오는 요청인 getTransaction() 메서드를 호출하기만 하면 된다. DefaultTransactionDefinition 오브젝트를 트랜잭션에 대한 속성을 담고 있다. TransactionStatus 타입의 변수에 저장된다. TransactionStatus는 트랜잭션에 대한 조작이 필요할 때 PlatformTransactionManager 메서드의 파라미터로 전달해주면 된다. 트랜잭션이 시작됐으니 이제 JdbcTemplate을 사용하는 DAO를 이용하는 작업을 진행한다.
PlatformTransactionManager로 시작한 트랜잭션은 트랜잭션 동기화 저장소에 저장된다. PlatformTransactionManager를 구현한 DataSourceTransactionManager 오브젝트는 JdbcTemplate에서 사용될 수 있는 방식으로 트랜잭션을 관리해준다. 따라서 PlatformTransactionManager를 통해 시작한 트랜잭션은 UserDao의 JdbcTemplate 안에서 사용된다. 트랜잭션 작업을 모두 수행한 후엔 트랜잭션을 만들 때 돌려받은 TransactionStatus 오브젝트를 파라미터로 해서 PlatformTransactionManager의 commit() 메서드를 호출하면 된다. 예외가 발생하면 rollback() 메서드를 부른다.
트랜잭션 추상화 API를 적용한 UserService 코드를 JTA를 이용하는 글로벌 트랜잭션으로 변경하려면,
PlatformTransactionManager 구현 클래스를 JTATransactionManager로 바꿔주기만 하면 된다.
Hibernate로 UserDao를 구현했다면 HibernateTransactionManager를 사용하면 된다.
JTATransactionManager는 주요 자바 서버에서 제공하는 JTA 정보를 JNDI를 통해 자동으로 인식하는 기능을 갖고 있다. 따라서 별다른 설정 없이 JTATransactionManager를 사용하기만 해도 서버의 트랜잭션 매니저/서비스와 연동해서 동작한다.
UserService에 PlatformTransactionManager 인터페이스 타입의 인스턴스 변수와 수정자 메서드를 추가해서 DI 가능하게 해준다.UserService 코드가 알고 있는 것은 DI 원칙에 위배된다. 컨테이너를 통해 외부에서 제공받게 하는 스프링 DI의 방식으로 바꾸자.어떤 클래스든 스프링 빈으로 등록할 때 검토해야 할 것은 싱글톤으로 만들어져 여러 스레드에서 동시에 사용해도 괜찮은가 하는 점이다. 스프링이 제공하는 모든 PlatformTransactionManager의 구현 클래스는 싱글톤으로 사용이 가능하기 때문에 스프링의 싱글톤 빈으로 등록해도 좋다.
수정자 메서드를 추가해서 DI가 가능하게 해준다. 일반적으로는 인터페이스 이름과 변수 이름, 수정자 메서드 이름을 모두 같은 것으로 통일하지만, PlatformTransactionManager의 경우는 관례적으로 transactionManager라는 이름을 사용한다.
UserService에 DataSource 변수와 수정자 메서드는 제거한다.
UserService는 이제 PlatformTransactionManager만으로도 Connection 생성과 트랜잭션 경계설정 기능을 모두 이용할 수 있기 때문이다.

JDBC 기반의 단일 DB를 사용하는 트랜잭션을 사용하기 위해 DataSourceTransactionManager 클래스를 사용하면 된다. 스프링 설정파일을 수정한다.

DataSourceTransactionManager는 dataSource 빈으로부터 Connection을 가져와 트랜잭션 처리를 해야 하기 때문에 dataSource 프로퍼티를 갖는다. userService 빈도 기존의 dataSource 프로퍼티를 없애고 새롭게 추가한 transactionManager 빈을 DI 받도록 프로퍼티를 설정한다.테스트도 수정한다.
트랜잭션 예외상황을 테스트하기 위해 수동 DI를 하는 upgradeAllOrNothing() 메서드에서 스프링 컨테이너로부터 transactionManager 빈을 @Autowired로 주입받게 하고 이를 직접 DI 해주도록 변경한다.

이제 UserService는 트랜잭션 기술에서 완전히 독립적인 코드가 됐다. 트랜잭션을 JTA를 이용하고 싶다면 설정파일의 transationManager 빈의 설정만 수정하면 된다. 또한 DAO를 Hibernate나 JPA, JPO 등을 사용하도록 수정했대도 그에 맞게 transactionManager의 클래스만 변경해주면 된다.
📌
JtaTransactionManager는 애플리케이션 서버의 트랜잭션 서비스를 이용하기 때문에 직접DataSource와 연동할 필요는 없다. 대신 JTA를 사용하는 경우는DataSource도 서버가 제공해주는 것을 사용해야 한다.
이제 스프링의 트랜잭션 서비스 추상화 기법을 이용한 다양한 트랜잭션 기술을 일관된 방식으로 제어할 수 있게 됐다. 사용자 관리의 핵심 코드 소스는 공개하지 않고, 설정을 고치는 것만으로 DB 연결 기술, 데이터 액세스 기술, 트랜잭션 기술을 자유롭게 바꿔서 사용할 수 있다.
UserDao와 UserService는 각각 담당하는 코드의 기능적인 관심에 따라 분리되고, 서로 독자적으로 확장이 가능하도록 만든 것이다. 같은 애플리케이션 로직을 담은 코드지만 내용에 따라 분리했다. 같은 계층에서 수평적인 분리라고 볼 수 있다.
트랜잭션의 추상화는 수직적인 분리다. 애플리케이션의 비즈니스 로직과 그 하위에서 동작하는 로우레벨의 트랜잭션 기술이라는 아예 다른 계층의 특성을 갖는 코드를 분리한 것이다.

UserService, UserDao : 애플리케이션의 로직을 담고 있는 애플리케이션 계층UserService : 순수하게 사용자 관리의 업무의 비즈니스 로직UserDao : 데이터를 어떻게 가져오고 등록할 것인가에 대한 데이터 액세스 로직UserDao와 UserService는 인터페이스와 DI를 통해 연결됨으로써 결합도가 낮다.UserDao와 DB 연결 기술도 결합도가 낮다.DataSource 인터페이스와 DI를 통해 추상화된 방식으로 로우레벨의 DB 연결 기술을 사용하기 때문이다. 그래서 UserDao는 DB 연결을 생성하는 방법에 대해 독립적이다. UserService와 트랜잭션 기술과도 독립적이다.PlatformTransactionManager 인터페이스를 통한 추상화 계층을 사이에 두고 사용하게 했기 때문에, 구체적인 트랜잭션 기술에 독립적인 코드가 됐다. UserDao와 DB 연결 기술, UserService와 트랜잭션 기술의 결합도가 낮은 분리는 애플리케이션 코드를 로우레벨의 기술 서비스와 환경에서 독립시켜준다. 📌 결합도가 낮다는 건 서로 영향을 주지 않고 자유롭게 독립적으로 확장될 수 있는 구조라는 뜻이다.
👉 단일 책임 원칙
하나의 모듈은 한 가지 책임을 가져야 한다는 의미다. 하나의 모듈이 바뀌는 이유는 한 가지여야 한다는 것이기도 하다.
이런 적절한 분리가 가져오는 특징은 단일 책임 원칙으로 설명할 수 있다.
(이전) UserService에 JDBC Connection의 메서드를 직접 사용하는 트랜잭션 코드가 들어있었을 때 UserService는 어떻게 사용자 레벨을 관리할 것인가와 어떻게 트랜잭션을 관리할 것인가라는 두 가지 책임을 갖고 있었다. 두 가지 책임을 갖고 있다는 건 UserService 코드가 변경되는 이유가 두 가지라는 뜻이다. 사용자 관리 로직이 바뀌든 트랜잭션 기술이 바뀌든 UserService 코드를 수정해야 한다.
-> 결국 단일 책임 원칙을 지키지 못하는 것이다.
(이후) 트랜잭션 서비스의 추상화 방식을 도입하고, 이를 DI를 통해 외부에서 제어하도록 수정했을 때 UserService가 바뀔 이유는 한 가지뿐이다. 사용자 관리 로직이 바뀌거나 추가되지 않는 한 코드를 수정할 일이 없다.
-> 따라서 단일 책임 원칙을 지키고 있다.
서비스 하나가 여러 개의 DAO를 사용하는 경우가 많아지면 의존관계가 매우 복잡해진다. 이 때 DAO를 하나 수정할 경우 그에 의존하고 있는 서비스 클래스도 같이 수정해야 하는 구조면 수백 개의 클래스를 다 같이 수정해줘야 한다.
기술적인 수정사항도 마찬가지다. 애플리케이션 계층의 코드가 특정 기술에 종속돼서 기술이 바뀔 때마다 코드의 수정이 필요하면 아마 그에 따라 엄청난 코드를 수정해야 할 것이다. 많은 코드를 수정하면 그만큼 실수가 일어날 확률이 놓고, 치명적인 버그도 도입될 가능성이 있다.
그래서 적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 다양한 추상화 기법을 도입하고, 애플리케이션 로직과 기술/환경을 분리하는 등의 작업이 반드시 필요하다. 이를 위한 핵심적인 도구가 바로 스프링이 제공하는 DI이다.
객체지향 설계와 프로그래밍의 원칙은 서로 긴밀하게 관련이 있다. 단일 책임 원칙을 잘 지키는 코드를 만들려면 인터페이스를 도입하고 이를 DI로 연결해야 하며, 그 결과로 단일 책임 원칙뿐 아니라 개방 폐쇄 원칙도 잘 지키고, 모듈 간에 결합도가 낮아서 서로의 변경이 영향을 주지 않고, 같은 이유로 변경이 단일 책임에 집중되는 응집도 높은 코드가 나오기 때문이다. 또한 이런 과정에서 전략 패턴, 어댑터 패턴, 브리지 패턴 등 많은 디자인 패턴이 자연스럽게 적용되기도 한다. 객체지향 설계 원칙을 잘 지켜서 만든 코드는 테스트하기도 편하다. 스프링이 지원하는 DI와 싱글톤 레지스트리 덕분에 더욱 편리하게 자동화된 테스트를 만들 수 있다.
User에 email 필드를 추가하면 된다. UserService의 upgradeLevel() 메서드에 메일 발송 기능을 추가한다.JavaMail을 이용한 메일 발송 기능사용자 정보에 이메일을 추가하는 일은 레벨을 추가했을 때와 동일하게 진행하면 된다.
USER 테이블에 email 필드를 추가한다.User 클래스에 email 프로퍼티를 추가한다. UserDao의 userMapper와 insert(), update()에 email 필드 처리 코드를 추가한다. UserDaoTest도 수정한다. JavaMail 메일 발송JavaMail을 사용하면 된다. javax.mail 패키지에서 제공하는 자바의 이메일 클래스를 사용한다. upgradeLevel()에서 메일 발송 메서드를 호출한다. JavaMail API를 사용하는 메서드를 추가한다. 

JavaMail을 이용해 메일을 발송하는 가장 전형적인 코드이다. JavaMail이 포함된 코드의 테스트테스트를 실행했는데 만약 메일 서버가 준비되어 있지 않았다면, MessagingException 예외가 발생하면서 실패한다. 메일 서버가 현재 연결 가능하도록 준비되어 있지 않기 때문이다.
그런데 테스트가 수행될 때 매번 메일이 발송되는 건 바람직하지 않다. 메일 발송은 매우 부하가 큰 작업이기 때문이다. 그것도 실제 운영 중인 메일 서버를 통해 테스트를 실행할 때마다 메일을 보내면 메일 서버에 상당한 부담을 줄 수 있다. 게다가 메일이 실제로 발송돼버린다는 문제도 있다.
테스트 때는 메일 서버 설정을 다르게 해서 테스트용으로 메일 서버를 이용하는 방법이 있다.
운영 중인 메일 서버에 부하를 주진 않게 된다. 하지만 메일 발송 기능은 사용자 레벨 업그레이드 작업의 보조적인 기능에 불과하다. 즉, 중요하지 않다.
실제 메일 전송을 수행하는 JavaMail 대신에 테스트에서 사용할, JavaMail과 같은 인터페이스를 갖는 오브젝트를 만들어서 사용한다. 즉, JavaMail API를 사용하지 않는 테스트용 오브젝트로 대체한다.
메일 서버 테스트란 엄밀히 말해서 불가능하다. 기껏해야 메일 발송용 서버에 전달됐음을 확인할 뿐이지, 메일이 정말 잘 도착했는지 확인이 힘들기 때문이다. 하지만 메일 서버는 충분히 테스트된 시스템이라 메일 서버까지만 잘 전달됐으면, 메일이 잘 전송됐다고 믿어도 된다. 따라서 메일 테스트를 한다고 매번 메일 수신 여부를 일일이 확인할 필요는 없고, 테스트 가능한 메일 서버까지만 잘 전송되는지 확인하면 된다. 그리고 테스트용 메일 서버는 메일 전송 요청은 받지만 실제로 메일이 발송되지 않도록 설정해주면 된다.

실제 메일 서버를 사용하지 않고 테스트 메일 서버를 이용해 테스트 하는 방법을 보여준다. 점선 안이 테스트가 동작하는 범위다. 테스트 메일 서버는 외부로 메일을 발송하지 않고, 단지 JavaMail과 연동해서 메일 전송 요청을 받는 것까지만 담당한다. 결국 이 테스트용으로 준비한 메일 서버는 업그레이드 작업 시 테스트에서 메일 전송 관련 예외가 발생하지 않고 테스트를 마치게 해주는 역할을 맡을 뿐이다.
JavaMail은 자바의 표준 기술이고 이미 검증된 안정적인 모듈이다. 따라서 JavaMail API를 통해 요청이 들어간다는 보장만 있다면 굳이 테스트할 때마다 JavaMail을 직접 구동시킬 필요가 없다.

운영 시에는 JavaMail을 직접 이용해서 동작하도록 해야겠지만, 개발 중이거나 테스트를 수행할 때는 JavaMail을 대신할 수 있는, 그러나 JavaMail을 사용할 때와 동일한 인터페이스를 갖는 코드가 동작하도록 만들어도 될 것이다.
JavaMail을 이용한 테스트의 문제점이 방법의 문제는 JavaMail의 API는 이 방법을 적용할 수 없다는 점이다.
JavaMail의 핵심 API는 DataSource처럼 인터페이스로 만들어져서 구현을 바꿀 수 있는 게 없다.JavaMail에선 Session 오브젝트를 만들어야만 메일 메시지를 생성할 수 있고 메일을 전송할 수 있다. 그런데 Session은 final 클래스여서 상속이 불가능하고, 생성자가 모두 private으로 되어 있어 직접 생성도 불가능하다.결국 JavaMail의 구현을 테스트용으로 바꿔치기하는 건 불가능하다고 볼 수 있다.
서비스 추상화를 적용해 JavaMail처럼 테스트하기 힘든 구조인 API를 테스트하자.
스프링은 JavaMail을 사용해 만든 코드의 테스트를 쉽게 하도록 하기 위해서 JavaMail에 대한 추상화 기능을 제공하고 있다.

MailSender : 스프링이 제공하는 메일 서비스 추상화의 핵심 인터페이스 SimpleMailMessage라는 인터페이스를 구현한 클래스에 담긴 메일 메시지를 전송하는 메서드로만 구성되어 있다. JavaMail을 사용해 메일 발송 기능을 제공하는 JavaMailSenderImpl 클래스를 이용하면 된다. JavaMailSenderImpl은 내부적으로 JavaMail API를 이용해 메일을 전송해준다.JavaMailSender 구현 클래스를 사용해서 만든 메일 발송용 코드이다.
SimpleMailMessage를 사용했고, 메일 전송 오브젝트는 JavaMailSenderImpl의 오브젝트를 만들어 사용했다. JavaMail을 처리하는 중에 발생한 각종 예외를 MailException이라는 런타임 예외로 포장해서 던져주기 때문에 try/cath 블록을 만들지 않아도 된다.MailMessage 타입의 SimpleMailMessage 오브젝트를 만들어서 메시지를 넣은 뒤에 JavaMailSender 타입 오브젝트의 send() 메서드에 전달해주면 된다. JavaMail API를 사용하는 JavaMailSenderImpl 클래스의 오브젝트를 코드에서 직접 사용한다. sendUpgradeEmail() 메서드에 JavaMailSenderImpl 클래스가 구현한 MailSender 인터페이스만 남기고, 구체적인 메일 전송 구현을 담은 클래스의 정보는 코드에서 모두 제거한다. UserService에 MailSender 인터페이스 타입의 변수를 만들고 수정자 메서드를 추가해 DI가 가능하도록 만든다. JavaMailSenderImpl 클래스로 빈을 만들고 UserService에 DI 해준다. MailSender의 구현 클래스들은 싱글톤으로 사용 가능해야 한다.mailSender 빈의 host 프로퍼티에는 메일 서버를 지정해준다.


이제 테스트를 실행하면 JavaMail API를 직접 사용했을 때와 동일하게 지정된 메일 서버로 메일이 발송된다.
JavaMail을 사용하지 않고, 메일 발송 기능이 포함된 코드를 테스트하게 됐다. 이를 위해 메일 전송 기능을 추상화해서 인터페이스를 적용하고 DI를 통해 빈으로 분리해놨다.
MailSender 인터페이스가 있으니 이를 구현해서 테스트용 메일 전송 클래스를 만든다.JavaMail을 사용해서 메일을 전송할 필요가 없으니, 그냥 아무것도 하지 않는 MailSender 구현 빈 클래스인 DummyMailSender를 만들면 된다.mailSender 빈 클래스를 JavaMail을 사용하는 JavaMailSenderImpl 대신 DummyMailSender로 변경한다. UserService에 새로운 DI용 프로퍼티가 추가됐으니 수동 DI 방식을 사용한 upgradeAllOrNothing() 메서드에도 mailSender를 추가해준다. 

스프링이 제공하는 MailSender 인터페이스를 핵심으로 하는 메일 전송 서비스 추상화의 구조다.
일반적으로 서비스 추상화라고 하면 트랜잭션과 같이 기능은 유사하나 사용 방법이 다른 로우레벨의 다양한 기술에 대해 추상 인터페이스와 일관성 있는 접근 방법을 제공해주는 것을 말한다. 반면에 JavaMail의 경우처럼 테스트를 어렵게 만드는 건전하지 않은 방식으로 설계된 API를 사용할 때도 유용하게 쓰일 수 있다.

JavaMailSenderImpl의 장점스프링이 직접 제공하는 MailSender를 구현한 추상화 클래스는 JavaMailServiceImpl 하나뿐이다. 하나뿐이지만 이 추상화된 메일 전송 기능을 사용해 애플리케이션을 작성함으로써 얻을 수 있는 장점은 크다.
JavaMail이 아닌 다른 메시징 서버의 API를 이용해 메일을 전송해야 하는 경우가 생겨도, 해당 기술의 API를 이용하는 MailSender 구현 클래스를 만들어서 DI 해주면 된다. MailSender 인터페이스를 구현한, 메일 발송 큐의 구현을 하나 만들어 두고 다시 DI를 통해 JavaMailServiceImpl 같은 실제 메일 발송용 오브젝트를 연결해서 사용할 수 있다. MailSender를 확장해서 메일 전송에 트랜잭션 개념을 적용하는 것이다.MailSender를 구현한 트랜잭션 기능이 있는 메일 전송용 클래스를 만든다. 이 오브젝트에 업그레이드 작업 이전에 새로운 메일 전송 작업 시작을 알려주고, 그 때부터는 mailSender.send() 메서드를 호출해도 실제로 메일을 발송하지 않고 저장해둔다. 그리고 업그레이드 작업이 끝나면 트랜잭션 기능을 가진 MailSender에 지금까지 저장된 메일을 모두 발송하고, 예외가 발생하면 모두 취소하게 할 수 있다. MailSender의 구현 클래스를 이용하는 방법은 서로 다른 종류의 작업을 분리해 처리한다는 면에서 장점이 있다.📌 서비스 추상화의 도입
기술이나 환경이 바뀔 가능성이 있음에도,JavaMail처럼 확장이 불가능하게 설계해놓은 API를 사용해야 하는 경우라면 추상화 계층의 도입을 적극 고려해볼 필요가 있다. 특별히 외부의 리소스와 연동하는 대부분 작업은 추상화의 대상이 될 수 있다.
DummyMailSender 클래스는 MailSender 인터페이스를 구현해놨을 뿐 아무것도 하는 일이 없다. 하지만 이 클래스를 이용해 JavaMail로 메일을 직접 발송하는 클래스를 대치했기 때문에 테스트가 원활하게 된 것이다.
스프링의 XML 설정파일을 테스트용으로 따로 만든 이유는 개발자 환경에서 손쉽게 이용할 수 있는 테스트용 DB를 사용하도록 만들기 위해서다.
이처럼 테스트 환경에서 유용하게 사용하는 기법이 있다. 대부분 테스트할 대상이 의존하고 있는 오브젝트를 DI를 통해 바꿔치기 하는 것이다.
UserDaoTest
원래 UserDao는 운영 시스템에서 사용하는 DB와 연결돼서 동작한다. 대용량의 DB 연결 기능에 최적화된 WAS에서 동작하는 DB 풀링 서비스를 사용하고, 이에 최적화된 복잡한 DataSource의 구현 클래스를 이용하도록 되어 있다.
하지만 UserDaoTest의 관심은 UserDao가 어떻게 동작하는지에 있지, 그 뒤에 존재하는 DB 커넥션 풀이나 DB 자체에 있지 않다. 하지만 UserDao가 제 기능을 수행하려면 반드시 DB를 사용해야 하기에 무시할 수는 없다. 그래서 이를 대신할 수 있도록 테스트환경에서도 잘 동작하고, 준비 과정도 간단한 DataSource를 사용하고, DB도 개발자 PC에 설치해서 사용해도 무방한 가벼운 버전을 이용하게 한 것이다.
UserServiceTest 중 메일 전송 기능
실제로 이 UserService가 운영 시스템에서 사용될 때는 당연히 JavaMailSenderImpl과 JavaMail을 통해 메일 서버로 이어지는 구성이 필요하다.
하지만 UserServiceTest의 관심사는 UserService에서 구현해놓은 사용자 정보를 가공하는 비즈니스 로직이지, 메일이 어떻게 전송이 될 것인지가 아니다. 그렇다고 메일 전송 기능을 아예 뺄 수도 없다. 테스트 대상이 되는 코드를 수정하지 않고, 메일 발송 작업 때문에 UserService 자체에 대한 테스트에 지장을 주지 않기 위해 도입한 것이 결국 DummyMailSender다. 이 클래스가 UserService가 반드시 이용해야 하는 의존 오브젝트의 역할을 해주면서 원할하게 테스트 중에 UserService의 코드가 실행되게 해준다.
이렇게 테스트 대상인 오브젝트가 의존 오브젝트를 갖고 있기 때문에 여러 가지 테스트상의 문제점이 있다.
UserDao의 경우처럼 테스트를 위해 간단한 환경으로 만들어주던가, 아니면 UserService의 메일 발송 기능의 경우처럼 아예 아무런 일도 하지 않는 빈 오브젝트로 대치해주는 방법이 있다. 스프링의 DI를 이용하는 것이다.단지 테스트만을 위해서도 DI는 유용하다. DataSource의 경우에서도, 운영 중인 시스템에서는 특정 클래스 외에는 절대로 다른 것을 사용하지 않는다고 확신하더라도 테스트 때는 바꿀 수밖에 없기 때문이다. 그래서 인터페이스를 사용하고, 어떤 클래스의 오브젝트를 사용할지 외부에서 주입해주도록 스프링의 DI를 적용해야 한다.
테스트용으로 사용되는 특별한 오브젝트들이 있다. 대부분 테스트 대상인 오브젝트의 의존 오브젝트가 되는 것들이다. UserDao의 DataSource이거나, UserService의 MailSender 인터페이스를 구현한 것들이다. 이렇게 테스트 환경을 만들어주기 위해, 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 이런 오브젝트를 통틀어서 테스트 대역이라고 부른다. 테스트가 원활하게 돌아가도록 의존 오브젝트로서 간접적인 도움을 준다는 개념과 달리, 어떤 테스트 대역은 테스트 과정에 매우 적극적으로 참여할 수 있다.
대표적인 테스트 대역은 테스트 스텁이다. 테스트 스텁은 테스트 대상 오브젝트의 의존객체로서 존재하면서 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것을 말한다.
일반적으로 테스트 스텁은 메서드를 통해 전달하는 파라미터와 달리, 테스트 코드 내부에서 간접적으로 사용된다. 따라서 DI 등을 통해 미리 의존 오브젝트를 테스트 스텁으로 변경해야 한다. DummyMailSender는 가장 단순하고 심플한 테스트 스텁의 예다.
많은 경우 테스트 스텁이 결과를 돌려줘야 할 때도 있다. MailSender처럼 호출만 하면 그만인 것도 있지만, 리턴 값이 있는 메서드를 이용하는 경우에는 결과가 필요하다. 이럴 땐 스텁에 미리 테스트 중에 필요한 정보를 리턴해주도록 만들 수 있다.
어떤 스텁은 메서드를 호출하면 강제로 예외를 발생시키게 해서 테스트 대상 오브젝트가 예외상황에서 어떻게 반응할지를 테스트할 때 적용할 수도 있다.
스텁을 이용하면 간접적인 입력 값을 지정해줄 수도 있다. 마찬가지로 어떤 스텁은 간접적인 출력 값을 받게 할 수 있다.
테스트 대상 오브젝트의 메서드가 돌려주는 결과뿐 아니라 테스트 오브젝트가 간접적으로 의존 오브젝트에 넘기는 값과 그 행위 자체에 대해서도 검증하고 싶다면 어떻게 해야 할까? 이 경우 단순하게 메서드의 리턴 값을 assertThat()으로 검증하는 것으로는 불가능하다.
이런 경우에는 테스트 대상의 간접적인 출력 결과를 검증하고, 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검증할 수 있도록 특별히 설계된 목 오브젝트를 사용해야 한다. 목 오브젝트는 스텁처럼 테스트 오브젝트가 정상적으로 실행되도록 도와주면서, 테스트 오브젝트와 자신의 사이에서 일어나는 커뮤니케이션 내용을 저장해뒀다가 테스트 결과를 검증하는 데 활용할 수 있게 해준다.

테스트 대상 오브젝트는 테스트로부터만 입력을 받는 것이 아니다. 테스트가 수행되는 동안 실행되는 코드는 테스트 대상이 의존하고 있는 다른 의존 오브젝트와도 커뮤니케이션하기도 한다. 테스트 대상은 의존 오브젝트에게 값을 출력하기도, 입력받기도 한다. 간접적으로 테스트 대상이 받아야 할 입력 값이 필요할 때는, 별도로 준비해둔 스텁 오브젝트가 메서드 호출 시 특정 값을 리턴하도록 만들어두면 된다.
때론 테스트 대상 오브젝트가 의존 오브젝트에게 출력한 값에 관심이 있을 경우가 있다. 또는 의존 오브젝트를 얼마나 사용했는가 하는 커뮤니케이션 행위 자체에 관심이 있을 수가 있다. 이때는 테스트 대상과 의존 오브젝트 사이에 주고받는 정보를 보존해두는 기능을 가진 테스트용 의존 오브젝트인 목 오브젝트를 만들어서 사용해야 한다. 테스트 대상 오브젝트의 메서드 호출이 끝나고 나면 테스트는 목 오브젝트에게 테스트 대상과 목 오브젝트 사이에서 일어났던 일에 대해 확인을 요청해서, 그것을 테스트 검증 자료로 삼을 수 있다.
UserServiceTest에 목 오브젝트를 적용해보자.
트랜잭션 기능을 테스트하려고 만든 upgradeAllOrNothing()의 경우는 테스트가 수행되는 동안에 메일이 전송됐는지 여부는 관심의 대상이 아니기 때문에 DummyMailSender가 잘 어울린다.
반면에 정상적인 사용자 레벨 업그레이드 결과를 확인하는 upgradeLevels() 테스트에서는 메일 전송 자체에 대해서도 검증할 필요가 있다. 조건을 만족하는 사용자의 레벨을 수정했다면, 메일도 발송했어야 하기 때문이다.
만약 JavaMail을 직접 사용하는 방식이었다면, 메일 발송 테스트는 메일 주소로 실제 메일이 들어왔는지 직접 확인하거나, 아니면 아주 복잡한 방법을 사용해 메일 서버의 로그를 뒤져서 메일 발송 로그가 그 시점에 남았는지를 확인해야 할 것이다. 하지만 스프링의 JavaMail 서비스 추상화를 적용했기 때문에 목 오브젝트를 만들어서 메일 발송 여부를 확인할 수 있다.
DummyMailSender 대신 새로운 MailSender를 대체할 클래스를 하나 만든다. UserServiceTest 안에서만 사용될 것이므로 스태틱 멤버 클래스로 정의한다.
UserService가 send() 메서드를 통해 자신을 불러서 메일 전송 요청을 보냈을 때 관련 정보를 저장해두는 기능이다.SimpleMailMessage 오브젝트에서 첫 번째 수신자 메일 주소를 꺼내온다. MockMailSender를 이용해 테스트 upgradeLevels() 테스트 코드를 수정해서 목 오브젝트를 통해 메일 발송 여부를 검증하도록 만든다.


1. 스프링 설정을 통해 사용할 메일 전송 검증용 목 오브젝트를 준비한다.
2. MockMailSender의 오브젝트를 만들고 UserService 오브젝트의 수정자를 이용해 수동 DI 해준다.
3. 생성된 MockMailSender 오브젝트는 뒤에서 검증정보를 얻을 때 사용할 것이므로 변수에 저장해둔다.
4. 테스트 대상의 메서드를 호출하고 먼저 업그레이드 결과에 대한 검증 과정을 거친다.
5. MockMailSender 오브젝트로부터 UserService 사이에서 일어난 일에 대한 결과를 검증한다.
5.1 userService에 DI 해줬던 목 오브젝트로부터 getRequests()를 호출해서 메일 주소가 저장된 리스트를 가져온다.
5.2 먼저 리스트의 크기를 확인한다.
5.3 리스트의 첫 번째 메일 주소와 두 번째 사용자의 메일 주소를 비교한다.
5.4 두 번째 메일 주소와 네 번째 사용자의 메일 주소가 같은지 검증한다.
테스트 조건에 따르면 업그레이드 대상은 두 번째와 네 번째 사용자 두 명이기 때문이다.
이제 레벨 업그레이드가 일어날 때 DB의 내용이 변경되는 것은 물론이고, 메일도 정상적으로 발송된다는 사실도 확인할 수 있다. 테스트가 수행될 수 있도록 의존 오브젝트에 간접적으로 입력 값을 제공해주는 스텁 오브젝트와 간접적인 출력 값까지 확인이 가능한 목 오브젝트, 이 두 가지는 테스트 대역의 가장 대표적인 방법이며 효과적인 테스트 코드를 작성하기 위한 중요한 도구다.
5장에서는 비즈니스 로직을 담은 UserService 클래스를 만들고 트랜잭션을 적용하면서 스프링의 서비스 추상화에 대해 살펴보았다.
JavaMail 같은 기술에도 적용할 수 있다. 테스트를 편리하게 작성하도록 도와주는 것만으로도 서비스 추상화는 가치가 있다.