본문 바로가기

Spring

토비의 스프링 #5. 서비스 추상화

5장 서비스추상화

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

비즈니스 로직 추가

  • 사용자의 레벨은 BASIC, SILVER, GOLD 세 가지 중 하나다.
  • 사용자가 처음 가입하면 BASIC 레벨이 되며, 이후 활동에 따라서 한 단계식 업그레이드 될 수 있다.
  • 가입 후 50회 이상 로그인을 하면 BASIC에서 SILVER 레벨이 된다.
  • SILVER 레벨이면서 30번 이상 추천을 받으면 GOLD 레벨이 된다.
  • 사용자 레벨의 변경 작업은 일정한 주기를 가지고 일괄적으로 진행된다. 변경 작업 전에는 조건을 충족하더라도 레벨의 변경이 일어나지 않는다.

5.1.1 필드 추가 (실습 진행)

Level Enum

User 필드 추가

UserDaoTest 테스트 수정

UserDaoJdbc 수정


5.1.2 사용자 수정 기능 추가(실습)

UserDao와 UserDaoJdbc 수정

수정 테스트 보완

  • 현재 update() 테스트는 수정할 로우의 내용이 바뀐 것만 확인할 뿐이지, 수정하지 않아야 할 로우의 내용이 그대로 남아 있는지를 확인해주지 못한다는 문제가 있다. 328p
  • 테스트 보강!!

5.1.3 UserService.upgradeLevels() (실습)

  • UserDao의 getAll() 메소드로 사용자를 다 가져와서 사용자별로 레벨 업그레이드 작업을 진행한다.
  • UserDao의 update()를 호출해 DB에 결과를 넣어준다.

UserService 클래스와 빈 등록

  • UserService 클래스 생성
    • UserDao 인스턴스 변수 선언 및 수정자 메소드로 DI
    • 빈 설정

UserServiceTest 테스트 클래스

upgradeLevels() 메소드

  • 비즈니스 로직 구현
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);
            }
        }
    }

upgradeLevels() 테스트

    @Test
    void upgradeLevels() {
        userDao.deleteAll();
        for(User user: users){
            userDao.add(user);
        }

        userService.upgradeLevels();

        checkLevel(users.get(0), Level.BASIC);
        checkLevel(users.get(1), Level.SILVER);
        checkLevel(users.get(2), Level.SILVER);
        checkLevel(users.get(3), Level.GOLD);
        checkLevel(users.get(4), Level.GOLD);
    }

    private void checkLevel(User user, Level expectedLevel) {
        User userUpdate = userDao.get(user.getId());
        assertThat(userUpdate.getLevel()).isEqualTo(expectedLevel);
    }

5.1.4 UserService.add() (실습)

  • 처음 가입하는 사용자는 기본적으로 BASIC 레벨
    • 비즈니스적인 의미를 지닌 정보!
    • UserService.add()

5.1.5 코드 개선(실습)

upgradeLevels() 메소드 코드의 문제점

  • 추상화의 준위가 다름
  • for 루프 속에 들어 있는 if/elseif/else 블록
  • 여러 로직이 한데 섞여 있음

upgradeLevels() 리팩토링

if(canUpgradeLevel(user))
    upgradeLevel(user)

public boolean canUpgradeLevel(User user) {
        Level currentLevel = user.getLevel();
        switch (currentLevel) {
            case BASIC:
                return (user.getLogin() >= MIN_LOGCOUNT_FOR_SILVER);
            case SILVER:
                return (user.getRecommend() >= MIN_RECOMMEND_FOR_GOLD);
            case GOLD:
                return false;
            default:
                throw new IllegalArgumentException("Unknown Level: " + currentLevel);
        }
    }

public void upgradeLevel(User user) {
        user.upgradeLevel();   //User에게 업그레이드 해달라고 요청
        userDao.update(user);
    }

객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다. 오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리이기도 하다. 343p

User 테스트

UserServiceTest 개선


5.2 트랜잭션 서비스 추상화

정기 사용자 레벨 관리 작업을 수행하는 도중에 네트워크가 끊기거나 서버에 장애가 생겨서 작업을 완료할 수 없다면, 그때까지 변경된 사용자의 레벨은 그대로 둘까요? 아니면 모두 초기 상태로 되돌려 놓아야 할까요? → 초기상태 ㄱㄱ

5.2.1 모 아니면 도 (실습)

  • 현재는 어떤상태일까?
  • 작업 중간에 예외가 발생하도록 테스트를 먼저 작성해보자
  • 어떻게 하면 작업 중간에 예외를 강제로 만들 수 있을까?

테스트용 UserService 대역

  • UserService를 상속, upgradeLevel을 overriding해서 예외발생 로직 추가
  • 현재 5개의 테스트용 사용자 정보 중에서 두 번째와 네 번째가 업그레이드 대상이다. 그렇다면 네 번째 사용자를 처리하는 중에 예외를 발생시키고, 그 전에 처리한 두 번째 사용자의 정보가 취소됐는지, 아니면 그대로 남았는지를 확인하자.
  • 테스트 클래스 내부에 스태틱 클래스 생성
    • static class TestUserService extends UserService{

강제 예외 발생을 통한 테스트

    @Test
    void upgradeAllOrNothing() {
        UserService testUserService = new TestUserService(users.get(3).getId());//터트려라
        testUserService.setUserDao(this.userDao);

        userDao.deleteAll();
        for (User user : users) {
            userDao.add(user);
        }

        try {
            testUserService.upgradeLevels();
            fail("TestUserServiceException expected");
        } catch (TestUserServiceException e) {
            System.out.println(e);
        }
        checkLevelUpgraded(users.get(1), false); //롤백이 되었는지?
    }

결과

테스트 실패의 원인

  • 바로 트랜잭션 문제다!
  • 모든 사용자의 레벨을 업그레이드하는 작업인 upgradeLevels() 메소드가 하나의 트랜잭션 안에서 동작하지 않았기 때문이다.

모든 사용자에 대한 레벨 업그레이드 작업은 새로 추가된 기술 요구사항대로 전체가 다 성공하든지 아니면 전체가 다 실패하든지 해야 한다. 레벨 업그레이드 작업은 그 작업을 쪼개서 부분적으로는 성공하기도 하고, 여러 번에 걸쳐서 진행할 수 있는 작업이 아니어야 한다. 더 이상은 쪼개질 수 없는 물질이라는 의미로 이름을 붙인 원자와 마찬가지로, 이 작업도 더 이상 쪼개서 이뤄질 수 없는 원자와 같은 성질을 띤다. 따라서 중간에 예외가 발생해서 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 초기 상태로 돌려놔야 한다. 이것이 바로 트랜잭션이다. 353p


5.2.2 트랜잭션 경계설정

  • 용어 354p
    • 트랜잭션 롤백
    • 트랜잭션 커밋

JDBC 트랜잭션의 트랜잭션 경계설정

  • JDBC의 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에 일어난다.
  • JDBC에서 트랜잭션을 시작하려면 자동커밋 옵션을 false로 만들어주면 된다.
  • JDBC 기본 설정은 DB 작업을 수행한 직후에 자동 커밋
  • 트랜잭션 경계설정(transaction demarcation) : setAutoCommit(false)로 트랜잭션의 시작을 선언하고 commit() 또는 rollback()으로 트랜잭션을 종료하는 작업
  • setAutocommit(false) 로 설정해주면 트랜잭션이 한 번 시작되면 commit() 또는 rollback() 메소드가 호출될 때까지의 작업이 하나의 트랜잭션으로 묶인다.
  • 로컬 트랜잭션 : 하나의 DB 커넥션 안에서 만들어지는 트랜잭션

UserService와 UserDao의 트랜잭션 문제

  • UserService의 upgradeLevels()에는 왜 트랜잭션이 적용되지 않았을까?
    • 트랜잭션 경계설정 코드가 존재하지 않았기 때문
  • 기존의 UserDao는 JdbcTemplate을 통해 매번 새로운 DB 커넥션과 트랜잭션을 만들어 사용한다. 356p

  • upgradeLevels()와 같이 여러 번 DB에 업데이트를 해야 하는 작업을 하나의 트랜잭션으로 만들려면 어떻게 해야 할까?

비즈니스 로직 내의 트랜잭션 경계설정

  • DAO 메소드 안으로 upgradeLevels 메소드의 내용을 옮기는 방법
    • 비즈니스 + 데이터 로직 == ?
  • 결국 트랜잭션의 경계설정 작업을 UserService 쪽으로 가져와야 한다.
  • UserService에서 DB Connection 생성
    • Connection 오브젝트를 오브젝트 파라미터로 전달

UserService 트랜잭션 경계설정의 문제점

  • DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더 이상 활용할 수 없다.
  • DAO의 메소드와 비즈니스 로직을 담고 있는 UserService의 메소드에 Connection 파라미터가 추가돼야 한다.
  • 데이터 액세스 기술에 더 이상 독립적일 수가 없다. (Connection 말고 다른 방식을 쓰면? ex) Hibernate)
  • 테스트 코드에도 영향을 미친다.

5.2.3 트랜잭션 동기화 (실습)

Connection 파라미터 제거

  • 스프링이 제안하는 방법!! 독립적인 트랜잭션 동기화 방식
    • 트랜잭션 동기화란? UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다가 사용하게 하는 것이다.

  • 361p 설명 참조

트랜잭션 동기화 적용

    public void upgradeLevels() throws SQLException {
        TransactionSynchronizationManager.initSynchronization(); 
        //트랜잭션 동기화 관리자를 이용해 동기화 작업 초기화
        Connection c = DataSourceUtils.getConnection(dataSource);
        //DB 커넥션을 생성하고 트랜잭션 시작, 이후의 DAO 작업은 트랜잭션 안에서 진행
        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();
        }
    }
  • TransactionSynchronizationManager : 스프링이 제공하는 트랜잭션 동기화 관리 클래스
  • DataSourceUtils : getConnection() 메소드로 Connection 생성 및 동기화 저장소에 바인딩

트랜잭션 테스트 보완

  • 이제 테스트를 해보자!

JdbcTemplate과 트랜잭션 동기화

  • JdbcTemplate은 영리하게 동작한다.
    • 커넥션이나 트랜잭션이 없으면 생성, 있으면 사용

5.2.4 트랜잭션 서비스 추상화

기술과 환경에 종속되는 트랜잭션 경계설정 코드

  • 여러 DB를 동시에 사용한다면?
    • 글로벌 트랜잭션 방식 사용 (여러 DB 작업을 하나의 트랜잭션으로 만들 수 있다.)
    • 자바는 글로벌 트랜잭션 방식을 지원하는 API인 JTA (Java Transaction API)를 제공
  • JDBC 로컬 트랜잭션을 JTA를 이용하는 글로벌 트랜잭션으로 바꾸려면 UserService의 코드를 수정해야 한다.
    • 로컬 트랜잭션을 사용하면 충분한 고객을 위해서는 JDBC를 이용한 트랜잭션 관리 코드를, 다중 DB를 위한 글로벌 트랜잭션을 필요로 하는 곳을 위해서는 JTA를 이용한 트랜잭션 관리 코드를 적용해야 한다는 문제가 발생
    • 하이버네이트를 이용하려고 한다면? Connection 대신 Session을 사용 + 독자적인 트랜잭션 관리 API 사용

트랜잭션 API의 의존관계 문제와 해결책

  • UserDao가 DAO패턴을 사용해 구현 데이터 액세스 기술을 유연하게 바꿔서 사용할 수 있게 했지만 UserService에서 트랜잭션 경계설정을 해야 할 필요가 생기면서 다시 특정 데이터 액세스 기술에 종속되는 구조가 되고 말았다.

  • UserService의 코드가 특정 트랜잭션 방법에 의존적이지 않고 독립적일 수 있게 만들려면 어떻게 해야할까?
  • 다행히도 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조다. 이렇게 여러 기술의 사용 방법에 공통점이 있다면 추상화를 생각해볼 수 있다. 추상화란 하위 시스템의 공통점을 뽑아내서 분리시키는 것을 말한다. 그렇게 하면 하위 시스템이 어떤 것인지 알지 못해도, 또는 하위 시스템이 바뀌더라도 일관된 방법으로 접근할 수 있다.

스프링의 트랜잭션 서비스 추상화 370 ~ 371p

  • 스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스는 PlatformTransactionManager다.
   private PlatformTransactionManager transactionManager;

   public void upgradeLevels() throws SQLException {
        TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            List<User> users = userDao.getAll();
            for (User user : users) {
                if (canUpgradeLevel(user))
                    upgradeLevel(user);
            }
            this.transactionManager.commit(status);
        } catch (Exception e) {
            this.transactionManager.rollback(status);
            throw e;
        }
    }
  • PlatformTransactionManager에서는 트랜잭션을 가져오는 요청인 getTransaction() 메소드를 호출하기만 하면 된다. 필요에 따라 트랜잭션 매니저가 DB 커넥션을 가져오는 작업도 같이 수행하기 때문이다.
  • 시작된 트랜잭션은 TransactionStatus 타입의 변수에 저장된다. TransactionStatus는 트랜잭션에 대한 조작이 필요할 때 PlatformTransactionManager메소드의 파라미터로 전달해주면 된다.

트랜잭션 기술 설정의 분리 & DI

5.3 서비스 추상화와 단일 책임 원칙

수직, 수평 계층구조와 의존관계

  • 애플리케이션 계층 UserService와 UserDao의 낮은 결합도 (수평 : 로직의 종류에 따른 구분)
    • UserDao는 데이터 액세스 로직
    • UserService는 비즈니스 로직
  • UserDao와 DataSource의 낮은 결합도 (수직 : 로직과 기술의 구분)
  • UserService와 TransactionManager의 낮은 결합도(수직 : 로직과 기술의 구분)
  • 애플리케이션 로직의 종류에 따른 수평적인 구분이든, 로직과 기술이라는 수직적인 구분이든 모두 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들 수 있는 데는 스프링의 DI가 중요한 역할을 하고 있다.

단일 책임 원칙

  • 모든 클래스는 하나의 책임만 가진다.
  • 다른 말로 바꾸면, 하나의 모듈이 바뀌는 이유는 한 가지여야 한다.
  • UserService에 JDBC Connection의 메소드를 직접 사용하는 트랜잭션 코드가 들어 있었을 때를 생각해보자. UserService 코드가 수정되는 이유가 두 가지다.
    • 사용자 레벨을 관리
    • 트랜잭션 관리

단일 책임 원칙의 장점

  • 어떤 변경이 필요할 때 수정 대상이 명확해진다.

객체지향 설계와 프로그래밍 원칙은 서로 긴밀하게 관련이 있다. 단일 책임 원칙을 잘 지키는 코드를 만들려면 인터페이스를 도입하고 이를 DI로 연결해야 하며, 그 결과로 단일 책임 원칙뿐 아니라 개방 폐쇄 원칙도 잘 지키고, 모듈 간에 결합도가 낮아서 서로의 변경이 영향을 주지 않고, 같은 이유로 변경이 단일 책임에 집중되는 응집도 높은 코드가 나온다.

  • 더욱 편리하게 자동화된 테스트를 만들 수 있다. 378 ~ 379p

5.4 메일 서비스 추상화

  • 레벨이 업그레이드되는 사용자에게 안내 메일을 발송하자!

5.4.1 JavaMail을 이용한 메일 발송 기능(실습)

  • 필드 추가, 기존 메소드 수정만 진행
  • JavaMail 발송 메소드 구현 x

5.4.2 JavaMail이 포함된 코드의 테스트

  • 어떻게 테스트를 할 수 있을까?
    • 메일 서버가 준비되어 있지 않다면?
    • 과연 테스트를 하면서 매번 메일이 발송되는 것이 바람직한가?
      • 메일 발송이란 매우 부하가 큰 작업이다.
  • 실제 메일서버를 사용하지 않고 테스트 메일서버를 사용

  • 더 나아가 실제 JavaMail을 사용하지 않고 테스트용 JavaMail을 사용하자

5.4.3 테스트를 위한 서비스 추상화

JavaMail을 이용한 테스트의 문제점

  • 테스트용으로 바꿔낄 수가 없다 ㅠㅠ
  • JavaMail의 핵심 API에는 DataSource처럼 인터페이스로 만들어져서 구현을 바꿀 수 있는게 없다.
Session s = Session.getInstance(props, null);
  • JavaMail에서는 Session 오브젝트를 만들어야만 메일 메시지를 생성할 수 있다. 그런데 이 Session은 인터페이스가 아니고 클래스다. 게다가 생성자가 모두 private으로 되어 있어서 직접 생성도 불가능하다.
  • 스태틱 팩토리 메소드를 이용해 오브젝트를 만드는 방법밖에없다.
  • 게다가 Session은 더 이상 상속이 불가능한 final 클래스다.
  • JavaMail의 구현을 테스트용으로 바꿔치기 불가능..

메일 발송 기능 추상화

  • MailSender를 사용하자 : 스프링이 제공하는 메일 서비스 추상화의 핵심 인터페이스
public interface MailSender {

    void send(SimpleMailMessage simpleMessage) throws MailException;
    void send(SimpleMailMessage[] simpleMessages) throws MailException;
}
private void sendUpgradeEmail(User user) {

        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setTo(user.getEmail());
        mailMessage.setFrom("useradmin@kiworkshop.com");
        mailMessage.setSubject("Upgrade 안내");
        mailMessage.setText("사용자님의 등급이 " + user.getLevel().name());

        this.mailSender.send(mailMessage); // JavaMailSenderImpl
    }
  • JavaMailSenderImpl은 내부적으로 JavaMail API를 이용해 메일을 전송해준다.

테스트용 메일 발송 오브젝트

  • 우리가 원하는 건 JavaMail을 사용하지 않고, 메일 발송 기능이 포함된 코드를 테스트하는 것이다. 이를 위해 메일 전송 기능을 추상화해서 인터페이스를 적용하고 DI를 통해 빈으로 분리해놨다!!
  • DummyMailSender를 구현해서 갈아껴보자
public class DummyMailSender implements MailSender {
    @Override
    public void send(SimpleMailMessage simpleMessage) throws MailException {
    }
    @Override
    public void send(SimpleMailMessage[] simpleMessages) throws MailException {
    }
}

테스트와 서비스 추상화

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

  • TODO : 메일 발송 기능에 트랜잭션 적용
    • 모아뒀다가 업그레이드 작업이 모두 성공적으로 끝났을 때 한번에

5.4.4 테스트 대역

의존 오브젝트의 변경을 통한 테스트 방법

  • UserDao 테스트 구조

  • UserService 테스트 구조

  • 테스트 대상이 되는 오브젝트가 또 다른 오브젝트에 의존하는 일은 매우 흔하다.
  • 이렇게 의존하기 때문에 발생하는 문제점
    • 간단한 오브젝트의 코드를 테스트하는 데 너무 거창한 작업이 뒤따르는 경우 ex) UserDao 뒤에 운영환경의 DB Pool
  • 이럴 때 테스트를 위한 오브젝트로 교체해보자

테스트 대역의 종류와 특징

  • 테스트 대역 : 테스트 대상인 오브젝트의 의존 오브젝트, 테스트 환경을 만들어 주기 위해, 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 오브젝트
  • 테스트 스텁 : 대표적인 테스트 대역, 테스트 대상 오브젝트의 의존객체로서 존재하면서 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것 ex) DummyMailSender
  • 목 오브젝트 : 테스트 대상의 간접적인 출력 결과를 검증하고, 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나느 일을 검증할 수 있도록 특별히 설계된 오브젝트
    • 테스트 오브젝트가 정상적으로 실행되도록 도와주면서, 테스트 오브젝트와 자신의 사이에 일어나는 커뮤니케이션 내용을 저장해뒀다가 테스트 결과를 검증하는 데 활용할 수 있게 해준다.

목 오브젝트를 이용한 테스트(실습)


5.5 정리

  • 비즈니스 로직을 담은 코드는 데이터 액세스 로직을 담은 코드와 깔끔하게 분리되는 것이 바람직하다. 비즈니스 로직 코드 또한 내부적으로 책임과 역할에 따라서 깔끔하게 메소드로 정리돼야 한다.
  • 이를 위해서는 DAO의 기술 변화에 서비스 계층의 코드가 영향을 받지 않도록 인터페이스와 DI를 잘 활용해서 결합도를 낮춰야 한다.
  • DAO를 사용하는 비즈니스 로직에는 단일 작업을 보장해주는 트랜잭션이 필요하다.
  • 트랜잭션의 시작과 종료를 지정하는 일을 트랜잭션 경계설정이라고 한다. 트랜잭션 경계설정은 주로 비즈니스 로직 안에서 일어나는 경우가 많다.
  • 시작된 트랜잭션 정보를 담은 오브젝트를 파라미터로 DAO에 전달하는 방법은 매우 비효율적이기 때문에 스프링이 제공하는 트랜잭션 동기화 기법을 활용하는 것이 편리하다.
  • 자바에서 사용되는 트랜잭션 API의 종류와 방법은 다양하다. 환경과 서버에 따라서 트랜잭션 방법이 변경되면 경계설정 코드도 함께 변경돼야 한다.
  • 트랜잭션 방법에 따라 비즈니스 로직을 담은 코드가 함께 변경되면 단일 책임 원칙에 위배되며, DAO가 사용하는 특정 기술에 대해 강한 결합을 만들어낸다.
  • 트랜잭션 경계설정 코드가 비즈니스 로직 코드에 영향을 주지 않게 하려면 스프링이 제공하는 트랜잭션 서비스 추상화를 이용하면 된다.
  • 서비스 추상화는 로우레벨의 트랜잭션 기술과 API의 변화에 상관없이 일관된 API를 가진 추상화 계층을 도입한다.
  • 서비스 추상화는 테스트하기 어려운 JavaMail 같은 기술에도 적용할 수 있다. 테스트를 편리하게 작성하도록 도와주는 것만으로도 서비스 추상화는 가치가 있다.
  • 테스트 대상이 사용하는 의존 오브젝트를 대체할 수 있도록 만든 오브젝트를 테스트 대역이라고 한다.
  • 테스트 대역은 테스트 대상 오브젝트가 원활하게 동작할 수 있도록 도우면서 테스트를 위해 간접적인 정보를 제공해주기도 한다.
  • 테스트 대역 중에서 테스트 대상으로부터 전달받은 정보를 검증할 수 있도록 설계된 것을 목 오브젝트라고 한다.