스터디/[토비의 스프링 3.1 Vol 1] (2025.03)

[토비의 스프링 3.1 Vol 1] 5장. 서비스 추상화 (~5.2.2)

ttoance 2025. 4. 6. 08:00
반응형

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 메소드를 호출할 때마다 하나의 새로운 트랜잭션이 만들어지는 구조가 될 수 밖에 없다. 

 

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

  • 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

반응형