본문 바로가기
Spring&SpringBoot/<토비의 스프링 3.1 Vol1.1>, 이일민

(미완) 데이터 액세스 기술 추상화하기

by 민휘 2023. 3. 26.

개요

(이 내용은 추후 로드 존슨의 Expert One-On-One J2EE Design and Development 9장을 읽어본 후에 다루려고 합니다.)

 

저번 포스팅에 이어 DataAccessException이 어떻게 데이터 액세스 기술 종류의 차이를 추상화하는지 알아보자. DataAccessException이 데이터 액세스 기술마다 다르게 던지는 예외 클래스를 추상화하여 동일한 상황에 일관된 런타임 예외를 던지는지 알아본다.

 

토비의 스프링 4장 2절의 목표는 DataAccessException이 어떻게 기술마다 다른 예외 클래스를 발생 맥락으로 묶어 맥락에 대응하는 추상적인 예외를 던지는지 이해하는 것이다. 이 과정에서 사용되는 런타임 예외 중심의 낙관적인 예외 처리 전략도 확인할 수 있다.

 

=======수정

🙏 : 코드량이 많아서 DB 종류 추상화와 데이터 액세스 기술 종류 추상화를 나누어 작성하였습니다. 이번 포스팅은 DB 종류 추상화를 다룹니다. 다음 포스팅에서 데이터 액세스 기술 종류 추상화를 다룹니다. 코드는 직접 뜯어서 해석해본 것이니 틀린 정보가 있을 수 있습니다. 지적 환영 — !!

 

데이터 액세스 기술 종류에 따른 예외 추상화의 필요성

DataAccessException은 JDBC의 SQLException 뿐만 아니라, 다른 자바 데이터 액세스 기술(JDO, JPA, 다른 하이버네이트 구현체 등)에서 발생하는 예외도 전환해준다. DB 추상화와 마찬가지로, DataAccessException는 의미가 같은 예외라면 데이터 액세스 기술의 종류와 상관없이 일관된 예외가 발생하도록 만들어준다.

 

데이터 액세스 기술이 달라지면 발생하는 문제

Jdbc을 다룰 때는 SQLException 안에 들어있는 에러 코드가 달랐다. 데이터 액세스 기술이 달라지면 던지는 예외의 종류가 달라진다. Jdbc는 SQLException을 던지고, JPA는 PersistentException을, Hinbernate은 HinbernateException을 던진다. 사용하는 데이터 액세스 기술이 달라지면 던지는 예외가 달라진다. 이렇게 되면 데이터 액세스 기술로부터 독립적인 Dao 인터페이스를 선언할 때 문제가 생긴다.

// 이상적인 DAO 인터페이스
public void add(User user);

// 데이터 액세스 기술마다 다른 예외를 던져서 DAO 인터페이스는 이렇게 됨
public void add(User user) throws SQLException; // 체크 예외라 꼭 던져야함
public void add(User user) throws PersistentException; // 런타임 예외
public void add(User user) throws HibernateException; // 런타임 예외
public void add(User user) throws JdoException; // 런타임 예외

// 모든 예외를 받는 무책임한 DAO 인터페이스
public void add(User user) throws Exception;

// 그렇다면 이건 어떠냐!!
// 체크 예외는 런타임 예외로 전환하고,
// 모든 런타임 예외를 무시하는 DAO 인터페이스
public void add(User user);

주석을 잘 읽어보면 다양한 방법으로 예외 추상화를 시도해보고 있는 것을 알 수 있다. 마지막 방법을 보면 결국 모든 런타임 예외는 무시하는 방식으로 인터페이스를 만들었는데, 이건 현실적이지 않다. 중복키 에러와 같은 애플리케이션 예외는 런타임 예외를 잡아서 처리해야하기 때문이다.

 

스프링이 제공하는 DataAccessException 계층 구조 예외 사용하기

그래서 DataAccessException는 데이터 액세스 기술에 따라 다른 예외 클래스들 대부분을 예외의 의미로 추상화해준다. (스프링 짱 - !!)

예를 들어 데이터 액세스 기술을 부정확하게 사용했을 때는 InvalidDataAccessResourceUsageException 예외가 던져진다. 구체적으로 세분화하면 JDBC에서 발생할 수 있는 BadSqlGrammarException, 하이버네이트에서 발생하는 HibernateQueryException 또는 잘못된 타입을 사용하려고 했을 때 발생하는 TypeMismatchDataAccessException 등으로 구분된다. InvalidData…Exception 예외는 거의 대부분 프로그램을 잘못 작성해서 발생하는 오류다. 스프링이 기술의 종류에 상관없이 이런 성격의 예외를 InvalidData…Exception 타입의 예외로 던져주므로 시스템 레벨의 예외처리 작업을 통해 개발자에게 빠르게 통보해주도록 만들 수 있다.

 

또는 특정 기술에서는 지원하지 않는 기능을 사용자가 직접 구현하고, 해당 상황에서 던지는 DataAccessException의 서브클래스를 상속받아서 다른 데이터 액세스 기술들과 동일한 의미의 예외를 던지게 만들 수도 있다. 예를 들어 낙관적 락킹 기능은 Jdbc에서는 지원하지 않는다. 반면 JDO, JPA, 하이버네이트는 낙관적 락킹을 지원하고 Object(기술마다 다름)OptimisticLockingFailureException을 던지는데, 이 예외는 OptimisticLockingFailureException라는 상위 런타임 예외로 묶어서 던진다. Jdbc에서 낙관적 락킹을 구현하고 OptimisticLockingFailureException의 서브클래스인 JdbcOptimisticLockingFailureException를 정의해서 던지면 다른 데이터 액세스 기술과 동일한 레벨로 예외 추상화를 유지할 수 있다.

 

의미에 따라 추상화된 예외를 제공하는 DataAccessException의 서브 클래스들.

 

 

 

서브클래스 번역 분석

 

 

(추가)

 

(이건 안넣어도 될듯함)독립적인 Dao 만들기

(아님 Jdbc 말고 Jpa 다루는 UserDao 사용하고 비교해보기, DataAccessException 추상화 사용해서!)

데이터베이스 종류에도, 데이터 액세스 기술 종류에도 독립적인 Dao를 만들어보자. Dao는 재사용이 가능하도록 인터페이스를 두어 DI 받도록 하고, 기술 종류에 따라 달라지는 예외는 DataAccessException를 사용해서 런타임 예외로 던지자. 애플리케이션 예외로 잡고 싶을 때는 사용자 정의 런타임 예외를 정의해서 던지면 된다.

기존 예제로 만들었던 UserDao를 기술 독립적으로 만들어보겠다.

우선 인터페이스를 정의한다.

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

그리고 기존에 UserDao 클래스 이름을 UserDaoJdbc로 변경하고, SQLException을 던지던 기존의 헤더에서 예외를 제거한다.

public class UserDaoJdbc implements UserDao {

    private JdbcTemplate jdbcTemplate;

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

    public void add(User user) {
        // static sql을 사용하는 콜백
        jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
                user.getId(), user.getName(), user.getPassword());
    }

    public User get(String id) throws EmptyResultDataAccessException {
        return this.jdbcTemplate.queryForObject(
                "select * from users where id = ?",
                new Object[]{id},
                getUserRowMapper()
        );
    }

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

    public int getCount() {
        return this.jdbcTemplate.query(
                con -> con.prepareStatement("select count(*) from users"),
                rs1 -> { rs1.next(); return rs1.getInt(1); }
        );
    }

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

    private static RowMapper<User> getUserRowMapper() {
        return (rs, rowNum) -> {
            User user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("name"));
            user.setPassword(rs.getString("password"));
            return user;
        };
    }
}

이 UserDao는 대부분의 런타임 예외에 대해서는 무시하는데, get으로 User를 가져왔을 때 빈 값인 경우에 대해서는 비즈니스 예외로 EmptyResultDataAccessException 런타임 예외를 던지는 것을 알 수 있다.