Post

토비의 스프링 5장

5. 서비스 추상화

5.1 사용자 레벨 관리 기능 추가

  • 5.1.1 필드 추가
    • Level 이늄(enum)
      • 각 레벨을 코드화해서 숫자로 넣음(범위가 작은 숫자로 관리하면 DB용량도 많이 차지하지 않고 가벼워서 좋다.)
      • 하지만 다른 종류의 정보를 넣는 실수를 해도 컴파일러가 체크해주지 못한다는 단점이 있다.
      • 또한, 범위를 벗어나는 값을 넣을 위험이 있다.
      • 그래서 enum 사용
  • 5.1.2 사용자 수정 기능 추가
    • 수정 테스트 보완
      • 현재는 update에 where이 빠져 있어도 성공한다.
      • 현재 update() 테스트는 수정할 로우의 내용이 바뀐 것만 확인할 뿐, 수정하지 않아야 할 로우의 내용이 그대로 남아 있는지는 확인해주지 못한다는 문제가 있다.
      • 해결방법
        1. update()가 돌려주는 리턴 값이 1인지 확인
        2. 사용자를 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 트랜잭션 경계설정의 문제점
      1. DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했뎐 JdbcTemplate을 활용할 수 없다.
      2. DAO의 메소드와 비즈니스 로직을 담고 있는 UserService의 메소드에 Connection 파라미터가 추가되어야 한다.
      3. Connection 파라미터가 UserDao 인터페이스 메소드에 추가되면 UserDao는 더 이상 액세스 기술에 독립적일 수가 없다.
      4. DAO 메소드에 Connection 파라미터를 받게 하면 테스트 코드에도 영향을 미친다.
  • 5.2.3 트랜잭션 동기화
    • Connection 파라미터 제거
      • 트랜잭션 동기화: UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다가 사용하게 하는 것
      • 트랜잭션 동기화는 작업 스레드미다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스레딩 환경에서도 충돌이 날 염려는 없다.
    • 트랜잭션 동기화 적용
      1. TransactionSynchronizationManager를 이용해 먼저 트랜잭션 동기화 작업을 초기화하도록 요청
      2. DataSourceUtils에서 제공하는 getConnection() 메소드를 통해 DB 커넥션을 생성
      3. 작업 진행
      4. 작업을 정상적으로 마치면 커밋/ 아니면 롤백
      5. 커넥션을 닫고, 동기화를 마침
    • 트랜잭션 테스트 보완
    • JdbcTemplate과 트랜잭션 동기화
      • 만약 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate이 직접 DB 커넥션을 만들고 트랜잭션을 시작해서 JDBC 작업을 진행한다.
      • 반면에 트랜잭션 동기화를 시작해놓았다면 그때부터 실행되는 JdbcTemplate의 메소드에서는 직접 DB 커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어 있는 DB 커넥션을 가져와서 사용한다.
  • 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 인터페이스를 이용하여 해결
    • 테스트용 메일 발송 오브젝트
    • 테스트와 서비스 추상화
      • 실전이라면 트랜잭션 코드도 추가해야 한다.
  • 5.4.4 테스트 대역
    • 의존 오브젝트의 변경을 통한 테스트 방법
      • 테스트 대상이 되는 오브젝트가 또 다른 오브젝트에 의존하는 일은 매우 흔하다.
      • 테스트 대상인 오브젝트가 의존 오브젝트를 갖고 있기 때문에 간단한 오브젝트를 테스트하는 데 너무 거창한 작업이 뒤따르는 문제점이 있다.
    • 테스트 대역의 종류와 특징
      • 테스트 대역: 테스트 환경을 만들어주기 위해, 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 오브젝트
      • 테스트 스텁: 테스트 대상 오브젝트의 의존객체로서 존재하면서 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것
      • 목 오브젝트: 테스트 대상의 간접적인 출력 결과를 검증하고, 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검증할 수 있도록 특별히 설계된 것
    • 목 오브젝트를 이용한 테스트
      • 테스트 대상인 UserService가 send() 메소드를 통해 자신을 불러서 메일 전송 요청을 보냈을 때 관련 정보를 저장해두는 기능
This post is licensed under CC BY 4.0 by the author.