이 글은 “토비의 스프링” 책 내용을 정리한 글입니다.

만약 저작권 관련 문제가 있다면 “gunjuko92@gmail.com”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.

서비스 추상화

  • 자바에는 표준 스펙, 상용 제품, 오픈소스를 통틀어서 사용방법과 형식은 다르지만 기능과 목적이 유사한 기술이 존재한다.
  • 환경과 상황에 따라서 기술이 바뀌고, 그에 따라 다른 API를 사용하고 다른 스타일의 접근 방법을 따라야 한다는 건 매우 피곤한 일이다.
  • 스프링이 어떻게 성격이 비슷한 여러 종류의 기술을 추상화하고 이를 일관된 방법으로 사용할 수 있도록 지원하는지를 살펴본다.

1. 사용자 레벨 관리 기능 추가

  • UserDao에 비즈니스 로직을 추가해보자
  • 정기적으로 사용자의 활동내역을 참고해서 레벨을 조정해주는 기능
    • 사용자의 레벨은 BASIC, SILVER, GOLD 세가지 중 하나이다.
    • 사용자가 처음 가입하면 BASIC 레벨, 이후 활동에 따라 한 단계씩 업그레이드된다.
    • 가입 후 50회 이상 로그인시 BASIC 레벨에서 SILVER 레벨이 된다.
    • SILVER 레벨이면서 30번 이상 추천을 받으면 GOLD 레벨이 된다.
    • 사용자 레벨의 변경작업은 일정한 주기를 가지고 일괄적으로 진행됨, 변경 작업 전에는 조건을 충족하더라도 레벨의 변경이 일어나지 않음

1.1 필드 추가

Level Enum

  • User 클래스에 사용자의 레벨을 저장할 필드를 추가한다.
    • DB : 각 레벨을 코드화해서 숫자로 넣는다. 범위가 작은 숫자로 관리하면 DB 용량도 만히 차지하지 않고 가볍다.
    • User : User에 추가할 프로퍼티 타입도 숫자로 하면 될까? 이건 별로 좋지 않다. 의미 없는 숫자를 프로퍼티에 사용하면 타입이 안전하지 않아서 위험할 수 있기 때문이다.
    • 숫자 타입을 직접 사용하는 것보다는 자바 5 이상에서 제공하는 이늄(enum)을이용하는 게 안전하고 편리하다.
public enum Level {
    BASIC(1), SILVER(2), GOLD(3);

    private final int value;

    Level(int value) {
        this.value = value;
    }

    public int intValue() {
        return value;
    }

    public static Level valueOf(int value) {
        switch (value) {
            case 1: return BASIC;
            case 2: return SILVER;
            case 3: return GOLD;
            default: throw new AssertionError("Unknown value" + value);
        }
    }
}

User 필드 추가

  • Level 타입의 변수 추가 및 로그인 횟수와 추천수도 추가
public class User {
    ...
    Level level;
    int login;
    int recommend;
    
}
  • DB의 유저 테이블에도 필드를 추가한다.

테스트 수정

  • 새롭게 추가된 필드에 맞게 테스트 코드를 수정한다.
  • 테스트를 먼저 수정했으니 테스트가 실패한다.

UserDaoJdbc 수정

  • 테스트가 성공하도록 UserDaoJdbc를 수정한다.
  • UserDaoJdbc를 수정하는 과정에서 실수가 나오더라도, UserDaoTest 덕분에 빠르게 버그를 찾을수 있다.
  • 빠르게 실행 가능한 포괄적인 테스트를 만들어두면 이렇게 기능의 추가나 수정이 일어날 때 그 위력을 발휘한다.

1.2 사용자 수정 기능 추가

수정할 정보가 담긴 User 오브젝트를 전달하면 id를 참고해서 사용자를 찾아 필드 정보를 UPDATE 문을 이용 해 모두 변경해주는 메소드를 하나 추가한다.

수정 기능 테스트 추가

@Test
public void update() {
    dao.deleteAll();

    dao.add(user1);
    dao.add(user2);

    user1.setName("오민규");
    user1.setPassword("springno6");
    user1.setLevel(Level.GOLD);
    user1.setLogin(1000);
    user1.setRecommend(999);
    dao.update(user1);

    User user1update = dao.get(user1.getId());
    checkSameUser(user1, user1update);
}
  • 픽스처 오브젝트를 하나 등록한다.
  • id를 제외한 필드를 변경한 뒤에 update()를 호출한다.
  • 다시 id로 조회해서 가져온 오브젝트와 수정한 픽스처 오브젝트를 비교한다.

UserDao와 UserDaoJdbc 수정

  • dao 변수의 타입인 UserDao 인터페이스에 update( ) 메소드가 없다는 컴파일 에러가 날 것이다. 따라서 update() 메소드를 추가한다.
  • UserDao 인터페이스에 update( )를 추가하고 나면 이번엔 UserDaoJdbc에서 메소드를 구현하지 않았다고 에러가 날 것이다. IDE에서 지원하는, 구현되지 않은 메소드를 자동 추가히는 기능을 사용하면 편리하다.
@Override
public void update(User user) {
    this.jdbcTemplate.update(
            "update users set name = ?, password = ?, level = ?, login = ?, " +
                    "recommend = ? where id = ? ", user.getName(), user.getPassword(),
            user.getLevel().intValue(), user.getLogin(), user.getRecommend(), user.getId());
}

IDE의 자동수정 기능과 테스트 코드 작성

  • 대부분의 자바 IDE에는 컴파일 에러에 대한 자동수정 기능이 있다.
  • 테스트를 먼저 만들면, 아직 준비되지 않은 클래스나 메소드를 테스트 코드 내에서 먼저 사용하는 경우가 있다. 이 경우 IDE의 자바 코드 에디터에서 에러가 표시되면 자동고침 기능을 이용해 클래스나 메소드를 생성하도록 만들면 매우 편리하다.

수정 테스트 보안

  • update의 WHERE 절에 대한 검증이 되진 않는다. 실제로 update문에서 WHERE절을 빼고 테스트를 돌려보면 테스트가 성공한다. 이는 테스트에 결함이 있다는 증거이다.
  • 첫 번째 방법은 JdbcTemplate의 update( )가 돌려주는 리턴 값을 확인하는 것이다. update()는 UPDATE나 DELETE 같이 테이블의 내용에 영향을 주는 SQL을 실행하면 영향받은 로우의 개수를 돌려준다.
  • 두 번째 방법은 테스트를 보강해서 원하는 사용자 외의 정보는 변경되지 않았음을 직접 확인하는 것이다.

실습

TDD의 3가지 원칙

  1. Write NO production code except to pass a failing test.
  2. Write only ENOUGH of a test to demonstrate a failure
  3. Write only ENOUGH production code to pass the test

그림

1.3 UserService.upgradeLevels()

  • 사용자 관리 로직은 어디에 두는게 좋을까?
    • UserDaoJdbc는 적당하지 않다. DAO는 데이터를 어떻게 가져오고 조작할지를 다루는 곳이지 비즈니스 로직을 두는 곳이 아니다.
    • 사용자 관리 비즈니스 로직을 담을 UserService 클래스를 하나 추가하자. 또한 이를 테스트하기 위한 UserServiceTest도 추가하자

UserService 클래스와 빈 등록

public class UserService {
    UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

UserServiceTest 테스트 클래스

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext-test.xml")
public class UserServiceTest {
    @Autowired
    UserService userService;
    
    @Test
    public void bean() {
        assertThat(this.userService, is(notNullValue()));
    }
}
  • bean() 테스트 : userService 빈이 생성돼서 userService 변수에 주입되는지 테스트

upgradeLevels() 메소드

사용자 관리 기능 추가

public void upgradeLevels() {
    List<User> users = userDao.getAll();
    for(User user: users) {
        Boolean changed = null;
        // BASIC 레벨 업그레이드
        if(user.getLevel() == Level.BASIC && user.getLogin() >= 50) {
            user.setLevel(Level.SILVER);
            changed = true;
        } else if(user.getLevel() == Level.SILVER && user.getRecommend() >= 30) {
            // SILVER 레벨 업그레이드
            user.setLevel(Level.GOLD);
            changed = true;
        } else if(user.getLevel() == Level.GOLD) {
            // GOLD 레벨 업그레이드는 일어나지 않는다.
            changed = false;
        } else {
            changed = false;
        }
        if(changed) { userDao.update(user); }
    }
}
  • changed 플래그를 확인해서 레벨 변경이 있는 경우에만 UserDao의 update()를 이용해 수정 내용을 DB에 반영한다.
  • 마찬가지로 위의 로직을 검증할 수 있는 테스트 코드를 추가한다.

upgradeLevels() 테스트

  • 적어도 가능한 모든 조건을 하나씩은 확인해봐야한다.
  • 테스트 픽스처 추가
public class UserServiceTest {
    
    ...
    List<User> users;

    @Before
    public void setUp() {
        users = Arrays.asList(
                new User("bumjin", "박범진", "p1", Level.BASIC, 49, 0),
                new User("joytouch", "강명성", "p2", Level.BASIC, 50, 0),
                new User("erwins", "신승한", "p3", Level.SILVER, 60, 29),
                new User("madnite1", "이상호", "p4", Level.SILVER, 60, 30),
                new User("green", "오민규", "p5", Level.GOLD, 100, 100)
        );
    }

    ...      
}
  • 테스트 코드 추가 : 다섯 가지 종류의 사용자 정보를 저장한 뒤에 upgradeLevels() 메소드를 실행한다. 업그레이드 작업이 끝나면 사용자 정보를 하나씩 가져와 레벨의 변경 여부를 확인한다.
@Test
public void upgradeLevels() {
    // given
    this.userDao.deleteAll();
    for(User user: users) userDao.add(user);

    // when
    userService.upgradeLevels();

    // then
    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(),is(expectedLevel));
}

1.4 UserService.add()

사용자 관리 비즈니스 로직에서 대부분은 구현했지만 아직 하나가 남았다. 처음 가입하는 사용자는 기본적으로 BASIC 레벨이어야 한다는 부분이다.

1.5 코드 개선

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

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

public void upgradeLevels() {
    List<User> users = userDao.getAll();
    for(User user: users) {
        Boolean changed = null;
        // BASIC 레벨 업그레이드
        if(user.getLevel() == Level.BASIC && user.getLogin() >= 50) {
            user.setLevel(Level.SILVER);
            changed = true;
        } else if(user.getLevel() == Level.SILVER && user.getRecommend() >= 30) {
            // SILVER 레벨 업그레이드
            user.setLevel(Level.GOLD);
            changed = true;
        } else if(user.getLevel() == Level.GOLD) {
            // GOLD 레벨 업그레이드는 일어나지 않는다.
            changed = false;
        } else {
            changed = false;
        }
        if(changed) { userDao.update(user); }
    }
}
  • for 루프 속에 들어 있는 if/else if/else 블록들이 읽기 불편하다.
    • 레벨의 변화 단계와 업그레이드 조건, 조건이 충족됐을 때 해야 할 작업이 한데 섞여 있다.
    • 플래그를 두고 이를 변경하고 마지막에 이를 확인해서 업데이트를 진행하는 방법도 그리 깔끔해 보이지 않는다.
  • 성격이 다른 여러 가지 로직이 한데 섞여있다.
  • 새로운 레벨이 추가된다면?
    • Level Enum을 수정
    • upgradeLevels() 메소드 수정 - if 조건식과 블럭을 추가
  • 업그레이드 조건이 복잡해지거나 업그레이드 작업에서 하는 일이 단지 level 필드를 변경하는 수준 이상이라면 upgradeLevels() 메소드는 점점 길어지고 복잡지며, 갈수록 이해하고 관리하기가 힘들어진다.

upgradeLevels( ) 리팩토링

  • 추상적인 레벨에서 로직을 작성해보자 - upgradeLevels() 메소드는 자주 변경될 가능성이 있는 구체적인 내용이 추상적인 로직의 흐름과 함께 섞여 있다.
public void upgradeLevels() {
    List<User> users = userDao.getAll();
    for (User user : users) {
        if (canUpgradeLevel(user)) {
            upgradeUser(user);
        }
    }
}
  • 위 코드는 이렇게 읽을 수 있다. 모든 사용자 정보를 가져와 한 명씩 업그레이드가 가능한지 확인하고, 가능하면 업그레이드를 한다.
  • canUpgradeLevel() 메소드 : 주어진 user에 대해 업그레이드가 가능하면 true, 가능하지 않으면 false를 리턴하면 된다.
    • 새로운 레벨이 추가됐지만 업그레이드 로직을 추가하지 않았다면, 예외가 발생 할테니 쉽게 확인할 수 있다.
private boolean canUpgradeLevel(User user) {
    Level currentLevel = user.getLevel();
    switch (currentLevel) {
        case BASIC:
            return user.getLogin() >= 50;
        case SILVER:
            return user.getRecommend() >= 30;
        case GOLD:
            return false;
        default : throw new IllegalArgumentException()
    }
}
  • upgradeLevel() 메소드 : 레벨 업그레이드를 위한 작업은 사용자의 레벨을 다음 단계로 바꿔주는 것과 변경사항을 DB에 업데이트해주는 것이다.
    • 다음 단계가 무엇인가 하는 로직과 그때 사용자 오브젝트의 level 필드를 변경해준다는 로직이 함께 있다.
    • 예외 상황에 대한 처리가 없다.
    • 레벨이 늘어난다면 if 문이 점점 길어질 것이다.
private void upgradeUser(User user) {
    if (user.getLevel() == Level.BASIC) {
        user.setLevel(Level.SILVER);
    } else if (user.getLevel() == Level.SILVER) {
        user.setLevel(Level.GOLD);
    }
    userDao.update(user);
}
UpgradeLevel 메소드를 분리해보자.
  • 레벨의 순서와 디음 단계 레벨이 무엇인지를 결정하는 일은 Level에게 맡기자. (Level의 순서를 UserService에 담아둘 이유가 없다)
    • Level 이늄에 next라는 다음 단계 레벨 정보를 담을 수 있도록 필드를 추가한다.
public enum Level {
    GOLD(3, null),
    SILVER(2, GOLD),
    BASIC(1, SILVER);

    private final int value;
    private final Level next;

    Level(int value, Level next) {
        this.value = value;
        this.next = next;
    }

    public int intValue() {
        return value;
    }

    public Level nextLevel() {
        return next;
    }

    public static Level valueOf(int value) {
        switch (value) {
            case 1: return BASIC;
            case 2: return SILVER;
            case 3: return GOLD;
            default: throw new AssertionError("Unknown value" + value);
        }
    }
}
  • 사용자 정보가 바뀌는 부분을 UserService 메소드에서 User로 옮겨보자.
    • User의 내부 정보가 변경되는 것은 UserService보다는 User가 스스로 다루는 게 적절하다.
    • User에게 레벨 업그레이드를 해야 하니 정보를 변경하라고 요청하는 편이 낫다.
    • 아래와 같은 메소드를 User 클래스에 추가한다.
public void upgradeLevel() {
    if (this.level.nextLevel() == null) {
        throw new IllegalStateException();
    }
    this.level = level.nextLevel();
}
  • User에 업그레이드 작업을 담당하는 독립적인 메소드를 두고 사용할 경우, 업그레이드시 기타 정보도 변경이 필요해지면 그 장점이 무엇인지 알 수 있을것이다.
    • 가장 최근에 레벨을 변경할 일자를 기록해야 한다면?
public void upgradeLevel() {
    if (this.level.nextLevel() == null) {
        throw new IllegalStateException();
    }
    this.level = level.nextLevel();
    this.lastUpdated = new Date();
}
  • UserService는 User 오브젝트에게 알아서 업그레이드에 필요한 작업을 수행하라고 요청만 해주면 된다.
private void upgradeUser(User user) {
    user.upgradeLevel();
    userDao.update(user);
}

Tell, Don’t Ask

  • 객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다. 오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리이기도 하다.
  • UserService -> User : 레벨 업그레이드 작업을 해달라
  • User -> Level : 다음 레벨이 무엇인지 알려달라

User 테스트

User에 로직을 추가했으니 이에 대한 테스트 코드도 추가하자

  • User 클래스에 대한 테스트는 굳이 스프링의 테스트 컨텍스트를 사용하지 않아도 된다.

UserServiceTest

  • checkLevel() : Level이 갖고 있어야 할 다음 레벨이 무엇인가 하는 정보를 테스트에 직접 넣어둘 이유가 없다. 레벨이 추가되거나 변경되면 테스트도 따라서 수정해야 하니 번거롭다.
  • 따라서 아래와 같이 수정하자. 아래와 같이 수정하면 다음과 같은 장점이 있다.
    • 테스트의 의도가 더 분명히 드러난다. 개선한 upgradeLevels() 테스트는 각 사용자에 대해 업그레이드를 확인하려는 것인지 아닌지가 좀 더 이해하기 쉽게 true, false로 나타나 있어서 보기 좋다.
    • 새로운 Level이 추가되더라도 테스트가 수정될 필요가 없다.
@Test
public void upgradeLevels() {
    this.userDao.deleteAll();
    for(User user: users) userDao.add(user);

    userService.upgradeLevels();

    checkLevelUpgraded(users.get(0), false);
    checkLevelUpgraded(users.get(1), true);
    checkLevelUpgraded(users.get(2), false);
    checkLevelUpgraded(users.get(3), true);
    checkLevelUpgraded(users.get(4), false);
}

private void checkLevelUpgraded(User user, boolean upgraded) {
    User userUpdated = userDao.get(user.getId());
    if (upgraded) {
        assertThat(userUpdated.getLevel(), is(user.getLevel().nextLevel()));
    } else {
        assertThat(userUpdated.getLevel(), is(user.getLevel()));
    }
}
  • 상수값 중복 제거 : 기준이 되는 로그인 횟수, 추천 횟수
    • 정수형 상수로 변경
public static final int MIN_LOGCOUNT_FOR_SILVER =50;
public static final int MIN_RECOMEND_FOR_GOLD =30;

private 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_RECOMEND_FOR_GOLD;
        case GOLD:
            return false;
        default : throw new IllegalArgumentException();
    }
}

2. 트랙잭션 서비스 추상화

사용자 레벨 조정 작업은 중간에 문제가 발생해서 작업이 중단된다면 그 때까지 진행된 변경 작업도 모두 취소시키도 록 결정했다.

2.1 모 아니면 도

  • 모든 사용자에 대해 업그레이드 작업을 진행하다가 중간에 예외가 발생해서 작업이 중단된다면 어떻게 될까? 테스트로 한번 확인해보자
    • 테스트용으로 특별히 만든 UserService의 대역을 사용하는 방법이 좋다.
    • 업데이트 중간에 강제로 예외가 발생하도록 동작하는 TestUserService를 만든다.
  • 테스트를 해보니 업데이터 중간에 예외가 발생하더라도, 이전에 업데이트된 데이터들이 롤백되지 않았다.

테스트 실패의 원인

  • 트랜잭션 문제다. 모든 사용자의 레벨을 업그레이드하는 작업인 upgradeLevels() 메소드가 하나의 트랜잭션 안에서 동작하지 않았기 때문이다.
  • 트랜잭션이란 더 이상 나눌 수 없는 단위 작업을 말한다. 작업을 쪼개서 작은 단위로 만들 수 없다는 것은 트랜잭션의 핵심 속성인 원자성을 의미한다.
    • 중간에 예외가 발생해서 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 초기 상태로 돌려뇌야 한다.

2.2 트랜잭션 경계 설정

  • 하나의 SQL 명령을 처리하는 경우는 DB가 트랜잭션을 보장해준다.
  • 트랜잭션 롤백 : 하나의 트랜잭션 안에서 여러개의 SQL문을 실행할 때, 모든 SQL문을 실행하기 전에 문제가 발생할 경우에는 앞에서 처리한 SQL 작업도 취소시켜야 한다. 이런 취소 작업을 트랜잭션 롤백이라 한다.
  • 트랜잭션 커밋 : 여러 개의 SQL을 하나의 트랜잭션으로 처리하는 경우에 모든 SQL 수행 작업이 다 성공적으로 마무리됐다고 DB에 알려줘서 작업을 확정시켜야 한다.

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

  • 트랜잭션 경계 : 애플리케이션 내에서 트랜잭션이 시작되고 끝나는 위치
  • 트랜잭션 경계 설정 : 트랜잭션이 시작하는 곳과 끝나는 곳을 지정하는 것
  • 트랙잭션 시작 : Connection.setAutoCommit(false)
    • JDBC에서 트랜잭션을 시작하려면 자동 커밋 옵션을 false로 만들어주면 된다. JDBC의 기본설정은 DB 작업을 수행한 직후에 자동으로 커밋이 되도록 되어 있다.
    • JDBC에서는 이 기능을 false로 설정해주면 새로운 트랜잭션이 시작되게 만들 수 있다. 트랜잭션이 한 번 시작되면 commit() 또는 rollback( ) 메소드가 호출될 때까지의 작업이 하나의 트랜잭션으로 묶인다.
  • 트랜잭션 완료
    • 커밋 : Connection.commit()
    • 롤백 : Connection.rollback()
  • JDBC의 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어난다. 트랜잭션의 시작과 종료는 Connection 오브젝트를 통해 이뤄지기 때문이다.
  • 트랜잭션의 경계는 하나의 Connection이 만들어지고 닫히는 범위 안에 존재한다는 점도 기억해두자. 이렇게 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 로컬 트랜잭션이라고도 한다.

UserService와 UserDao의 트랜잭션 문제

  • JdbcTemplate의 메소드를 사용히는 UserDao는 각 메소드마다 하나씩의 독립적인 트랜잭션으로 실행될 수 밖에 없다.
    • UserDao는 update() 메소드가 호출될 때마다 JdbcTemplate을 통해 매번 새로운 DB 커넥션과 트랜잭션을 만들어 사용한다.
  • 데이터 액세스 코드를 DAO로 만들어서 분리해놓았을 경우에는 이처럼 DAO 메소드를 호출할 때마다 하나의 새로운 트랜잭션이 만들어지는 구조가 될 수밖에 없다.
    • DAO 메소드에서 DB 커넥션을 매번 만들기 때문에 어쩔 수 없이 나타나는 결과다.
  • 어떤 일련의 작업이 하나의 트랜잭션으로 묶이려면 그 작업이 진행되는 동안 DB 커넥션도 하나만 사용돼야 한다. 트랜잭션은 Connection 오브젝트 안에서 만들어지기 때문이다.

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

  • UserService와 UserDao를 그대로 둔 채로 트랜잭션을 적용하려면 결국 트랜잭션의 경계설정 작업을 UserService 쪽으로 가져와야 한다.
public void upgradeLevels() throws Exception { 
    (1) DB Connection 생성
    (2) 트랜잭션 시작
    try (
        (3) DAO 메소드 호출
        (4) 트랜잭션 커밋
    catch(Exception e) {
        (5) 트랜잭션 롤백
        throw e;
    finally {
        (6) DB Connection 종료
    }
}
  • 트랜잭션 경계를 upgradeLevels() 메소드 안에 두려면 DB 커넥션도 이 메소드 안에서 만들고, 종료시킬 필요가 있다.
  • 여기서 생성된 Connection 오브젝트를 가지고 데이터 액세스 작업을 진행하는 코드는 UserDao의 update( ) 메소드 안에 있어야 한다. 트랜잭션 때문에 DB 커넥션과 트랜잭션 관련 코드는 어쩔 수 없이 UserService로 가져왔지만 순수한 데이터 액세스 로직은 UserDao에 둬야 하기 때문이다.
  • UserService에서 만든 Connection 오브젝트를 UserDao에서 사용하려면 DAO 메소드를 호출할 때마다 Connection 오브젝트를 파라미터로 전달해줘야 한다.
public interface UserDao {
    void add(Connection c, User user);
    ...
    void update(Connection c, User user);
}
  • 이렇게 Connection 오브젝트를 전달해서 사용하면 UserService의 upgradeLevels() 안에서 시작한 트랜잭션에 UserDao의 메소드들도 참여하게 할 수 있다.

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

  • 첫번째 : DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더 이상 활용할 수 없다는 점이다.
  • 두번째 : DAO의 메소드와 비즈니스 로직을 담고 있는 UserService의 메소드에 Connection 파라미터가 추가돼야 한다는 점이다.
  • 세번째 : Connection 파라미터가 UserDao 인터페이스 메소드에 추가되면 UserDao는 더 이상 데이터 액세스 기술에 독립적일 수가 없다는 점이다. JPA나 하이버네이트로 UserDao의 구현 방식을 변경하려고 하면 Connection 대신 EntityManager나 Session 오브젝트를 UserDao 메소드가 전달받도록 해야 한다.
  • 네번째 : DAO 메소드에 Connection 파라미터를 받게 하면 테스트 코드에도 영향을 미친다.

2.3 트랜잭션 동기화

Connection 파라미터 제거

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

  • 아래는 트랜잭션 동기화 방식을 사용한 경우의 작업 흐름을 보여준다.

그림

  1. UserService는 커넥션을 생성한다.
  2. Connection을 트랜잭션 동기화 저장소에 저장한다. 그리고 Connection의 setAutoCommit(false)를 호출해 트랜잭션을 시작시킨 후에 본격적으로 DAO의 기능을 이용하기 시작한다.
  3. 첫번째 update가 호출된다.
  4. update 메소드 내부에서 이용하고 있는 JdbcTemplate 메소드에서는 가장 먼저 트랜잭션 동기화 저장소에 현재 시작된 트랜잭션을 가진 Connection 오브젝트가 존재하는지 확인한다. 저장된 Connection 오브젝트가 있다면 이를 가져온다.
  5. 가져온 Connection을 이용해 SQL을 실행한다. 트랜잭션 동기화 저장소에서 커넥션을 가져왔을 때는 JdbcTemplate은 Connnection을 닫지 않은 채로 작업을 마친다. 따라서 여전히 Connenction은 열려있고, 트랜잭션은 진행중인 상태이다.
  6. 3~5 과정을 모든 update 메소드가 호출될 때마다 반복한다.
  7. 트랜잭션 내의 모든 작업이 정상적으로 끝났으면 UserService는 Connection의 commit()을 호출해서 트랜잭션을 완료시킨다.
  8. 마지막으로 트랜잭션 저장소가 더 이상 Connection 오브젝트를 저장해두지 않도록 이를 제거한다.
  • 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스레드 환경에서도 충돌이 날 염려는 없다.
  • 트랜잭션 동기화 기법을 사용하면 파라미터를 통해 일일이 Connection 오브젝트를 전달할 필요가 없어진다.

트랜잭션 동기화 적용

  • 스프링은 JdbcTemplate과 더불어 이런 트랜잭션 동기화 기능을 지원하는 간단한 유틸리티 메소드를 제공하고 있다.
    • TransactionSynchronizationManager
DataSource dataSource;

public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
}

public void upgradeLevels() throws Exception {
    // 
    TransactionSynchronizationManager.initSynchronization();
    // DB 커넥션을 생성하고 트랜잭션을 시작한다. 
    // 이후의 DAO 작업은 모두 여기서 시작한 트랜잭션 안에서 진행된다.
    Connection c = DataSourceUtils.getConnection(dataSource);
    c.setAutoCommit(false);

    try {
        List<User> users = userDao.getAll();
        for (User user : users) {
            if (canUpgradeLevel(user)) {
                upgradeUser(user);
            }
        }
        c.commit();
    } catch (Exception e) {
        // 예외가 발생하면 롤백한다.
        c.rollback();
        throw e;
    } finally {
        DataSourceUtils.releaseConnection(c, dataSource);
        // 스프링 유틸리티 메소드를 이용해 DB 커넥션을 안전하게 닫는다.
        TransactionSynchronizationManager.unbindResource(this.dataSource);
        TransactionSynchronizationManager.clearSynchronization();
    }
}

  • 먼저 TransactionSynchronizationManager 클래스를 이용해 트랜잭션 동기화 작업을 초기화하도록 요청한다.
  • DataSourceUtils에서 제공히는 getConnection() 메소드를 통해 DB 커넥션을 생성한다. 스프링이 제공하는 유틸리티 메 소드를 쓰는 이유는 DataSourceUtils의 getConnection( ) 메소드는 Connection 오브젝트를 생성해줄 뿐만 아니라 트랜잭션 동기화에 사용하도록 저장소에 바인딩해주기 때문이다.
  • 동기화 준비가 됐으면 트랜잭션을 시작하고 DAO의 메소드를 사용하는 트랜잭션 내의 작업을 진행한다.
    • 트랜잭션 동기화가 되어 있는 채로 JdbcTemplate을 사용하면 JdbcTemplate의 작업에서 동기화시킨 DB 커넥션을 사용하게 된다.
  • 작업을 정상적으로 마치면 트랜잭션을 커밋해준다.
  • 스프링 유틸리티 메소드의 도움을 받아 커넥션을 닫고 트랜잭션 동기화를 마치도록 요청하면 된다.

JdbcTemplate과 트랜잭션 동기화

  • JdbcTemplate은 영리하게 동작하도록 설계되어 있다. 만약 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate이 직접 DB 커넥션을 만들고 트랜잭션을 시작해서 JDBC 작업을 진행한다.
  • 반면에 upgradeLevels() 메소드에서처럼 트랜잭션 동기화를 시작해놓았다면 그때부터 실행되는 JdbcTemplate의 메소드에서는 직접 DB 커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어 있는 DB 커넥션을 가져와서 사용한다. 이를 통해 이미 시작된 트랙잭션에 참여하는것이다.
  • 덕분에 트랜잭션 적용 여부에 맞쳐 Dao 코드를 수정할 필요가 없다.

2.4 트랜잭션 서비스 추상화

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

  • 두 개 이상의 DB로의 작업을 하나의 트랜잭션으로 만드는 건 JDBC의 Connection을 이용한 트랜잭션 방식인 로컬 트랜잭션으로는 불가능하다.
  • 두 개 이상의 DB 작업을 하나의 트랜잭션으로 처리하기 위해서는 각 DB와 독립적으로 만들어지는 Connection을 통해서가 아니라, 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리히는 글로벌 트랜잭션 방식을 사용해야한다.
    • 자바는 JDBC 외에 이런 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위한 API인 JTA를 제공하고 있다.
    • 하나 이상의 DB가 참여하는 트랜잭션을 만들려면 JTA를 사용해야 한다는 사실만 기억해두자.
  • 문제는 JDBC 로컬 트랜잭션을 JTA를 이용하는 글로벌 트랜잭션으로 바꾸려면 UserService의 코드를 수정해야 한다는 점이다.
    • UserService는 자신의 로직이 바뀌지 않았음에도 기술환경에 따라서 코드가 바뀌는 코드가 돼버리고 말았다.
  • 하이버네이트를 이용한 트랜잭션 관리 코드는 JDBC나 JTA의 코드와는 또 다르다는 것이다. 하이버네이트는 Connection을 직접 사용하지 않고 Session이라는 것을 사용하고, 독자적인 트랜잭션 관리 API를 사용한다.

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

  • UserService는 UserDao 인터페이스에만 의존하는 구조였다.
    • 구현 기술이 JDBC에서 하이버네이트나 여타 기술로 바뀌더라도 UserService 코드는 영향을 받지 않았다.
  • 문제는 JDBC에 종속적인 Connection을 이용한 트랜잭션 코드가 UserService에 등장하면서부터 UserService는 UserDaoJdbc에 간접적으로 의존하는 코드가 돼버렸다는 점이다.
  • 다행히도 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조다. 이렇게 여러 기술의 사용 방법에 공통점이 있다면 추상화를 생각 해 볼 수 있다. 추상화란 하위 시스템의 공통점 뽑아내서 분리시키는 것을 말한다. 그렇게 하면 하위 시스템이 어떤 것인지 알지 못해도 또는 하위 시스템이 바뀌더라도 일관된 방법으로 접근할 수 가 있다.
  • DB에서 제공하는 DB 클라이언트 라이브러리와 API는 서로 전혀 호환이 되지 않는 독자적인 방식으로 만들어져 있다. 하지만 모두 SQL을 이용하는 방식이라는 공통점이 있다. 이 공통점을 뽑아내 추상화한 것이 JDBC다. JDBC라는 추상화 기술이 있기 때문에 자바의 DB 프로그램 개발자는 DB의 종류에 상관없이 일관된 방법으로 데이터 액세스 코드를 작성할 수가 있다.
  • 공통적인 특징을 모아서 추상화된 트랜잭션 관리 계층을 만들 수 있다. 그리고 애플리케이션 코드에서는 트랜잭션 추상 계층이 제공히는 API를 이용해 트랜잭션을 이용하게 만들어준다면 특정 기술에 종속되지 않는 트랜잭션 경계설정 코드를 만들 수 있을 것이다.

스프링의 트랜잭션 서비스 추상화

  • 스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다.
  • 스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스는 PlatformTransactionManager다. JDBC 의 로컬 트랜잭션을 이용한다면 PlatformTransactionManager를 구현한 DataSourceTransactionManager를 사용하면 된다.
public void upgradeLevels() throws Exception {
    PlatformTransactionManager transactionManager
        = new DataSourceTransactionManager(dataSource);

    TransactionStatus status =
        transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        List<User> users = userDao.getAll();
        for (User user : users) {
            if (canUpgradeLevel(user)) {
                upgradeUser(user);
            }
        }
        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw e;
    }
}
  • PlatformTransactionManager에서는 트랜잭션을 가져오는 요청인 getTransaction() 메소드를 호출하기만 하면 된다. 필요에 따라 트랜잭션 매니저가 DB 커넥션을 가져오는 작업도 같이 수행해주기 때문이다. 여기서 트랜잭션을 가져온다는 것은 일단 트랜잭션을 시작한다는 의미라고 생각하자.
  • 파라미터로 DefaultTransactionDefinition 오브젝트는 트랜잭션에 대한 속성을 담고 있다.
  • 스프링의 트랜잭션 추상화 기술은 앞에서 적용해봤던 트랜잭션 동기화를 사용한다. PlatformTransactionManager로 시작한 트랜잭션은 트랜잭션 동기화 저장소에 저장된다. PlatformTransactionManager를 구현한 DataSourceTransactionManager 오브젝트는 JdbcTemplate에서 사용될 수 있는 방식으로 트랜잭션을 관리해준다. 따라서 PlatformTransactionManager를 통해 시작한 트랜잭션은 UserDao의 JdbcTemplate 안에서 사용된다.
  • 다른 기술을 사용해야한다면? 코드는 변경할 필요없이 PlatformTransactionManager 구현체만 변경해주면 된다.

트랜잭션 기술 설정의 분리

  • PlatformTransactionManager를 DI 받도록 수정한다.
  • DAO를 하이버네이트나 JPA 등을 사용하도록 수정했다면 그에 맞게 transactionManager의 클래스만 변경해주면 된다

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

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

  • 기술과 서비스에 대한추상화 기법을 이용하면 특정 기술환경에 종속되지 않는 포터블한 코드를 만들 수 있다.
  • 수평적 분리 : 같은 애플리케이션 로직을 담은 코드지만 내용에 따라 분리했다. 같은 계층에서 수평적인 분리라고 볼 수 있다.
    • UserService와 UserDao
  • 아래 그림은 지금까지 만들어진 사용자 관리 모듈의 의존관계를 나타낸다.
    • 애플리케이션 계층 : UserService, UserDao
    • UserDao와 DB 연결 기술은 결합도가 낮다. 왜냐하면 DataSource 인터페이스에만 의존하기 때문이다.
    • 마찬가지로 UserService는 트랜잭션 기술과도 스프링이 제공하는 PlatformTransactionManager 인터페이스를 통한 추상화 계층을 사이에 두고 사용하게 했기 때문에, 구체적인 트랜잭션 기술에 독립적인 코드가 됐다.

그림

  • 애플리케이션 로직의 종류에 따른 수평적인 구분이든, 로직과 기술이라는 수직적인 구분이든 모두 결합도가 낮으며 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들수 있는데는 스프링의 DI가 중요한 역할을 하고 있다. DI의 가치는 이렇게관심, 책임, 성격이 다른 코드를 깔끔하게 분리하는데 있다.

단일 책임 원칙

  • 단일 책임 원칙은 하나의 모듈은 한 가지 책임을 가져야 한다는 의미다. 하나의 모듈이 바뀌는 이유는 한 가지여야 한다고 설명할 수도 있다. 변경의 이유가 2가지 이상이라면 단일 책임의 원칙을 지키지 못한 것이다.

단일 책임 원칙의 장점

  • 단일 책임 원칙을 잘 지키고 있다면, 어떤 변경이 필요할 때 수정 대상이 명확해진다.
  • 적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 다양한 추상화 기법을 도입하고, 애플리케이션 로직과 기술/환경을 분리하는 등의 작업은 갈수록 복잡해지는 엔터프라이즈 애플리케이션에는 반드시 필요하다. 이를 위한 핵심적인 도구가 바로 DI이다.
  • 스프링의 의존관계 주입 기술인 DI는 모든 스프링 기술의 기반이 되는 핵심 엔진이자 원리이며, 스프링이 지지하고 지원하는, 좋은 설계와 코드를 만드는 모든 과정에서 사용되는 가장 중요한 도구다.

4. 메일 서비스 추상화

1. 메일 기능 테스트의 어려운 점

  • 실제 메일 서버에 메일 전송 요청을 보낸다면, 테스트로 인해 메일 서버에 부담을 줄 수 있다.

  • 테스트용으로 따로 준비된 메일 서버를 이용하는 방법은 어떨까? 운영 중인 메일 서버에 부담을 주지 않는다는 점에서는 분명히 나은 방법이다.

    • 테스트용 메일 서버는 메일 전송 요청은 받지만 실제로 메일을 보내진 않는다.
    • 테스트 시에는 테스트 용 메일 서버로 메일 전송 요청을 보냈는지만 확인한다.
  • JavaMail API를 통해 요청이 들어간다는 보장만 있다면 굳이 테스트 할 때마다 JavaMail을 직접 구동시킬 필요가 없지 않을까?

  • 운영시 : JavaMail을 직접 이용해서 동작하도록 한다.
  • 테스트시 : 테스트용 JavaMail을 이용해서 특정 메소드가 호출되는지 여부만 확인한다.

그림

2. 테스트를 위한 서비스 추상화

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

  • JavaMail의 API는 이 방법을 적용할 수 없다. JavaMail의 핵심 API에는 DataSource처럼 인터페이스로 만들어져서 구현을 바꿀수 있는게 없다.
    • 대부분이 인터페이스가 아니라 그냥 클래스이며, private 생성자를 가진 클래스도 존재한다.
    • JavaMail의 구현을 테스트용으로 바꿔치기하는 건 불가능하다고 볼 수밖에 없다.
  • JavaMail처럼 테스트하기 힘든 구조인 API를 테스트하기 좋게 만드는 방법이 있다. 트랜잭션을 적용하면서 살펴봤던 서비스 추상화를 적용하면 된다.
    • 스프링은 JavaMail에 대한 추상화 기능을 제공하고 있다.
public interface MailSender {
	void send(SimpleMailMessage simpleMailMessage) throw MailException;
    void send(SimpleMailMessage[] simpleMailMessage) throw MailException;
}
  • JavaMailSenderlmpl는 JavaMail 클래스를 사용해 메일 발송 기능을 제공한다. 실제 운영환경에서는 JavaMailSenderlmpl을 DI 받아서 동작하도록 한다.

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

  • 테스트용 MailSender 구현체를 만든다. 테스트가 수행될 때에는 JavaMail을 사용해서 메일을 보낼 필요가 없다.
public class DummyMailSender {
	public void send(SimpleMailMessage simpleMailMessage) throw MailException {
    
    }
    
    public void send(SimpleMailMessage[] simpleMailMessage) throw MailException {
    
    }	
}
  • 테스트용 설정 파일은 JavaMailSenderImpl 대신 DummyMailSender를 빈으로 등록한다.
  • 그러면 UserService 코드는 변경하지 않고 테스트 시에는 메일을 보내지 않도록 할 수 있다.

테스트 서비스 추상화

그림

  • 서비스 추상화라고 하면 트랜잭션과 같이 기능은 유사하나 사용 방법이 다른 로우레벨의 다양한 기술에 대해 추상 인터페이스와 일관성 있는 접근 방법을 제공해주는 것을 말한다. 반면에 JavaMail의 경우처럼 테스트를 어렵게 만드는 건전하지 않은 방식으로 설계된 API를 사용할 때도 유용하게 쓰일 수 있다.
  • 서비스 추상화란 이렇게 원활한 테스트만을 위해서도 충분히 가치가 있다. 기술이나 환경이 바뀔 가능성이 있음에도, JavaMail 처럼 확장이 불가능하게 설계 해놓은 API를 사용해야 하는 경우라면 추상화 계층의 도입을 적극 고려해볼 필요가 있다.
  • 외부의 리소스와 연동하는 대부분의 작업은 추상화의 대상이 될 수 있다.

테스트 대역

  • 테스트 환경에서 유용하게 사용하는 기법이 있다. 대부분 테스트할 대상이 의존하고 있는 오브젝트를 DI를 통해 바꿔치기 하는 것이다.

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

  • UserDao
    • 테스트 시 : DB 커넥션 풀을 사용하지 않은 단순한 DataSource인 SimpleDataSource를 사용
  • UserService
    • 테스트 시 : DummyMailSender를 사용
    • UserServiceTest의 관심사는 UserService에서 구현해놓은 사용자 정보를 가공히는 비즈니스 로직이지, 메일이 어떻게 전송이 될 것인지가 아니기 때문

그림

  • 사용할 오브젝트를 교체하지 않더라도, 단지 테스트만을 위해서도 Dl는 유용하다.

테스트 대역의 종류와 특징

테스트 대역 : 테스트용으로 시용되는 특별한 오브젝트들

  • 테스트 스텁 : 테스트 스텁은 테스트 대상 오브젝트의 의존객체로서 존재하면서 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것을 말한다. 스텁에 미리 테스트 중에 필요한 정보를 리턴해주도록 만들 수 있다. 또는 어떤 스텁은 메소드를 호출하면 강제로 예외를 발생시키게 해서 테스트 대상 오브젝트가 예외상황에서 어떻게 반응하지를 테스트할 때 적용할 수도 있다.
  • 목 오브젝트 : 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검증할 수 있도록 특별히 설계된 오브젝트
    • 목 오브젝트는 스텁처럼 테스트 오브젝트가 정상적으로 실행되도록 도와주면서, 테스트 오브젝트와 자신의 사이에서 일어나는 커뮤니케이션 내용을 저장해뒀다가 테스트 결과를 검증하는데 활용할 수 있게 해준다.
    • 의존 오브젝트를 얼마나 사용했는가 하는 커뮤니케이션 행위 자체에 관심이 있을 수가 있다. 이럴 때는 테스트 대상과 의존 오브젝트 사이에 주고 받는 정보를 보존해두는 기능을 가진 테스트용 의존 오브젝트인 목 오브젝트를 만들어서 사용해야 한다.

그림

  • 테스트 대상은 의존 오브젝트에게 값을 출력하기도 하고 값을 입력받기도 한다. 출력은 무시한다고 칠 수 있지만, 간접적으로 테스트 대상이 받아야 할 입력 값은 필요하다. 이를 위해 별도로 준비해둔 스텁 오브젝트가 메소드 호출 시 특정 값을 리턴하도록 만들어두면 된다.

목 오브젝트를 이용한 테스트

  • 아래와 같이 MockMailSender를 만든다. MockMailSender는 아무런 일을 하지 않지만, 메일 전송 요청이 왔을때 수신자 메일을 리스트에 저장해둔다.
  • DummyMailSender 대신에 MockMailSender를 사용하면, 어떤 수신자에게 메일을 보냈는지 확인할 수 있다.
public class MockMailSender {
	
    private List<String> requests =new ArrayList<String>();
    
    public List<String> getRequests() {
    	return requests;
    }
    
    public void send(SimpleMailMessage simpleMailMessage) throw MailException {
        requests.add(simpleMailMessage.getTo()[0]);
    }
    
    public void send(SimpleMailMessage[] simpleMailMessage) throw MailException {
    
    }	
}
  • 테스트가 수행 될 수 있도록 의존 오브젝트에 간접적으로 입력 값을 제공해주는 스텁 오브젝트와 간접적인 출력 값까지 확인이 가능한 목 오브젝트, 이 두 가지는 테스트 대역의 가장 대표적인 방법이며 효과적인 테스트 코드를 작성하는 데 빠질 수 없는 중요한 도구다.

5. 정리

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