티스토리 뷰

public class UserService {
    UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    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);
            }
        }
    }

    public void add(User user) {
        if (user.getLevel() == null) {
            user.setLevel(Level.BASIC);
        }
        userDao.add(user);
    }
}

upgradeLevels() 메소드 리팩토링

문제점

  • 현재 레벨 파악 조건문 & 업그레이드 조건문 합쳐져 있음
  • if 조건 블록이 레벨 개수만큼 반복
    • 새로운 레벨 추가시 enum 수정 & if 조건식 블록 추가 -> 번거로움

리팩토링

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

//upgrade 가능 여부 책임 분리
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("unknown level : " + currentLevel);
    }
}

// 업그레이드 작업 메소드
// 문제점 1. 다음 단계 레벨 설정 책임 & level 변경 책임 혼재
// 2. 예외상황 처리 없음
private void upgradeLevel(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 리팩토링

public enum Level {
    GOLD(3, null), SILVER(2, GOLD), BASIC(1, SILVER);

    private final int value;
    //다음 레벨이 무엇인지 결정하는 일을 Level 에게 맡긴다.
    private final Level next;

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

    public int intValue() {
        return value;
    }

    public Level nextLevel() {
        return this.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);
        }
    }
}

public class User {
    //레벨 업그레이드할 때 정보를 변경하는 일을 User 에게 맡긴다.
    public void upgradeLevel() {
        Level nextLevel = this.level.nextLevel();
        if (nextLevel == null) {
            throw new IllegalStateException(this.level + " 업그레이드 불가");
        } else {
            this.level = nextLevel;
        }
    }

}

public class UserService {
    //...
    private void upgradeLevel(User user) {
        user.upgradeLevel();
        userDao.update(user);
    }
}
  • 각 오브젝트와 메소드가 각각 자기 몫의 책임을 맡아 일을 하는 구조
  • UserService, User, Level 이 내부 정보를 다루는 자신의 책임에 충실한 기능을 가지고 있으면서 필요가 생기면 이런 작업을 수행해달라고 서로 요청하는 구조
  • 객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다

트랜잭션

  • 레벨 관리 작업 수행 도중 장애가 생겨서 작업을 완료할 수 없다면? -> 그때까지 진행된 변경 작업을 모두 취소 (rollback)

트랜잭션 적용 여부 확인 테스트 코드

  • Application 코드인 UserService를 상속 받아서 일부 로직만 변경한다.
public class TestUserService extends UserService {

    private String id;

    public TestUserService(UserDao userDao, String id) {
        super(userDao);
        this.id = id;
    }

    @Override
    protected void upgradeLevel(User user) {
        if (user.getId().equals(this.id)) {
            throw new TestUserServiceException();
        }

        super.upgradeLevel(user);
    }
}
//UserService.java
@Test
void upgradeAllOrNothing() {
    TestUserService testUserService = new TestUserService(userDao, users.get(3).getId());

    userDao.deleteAll();
    for (User user : users) {
        userDao.add(user);
    }
    try {
        testUserService.upgradeLevels();
        Assertions.fail("TestUserServiceException expected");
    } catch (TestUserServiceException e) {
    }
    //예외 발생 이전에 이미 레벨이 변경된 사용자 레벨이 처음으로 롤백 되었는지 확인
    checkLevelUpgraded(users.get(1), false);
}
  • 결과: 실패
    • upgradeLevels() 메소드가 하나의 트랜잭션 안에서 동작하지 않았기 때문
    • JdbcTemplate
      • 템플릿 메소드 호출 한 번에 한 개의 DB 커넥션이 만들어지고 닫히는 일까지 일어난다
      • 템플릿 메소드가 호출될 때마다 트랜잭션이 새로 만들어지고 메소드를 빠져나오기 전에 트랜잭션이 종료된다.

문제 해결

  • 트랜잭션 경계설정 구조를 UserService.upgradeLevels() 로 옮긴다.
public void upgradeLevels() throws Exception {
    // 1. DB connection 생성
    // 2. 트랜잭션 시작
    try {
        //3. DAO 메소드 호출
        //4. 트랜잭션 커밋
    }
    catch(Exception e){
        //5. 트랜잭션 롤백
        throw e;
    }
    finally{
        //6. DB connection 종료
    }
}
class UserService {
    public void upgradeLevels() throws Exception {
    Connection c = ...;
    //...
    try {
        //...
        upgradeLevel(c, user);
        //...
    }
    //...
}

    protected void upgradeLevel(Connection c, User user) {
        user.upgradeLevel();
        userDao.update(c, user);
    }

}
  • 문제
    • 지저분한 Connection 파라미터 -> DAO 를 쓰는 곳마다 붙여야 함
    • UserDao 는 데이터 access 기술에 독립적이지 않음.
      • JPA, Hibernate 는 Connection 이 아닌 EntityManager, Session 오브젝트를 사용.

문제 해결 : 스프링에서 제공하는 트랜잭션 동기화 활용

public void upgradeLevels() throws Exception{
    //동기화 작업 초기화
    TransactionSynchronizationManager.initSynchronization();
    //DB 커넥션 생성 & 동기화
    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 {
        //DB connection 닫기
        DataSourceUtils.releaseConnection(c, dataSource);
        //동기화 작업 종료 및 정리
        TransactionSynchronizationManager.unbindResource(this.dataSource);
        TransactionSynchronizationManager.clearSynchronization();
    }
}
  • 지저분한 Connection 파라미터 제거
  • 트랜잭션 동기화가 되어 있는 채로 JdbcTemplate을 사용하면 JdbcTemplate의 작업에서 동기화시킨 DB 커넥션을 사용하게 된다.

JdbcTemplate 과 트랜잭션 동기화

  • JdbcTemplate 은 미리 생성되어 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate이 직접 DB 커넥션을 만들고 트랜잭션을 시작해서 JDBC 작업 진행
  • 미리 트랜잭션 동기화를 시작해놓았다면 JdbcTemplate 은 직접 DB 커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어 있는 DB 커넥션을 가져와서 사용

트랜잭션 서비스 추상화

새로운 문제 상황

  • 여러 개의 DB 에 데이터를 저장. -> 글로벌 트랜잭션 적용 필요.
  • Hibernate로 UserDao를 구현한 경우. -> Connection 대신 Session 사용, 독자적인 트랜잭션 관리 API 사용

문제 해결 : 추상화

  • 추상화
    • 하위 시스템의 공통점을 뽑아내서 분리시키는 것
  • 스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공
public void upgradeLevels() throws Exception{

    //Jdbc 트랜잭션 추상 오브젝트 생성
    PlatformTransactionManager 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 (Exception e) {
        //예외 발생시 롤백
        transactionManager.rollback(status);
        throw e;
    }
}
  • 문제점
    • UserService 가 구체 Transaction Manager 에 의존적 (DataSourceTransationManager : Jdbc Transcation 관리)

개선 코드

public void upgradeLevels() throws Exception{
    // 트랜잭션 시작
    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 (Exception e) {
        //예외 발생시 롤백
        transactionManager.rollback(status);
        throw e;
    }
}

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

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

  • 어플리케이션 계층 : UserService , UserDao

  • 서비스 추상화 계층 : TransactionManager, DataSource

  • 기술 서비스 계층 : JDBC, JTA, Connection Pooling, …

  • 의존관계

    • UserService -> UserDao 결합도 낮음
    • UserService -> TransactionManager 결합도 낮음

단일 책임 원칙

  • 하나의 모듈은 한 가지 책임을 가져야 한다.
  • 개선 이전, UserService 는 1) 어떻게 사용자 레벨을 관리할 것인가 2) 어떻게 트랜잭션을 관리할 것인가 라는 두 가지 책임을 갖고 있었다.
    • 이는 UserService 코드가 수정되는 이유가 두 가지라는 것을 의미.

서비스 추상화 예시 : 메일 서비스 추상화

  • JavaMail 문제점
    • 테스트용으로 바꿔치기하는 것이 불가능에 가깝다.
  • 스프링에서는 테스트 편의성을 위해 JavaMail 을 추상화한 MailSender 인터페이스를 제공.

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


private final MailSender mailSender;

protected void upgradeLevel(User user) {
    user.upgradeLevel();
    userDao.update(user);
    sendUpgradeEmail(user);
}

private void sendUpgradeEmail(User user) {
    SimpleMailMessage mailMessage = new SimpleMailMessage();
    mailMessage.setTo(user.getEmail());
    mailMessage.setFrom("useradmin@ksug.org");
    mailMessage.setSubject("subject");
    mailMessage.setText("name: " + user.getLevel().name());

    this.mailSender.send(mailMessage);

}
//메일 발송 테스트를 위한 클래스 
public class DummyMailSender implements MailSender {
    @Override
    public void send(SimpleMailMessage simpleMailMessage) throws MailException {
    }

    @Override
    public void send(SimpleMailMessage... simpleMailMessages) throws MailException {
    }
}
class UserServiceTest {
    @Autowired
  MailSender mailSender;

    //테스트 코드에서 mailSender 호출

}

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

public class MockMailSender implements MailSender {

    private List<String> requests = new ArrayList<>();

    public List<String> getRequests() {
        return requests;
    }

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

    @Override
    public void send(SimpleMailMessage... simpleMailMessages) throws MailException {

    }
}
class UserServiceTest {
MockMailSender mockMailSender;

//...

public void upgradeLevels() throws Exception {
    userDao.deleteAll();
    for (User user : users) {
        userDao.add(user);
    }

    //mockMailSender 활용해 메일 발송
    userService.upgradeLevels();

    List<String> requests = mockMailSender.getRequests();
    assertThat(requests.size()).isEqualTo(2);
assertThat(requests.get(0)).isEqualTo(users.get(1).getEmail());

  }
}
  • MockObject 활용한 테스트 장점
    • 테스트 대상 오브젝트 내부에서 일어나는 일 검증 가능

'SPRING' 카테고리의 다른 글

토비의 스프링 7. AOP (2)  (0) 2022.09.18
토비의 스프링 6. AOP (1)  (0) 2022.05.27
토비의 스프링 4. 예외  (0) 2022.05.27
토비의 스프링 3. 템플릿  (0) 2022.05.27
토비의 스프링 2. 테스트  (0) 2022.05.27
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함