[Spring] 토비의 스프링 Vol.1 4장 예외

Shiba·2023년 8월 16일
0

🍀 스프링 정리

목록 보기
5/21
post-thumbnail

📗 예외

❗ 토비의 스프링 3.1 vol 1 정리입니다.
책을 읽지 않으셨다면 이해가 어려울 수 있습니다!


📖 4.1 사라진 SQLException

📜 3장에서 만든 JdbcTemplate을 적용하도록 바꾸기 전 후의 deleteAll()메소드

//적용 전
public void deleteAll() throws SQLException {
	this.jdbcContext.executeSql("delete from users");
}
//적용 후
public void deleteAll(){
	this.jdbcTemplate.update("delete from users");
}

⁕ JdbcTemplate적용 후 예외처리가 사라짐!! -

📝 초난감 예외처리

초난감 예외처리의 대표주자들을 살펴보자!

◼ 예외 블랙홀

  • 예외를 그냥 try/catch문으로 감싸기만 하는 코드
try{
	...
}
catch(SQLException e) { //예외를 잡은뒤 그냥 넘겨버림
}

- 예외를 그냥 넘겨버리고 실행되므로, 치명적인 오류가 발생할 수 있으며, 오류가 어디서 어떤이유로 생겼는지 알기가 힘들어짐.

  • 예외를 콘솔로그에 프린트함으로써 처리하는 코드
} catch(SQLException e){
	System.out.println(e);
}
} catch(SQLException e){
	e.printStackTrace();
}

- 개발 중 테스트를 위함이 아닌, 실제 서비스 코드에 이러한 코드가 있다면 콘솔로그를 계속 모니터링 하고 있지 않는 한, 오류가 뜨는걸 놓쳐버려 치명적인 오류가 생길 수도 있다.

▶ 모든 예외는 적절하게 복구되거나 작업을 중단시키고 운영자 혹은 개발자에게 통보되어야 한다!

  • 그나마 나은 예외처리 코드
} catch(SQLException e){
	e.printStackTrace();
    System.exit(1); // 차라리 종료시켜서 치명적인 오류를 막도록하자
}

◼ 무의미하고 무책임한 throws

  • 예외를 던지는 코드 작성이 귀찮아져 기계적으로 붙인 Exception
public void method1() throws Exception {
	method2();
    ...
}
public void method2() throws Exception {
	method3();
    ...
}
public void method3() throws Exception {
    ...
}

//3에서 예외발생시 2 -> 1 -> Exception으로 예외를 전가시켜버리는 코드

- 예외 블랙홀 코드보다는 낫지만.. 어떤 예외로 오류가 발생했는지를 알지 못함.
따라서 제대로 복구를 할 수 있는 예외조차도 이유를 몰라 고칠 수 없는 경우 발생

📝 예외의 종류와 특징

Error

java.lang.Error 클래스의 서브클래스들. 시스템에 비정상적인 상황이 발생했을 경우에 사용
- 자바 가상머신에서 발생시키는 것이므로 애플리케이션 코드로 잡을수 없음
▶ 시스템 레벨에서 특별한 작업을 하는게 아니라면 신경쓰지 않아도 된다.

Exception과 체크 예외

java.lang.Exception 클래스와 그 서브클래스로 정의. 개발자가 만든 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용
  Exception 클래스는 다시 체크 예외와 언체크 예외로 구분.

  • 체크 예외
    - 던질 때, catch문으로 잡거나 throws로 다시 메소드 밖으로 던져야 한다.
    그렇지 않으면 컴파일 에러 발생
    ◈ 요즈음에는 예외처리를 강제하는 것 때매 비난을 받는 경우가 발생

▼ 자바의 정석 기초편 제 8장 예외

RuntimeException과 언체크/런타임 예외

java.lang.RuntimeException 클래스를 상속한 예외들. catch문으로 잡거나 throws로 선언하지 않아도 상관은 없다.
- 개발자들의 부주의로 발생하는 예외들이므로 예상가능한 범위내의 예외이기 때문

📝 예외처리 방법

◼ 예외 복구

예외상황을 파악하고 문제를 해결하여 정상 상태로 돌려놓는 방법

ex) 사용자의 네트워크 불안정으로 접속이 안되는 경우
- DB에 접속하다가 SQLException이 발생 가능
- 이 경우, 일정시간 간격으로 연결을 재시도하는 방법을 시도해볼 수 있음
연결이 된다면, 정상적으로 서비스를 이용할 수 있으므로 예외를 복구한 상태가 됨.

int maxRetry = MAX_RETRY; //최대 재시도수
while(maxRetry --> 0) {
	try{
    	...
        return;
    }
    catch(SomeException e) {
    	//로그 출력, 정해진 시간만큼 대기
    }
    finally {
    	//리소스 반납. 정리 작업
    }
}
throws new RetryFailedException(); //최대 재시도 횟수를 넘기면 직접 예외 발생

◼ 에외처리 회피

예외처리자신을 호출한 쪽으로 던져버리는 방법

예외를 throws문으로 선언하여 알아서 던져지게 하거나 catch문으로 로그를 남긴후 다시 던지는 것.
- 예외를 던져서 회피할 때는 꼭 예외를 대신 처리할 수 있는 오브젝트나 메소드에 던져야 한다.

public void add() throws SQLException {
    //JDBC API
}
public void add() throws SQLException {
	try {
    	//JDBC API
    }
    catch(SQLException e) {
    	//로그출력
        throw e;
    }
}

◼ 예외 전환

예외를 메소드 밖으로 던질 때, 적절한 예외로 전환해서 던지는 방법

◼ 목적

1). 예외를 그대로 던지는 것적절한 의미를 부여해주지 못하는 경우, 상황에 적합한 의미를 가진 예외로 변경하기 위함
ex) DB에 같은 사용자가 있어 SQLException오류 발생 시,
SQLException -> DuplicatedUserIdException으로 변경하여 적절한 복구 작업을 수행하도록 함

public void add(User user) throws DuplicatedUserIdException, SQLException{
	try{
    	//JDBC를 이용해 user정보를 DB에 추가하는 코드
        // 또는 그런 기능을 가진 다른 SQLException을 던지는 메소들르 호출하는 코드
    }
    catch (SQLException e){
    	//ErrorCode가 MySQL의 "Duplicate Entry(1062)"이면 예외 전환
        if(e.getErrorCode() == MySqlErrorNumbers.ER_DUP_ENTRY)
        	throw DuplicatedUserIdException();
        else
        	throw e; //그외의 경우는 그대로 던지기
    }
}

* 예외를 던질 때, 중첩 예외로 만드는 것이 좋음

//중첩예외1
catch(SQLException e) {
	...
    throw DuplicatedUserIdException(e);
//중첩예외2
catch(SQLException e) {
	...
    throw DuplicatedUserIdException().initCause(e);
}

2). 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것. 중첩예외로 던지는 방식은 같으나 의미의 명확성이 아닌, 체크 예외를 언체크 예외로 바꾸는 경우에 사용
ex) EJB에서 발생하는 대부분의 체크 예외는 의미 있는 예외이거나 복구 가능한 예외가 아니다. 따라서 런타임 예외인 EJBException로 던지는게 낫다.

try{
	OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
    Order order = orderHome.findByPrimaryKey(Integer id);
} catch (NamingException ne) {
	throw new EJBException(ne); //예외 포장
} catch (SQLException se) {
	throw new EJBException(se); //예외 포장
} catch (RemoteException re) {
	throw new EJBException(re); //예외 포장
}

📝 예외처리 전략

◼ 런타임 예외의 보편화

서버환경에서는 체크 예외의 활용도와 가치가 점점 떨어지고 있음.
- 체크 예외를 언체크 예외로 정의하는 것이 일반화되고 있음
- 복구할 수있는 예외가 아니라면 일단 언체크로 만들어 런타임예외를 던지도록 함.

◼ add() 메소드의 예외처리

  • 위에서 보았던 add()메소드
public void add(User user) throws DuplicatedUserIdException, SQLException{
	try{
    	//JDBC를 이용해 user정보를 DB에 추가하는 코드
        // 또는 그런 기능을 가진 다른 SQLException을 던지는 메소들르 호출하는 코드
    }
    catch (SQLException e){
    	//ErrorCode가 MySQL의 "Duplicate Entry(1062)"이면 예외 전환
        if(e.getErrorCode() == MySqlErrorNumbers.ER_DUP_ENTRY)
        	throw DuplicatedUserIdException();
        else
        	throw e; //그외의 경우는 그대로 던지기
    }
}
  • SQLException은 복구 불가능하므로 잡아봤자 할 수있는게 없음
    - 런타임예외로 포장하여 다른 메소드들이 신경쓰지 않도록하는게 낫다.
  • DuplicatedUserIdException를 굳이 체크예외로 둬야 하는 것은 아님
    - 어디에서든 잡아서 처리가 가능하다면 런타임 예외로 만드는 것이 낫다.
    하지만, add()메소드는 명시적으로 DuplicatedUserIdException을 던진다고 선언해야 함.
  • 수정한 메소드
// 예외 클래스 선언 - 런타임 예외로 만들기
public class DuplicatedUserIdException extends RuntimeException {
	public DuplicatedUserIdException(Throwable cause) {
    	super(cause);
    }
}

//수정한 add()메소드
public void add() throws DuplicatedUserIdException {
	try{
    	//JDBC를 이용해 user정보를 DB에 추가하는 코드
        // 또는 그런 기능을 가진 다른 SQLException을 던지는 메소들르 호출하는 코드
    }
    catch (SQLException e){
    	//ErrorCode가 MySQL의 "Duplicate Entry(1062)"이면 예외 전환
        if(e.getErrorCode() == MySqlErrorNumbers.ER_DUP_ENTRY)
        	throw DuplicatedUserIdException(e); //예외 전환
        else
        	throw new RuntimeException(e); //예외 포장
    }
}

◼ 애플리케이션 예외

◼ 애플리케이션 예외란?

시스템 또는 외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고, 반드시 catch해서 무엇인가 조치를 취하도록 요구하는 예외
ex) 은행에서 출금을 할 때, 계좌 잔액을 넘어서는 범위의 출금을 할 시, 경고를 보내는 것

◼ 처리 방법

1). 정상적인 출금처리를 한 경우, 잔고 부족이 발생한 경우 서로 다른 리턴 값을 주는 것
- 리턴 값을 명확하게 코드화하여 관리하지 않으면 혼란이 생길 수도 있다.


2). 잔고 부족과 같은 예외 상황에서는 비지니스적인 의미를 띤 예외를 던지도록 만들기
ex) 잔고 부족 예외 발생시 InSufficientBalanceException 예외를 만들어 던지기
- 의도적으로 체크 예외로 만들어 예외상황을 구현하도록 강제해주기

📝 SQLException은 어떻게 됐나?

◼ SQLException은 복구가능한 예외인가?

99%SQLException은 코드 레벨에서는 복구할 방법이 없다.
- 예외처리 전략을 적용해 언체크/런타임 예외로 전환해줘야 한다.

◼ JdbcTemplate의 예외처리 전략

JdbcTemplate템플릿과 콜백 안에서 발생하는 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던져줌.
- JdbcTemplate을 사용하는 UserDao메소드에선 꼭 필요한 경우에만 DataAccessException을 잡아 던져주면 됨.

▶ JdbcTemplate에서 이미 예외를 잡아서 던지므로, 굳이 deletAll()메소드에서 예외를 던질 이유가 없음!

📖 예외 전환

📝JDBC의 한계

DB종류에 상관없이 표준 드라이버를 통해 일관적인 개발이 가능하게 해주지만, DB를 자유롭게 바꾸어 사용할 수 있도록 데이터 액세스 코드를 작성하는 것은 어렵다.
- 두 가지 걸림돌이 존재

데이터 액세스 기술이란?

데이터베이스에 저장된 데이터에 접근하여 사용할 수 있도록 하는 기술
- mybatis, Spring JDBC, JPA, Hibernate 등이 있다.
- 데이터 액세스 기술마다 성격 또는 예외가 다름.

◼ 비표준 SQL

SQL을 만들 때, DB의 최적화를 위하여 비표준 SQL 문장(DB마다 다름)을 사용
- 비표준 문장 사용으로 특정 DB에 대해 클래스가 종속적인 형태가 되어버림

◼ SQLException의 DB에러 정보

SQLException발생 시, 등장하는 에러의 정보가 DB마다 다름
- 에러 코드가 DB마다 달라 호환이 되지않아 다른 DB사용 시 오류 발생

📝 DB에러 코드에 대한 해결책

호환이 되지않는 DB에러 코드에 대한 해결책을 알아보자!

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

스프링은 에러코드를 매핑해두어 같은 오류일 경우 같은 예외로 취급!

public void add() throws DuplicateKeyException { // 키값이 중복되는 예외
	//JdbcTemplate을 이용해 User를 add하는 코드
}
public void add() throws DuplicateUserIdException {
	try{
    	// jdbcTemplate을 이용해 User를 add하는 코드
    }
    catch(DuplicateKeyException e){
    	//로그를 남기는 등의 필요한 작업
        throw new DuplicateUserIdException(e); //예외 중첩
    }
}

📝 DAO 인터페이스와 DataAccessException 계층구조

DataAccessException은 JDBC의 SQLException을 전환하는 용도 뿐만 아니라 자바 데이터 액세스 기술에서 발생하는 예외에도 적용된다.
- 왜 DataAccessException 계층구조를 이용해 독립적인 예외를 정의하고 사용할까?

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

DAO를 따로 만들어서 사용하는 이유?
- 데이터 엑세스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓기 위해서
- 분리된 DAO는 전략패턴을 통해 구현방법을 변경할 수 있게됨

하지만 메소드 선언에 나타나는 예외 정보가 문제가 될 수 있음!

public interface UserDao {
	public void add(User user); //add()는 예외 발생시 SQLException을 던짐
    ...
//이렇게 선언하면 예외를 처리할 수 없음!
public interface UserDao {
	public void add(User user) throws SQLException;
    ...
//이렇게 선언하면 데이터 액세스 기술로 DAO를 전환하면 사용 불가!
//데이터 액세스 기술의 API는 독자적인 예외를 던지기 때문

예외를 런타임 예외로 포장할 수 있다면 처음에 의도했던대로 선언이 가능!
- 하지만 데이터 액세스 기술이 달라지면 같은 상황에서도 다른 예외가 던져진다..

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

위와 같은 문제점 때문에 스프링은 DataAccessException 계층구조 안에 자바 액세스 기술을 사용할 때 발생하는 예외들을 추상화하여 정리해놓았다.
-공통적인 예외 뿐만 아니라 일부 기술에서만 발생하는 예외들도 대부분 정리해둠.

📝 기술에 독립적인 UserDao 만들기

◼ 인터페이스 적용

사용자 처리 DAO는 UserDao로, JDBC를 이용해 구현한 클래스의 이름을 UserDaoJdbc라고 하자.

//UserDao 인터페이스 만들기
public interface UserDao {
	void add(User user);
    User get(String id);
    List<User> getAll();
    void deleteAll();
    int getCount();
    //setDataSource()는 UserDao에 따라 달라질 수 있음!
}

//이제 기존의 UserDao클래스의 이름을 UserDaoJdbc로 변경후 implements UserDao를 붙여주자!
public class UserDaoJdbc implements UserDao {
	...
}

◼ 테스트 보완

기존의 UserDao를 테스트하던 코드를 보완해보자!

public class UserDaoTest{
	@Autowired
    private UserDao dao; // UserDao는 클래스가 아닌 인터페이스가 되었다!
    ...
}

⁕ UserDao인스턴스 변수 선언도 UserDaoJdbc로 변경해야 할까?
- 변경할 필요가 없다!
- UserDaoJdbc는 UserDao 인터페이스를 구현한 클래스이므로 같은 타입이기 때문

  • 중복된 키를 가진 정보를 등록했을 때 어떤 예외가 발생하는지를 확인하는 테스트
@Test(expected=DataAccessException.class)
public vodi duplicateKey() {
	dao.deleteAll();
    
    dao.add(user1);
    dao.add(user1); // 같은 값을 두번 등록해 예외 발생
}

DataAccessException사용 시 주의사항

아직까지 완벽하게 예외들이 세분화되어있지않아 사용에 주의가 필요함!
ex) DuplicateKeyException은 Hibernate에서는 던져지지않는 예외
- 따라서, 미리 테스트를 통해 던져지는 예외의 종류를 확인할 필요가 있음


❗ 더욱 상세한 내용을 알고싶으시다면 책을 구매하시는 것을 추천드립니다.

profile
모르는 것 정리하기

1개의 댓글

comment-user-thumbnail
2023년 8월 16일

많은 도움이 되었습니다, 감사합니다.

답글 달기