토비의 스프링 5장
5. 서비스 추상화
5.1 사용자 레벨 관리 기능 추가
- 5.1.1 필드 추가
- Level 이늄(enum)
- 각 레벨을 코드화해서 숫자로 넣음(범위가 작은 숫자로 관리하면 DB용량도 많이 차지하지 않고 가벼워서 좋다.)
- 하지만 다른 종류의 정보를 넣는 실수를 해도 컴파일러가 체크해주지 못한다는 단점이 있다.
- 또한, 범위를 벗어나는 값을 넣을 위험이 있다.
- 그래서 enum 사용
- Level 이늄(enum)
- 5.1.2 사용자 수정 기능 추가
- 수정 테스트 보완
- 현재는 update에 where이 빠져 있어도 성공한다.
- 현재 update() 테스트는 수정할 로우의 내용이 바뀐 것만 확인할 뿐, 수정하지 않아야 할 로우의 내용이 그대로 남아 있는지는 확인해주지 못한다는 문제가 있다.
- 해결방법
- update()가 돌려주는 리턴 값이 1인지 확인
- 사용자를 2명 등록한 뒤, 1명만 수정한 뒤에 수정된 사용자와 수정하지 않은 사용자의 정보를 모두 확인
- 수정 테스트 보완
- 5.1.3 UserService.upgradeLevels()
- 사용자 관리 로직을 위해 UserService 추가
- DAO의 인터페이스를 사용하고 DI를 적용해야 한다.
- UserService 클래스와 빈 등록
- UserServiceTest 테스트 클래스 등록
- upgradeLevels() 메소드
- upgradeLevels() 테스트
- 업그레이드 조건 전후인 숫자들로 테스트
- 5.1.4 UserService.add()
- 처음 가입하는 사용자를 BASIC 레벨로 추가해야 하는 기능이 필요함
- UserDaoJdbc의 add()에 넣는 것은 비즈니스적인 의미에 관심을 가지게 되므로 적합하지 않다.
- User 클래스의 level 필드를 BASIC으로 초기화하는 것은 처음 가입할 때를 제외하면 무의미하므로 좀 문제가 있다.
- 그래서 UserService에서 add()로 추가
- 레벨이 미리 정해진 경우와 레벨이 비어 있는 두 가지 경우에 각각 add() 메소드를 호출하고 결과를 확인하는 테스트 추가
- 5.1.5 코드 개선
- 중복된 부분은 없는가?
- 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가?
- 코드가 자신이 있어야 할 자리에 있는가?
- 앞으로 변경이 일어난다면 어떤 것이 있을 수 있고, 그 변화에 쉽게 대응할 수 있게 작성되어 있는가?
- upgradeLevels() 메소드 코드의 문제점
- for 루프 속에 있는 if/elseif/else 블록들이 읽기 불편(성격이 다른 여러 가지 로직이 한데 섞여 있기 때문)
- 그래서 첫 단계에서는 레벨을 확인하고 각 레벨별로 다시 조건을 판단하는 조건식을 넣어야 한다.
- 수정한 뒤 upgradeLevel() 메소드에서 예외처리와 다음 단계에 대한 로직이 노골적으로 드러나 있다는 단점이 있다.
- upgradeLevels() 리팩토링
- 그래서 Level enum에서 업그레이든 순서를 담고 있도록 수정한다.
- upgradeLevels() 코드를 UserService에서 User로 옮긴다.
- User 테스트
- UserServiceTest 개선
- 중복되는 숫자 상수로 처리
- 업그레이드하는 정책을 유연하게 변경할 수 있도록 개선(인터페이스)
5.2 트랜잭션 서비스 추상화
- 5.2.1 모 아니면 도
- 예외가 던져지는 상황을 의도적으로 만들어서 확인
- 테스트용 UserService 대역
1 2 3 4 5 6 7 8 9 10 11 12
static class TestUserService extends UserService { private String id; private TestUserService(String id) { // 예외를 발생시킬 User 오브젝트의 id를 지정할 수 있게 만든다. this.id = id; } protected void upgradeLevel(User user) { // UserService 메소드를 오버라이드한다. if(user.getId().equals(this.id)) throw new TestUserServiceException(); // 지정된 id의 User 오브젝트가 발견되면 예외를 던져서 작업을 강제로 중단 super.upgradeLevel(user); } }
강제 예외 발생을 통한 테스트
- 두 번째 사용자의 레벨이 BASIC에서 SILVER로 바뀐 것이 네 번째 사용자 처리 중 예외가 발생했지만 그대로 유지되기 때문에 실패
- 테스트 실패의 원인
- 트랜잭션: 더 이상 나눌 수 없는 단위 작업
- 중간에 예외가 발생해서 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 초기 상태로 돌려놔야 한다.
- 5.2.2 트랜잭션 경계설정
- 트랜잭션 롤백: 문제가 발생했을 경우 앞에서 처리한 SQL 작업도 취소하는 것
- 트랜잭션 커밋: 여러 개의 SQL을 하나의 트랜잭션으로 처리하는 겨우에 모든 SQL 수행 작업이 다 성공적으로 마무리됐다고 DB에 알려줘서 작업을 확정시키는 것
- JDBC 트랜잭션의 트랜잭션 경계설정
- JDBC의 트랜잭션은 하나의 Conncetion을 가져와 사용하다가 닫는 사이에서 일어난다.
- 트랜잭션의 경계설정: setAutoCommit(false)로 트랜잭션의 시작을 선언하고 commit() 또는 rollback()으로 트랜잭션을 종료하는 작업
- 로컬 트랜잭션: 하나의 DB 커넥션 안에서 만들어지는 트랜잭션
- UserService와 UserDao의 트랜잭션 문제
- DAO를 사용하면 비즈니스 로직을 담고 있는 UserService 내에서 진행되는 여러 가지 작업을 하나의 트랜잭션으로 묶는 일이 불가능해진다.
- 비즈니스 로직 내의 트랜잭션 경계설정
- DAO 메소드 안으로 upgradeLevels() 메소드의 내용을 옮기는 방법이 있지만, 비즈니스 로직과 데이터 로직을 한데 묶어버리는 한심한 결과를 초래한다.
- 트랜잭션 경계를 upgradeLevels() 메소드 안에 두려면 DB 커넥션도 이 메소드 안에서 만들고, 종료시켜야 한다.
- 생성된 Connection을 UserDao에서 사용해야 하기 때문에 파라미터로 전달해야 한다.
- UserService 트랜잭션 경계설정의 문제점
- DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했뎐 JdbcTemplate을 활용할 수 없다.
- DAO의 메소드와 비즈니스 로직을 담고 있는 UserService의 메소드에 Connection 파라미터가 추가되어야 한다.
- Connection 파라미터가 UserDao 인터페이스 메소드에 추가되면 UserDao는 더 이상 액세스 기술에 독립적일 수가 없다.
- DAO 메소드에 Connection 파라미터를 받게 하면 테스트 코드에도 영향을 미친다.
- 5.2.3 트랜잭션 동기화
- Connection 파라미터 제거
- 트랜잭션 동기화: UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다가 사용하게 하는 것
- 트랜잭션 동기화는 작업 스레드미다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스레딩 환경에서도 충돌이 날 염려는 없다.
- 트랜잭션 동기화 적용
- TransactionSynchronizationManager를 이용해 먼저 트랜잭션 동기화 작업을 초기화하도록 요청
- DataSourceUtils에서 제공하는 getConnection() 메소드를 통해 DB 커넥션을 생성
- 작업 진행
- 작업을 정상적으로 마치면 커밋/ 아니면 롤백
- 커넥션을 닫고, 동기화를 마침
- 트랜잭션 테스트 보완
- JdbcTemplate과 트랜잭션 동기화
- 만약 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate이 직접 DB 커넥션을 만들고 트랜잭션을 시작해서 JDBC 작업을 진행한다.
- 반면에 트랜잭션 동기화를 시작해놓았다면 그때부터 실행되는 JdbcTemplate의 메소드에서는 직접 DB 커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어 있는 DB 커넥션을 가져와서 사용한다.
- Connection 파라미터 제거
- 5.2.4 트랜잭션 서비스 추상화
- 기술과 환경에 종속되는 트랜잭션 경계설정 코드
- 하나의 트랜잭션 안에서 어려 개의 DB에 데이터를 넣는 작업을 할 때, 로컬 트랜잭션으로 불가능
- 그래서 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리하는 글로벌 트랜잭션 방식이 필요
- JTA API 사용
- Session을 사용하는 회사도 있기 때문에 문제
- 트랜잭션 API의 의존관계 문제와 해결책
- JDBC에 종속적인 Connection을 이용한 트랜잭션 코드가 UserService에 등장하면서 UserService는 UserDaoJdbc에 간접적으로 의존하는 코드가 되었다.
- 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조이기 때문에 추상화를 통해 해결 가능
- 스프링의 트랜잭션 서비스 추상화
- 트랜잭션 경계설정을 위한 추상 인터페이스인 PlatformTransactionManager 사용
- 트랜잭션 기술 설정의 분리
- 어떤 트랜잭션 매니저 구현 클래스를 사용할지 UserService 코드가 알고 있는 것은 DI 원칙에 위배된다.
- 그래서 컨테이너를 통해 외부에서 제공받게 하는 스프링의 DI 방식 사용
- PlatformTransactionManager은 싱글톤으로 구현해도 안전하기 때문에 빈으로 등록한다.
- 이렇게 하면 다른 트랜잭션을 사용하려고 할 때, 빈의 설정만 고치면 된다.
- 기술과 환경에 종속되는 트랜잭션 경계설정 코드
5.3 서비스 추상화와 단일 책임 원칙
- 수직, 수평 계층구조와 의존관계
- UserService -> UserDao
- TransactionManager -> DataSource
- JDBC, JTA, Connection Pooling, JNDI, WAS, Database …
- 단일 책임 원칙(Single Responsibility Principle)
- 단일 책임 원칙: 하나의 모듈이 한 가지 책임을 가져야 한다
- 단일 책임 원칙의 장점
- 어떤 변경이 필요할 때 수정 대상이 명확해진다.
5.4 메일 서비스 추상화
레벨이 업그레이드되는 사용자에게 안내 메일 발송 요청
- 5.4.1 JavaMail을 이용한 메일 발송 기능
- javax.mail 패키지에서 제공하는 JavaMail 사용
- 5.4.2 JavaMail이 포함되 코드의 테스트
- 메일 발송은 부하가 큰 작업이기 때문에 테스트를 실행할 때마다 메일을 보내면 메일 서버에 상당한 부담을 준다.
- 실제 DB 대신 테스트 DB 사용
- JavaMail API를 통해 요청이 들어간다는 보장만 있다면 굳이 테스트할 때마다 JavaMail을 직접 구동시킬 필요가 없다.
- 5.4.3 테스트를 위한 서비스 추상화
- JavaMail을 이용한 테스트의 문제점
- JavaMail에서는 Session 오브젝트를 만들어야만 메일 메시지를 생성할 수 있고, 메일을 전송할 수 있다. 하지만, final 클래스이기 때문에 바꾸기가 불가능
- 메일 발송 기능 추상화
- MailSender 인터페이스를 이용하여 해결
- 테스트용 메일 발송 오브젝트
- 테스트와 서비스 추상화
- 실전이라면 트랜잭션 코드도 추가해야 한다.
- JavaMail을 이용한 테스트의 문제점
- 5.4.4 테스트 대역
- 의존 오브젝트의 변경을 통한 테스트 방법
- 테스트 대상이 되는 오브젝트가 또 다른 오브젝트에 의존하는 일은 매우 흔하다.
- 테스트 대상인 오브젝트가 의존 오브젝트를 갖고 있기 때문에 간단한 오브젝트를 테스트하는 데 너무 거창한 작업이 뒤따르는 문제점이 있다.
- 테스트 대역의 종류와 특징
- 테스트 대역: 테스트 환경을 만들어주기 위해, 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 오브젝트
- 테스트 스텁: 테스트 대상 오브젝트의 의존객체로서 존재하면서 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것
- 목 오브젝트: 테스트 대상의 간접적인 출력 결과를 검증하고, 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검증할 수 있도록 특별히 설계된 것
- 목 오브젝트를 이용한 테스트
- 테스트 대상인 UserService가 send() 메소드를 통해 자신을 불러서 메일 전송 요청을 보냈을 때 관련 정보를 저장해두는 기능
- 의존 오브젝트의 변경을 통한 테스트 방법
This post is licensed under CC BY 4.0 by the author.