Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6주차](임주민, 양재승, 박준영) #6

Open
sunwootest opened this issue Aug 1, 2023 · 2 comments
Open

[6주차](임주민, 양재승, 박준영) #6

sunwootest opened this issue Aug 1, 2023 · 2 comments
Assignees
Labels

Comments

@sunwootest
Copy link
Collaborator

sunwootest commented Aug 1, 2023

  • 6주차
    • 4장
    • 5장
    • 6.1 ~ 6.4장
@jumining
Copy link

jumining commented Aug 7, 2023

4장 예외

목표 : JdbcTemplate를 대표로 하는 스프링의 데이터 액세스 기능에 담겨있는 예외처리와 관련된 접근 방법 알아보고 예외 처리하는 베스트 프랙티스 살펴보기


- 예외 블랙홀

try {
...
}
catch (SQLException e) {
} // 예외 잡고 아무것도 안함

catch (SQLException e) {
	System.out.prinln(e);
    e.printStackTrace();
} // 또는 단순 출력

예외 발생했는데 아무것도 하지 않고 별문제 없는 것처럼 넘어가버림
👉 결국 예외로 인해 기능이 비정상적으로 동작 또는 메모리나 리소스 소진되거나 다른 문제들 일으킬 가능성 O

- 예외 처리 핵심 원칙

: 모든 예외는 적절하게 복구되어야 함
: 또는 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 함

👉 예외를 무시하거나 잡아먹어 버리는 코드를 만들지 말자!

} catch (SQLException e) {
	e.printStackTrace();
 	System.exit(1);
} // 그나마 나은 예외처리

- 무의미하고 무책임한 throws

예외를 상위 메소드로 던져버림
메소드를 사용하는 모든 메소드에 throws를 붙이면 실행 중에 예외적인 상황이 발생할 수 있기에 throws를 붙인건지, 그냥 복사 붙여넣기 한 것인지 알기 어려움

예외의 종류와 특징

체크 예외(Checked exception) : 명시적인 처리가 필요한 예외를 사용하고 다루는 방법

- 자바에서 throw를 통해 발생시킬 수 있는 예외 3가지

  1. Error
java.lang.Error 클래스의 서브 클래스들
에러 : 시스템에 뭔가 비정상적인 상황이 발생한 경우에 사용됨 
: 주로 자바 VM에서 발생시키는 것
: 애플리케이션 코드에서 잡으려고 하면 X
: 아무런 대응 방법이 없음, 신경 X
  1. Exception체크 예외
java.lang.Exception 클래스와 서브 클래스들
에러와 달리 개발자들이 만든 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용됨
어떤 식으로든 복구할 가능성이 있는 경우에 사용됨
1)체크 예외와 2)언체크 예외로 구분됨

체크 예외는 반드시 예외를 처리하는 코드를 함께 작성
(catch문으로 잡기 / throws로 메소드 밖으로 던지기)
그렇지 않으면 컴파일 에러 발생

  1. RuntimeException언체크/런타임 예외
언체크 예외 : java.lang.RuntimeException 상속
명시적인 예외처리를 강제하지 않음
: 프로그램의 오류가 있을 때 발생하도록 의도된 것들
: 개발자가 부주의해서 발생할 수 있는 경우에 발생되도록 만든 것 
-> 예상하지 못했던 예외상황에서 발생하는게 X

ex)
- NullPointerException (객체 할당 안된 레퍼런스 변수 사용 시도)
- IllegarArgumentException (허용되지 않는 값 사용해서 메소드 호출)


예외처리 방법

예외처리 일반적인 방법들과 효과적인 예외처리 전략

- 1) 예외 복구

예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓음
기본 작업 흐름이 불가능하면 다른 작업 흐름으로 자연스럽게 유도
ex.1 ) 해당 파일 없어서 IOException 발생시 다른 파일 이용하도록 안내
ex.2 ) 원격 DB 서버 접속 실패해서 SQLException 발생시 일정 시간 대기 후 접속 다시 시도

- 2) 예외처리 회피

예외처리를 자신이 담당 X, 자신을 호출한 쪽으로 던짐 (다른 오브젝트나 메소드가 예외를 대신 처리)

  1. throws문으로 선언
  2. catch문으로 예외 잡고 rethrow
// 1)
public void method() throws Exception { }

// 2)
public void method() throws Exception {
	try {
   	...
    } 
    catch(Exception e) {
    	throw e; 
    }
}

ex ) 콜백템플릿 (역할 분담하고 있는 관계)
JdbcContext나 JdbcTemplate이 사용하는 콜백 오브젝트는 ResultSet이나 PreparedStatement를 이용해서 작업하다 발생하는 SQLException을 자신이 처리하지 X
템플릿으로 던져버림
예외처리는 콜백 오브젝트의 역할이 아니라고 보고 SQLException에 대한 예외를 회피하고 템플릿 레벨에서 처리하도록 던짐

- 3) 예외 전환

예외를 메소드 밖으로 던지는 것
예외 회피와 달리, 예외를 그대로 넘기는 것이 아니라 적절한 예외로 전환해서 던짐

사용 목적 1 )
내부에서 발생한 예외를 그대로 던지는 것이 그 예외 상황에 대한 적절한 의미를 부여해주지 못하는 경우에 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서
-> 상황에 적합한 의미를 가진 예외로 변경하는 것

ex )
중복 아이디로 사용자 등록 시도시 DB에러로 SQLException 발생했을 때 DAO 메소드가 이 예외 그대로 던지면 서비스 계층에서는 예외 발생 원인(중복 아이디)를 모름
-> SQLException을 DuplicateUserIdException 예외로 바꿔서 던져주면 서비스 계층에서 적절한 복구 작업 시도 가능

전환하는 예외에 원래 발생한 예외를 담아서 중첩 예외로 만드는 것이 좋음

public void add() throws DuplicateUserIdException, SQLException { 
	try { ... }
	catch(SQLException e) {
		if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
    		throw DuplicateUserIdException(e);
            // 또는 throw DuplicateUserIdException().initCause(e);
    	else throw e;
}

사용 목적 2 )
: 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것
: 중첩 예외를 이용해 새로운 예외 만들고 원인이 되는 예외를 내부에 담아 던지는 방식(위 방식과 동일)
: 예외처리 강제하는 체크 예외언체크 예외인 런타임 예외로 바꾸는 경우에 사용
: 굳이 필요하지 않은 catch/throws를 줄여줌

ex ) EJBException
EJB 컴포넌트 코드에서 발생하는 대부분의 체크 예외는 비즈니스 로직으로 볼 때 의미있는 예외이거나 복구 가능한 예외가 X
런타임 예외EJBException으로 포장해서 던지기



예외 전환

JDBC의 한계

- 1) 비표준 SQL

작성된 비표준 SQL은 DAO 코드에 들어가고, 해당 DAO는 특정 DB에 대해 종속적인 코드가 됨 
다른 DB로 변경하려면 DAO에 담긴 SQL을 적지 않게 수정해야함
DB의 변경 가능성을 고려해서 유연하게 만들어야 하면 큰 걸림돌이 됨

<해결책>
: 표준 SQL만 사용하는 방식을 현실성 없음
: DAO를 DB별로 만들어 사용하거나 SQL을 외부에서 독립시켜서 바꿔 쓸 수 있게 하는 방식 시도

- 2) 호환성 없는 SQLException의 DB 에러정보

JDBC는 데이터 처리 중 발생하는 다양한 예외를 그냥 SQLException에 모두 담아버림
에러 코드와 SQL 상태정보를 참조하여 예외 발생 원인 확인 가능하지만 DB의 JDBC 드라이버에서 SQLException을 담을 상태 코드를 정확하게 만들어주지 않음
👉 SQLException에 담긴 SQL 상태 코드는 신뢰할 만한 게 아님


DB 에러 코드 매핑을 통한 전환

SQLException의 비표준 에러코드와 SQL 상태정보에 대한 해결책
DB 업체별로 만들어 유지해오고 있는 DB 전용 에러 코드가 더 정확한 정보
👉 DB별 에러 코드를 참고해서 발생한 예외의 원인이 무엇인지 해석해 주는 기능 만드는 방법 시도

스프링은 DB별로 에러 코드를 분류해서 스프링이 정의한 예외 클래스와 매핑해놓은 에러 코드 매핑정보 테이블을 만들어두고 이를 이용함


DAO 인터페이스와 구현의 분리

- DAO를 굳이 따로 만들어서 사용하는 이유

  1. 데이터 액세스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓기 위해서
  2. 분리된 DAO는 전략 패턴을 적용해 구현 방법을 변경해서 사용할 수 있게 만들기 위해서

사용기술과 구현 코드는 전략 패턴과 DI를 통해 DAO를 사용하는 클라이언트에게 감출 수 있지만, 메소드 선언에 나타나는 예외정보가 문제가 될 수 있음

- 데이터 액세스 예외 추상화와 DataAccessException 계층구조

스프링은 자바의 Data Access 기술을 사용할 때 발생하는 예외들을 추상화해서 DataAccessException 계층구조 안에 정리해놓음
데이터 액세스 작업 중 발생할 수 있는 예외(예외 상황)를 처리하는 데 사용되는 계층 구조

DataAccessException 예외 추상화를 적용하면

  • 데이터 액세스 기술과 구현 방법에 독립적인 이상적인 DAO 만들기 가능
  • 데이터 액세스 예외를 처리하기 위해 구체적인 예외 처리 로직을 분리하고, 여러 유형의 데이터 액세스 작업에 대해 서로 다른 예외 처리 전략을 정의하기 가능
public class DatabaseDataAccessErrorHandler implements DataAccessErrorHandler {
    @Override
    public void handleDataAccessException(DataAccessException ex) {
        // 
    }
}

public class FileDataAccessErrorHandler implements DataAccessErrorHandler {
    @Override
    public void handleDataAccessException(DataAccessException ex) {
        // 
    }
}

@sheepseung
Copy link

sheepseung commented Aug 7, 2023

chapter 5. 서비스 추상화

  • 📗사용자 레벨 관리 기능 추가

    enum활용(Level property 추가)

    varchar타입이 아닌 int타입으로 변경하여 저장 ⇒ 데이터가 가벼워짐

    단순 int 값이 아닌 enum을 활용할 시 얻을 수 있는 이점이 무엇일까?

    • 여러가지 발생 가능한 버그를 컴파일러에서 잡아줄 수 있음
    • 발생가능 예외처리 또한 관리가 편함
    • 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; //Level 오브젝트 -> int 타입
    	}
    
    	public static Level valuOf(int value){
    		//값으로부터 Level타입 오브젝트를 가져옴 (int 타입 -> Level 오브젝트)
    	} 

    내부에는 DB에 저장할 int 타입의 값을 갖고 있지만, 겉으로는 Level 타입의 오브젝트이기 때문에 안전하게 사용할 수 있다.

    UserService.upgradeLevels()

    : 구현할 함수는 이름 그대로 User 레벨을 올려주는 함수이다.

    이처럼 사용자 관리 로직은 비지니스 로직이기 때문에 UserService 클래스를 따로 만들어 다루는 것이 적절gka ⇒ 관심사의 분리

    이 때 UserService는 UserDao인터페이스 타입으로 userDao 빈을 DI받아 사용한다.

    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(Levell.GOLD);
    			changed = ture
    		}
    		else if (user.getLevel() == Level.GOLD) { changed = false; }
    		else { changed = false;}
    		
    		if (changed) { userDao.update(user); }
    	}
    }

    문제점

    • 조건문의 남발
      • 레벨 체크, 레벨에 대한 개별적 수행, 다음 단계 로직 등 관심사가 엉켜있음
    • 플래그를 사용하여 업데이트 로직을 수행
      • 이 또한 변경이 일어났는지 확인하는 관심사

    ⇒ 성격이 다른 여러 가지 로직이 한데 섞임 (관심사 분리 x)

    upgradeLevels() 리팩토링

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

    추상적인 로직 분리(기본 작업 흐름만 남겨둠)

    upgradeLevels() 메소드가 어떤 작업을 하는지 쉽게 이해할 수 있음.

    ⇒ 모든 사용자 정보를 가져와 한 명씩 업그레이드가 가능한지 확인하고, 가능하면 업그레이드.

    private boolean canUpgradeLevel(User user){
    	Level currentLevel = user.getLevel();
    
    	if(currentLevel == BASIC) return (user.getLogin() >= 50);
    	else if(currentLevel == SILVER) return (user.getRecimmend() >= 30);
    	else if(currentLevel == GOLD) return false;
    	else throw new IllegalArgumentException("Unknown Level: " + currentLevel);
    }

    업그레이드 가능 확인 메소드

    ⇒ 주어진 user에 대해 업그레이드가 가능하면 true, 불가능하면 false 리턴.

    public enum Level{
    	GOLD(3, null), SILVER(2, GOLD), BASIC(1, SILVER);
    	
    	private final level next;
    
    	Level(int value, Level next){
    		this.value = value;
    		this.next = next;
    	}
    
    	//생략...
    
    	public Level nextLevel(){
    		return this.next;
    	}

    Level enum에 레벨의 업그레이드 순서를 관리한다면 다음 단계의 레벨이 무엇인지를 일일이 if 조건식을 만들어 비즈니스 로직에 담아둘 필요가 없음.

    또한 User의 내부 정보가 변경되는 것은 서비스로직 보다 User가 스스로 다루는 것이 적절함.

    오브젝트에게 데이터를 요구 하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리

    //UserService.upgradeLevel(User user)
    //nextLevel에 관련된 로직은 user에게 맡김
    private void upgradeLevel(User user){
    	user.upgradeLevel();
    	userDao.update(user);
    }
    
    //User.upgradeLevel()
    public void upgradeLevel(){
    	Level nextLevel = this.level.nextLevel();
    	if(nextLevel == null){
    		throw new IllegalStateException(this.leve + "은 업그레이드 불가");
    	}
    	else{
    		this.level = nextLeve;
    	}
    }

    레벨 업그레이드 작업 메소드

    처음 upgradeLevel()의 경우, 관심사 분리가 안되었고 적절한 예외처리도 안되었다.

    이를 레벨 순서와 다음 단계 레벨에 대한 로직을 Level객체에 맡김으로 해결할 수 있음.

  • 📗트랜잭션 서비스 추상화

    트랜잭션 경계설정

    • 트랜잭션 : 나눌수 없는 atomic한 작업의 단위다. 몇 개의 작업이 하나의 트랜잭션에 묶이든 상관없이 하나의 작업처럼 취급이 된다. 즉 트랜잭션은 모두 성공하던지 모두 실패하여야 한다. 여러 개의 SQL이 사용되는 작업을 하나의 트랜잭션으로 취급해야 하는 경우가 있음 ex)계좌의 송금과 입급, upgradeLevels(), …
    • 롤백 : 하나의 트랜잭션이 완전히 실행되지 못했다면 앞서 실행된 일부분의 SQL 작업을 취소시켜야 하는데, 이를 Transaction Rollback이라고 함.
    • 커밋 : 하나의 트랜잭션은 여러 작업으로 이루어질 수도 있다. 하나의 트랜잭션을 가르는 기준이 커밋이다. 커밋은 게임의 세이브 포인트와 같다고 생각하면 된다. 커밋 이후 실행되는 rollback은 가장 최근의 commit을 한 상태로 돌아오게 된다.
    Connection c = dataSource.getConnection(); 
    
    c.setAutoCommit(false); // 트랜잭션 시작 
    try { 
            PreparedStatement st1 = c.prepareStatement("(update sql)"); 
            st1.executeUpdate(); 
            PreparedStatement st2 = c.prepareStatement("(delete sql)"); 
            st2.executeUpdate(); 
            c.commit(); // 트랜잭션 커밋 
    } catch (Exception e) { 
            c.rollback(); // 트랜잭션 롤백 
    } 
    
    c.close();
    • 자동 커밋 옵션을 false로 설정함
    • commit 또는 rollback 메소드가 호출 될 때까지의 작업이 하나의 트랜잭션
    • 위 예제처럼 허너으 DB커넥션 안에서 만들어지는 트랜잭션을 로컬 트랜잭션 이라고 함.

    기존 upgradeLevels() 메소드의 트랜잭션

    • 템플릿 메소드 호출 한 번에 한 개의 DB 커넥션이 만들어지고 닫힘.
    • 일반적으로 트랜잭션은 커넥션보다 존재 범위가 짧고 템플릿 메소드가 호출될 때마다 트랜잭션이 새로 만들어지고 메소드를 빠져나오기 전에 종료됨.

    ⇒ JdbcTemplete을 사용하는 UserDao의 update() 메소드는 자동으로 UPDATE 작업의 트랜잭션을 종료시킬 것이고, 수정 결과는 영주적으로 DB에 반영

    즉, 의도한 트랜잭션 경계설정 X

    해결책 제시1) 커넥션 관리를 UserService에 맡기기

    public void upgradeLevels() throws Exception{
    	(1) DB Connection 생성
    	(2) 트랜잭션 시작
    	try{
    		(3) DAO 메소드 호출
    		(4) 트랜잭션 커밋
    	}
    	catch(Exception e){
    		(5) 트랜잭션 롤백
    		throw e;
    
    	}
    	finally{
    		(6) DB Connection 종료
    	}
    }
    • 비즈니스 로직만을 담고 있는 UserService의 메서드에 Connection 파라미터가 추가되고, 이 Connection은 모든 메서드에 파라미터를 통해서 전달되어야 한다.
    • Connection 파라미터가 UserDao에 추가되면 데이터엑세스 기술에 독립적일 수가 없다.

    해결책 제시2) 트랜잭션 동기화

    private DataSource dataSource;
    
    public void setDataSource(DataSource dataSource){ //Connection을 생성할 때 사용할 DataSource DI
    	this.dataSource = dataSource;
    }
    
    public void upgradeLevels() throws Exception {
        TransactionSynchronizationManager.initSynchronization();
            // 동기화 작업 시작.
        Connection c = DataSourceUtils.getConnection(dataSource);
            // DataSource에서 직접 가져오는 대신 DataSourceUtils에서 
            // Connection을 가져오는 이유는, ThreadSafe한 저장소에 바인딩하기 위함이다.
        c.setAutoCommit(false);
        try {
            List<User> users = userDao.findAll();
            for (User user : users) {
                if (canUpgradeLevel(user)) {
                    upgradeLevel(user); // 하나의 커넥션으로 반복적인 쿼리 실행
                }
            }
            c.commit();
        } catch (Exception e) {
            c.rollback();
    				throw e;
        } finally {
            DataSourceUtils.releaseConnection(c, dataSource);
            TransactionSynchronizationManager.unbindResource(this.dataSource);
            TransactionSynchronizationManager.clearSynchronization();
                    // 동기화 작업 종료 및 정리
        }
    }

    스크린샷 2023-08-08 오전 2.43.45.png

    ⇒ UserDao를 통해 진행되는 모든 JDBC 작업은 upgradeLevels() 메소드에서 만든 Connection 오브젝트를 사용하고 같은 트랜잭션에 참여하게 됨.

    <JdbcTemplete과 트랜잭션 동기화>

    이미 작성한 JdbcTemplate는 영리하게 동작한다. 만약 트랜잭션 동기화 저장소에 별도로 생성된 커넥션/트랜젝션이 없는 경우 원래 하던대로 커넥션을 생성한다.

    DataSourceUtils 및 TransactionSynchronizationManager로 커넥션을 생성했다면 그때부터 실행되는 JdbcTemplete의 메소드는 동기화 저장소에 들어있는 DB 커넥션을 가져와 사용

    트랜잭션 서비스 추상화

    • 하나의 트랜잭션 안에서 여러 개의 DB에 데이터를 넣는 작업이 필요
    • 별도의 트랜잭션 관리자를 통해 관리하는 글로벌 트랜잭션 방식을 사용해야 함

    ⇒ 트랜잭션을 JDBC API를 사용해서 직접 제어하지 않고 JTA를 통해 트랜잭션 매니저가 관리하도록 위임

    //JTA를 이용한 트랜잭션 코드 구조
    public void upgradeLevels() throws Exception {
        InitialContext ctx = new InitialContext();
        UserTransaction tx = (UserTransaction)ctx.lookup(USER_TX_JNDI_NAME);
        tx.begin();
        Connection c = dataSource.getConnection();
        try {
            // 데이터 액세스 코드
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            throw e;
        } finally {
            c.close();
        }

    문제점 : UserService는 자신의 로직이 바뀌지 않았음에도 기술환경에 따라서 코드가 바뀌는 코드가 돼버리고 말았다. ⇒ 서비스 추상화 필요

    스크린샷 2023-08-08 오전 3.07.31.png

    위는 스프링이 제공하는 트랜잭션 추상화 계층을 나타낸 그림이다.

    PlatformTransactionManager를 적용해보자.

    public class UserService {
        UserDao userDao;
        DataSource dataSource;
        PlatformTransactionManager transactionManager;
        ...
    
    	public void upgradeLevels() {
    	        // 트랜잭션 시작
    	        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;
    	        }
    	   }
    	...
    }
  • 📗서비스 추상화 단일 책임 원칙

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

    • UserDao-UserService의 분리 : 같은 계층에서 비즈니스 로직과 데이터 접근 로직을 분리한 수평적인 분리

    • 트랜잭션의 추상화 : 애플리케이션 레벨의 로직과 로우레벨의 트랜잭션 기술에 독립적인 코드를 분리

      스크린샷 2023-08-08 오전 3.13.39.png

    • 단일 책임 원칙 : 모든 모듈은 단 하나의 책임을 가져야 함. 하나의 모듈이 바뀌어야 하는 이유는 단 하나여야만 함.

      • 변경이 필요할 때 수정 대상이 명확해진다.
      • 코드의 수정 시 실수가 줄어든다.

      ⇒ 유지보수가 쉬워진다.

  • 📗메일 서비스 추상화

  • JavaMail을 이용한 메일 발송 기능

자바에서 메일을 발송할 때는, Java 표준인 JavaMail을 사용하면 된다.

private void sendUpgradeEmail(User user) {
    Properties props = new Properties();
    props.put("mail.smtp.host", "mail.ksug.org");
    Session s = Session.getInstance(props, null);

    MimeMessage message = new MimeMessage(s);

    try {
        message.setFrom(new InternetAddress("[email protected]"));
        message.addRecipient(Message.RecipientType.TO, new InternetAddress(user.getEmail()));
        message.setSubject("Upgrade 안내");
        message.setText("사용자님의 등급이" + user.getLevel().name()
                                                        + "로 업그레이드 되었습니다.");
    } catch (AddressException e) {
        throw new RuntimeException(e);
    } catch (MessagingException e) {
        throw new RuntimeException(e);
    }
}
// 한글 encoding 설정이 생략되어 있는 코드입니다.

SMTP 프로토콜을 지원하는 메일 서버가 있다면, 안내 메일은 잘 작동될 것이다.

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

테스트를 서버에 부하를 가하지 않고도 예외없이 실행할 수 있도록 JavaMail과 같은 인터페이스를 갖는 오브젝트를 만들면 될 것 같지만, Session 오브젝트를 만들어야 메일 메세지를 생성할 수 있고, 전송할 수 있다.,

  • Session은 인터페이스가 아닌 클래스다.
  • 생성자가 private라서 직접 생성도 불가능하다.
  • 상속도 불가능하게 final.

테스트와 서비스 추상화

일반적으로 추상화는 수행하는 기능은 같지만 로우레벨의 구현이 다른 트렌잭션이나 다양한 기술에 대해 일관적인 사용법을 제공하기 위한 접근 방법을 의미하지만, 이렇게 테스트에 도움이 안되도록 작성한 API를 테스트할 때도 유용하게 쓸 수 있다.

!https://blog.kakaocdn.net/dn/bIWCKR/btrfXHdJ9IJ/1Joh6eC26UFEgC5YiixoX1/img.png

스프링이 제공하는 MailSender를 구현한 추상화 클래스를 이용하면, JavaMail이 아닌 다른 메세징 서버 API를 이용하고자 할 때도 MailSender를 구현한 클래스를 만들어 DI해주면 끝이다.

UserService는 그냥 MailSender 인터페이스를 통해 메일을 보낸다는 사실만 알면 되지 구체적인 구현은 알 방법이 없으며, MailSender 구체 클래스가 바뀌더라도 DI만 새로 해주면 끝이니 엄청 편리하다.

만약 DB의 트랜젝션 개념을 메일에 적용한다면?

  1. 메일을 업그레이드할 유저 목록을 별도의 목록에 저장하고, 업그레이드 작업이 성공적으로 끝마쳤다면 한 번에 전송하도록 한다
  2. 유사 트랜잭션을 구현한다. send() 메서드를 호출하더라도 실제로 메일을 발송하지 않고 있다가, 작업이 끝나면 메일을 모두 발송하고, 예외가 발생한다면 메일 발송을 취소하는 방법으로 구현한다.

테스트 대역

DummyMailSender는 하는 것이 없다. 테스트를 편히 하기 위해 작성했다.

테스트 때 발송용 메서드를 제거했다가 테스트 끝나고 추가하는 것은 불가능한 일이다. 그 대신DummyMailSender를 주입함으로써 간단하게 해결했다.

우리가 테스트하고자 하는 것은 UserService였는데, 이 UserService도 여러 객체에 의존하고 있다. MailSender 구현 클래스와 같은 것들을 말한다. 테스트 환경에서 테스트 대상이 되는 오브젝트의 기능을 충실하게 수행하면서 테스트를 자주 실행할 수 있도록 하는 오브젝트들을 테스트 대역(test double)이라고 한다.

Mock

DummyMailSender는 호출이 되던 되지않던 상관없다. 테스트에 큰 영향을 끼치지 않는다.

테스트는 일반적으로 인풋에 따른 아웃풋을 검증한다. 그러나 테스트 대상 오브젝트가 의존 오브젝트에 넘기는 값과 행위를 검증하고 싶다면, 목 오브젝트(mock object)를 사용해야 한다.

이 목 오브젝트는 테스트 대상이 되는 오브젝트와 목 오브젝트에서 일어나는 커뮤니케이션을 추후 검증에 사용할 수 있도록 저장한다. 인풋과 아웃풋이 될 수도 있고, 얼마나 자주 커뮤니케이션이 일어나는지 통계적인 수치가 될 수도 있다.

@sunwootest sunwootest changed the title [6주차](임주민, 양재승, 박준영)) [6주차](임주민, 양재승, 박준영) Aug 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants