티스토리 뷰
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
링크
TAG
- 백기선
- 프록시패턴
- 예외처리
- 코딩테스트
- gracefulshutdown
- 토비
- 프로그래머스
- c++
- 자바스터디
- 객체지향
- 자바
- 서비스추상화
- 코테
- SOLID
- 메서드레퍼런스
- 프록시
- OOP
- 카카오
- BOJ
- java
- AOP
- provider
- 데코레이터패턴
- 디자인패턴
- 토비의봄TV
- 템플릿콜백
- 스프링
- ec2
- 김영한
- 토비의스프링
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함