4.1 사라진 SQLException


jdbcTemplate 로 바꾼후에 thorws SQLException을 없애도 에러가 발생하지 않는 것을 볼 수 있다. catch블록으로 예외를 잡은 이후에 아무런 처리를 하지 않았기 때문에 그대로 예외가 소멸해버린 케이스이다.

 

 예외를 처리할 때 반드시 지켜야 할 핵심 원칙은 한가지다. 모든 예외는 적절하게 복구되던지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 한다. 


 예외의 종류와 특징

- Error

java.lang.Error클래스의 서브클래스들이다. 시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용되는데 애플리케이션 코드에서 잡으려고 하면 안된다. 애플리케이션단에서 처리할 방법이 없기 때문이다.



- Exception과 체크 예외

Exception클래스는 checked exception과 unchecked exception으로  구분되는데 전자는 RuntimeException클래스를 상속하지 않은 것들이고, 후자는 RuntimeException을 상속한 클래스들을 말한다.

일반적으로 예외라고하면 RuntimeException을 상속하지 않은 체크예외라고 생각하면 되는데, catch문으로 잡든지 아니면 throws를 정의해서 메소드 밖으로 던져야 한다. 그렇지 않으면 컴파일 에러가 발생한다.


- RuntimeException과 uncheck/runtime exception

java.lang.RuntimeException 클래스를 상속한 예외들은 명시적인 예외처리를 강제하지 않기때문에 uncheckException이라고 불린다. 이런 uncheck exception들은 굳이 catch문으로 잡거나 throws로 던지지 않아도 된다.(물론 던져도 된다.) 대부분 개발자의 부주의로 발생하는 에러로 조건을 세심히 체크하면 피할 수 있는 에러들이다.


예외처리 방법

- 예외 복구

 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려 놓는 방법이다. 예를들면 특정 파일에 접근했으나 해당 파일이 존재하지 않아 IOException이 발생했을 때, 해당 예외를 감지하고 상황을 사용자에게 알린 후, 다른 파일에 접근하도록 해서 예외상황을 해결한 경우가 예외 복구 방법이다.


- 예외처리 회피

예외를 자신이 담당하지 않고 호출한 쪽으로 던져버리는 경우에 해당한다. 당연히 catch로 잡아서 외부에서 잡을 수 있도록 다시 throw해줘야한다. 


- 예외 전환

예외 회피와 비슷하게 자신이 담당하지 않고 호출한 쪽으로 던져버리는 경우이지만, 차이점은 발생한 예외를 적절한 다른 예외로 바꿔서 던지는 경우이다. 예외전환의 목적은 두가지인데 첫번째로, 불필요한 catch/throws를 줄여주는 용도이고 두번째는 예외를 좀더 의미있고 추상화된 예외로 바꿔서 던져주기 위함이다.



check 예외는 복구할 수 없는 환경에서도 무분별하게 사용되어 예외처리를 짜증나게 만들었다. 요즘 트렌드는 복구할 수 없는 환경이 발생할 가능성이 높은 예외들은 uncheck 예외로 처리하는 경향이 많다. 


애플리케이션 예외 : 시스템 또는 외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고, 반드시 catch해서 무엇인가 조치를 취하도록 요구하는 예외. 일반적으로 이러한 경우에는 복구 가능한 예외들이 대다수고 따라서 일반적으로 의도적인 체크 예외를 만든다. 


 예를 들어보자면 고객이 은행 계좌에서 출금을 요청했는데 은행 계좌에 잔고가 없는 경우라고 해보자. 일반적으로 C에서는 이런 경우에 특별한 값을 리턴하게 만들었다. -1이나 -999등이 이에 해당한다. 하지만 이런 식의 코드는 이해하기 어렵고 실수를 유발 할 수 있다. 이런 경우에 애플리케이션 예외를 발생시켜 예외에 관한 자세한 정보를 넘겨주면 깔끔하게 처리 해 줄 수 있을 것이다. 



그렇다면 SQLException은 복구 가능한 예외인가에 관해서 이야기 해보도록 하자. SQL문이 잘못됬거나 DB서버가 다운 됐거나, DB커넥션 풀이 꽉찼거나, 제약조건을 위반했거나... 발생하는 대부분의 예외들이 복구 불가능한 예외들이다.( 코드를 수정해야하거나 시스템적으로 만져야하는 경우이다.) 따라서 이러한 복구 불가능한 예외들은 uncheck 예외 즉, runtimeException으로 전환해줘야한다.

 스프링의 JdbcTemplate는 이 예외처리 전략을 따르고 있는데, JdbcTemplate안에서 발생한 모든 SQLException을 런타임 예외인 DataAccessException으로 전환해서 던져준다. UserDao메소드에서 꼭 필요한 경우에만 런타임 예외인 DataAccessException을 잡아서 처리하면 되고 그 외의 경우는 무시해도 된다. 



4.2 예외 전환

 JDBC에는 한계가 존재하는데 첫번째는 비표준 SQL을 다룰 수 없다는 것이다. 아무래도 여러 데이터베이스를 범용적으로 커버하기 위해서는 표준에서 벗어난 SQL을 JDBC에 담을 수는 없었을 것이다.

 정말 재밌게도 두번째 한계는 SQLException이 데이터베이스에 종속이라는 점이다. JDBC는 SQLException하나만을 던지는데 정확히 어떤 예외가 발생한 것인지 확인하기 위해서는 SQLException의 에러코드를 확인해야만 한다. 이 에러코드는 데이터베이스마다 다르기 때문에 데이터베이스를 변경하게 될 경우, 코드를 변경해야하는 상황이 발생할 것이다. 데이터베이스의 에러상태코드가 표준화 되어 있기는하지만 DB의 JDBC드라이버에서 SQLException을 담을 상태코드를 정확하게 만들어주지 않는다는 문제도 있다. 즉, 아무래도 상태코드를 믿고 코드를 작성하는 것은 위험할 수 있다.


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

 각각의 데이터베이스 회사들은 에러 코드 매핑 파일을 제공한다. 이 맵핑 파일은 상태코드와 상태코드가 뜻하는 바를 담고 있는데, JdbcTemplate는 드라이버의 DB메타 정보를 참고해서 적절한 예외를 던져준다.이 예외들은 모두 DataAccessException 계층 구조의 예외로 포장해서 던져준다. 따라서 JdbcTemplate을 이용한다면 JDBC에서 발생하는 DB관련 예외는 거의 신경 쓰지 않아도 된다. 예를들자면 add메소드의 예외, 즉 키 중복 예외의 경우에는 DataAccessException의 서브 클래스인 DuplicateKey Exception으로 매핑되서 던져진다. 아래는 코드 예시다.


1
2
3
4
5
6
7
8
9
public void add(final User user) throws DuplicateUserIdException{
    try {
        jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
                user.getId(), user.getName(), user.getPassword());
    }
    catch(DuplicateKeyException e){
        throw new DuplicateUserIdException(e);
    }
}
cs




이 이후에 설명되는 내용들은 특정 Dao가 db에 종속적인 부분이 있음을 알리고, 이 부분을 디비에 종속적인 부분을 따로 분리하는 방법을 소개한다. 


하지만 나는 아직 그정도 레벨은 아니라고 생각해서 읽어만 봤다(실제로 데이터베이스를 변경하면서 테스트할 여력도 없다. 시간이없엉!)



소스코드 : https://github.com/choiking10/spring_test/tree/vol1_4.1_4.3/lifeignite



아직 작은 서비스에 관해서 생각하고 있어서 인지, 데이터베이스를 변경하는 큰 작업..정도는 손으로해도 되지 않을까하는 안일한 마음이 있다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ


언제나 개발할 때 , 미래를 생각하는 것도 중요하지만 어느정도 퍼포먼스가 나와야 한다고 생각하는데, 일단 데이터베이스 변경은 내게 있어서 와닿지가 않아서... 인덱스만 걸어두고 언제가 필요할 때 쯤 찾아봐야겠다... 

'web > spring' 카테고리의 다른 글

스프링 공부 일시 중지  (0) 2017.08.04
[토비 vol1] 3.5~3.7 템플릿(2)  (0) 2017.07.26
[토비 vol1] 3.1~3.4 템플릿(1)  (0) 2017.07.26
[토비 vol1] 2.4~2.6 테스트(2)  (0) 2017.07.25
[토비 vol1] 2.1~2.3 테스트(1)  (0) 2017.07.25

공부하기 싫다. 





3.5 템플릿과 콜벡


 우리는 지금까지 전략패턴을 적용하고 익명 내부 클래스를 활용한 방식으로 코드를 작성해왔다. 이런 방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다. 

이 때, 템플릿은 미리 만들어둔 모양이 있는 툴을 가리킨다. 

콜백은 실행되는 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 이 때, 이 오브젝트는 파라미터로 전달되지만 값을 참조하기 위함이아니라 특정 로직을 담은 메소드를 실행시키기 위해 사용한다. 


템플릿/콜백의 특징

1) 일반적으로 단일 메소드 인터페이스를 사용한다. 다만, 여러가지 종류의 전략을 사용해야한다면 하나이상의 코백 오브젝트를 사용할 수 있다.

2) 콜백 인터페이스의 메소드에는 보통 파라미터가 있는데, 이 파라미터는 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달 받을 때 사용한다.


콜백의 분리와 재활용

우리가 구현해온 탬플릿/콜백 패턴은 익명 내부클래스를 통해서 작성된 코드이다보니 적지 않게 중첩된 괄호가 만들어지게된다. 이 부분을 분리해 보도록하자.


1
2
3
4
5
6
7
8
9
10
11
public void deleteAll() throws SQLException{
    executeSql("delete from users");
}
public void executeSql(final String query) throws SQLException{
    this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
        @Override
        public PreparedStatement makePreParedStatement(Connection c) throws SQLException {
            return c.prepareStatement(query);
        }
    });
}
cs


애초에 필요한 것은 sql문장 뿐이었으니 위처럼 익명 클래스를 만드는 작업을 분리를 해서 재활용해보도록 하자. 또한, 이 executeSql은 UserDao뿐만 아니라 다른 Dao에서도 생성 될 수 있다. 따라서 우리가 이전에 만들어둔 JdbcContext클래스로 옮기도록하자


deleteAll 메소드가 더 간결하고 보기 편해졌다. 


 * * * 예제 * * *

 파일에서 라인별로 숫자를 입력받아 더한 후 출력하는 것을 템플릿 콜백으로 작성하라
 파일에서 라인별로 숫자를 입력받아 곱한 후 출력하는 것을 템플릿 콜백으로 작성하라





이제 JdbContext를 버리고 스프링이 제공하는 JDBC 코드용 기본 템플릿을 사용해보자. 클래스명은 JdbcTemplate이다


1
2
3
4
5
public void setDataSource(DataSource dataSource){
    this.dataSource = dataSource;
    
    jdbcTemplate = new JdbcTemplate(dataSource);
}
cs


다음과 같이 설정한 후 deleteAll에 적용해보도록 하자.

1
2
3
4
5
6
7
8
public void deleteAll() throws SQLException{
    jdbcTemplate.update(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
            return connection.prepareStatement("delete from users");
        }
    });
}
cs


더 편하게 다음과 같이 사용할 수도 있다.

1
2
3
public void deleteAll() throws SQLException{
    jdbcTemplate.update("delete from users");
}
cs


add 메소드는 다음과 같이 편하게 구현할 수 있다.



1
2
3
4
public void add(final User user) throws ClassNotFoundException, SQLException{
    jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
            user.getId(), user.getName(), user.getPassword());
}
cs



getCount는 다음과 같이 구현할 수 있다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int getCount() throws SQLException{
    return this.jdbcTemplate.query(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
 
            return connection.prepareStatement("select count(*) from users");
        }
    }, new ResultSetExtractor<Integer>() {
        @Override
        public Integer extractData(ResultSet resultSet) throws SQLException, DataAccessException {
            resultSet.next();
            return resultSet.getInt(1);
        }
    });
}
cs


JdbcTemplate은 이보다 더 간단한 방식으로 처리하는 메소드로 queryForInt()라는 친구를 제공한다고 써저 있으나 spring의 3.2.2버전에서 deprecated됐다. 

 


1
2
3
public int getCount() throws SQLException{
    return this.jdbcTemplate.queryForObject("select count(*) from users",Integer.class);
}
cs


대신 위와 같이 구현이 가능하다.


get 메소드는 이보다 좀더 복잡한데, qureyForObject를 통해서 구현이 가능하다



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public User get(String id) throws ClassNotFoundException, SQLException{
    return this.jdbcTemplate.queryForObject(
            "select * from users where id = ?",
            new Object[]{id},
            new RowMapper<User>() {
                @Override
                public User mapRow(ResultSet resultSet, int i) throws SQLException {
                    User user= new User();
                    user.setId(resultSet.getString("id"));
                    user.setName(resultSet.getString("name"));
                    user.setPassword(resultSet.getString("password"));
                    return user;
                }
            });
}
cs



원래 get 메소드는 EmptyResultDataAccessException을 던지게 만들어져 있었는데, 위의 queryForObject가 조회결과가없을 경우에는 해당 Exception을 발생시켜준다.


여기까지 테스트하고 확인!



 * * * 예제 * * *

 getAll을 구현해보자!

 TDD할 것 (즉 테스트 먼저 생성 후,  


이 이후에 네거티브 테스트에 관하여 나온다. 요약해보자면, 테스트를 만들 때, 부정적인 테스트를 먼저 만들라는 것이다. 예를 들자면, 나이에 음수값을 넣어보거나 생일에 문자를 넣어보는 등에 관련 된 테스트에 관하여 중요하게 설명하고 있다. 



소스코드 : https://github.com/choiking10/spring_test/tree/vol1_3.5_3.7/lifeignite  



'web > spring' 카테고리의 다른 글

스프링 공부 일시 중지  (0) 2017.08.04
[토비 vol1] 4.1~4.3 예외  (0) 2017.07.26
[토비 vol1] 3.1~3.4 템플릿(1)  (0) 2017.07.26
[토비 vol1] 2.4~2.6 테스트(2)  (0) 2017.07.25
[토비 vol1] 2.1~2.3 테스트(1)  (0) 2017.07.25

+ Recent posts