5장. 서비스 추상화
DAO에 트랜잭션을 적용해보면서 스프링이 어떻게 성격이 비슷한 여러 종류의 기술을 추상화하고 일관된 방법으로 사용할 수 있는지 살펴본다.
5.1 사용자 레벨 관리 기능 추가
- 다수의 회원이 가입할 수 있는 인터넷 서비스외 사용자 관리 모듈에 적용한다고 가정
- 사용자 관리 기능에는 단지 정보를 넣고 검색하는 것 외에도 정기적으로 사용자의 활동내역을 참고해서 레벨 조정해주는 기능이 필요
구현해야 하는 비즈니스 로직
- 사용자의 레벨은 BASIC, SILVER, GOLD 세 가지 중 하나다.
- 사용자가 처음 가입하면 BASIC 레벨이 되며, 이후 활동에 따라서 한 단계씩 업그레이드 될 수 있다.
- 가입 후 50회 이상 로그인 하면 BASIC에서 SILVER 레벨이 된다.
- SIVER 레벨이면서 30회 이상 추천 받으면 GOLD 레벨이 된다.
- 사용자 레벨의 변경 작업은 일정한 주기 가지고 일괄적으로 진행된다. 변경 작업 전에는 조건 충족하더라도 레벨의 변경이 일어나지 않는다.
5.1.1 필드 추가
- User 테이블에 level 컬럼 추가
toby-spring3-1/Vol1-30/Ch5/5.1.1/src/springbook/user/domain/Level.java
package springbook.user.domain;
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);
}
}
}
5.1.2 사용자 수정 기능 추가
- update 문에 where문 없어도 아무런 경고 없이 정상적으로 동작하는 것처럼 보이는 문제를 해결하려면
- 첫번째는 JdbcTemplate의 update()가 돌려주는 리턴 값을 확인하거나
- 두번째는 테스트 보강해서 원하는 사용자 외의 정보는 변경되지 않음을 확인하면 된다. < 아래 코드에 적용된 방법
toby-spring3-1/Vol1-30/Ch5/5.1.5/src/springbook/user/dao/UserDaoTest.java
@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);
User user2same = dao.get(user2.getId());
checkSameUser(user2, user2same);
}
5.1.3 UserService.upgardeLevels()
사용자 관리 로직은 어디에 두는 것이 좋을까 ?
- DAO는 데이터를 어떻게 가져오고 조작할지를 다루는 곳이기 때문에 새로 비즈니스 로직을 두는 UserService를 추가한다.
- UserService는 인터페이스 타입으로 userDao 빈을 DI 받아 사용하게 된다.
- UserService는 UserDao의 구현 클래스가 바뀌어도 영향받지 않도록 해야 한다.
- 데이터 엑세스 로직이 바뀌었다고 비즈니스 로직 코드를 수정하는 일이 있어서는 안 된다.
- 따라서 DAO의 인터페이스를 사용하고 DI를 적용해야 한다.
upgradeLevels() 메소드
- 모든 사용자 정보를 DAO에서 가져온 후에 한 명씩 레벨 변경 작업을 수행한다.
upgradeLevels() 테스트
- 준비한 다섯 가지 종류의 사용자 정보를 저장한 뒤에 upgradeLevels() 메소드를 실행한다.
- 업그레이드 작업이 끝나면 사용자 정보를 하나씩 가져와 레벨의 변경 여부를 확인한다.
5.1.4 UserService.add()
처음 가입하면 사용자는 기본적으로 BASIC 레벨이어야 한다. 이 로직은 어디에 담을까 ?
- UserDaoJdbc의 add() 메소드는 적합하지 않다.
- UserDaoJdbc는 User오브젝트를 DB에 정보 넣고 읽는 것에만 관심을 가여쟈 하고, 비즈니스적인 의미 지닌 정보를 설정하는 책임을 지는 것은 바람직하지 않다.
- User클래스에서 아예 level 필드를 Level.BASIC으로 초기화한다.
- 나쁘지 않은 생각이지만, 처음 가입할 때 제외하면 무의미한 정보인데, 단지 이 로직 담기 위해 클래스에서 직접 초기화하는 것은 문제가 있어 보인다.
- 비즈니스 로직인 UserService에 이 로직을 넣어준다.
- UserDao의 add()는 사용자 정보 받은 User 오브젝트를 DB에 넣어주는데만 충실한 역할을 하고, UserService에서 add()를 만들어주고 사용자가 등록될 때 적용할 만한 비즈니스 로직을 담당하게 하면 된다.
5.1.5 코드 개선
작성된 코드를 살펴볼 때 다음과 같은 질문을 한다.
- 코드에 중복된 부분은 없는가?
- 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가?
- 코드가 자신이 있어야 할 자리에 있는가 ?
- 앞으로 변경이 일어난다면 어떤 것이 있을 수 있고, 그 변화에 쉽게 대응할 수 있게 작성되어 있는가 ?
upgradeLevels() 메소드 코드의 문제점
- 등급 추가할때마다 if 조건이 추가되어야 한다.
- 일단 for 루프 속에 들어있는 if/elseif/else 블록이 읽디 불편하다.
- 1 은 현재 레벨이 무엇인지
- 2 는 업그레이드 조건
- 3 은 다음 단계의 레벨
- 4 는 멀리 떨어져 있는 5와 연결된 플래그
- 현재 레벨과 업그레이드 조건을 동시에 확인하는 것도 문제가 될 수 있다.
- BASIC이면서 로그인 횟수가 50이 되지 않는 경우는 마지막 else 블록으로 이동한다.
- 새로운 레벨이 추가돼도 기존의 if 조건에 맞지 않으므로 else 블록으로 디동한다.
- 첫 단계에서는 레벨을 확인하고 각 레벨별로 다시 조건을 판단하는 조건식을 넣는다.
upgradeLevels() 리팩토링
- 기본 작업 흐름만 남겨두고 리펙토링을 한다.
toby-spring3-1/Vol1-30/Ch5/5.1.5/src/springbook/user/service/UserService.java
public void upgradeLevels() {
List<User> users = userDao.getAll();
for(User user : users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
- 업그레이드가 가능한지 확인하는 방법은
- User오브젝트에서 레벨 가져와서 switch문으로 레벨 구분하고
- 각 레벨에 대한 업그레이드 조건 만족하는지 확인해주면 된다.
toby-spring3-1/Vol1-30/Ch5/5.1.5/src/springbook/user/service/UserService.java
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_RECCOMEND_FOR_GOLD);
case GOLD: return false;
default: throw new IllegalArgumentException("Unknown Level: " + currentLevel);
}
}
- upgradeLevel() 메소드를 만들어 본다.
- 레벨 업그레이드를 위한 작업은 사용자의 레벨을 다음 단계로 바꿔주는 것과 변경사항을 DB에 업데이트해주는 것이다.
- 업그레이드 작업용 메소드를 따로 분리해두면 나중에 작업이 추가되더라도 어느 곳을 수정할지 명확해진다.
- 다음의 한계가 존재한다.
- 다음 단계가 무엇인가 하는 로직과 그때 사용자 오브젝트의 level 필드를 업데이트 한다는 로직이 함께 있고, 너무 노골적으로 드러나 있다.
- 예외상황에 대한 처리도 없다.
- 다음 레벨이 무엇인지 결정하는 일은 Level에게 맡기고, 업그레이드와 예외상황에 대한 검증은 User에서 추가한다.
toby-spring3-1/Vol1-30/Ch5/5.1.5/src/springbook/user/domain/User.java
public void upgradeLevel() {
Level nextLevel = this.level.nextLevel();
if (nextLevel == null) {
throw new IllegalStateException(this.level + "은 업그레이드가 불가능합니다");
}
else {
this.level = nextLevel;
}
}
}
Vol1-30/Ch5/5.1.5/src/springbook/user/domain/Level.java
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;
}
}
객체지향 코드란
- 지금 개선된 코드를 살펴보면 각 오브젝트와 메소드가 각각 자기 몫의 책임을 맡아 일을 하는 구조이다.
- 각자 자기 책임에 충실한 작업만 하고 있으니 코드 이해하기 쉽다. 변경이 필요할 때 어디 수정할지도 쉽게 알 수 있다.
- 객체지향적인 코드는 다른 오브젝트의 데이터 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다.
- 오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이다.
5.2 트랜잭션 서비스 추상화
5.2.1 모 아니면 도
- 모든 사용자에 대해 업그레이드 작업을 진행하다가 중간에 예외가 발생해서 작업이 중단된다면 어떻게 될까 ?
- 이미 변경된 사용자의 레벨은 작업 이전 상태로 돌아가야 할까 ?
- 아니면 바뀐 채로 남아 있어야 할까 ?
- UserService의 테스트용 대역을 만들어서 테스트한다.
- 현재는 트랜잭션 처리가 되어 있지 않아 예외가 발생하더라도 이미 변경된 사용자의 레벨은 작업 이전 생타로 남아있게 된다.
5.2.2 트랜잭션 경계설정
- setAutoCommit(false)로 트랜잭션의 시작 선언하고 commit() 또는 rollback()으로 트랜잭션 종료하는 작업을 트랜잭션 경계설정 이라고 한다.
- 트랜잭션 경계는 하나의 Connection이 만들어지고 닫히는 범위 안에 존재한다
- 이렇게 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 로컬 트랜잭션 이라고 한다.
- upgradeLevels() 에서 세 번에 결쳐 UserDao의 update() 호출했다고 가정
- UserDao는 JdbcTemplate 통해 매번 새로운 DB 커넥션과 트랜잭션 만들어 사용한다.
- 첫 번째 update() 호출할 때 작업 성공했다면 그 결과는 이미 트랜잭션이 종료되어서 커밋됐기 때문에 두 번째 update()를 호출하는 시점에서 오류가 발생해서 작업이 중단된다고 해도 첫 번째 커밋한 트랜잭션의 결과는 DB에 그대로 남는다.
- 데이터 액세스 코드를 DAO로 만들어서 분리해놓았을 경우에는 이처럼 DAO 메소드를 호출할 때마다 하나의 새로운 트랜잭션이 만들어지는 구조가 될 수 밖에 없다.
- UserDao는 JdbcTemplate 통해 매번 새로운 DB 커넥션과 트랜잭션 만들어 사용한다.
비즈니스 로직 내의 트랜잭션 경계 설정
- UserService와 UserDao를 그대로 둔 채로 트랜잭션 적용하려면 결국 트랜잭션의 경계설정 작업을 UserService 쪽으로 가져와야 한다.
- 프로그램의 흐름을 볼 때 upgradeLevels() 메소드의 시작과 함께 트랜잭션이 시작하고 메소드를 빠져나올 때 트랜잭션이 종료돼야 하기 때문이다.
- 이 로직을 적용하려면 Connection 오브젝트를 파라미터로 전달해주어야 한다.
UserService 트랜잭션 경계설정의 문제점
이런식으로 수정하면 문제는 해결되지만 여러가지 새로운 문제가 발생한다.
- DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate 을 더 이상 활용할 수 없다.
- DAO와 UserService에 Connection 파라미터가 추가되어야 한다
- UserDao는 더 이상 데이터 액세스 기술에 독립적일 수가 없다.
- 테스트 코드에도 영향을 미치게 된다.
토비 스프링 소스코드 ▶ https://github.com/AcornPublishing/toby-spring3-1/tree/main
토비 스프링 ▶ https://product.kyobobook.co.kr/detail/S000000935358
'스터디 > [토비의 스프링 3.1 Vol 1] (2025.03)' 카테고리의 다른 글
[토비의 스프링 3.1 Vol 1] 4장. 예외 처리 (0) | 2025.03.29 |
---|---|
[토비의 스프링 3.1 Vol 1] 3장. 템플릿 (0) | 2025.03.23 |
[토비의 스프링 3.1 Vol 1] 2장. 테스트 (0) | 2025.03.16 |
[토비의 스프링 3.1 Vol 1] 1장. 오브젝트와 의존관계 (1) | 2025.03.09 |