개발자0 2022. 3. 16. 21:31

1.  사라진 SQLException

 

JdbcTemplate 적용 전

public void deleteAll() throws SQLException {
	this.jdbcTemplate.update("delete from users");
}

JdbcTemplate 적용 후

public void deleteAll() {
	this.jdbcTemplate.update("delete from users");
}

초난감 예외처리 대표 유형

 

예외 블랙홀

 

catch 부분에서  해결하지 않고 그냥 넘어 가버리는 것

 

문제 1

try{
	...
}
catch(SQLException e) {
}

문제 2

try{...}
catch (SQLException e){
	System.out.println(e);
}

문제 3

try{...}
catch (SQLException e){
	e.printStackTrace();
}

 

그나마 괜찮은 예외처리

try{ ... }
catch(SQLException e){
	e.printStackTrace();
    System.exit(1);
}

 

무의미하고 무책임한 throws

 

다른 예외 처리를 매번 throws를 하기 귀찮아서 아예 throws Exception 해버리는 케이스

public void method1() throws Exception {
	method2();
    ...
}

public void method2() throws Exception {
	method3();
    ...
}

public void method3() throws Exception {
	...
}

예외 종류/특징

 

1. Error

 

java.lang.Error 클래스의 서브 클래스

 

시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용(OutOfMemoryError, ThreadDeath)

catch로 잡아봤자 대응 방법이 없음

 

애플리케이션에서 처리 X

 

2. Exception과 체크 예외

 

java.lang.Exception 클래스와 서브 클래스로 정의되는 예외

 

개발자들이 만든 애플리케이션 코드의 작업 중 예외상황이 발생했을 경우

 

Exception 클래스는 체크 예외와 언체크 예외(unchecked exception) 구분

 

체크 예외는 Exception 클래스의 서브클래스이면서 RuntimeException 클래스를 상속하지 않은 것

언체크 예외는 RuntimeException을 상손한 클래스

 

3. RuntimeException, 언체크/런타임 예외

 

java.lang.RuntimeException 클래스를 상속한 예외들은 명시적인 예외처리를 강제하지 않기 때문에

언체크 예외 또는 런타임 예외라고 함

 

catch문으로 처리하거나 안해도 되고 throw로 던지거나 안 던져도 된다.

 

대체적인 케이스 : NullPointerException, IllegalArgumentExcetion

 

예외처리 방법

 

예외 복구

 

예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려 놓기

 

1. 파일 처리 중 IOException이 발생 시

사용자에게 상황을 알려주고 다른 파일을 이용하도록 안내하기

 

2. 네트워크가 불안해서 가끔 서버에 접속이 잘 안 될 경우 정해진 횟수만큼 재시도하기. 넘기면 예외 발생

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

예외 회피

 

예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던지기

public void add() throws SQLException{
	// JDBC API
}

 

thorws문으로 선언해서 던지거나 catch로 로그 출력 후 rethrow 하기

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

getAll() 메서드

public List<User> getAll() {
	return this.jdbcTemplate.query("select * from users order by id", this.userMapper);
}

query 메서드 내부

콜백 오브젝트는 SQLException을 자신이 처리 안 하고 템플릿에게 던져 버림

execute 메서드가 catch로 잡아서 처리를 함.

 

예외 회피는 의도가 분명해야 함

콜백/템플릿처럼 긴밀한 관계에 있는 다른 오브젝트에게 예외처리 책임을 지게 하거니

자신이 사용하는 쪽에서 예외를 다루는 게 최선의 방법

 

예외 전환(exception translation)

 

예외 회피와 비슷하게 메서드 밖으로 던진다.

 

예외 전환은 2가지의 목적으로 사용

 

1. 내부에서 발생한 예외를 그대로 안 던지고 의미를 분명하게 해 줄 수 있는 예외로 변경

ex) 아이디 등록 시 아이디 중복일 경우 SQLException이 발생하는데 이 Exception의 의도를 명확하게 하기 위해 DuplicateUserIdException이라는 예외로 던짐

=> 상위 서비스 계층에서 대응 가능

public void add(User user) throws DuplicateUserIdException, SQLException {
	try {
    	// 저장 로직
    }
    catch(SQLException e){
    // ErrorCode가 MySQL의 "Duplicate Entry(1062)"이면 예외 전환
    if(e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
    	throw DuplicateUserIdException();
    else
    	throw e;
	}
}

중첩 예외(nested exception) 가능

 

생성자

catch(SQLException e){
	...
    throw DuplicateUserIdException(e);
}

initCause()

catch(SQLException e){
	...
    throw DuplicateUserIdException().initCause(e);
}

근본 원인이 되는 예외를 넣기

 

2. 예외를 처리하기 쉽고 단순하게 만들기 위해 포장(wrap)하기

 

중첩 예외와 방식은 같으나 의미를 명확하게 하려고 하는 게 아니라 예외처리를 강제하는 체크 예외를 런타임 예외로 바꾸는 경우 사용

 

ex) EJBException

try {
	OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
    Order order = orderHome.findByPrimaryKey(Integet id);
} catch (NamingException ne) {
	throw new EJBException(ne);
} catch (SQLException se) {
	throw new EJBException(se);
} catch (RemoteException re) {
	throw new EJBException(re);
}

EJBException은 런타임 예외이다.

런타임 예외에 근본의 원인을 포함시켜 던지게 되면 시스템 익셉션으로 인식하고 트랜잭션을 자동으로 롤백해준다.

 

예외처리 전략

 

add() 메서드의 예외처리

 

SQLException은 대부분 복구 불가능한 예외이므로 굳이 처리할 필요가 없으므로 RuntimeException에 담아서 trow

DuplicateUserIdException은 충분히 복구 가능한 예외이고 add() 메서드를 사용하는 쪽에서 활용할 수 있기 때문에

 throw

public class DuplicateUserIdException extends RuntimeException {
	
	public DuplicateUserIdException(Throwable cause) {
		super(cause);
	}

}
public void add() throws DuplicateUserIdException {
	try {
    	// JDBC를 이용한 user 정보 등록
    }
    catch (SQLException e) {
    	if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
        	throw new DuplicateUserIdException(e);
        else
        	throw new RuntimeException(e);
    }
}

 

애플리케이션 예외

 

시스템 또는 외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고,

반드시 catch 해서 무엇인가 조치를 취하도록 요구하는 예외

 

ex) 사용자가 요청한 금액을 은행계좌에서 출금하는 기능을 가진 메소드

 

방법 1. 정상적인 출금 처리를 했을 경우와 잔고 부족이 발생했을 경우의 다른 종류의 리턴 값

단점 : 리턴 값을 명확하게 코드 화하고 잘 관리하지 않으면 혼란, 결과 값을 확인하는 조건문 자주 등장으로 인한 코드가 지저분

 

방법 2. 정상적인 흐름을 따르는 코드는 그대로 두고, 잔고 부족과 같은 예외 상황에서는 비즈니스적인 의미를 띤 예외를 던지도록 만듦

InsufficientBalanceException은 체크 예외로 만들고 무조건 예외처리하도록 함.

try {
	BigDecimal balance = account.withdraw(amount);
    ...
    // 정상적인 처리 결과를 출력하도록 진행
}
catch(InsufficientBalanceException e) {
	// InsufficientBalanceException에 담긴 인출 가능한 잔고금액 정보를 가져옴
    BigDecimal availFunds = e.getAvailFunds();
    ...
    // 잔고 부족 안내 메시지를 준비하고 이를 출력하도록 진행
}

SQLException은 어디에?

 

SQLException은 대체적으로 99% 코드레벨에서는 복구할 방법이 없음

JdbcTemplate는 예외처리 전략을 따르고 있음

 

모든 SQLException은 DataAccessException에 포장해서 던져준다.

2. 예외전환

JDBC의 한계

 

예외가 발생한 원인은 SQLException 안에 담긴 에러 코드와 SQL 상태정보를 참조해야 한다.

SQLException의 getErrorCode()로 가져올 수 있는 DB 에러 코드는 DB 별로 다름(DB 벤더가 정의한 고유한 에러 코드를 사용하기 때문)

if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)

=> 만약 DB가 바뀐다면 에러 코드도 달라지므로 좋은 방법은 아님

 

SQLException은 예외가 발생했을 때의 DB 상태를 담은 SQL 상태정보를 제공

상태정보는 DB별로 달라지는 에러코드를 대신할 수 있도록 Open Group의 XOPEN SQL 스펙에 정의된 SQL 상태 코드를 따르도록 되어 있음

ex) 통신장애로 DB연결 실패 08S01

08인 앞 두자리는 클래스 코드, S01인 뒤 세자리는 서브클래스 코드로 분류 되어있음

=> DB의 드라이버에서 SQLException을 담을 상태코드를 정확하게 만들어주지 않음

ex) 표준 코드와는 상관없는 엉뚱한 값, 서브클래스 코드는 일체 무시하고 값 다 0인 값

 

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

 

DB 에러 코드는 DB 버전이 올라가더라고 어느 정도 일관성이 유지된다.

 

스프링은 DataAccessException의 서브클래스로 세분화된 예외 클래스들을 정의

ex) BadSqlGrammarException, DataAccessResourceFailureException, DataIntegrityViolationException, DuplicatedKeyException 등등

 

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

<bean id="Oracle" class="org.springframework.jdbc.support.SQLErrorCodes">
	<property name="badSqlGrammarCodes">
    	<value>900,903,904,917,936,942,17006</value>
    </property>
    ...
</bean>

드라이버나 DB 메타 정보를 참고해서 DB 종류를 확인하고 DB별로 미리 준비된 매핑정보 테이블을 이용해 적절한 예외 클래스를 만들어줌

 

키 중복일 경우 throws DuplicateKeyException을 던지면 된다.

public void add(final User user) throws DuplicateKeyException {
	this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)", user.getId(), user.getName(), user.getPassword());
}

애플리케이션 레벨의 체크 예외인 DuplicateUserIdException을 던지게 할려면 catch로 DuplicateKeyException로 잡아서

DuplicateUserIdxException에 담아서 던지면 된다.

public void add(final User user) throws DuplicateKeyException {
	try{
    	this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)", user.getId(), user.getName(), user.getPassword());
    } catch(DuplicateKeyException e){
    	// 로그 남기기
        throws new DuplicateUserIdException(e);
    }
	
}

 

기술에 독립적인 UserDao 만들기

 

UserDao interface 만들기

public interface UserDao {
	
	void add(final User user);
	
	User get(String id);
	
	int getCount();
	
	void deleteAll();
	
	List<User> getAll();

}

기존 UserDao를 UserDaoJdbc로 클래스명을 변경해주고 UserDao를 implements를 해준다.

public class UserDaoJdbc implements UserDao {
	
	private JdbcTemplate jdbcTemplate;
	
	private RowMapper<User> userMapper = new RowMapper<User>() {
		@Override
		public User mapRow(ResultSet rs, int rowNum) throws SQLException {
			User user = new User();
			user.setId(rs.getString("id"));
			user.setName(rs.getString("name"));
			user.setPassword(rs.getString("password"));
			return user;
		}
	};

	public void setDataSource(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

	public void add(final User user) throws DuplicateKeyException {
		this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)", user.getId(), user.getName(), user.getPassword());
	}
	
	public User get(String id) {
		return this.jdbcTemplate.queryForObject("select * from users where id = ?", new Object[] { id },
				new int[] { java.sql.Types.VARCHAR }, this.userMapper);
	}
	
	public int getCount() {
		return this.jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
	}
	
	public void deleteAll() {
		this.jdbcTemplate.update("delete from users");
	}
	
	public List<User> getAll() {
		return this.jdbcTemplate.query("select * from users order by id", this.userMapper);
	}

}

스프링 설정파일의 userDao부분을 UserDaoJdbc로 클래스명을 변경해준다.

<bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
	<property name="dataSource" ref="dataSource" />
</bean>