5장 서비스 추상화

비즈니스 로직을 담은 UserService 클래스를 만들고

트랜잭션을 적용하면서 스프링의 서비스 추상화에 대해 살펴보기

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

  • Level 이늄
  • User 필드 추가
  • UserDaoTest, UserDaoJdbc 수정
  • 사용자 수정 기능 추가
  • ...

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

UserServiceTest

UserService

UserDao

UserDaoJdbc

UserDaoJpa

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

public void upgradeLevels() {
    List<User> users = userDao.getAll();
    for(User user : users) {
        Boolean changed = null;
        if (user.getLevel() == Level.BASIC && user.getLogin() >= 50) {
            user.setLevel(Level.SILVER);
            changed = true;
        } else if (user.getLevel() == Level.SILVER && user.getRecommend() >= 30) {
            user.setLevel(Level.GOLD);
            changed = true;
        } else if (user.getLevel() == Level.GOLD) {
            changed = false;
        } else {
            changed = false;
        }

        if (changed) { userDao.update(user);
    }
}

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

  • 코드에 중복된 부분은 없는가?
  • 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가?
  • 코드가 자신이 있어야 할 자리에 있는가?
  • 앞으로 변경이 일어난다면 어떤 것이 있을 수 있고, 그 변화에 쉽게 대응할 수 있게 작성되어 있는가?

337 page 참고

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

public void upgradeLevels() {
    List<User> users = userDao.getAll();
    for(User user : users) {
        if (canUpgradeLevel(user)) {
            upgradeLevel(user);
        }
    }
}

private boolean canUpgradeLevel(User user) {
    Level currentLevel = user.getLevel();
    switch(currentLevel) {
        case BASIC: return (user.getLogin() >= 50);
        case SILVER: return (user.getRecomment() >= 30);
        case GOLD: return false;
        default: throw new IllegalArgumentException("Unknown Level : " + currentLevel);
    }
}

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

private void upgradeLevel(User user) {
    // 객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신
    // 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다.
    user.upgradeLevel();
    userDao.update(user);
}

// User의 레벨 업그레이드 작업용 메소드
public void upgradeLevel() {
    Level nextLevel = this.level.nextLevel();
    if (nextLevel == null) {
        throw new IllegalStateException(this.level + "은 업그레이드가 불가능합니다.");
    } else {
        this.level = nextLevel;
    }
}

정리

  • 비즈니스 로직을 담은 코드는 데이터 액세스 로직을 담은 코드와 깔끔하게 분리되는 것이 바람직하다. 비즈니스 로직 또한 내부적으로 책임과 역할에 따라서 깔끔하게 메소드로 정리돼야 한다.
  • 이를 위해서는 DAO의 기술 변화에 서비스 계층의 코드가 영향을 받지 않도록 인터페이스와 DI를 잘 활용해서 결합도를 낮춰줘야 한다.

5.2 트랜잭션 서비스 추상화

  • "사용자 레벨 조정 작업은 중간에 문제가 발생해서 작업이 중단되면 그때까지 진행된 변경 작업도 모두 취소시키자."
  • 레벨 업그레이드 작업은 하나의 트랜잭션 안에서 동작해야 한다.
    • 트랜잭션 : 더 이상 나눌 수 없는 단위 작업

5.2 트랜잭션 서비스 추상화

  • 현재 상태 문제점
    • UserDao 각 메소드마다 하나씩의 독립적인 트랜잭션으로 실행된다.
    • UserService에서 DB 커넥션을 다룰 수 있는 방법이 없다.
  • DAO 메소드 안으로 upgradeLevels()  내용을 옮길까? (X)
  • 트랜잭션의 경계설정 작업을 UserService 쪽으로 가져와야 한다.
public void upgradeLevels() throws Exception {
    // (1) DB Connection 생성
    // (2) 트랜잭션 시작
    try {
        // (3) DAO 메소드 호출
        // (4) 트랜잭션 커밋
    }
    catch (Exception e) {
        // (5) 트랜잭션 롤백
        throw e;
    }
    finally {
        // (6) DB Connection 종료
    }
}

5.2 트랜잭션 서비스 추상화

Connection 오브젝트를 파라미터로 전달해줘야 한다.

359 페이지 참고

5.2 트랜잭션 서비스 추상화

  • 문제점
    • DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더 이상 활용할 수 없다.
    • UserService의 메소드에 Connection 파라미터가 추가돼야 한다. (Connection 오브젝트가 계속해서 전달돼야 한다.)
    • UserDao는 더 이상 데이터 액세스 기술에 독립적일 수가 없다.
    • 테스트 코드에도 영향을 미친다.

5.2 트랜잭션 서비스 추상화

  • 스프링이 제안하는 방법은 트랜잭션 동기화
    • UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다가 사용하게 하는 방법
    • 361페이지 참고
public void upgradeLevels() throws Exception {
    TransactionSynchronizationManager.initSynchronization();
    Connection c = DataSourceUtils.getConnection(dataSource);
    c.setAutoCommit(false);
    
    try {
        List<User> users = userDao.getAll();
        for (User user : users) {
            if (canUpgradeLevel(user)) {
                upgradeLevel(user);    
            }
        }
        c.commit();
    } catch (Exception e) {
        c.rollback();
        throw e;
    } finally {
        DataSourceUtils.releaseConnection(c, dataSource);
        TransactionSynchronizationManager.unbindResource(this.dataSource);
        TransactionSynchronizationManager.clearSynchronization();
    }
}

5.2 트랜잭션 서비스 추상화

정리

  • DAO를 사용하는 비즈니스 로직에는 단위 작업을 보장해주는 트랜잭션이 필요하다.
  • 트랜잭션의 시작과 종료를 지정하는 일을 트랜잭션 경계설정이라고 한다. 트랜잭션 경계설정은 주로 비즈니스 로직 안에서 일어나는 경우가 많다.
  • 시작된 트랜잭션 정보를 담은 오브젝트를 파라미터로 DAO에 전달하는 방법은 매우 비효율적이기 때문에 스프링이 제공하는 트랜잭션 동기화 기법을 활용하는 것이 편리하다.

5.2 트랜잭션 서비스 추상화

  • 새로운 문제 발생
    • 여러개의 DB를 사용하고 있을 경우, JDBC의 Connection을 이용한 트랜잭션 방식인 로컬 트랜잭션으로는 불가능하다.
  • 자바는 JDBC외에 이런 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위한 API인 JTA를 제공하고 있다.
  • 해결 방안
    • JDBC API가 아닌 JTA를 사용해 트랜잭션을 관리한다.

5.2 트랜잭션 서비스 추상화

  • 또 새로운 문제 발생
    • JDBC를 이용한 트랜잭션 관리 코드, JTA를 이용한 트랜잭션 관리 코드를 필요에 따라 다르게 적용해야 한다.
    • => UserService는 자신의 로직이 바뀌지 않았음에도 기술환경에 따라서 코드가 바뀌는 코드가 돼버림
    • 하이버네이트를 이용한 트랜잭션 관리 코드는 JDBC나 JTA의 코드와는 또 다르다.

5.2 트랜잭션 서비스 추상화

  • 다행히도 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조다.
    • => 추상화를 생각해볼 수 있다.
  • 추상화 : 하위 시스템의 공통점을 뽑아내서 분리시키는 것
// 스프링의 트랜잭션 추상화 API를 적용한 upgradeLevels()
public void upgradeLevels() {
    PlatformTansactionManager transactionManager =
        new DataSourceTransactionManager(datasource);
    TransactionStatus status =
        transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        List<User> users = userDao.getAll();
        for (User user : users) {
            if (canUpgradeLevel(user)) {
                upgradeLevel(user);
            }
        }
        transactionManager.commit(status);
    } catch (RuntimeException e) [
        transactionManager.rollback(status);
        throw e;
    }
}

5.2 트랜잭션 서비스 추상화

정리

  • 자바에서 사용되는 트랜잭션 API의 종류와 방법은 다양하다. 환경과 서버에 따라서 트랜잭션 방법이 변경되면 경계설정 코드도 함께 변경돼야 한다.
  • 트랜잭션 방법에 따라 비즈니스 로직을 담은 코드가 함께 변경되면 단일 책임 원칙에 위배되며, DAO가 사용하는 특정 기술에 대해 강한 결합을 만들어낸다.
  • 트랜잭션 경계설정 코드가 비즈니스 로직 코드에 영향을 주지 않게 하려면 스프링이 제공하는 트랜잭션 서비스 추상화를 이용하면 된다.
  • 서비스 추상화는 로우레벨의 트랜잭션 기술과 API의 변화에 상관없이 일관된 API를 가진 추상화 계층을 도입한다.

5.4 메일 서비스 추상화

  • 새로운 요청사항 : 레벨이 업그레이드되는 사용자에게는 안내 메일을 발송하자.
  • JavaMail은 자바의 표준 기술이고 이미 수많은 시스템에 사용돼서 검증된 안정적인 모듈이다. 따라서 JavaMail API를 통해 요청이 들어간다는 보장만 있다면 굳이 테스트 할 때마다 JavaMail을 직접 구동시킬 필요가 없다.
  • 스프링이 제공하는 메일 서비스 추상화 인터페이스 활용

정리

  • 서비스 추상화는 테스트하기 어려운 JavaMail 같은 기술에도 적용할 수 있다. 테스트를 편리하게 작성하도록 도와주는 것만으로도 서비스 추상화는 가치가 있다.

5.4 메일 서비스 추상화

  • 테스트 대상 오브젝트의 메소드가 돌려주는 결과뿐 아니라 테스트 오브젝트가 간접적으로 의존 오브젝트가 넘기는 값과 그 행위 자체에 대해서도 검증하고 싶다면?
    • 목 오브젝트를 사용
  • 목 오브젝트는 스텁처럼 테스트 오브젝트가 정상적으로 실행되도록 도와주면서, 테스트 오브젝트와 자신의 사이에서 일어나는 커뮤니케이션 내용을 저장해뒀다가 테스트 결과를 검증하는 데 활용할 수 있게 해준다.

5.4 메일 서비스 추상화

public class DummyMailSender implements MailSender {
    public void send(SimpleMailMessage mailMessage) throws MailException {
    }

    public void send(SimpleMailMessage[] mailMessage) throws MailException {
    }
}

static class MockMailSender implements MailSender {
    private List<String> requests = new ArrayList<String>();
    
    public List<String> getRequests() }
        return requests;
    }

    public void send(SimpleMailMessage mailMessage) throws MailException {
        requests.add(mailMessage.getTo()[0]);
    }

    public void send(SimpleMailMessage[] mailMessage) throws MailException {
    }

}

정리

  • 테스트 대상이 사용하는 의존 오브젝트를 대체할 수 있도록 만든 오브젝트를 테스트 대역이라고 한다.
  • 테스트 대역은 테스트 대상 오브젝트가 원활하게 동작할 수 있도록 도우면서 테스트를 위해 간접적인 정보를 제공해주기도 한다.
  • 테스트 대역 중에서 테스트 대상으로부터 전달받은 정보를 검증할 수 있도록 설계된 것을 목 오브젝트라고 한다.
Made with Slides.com