[스프링 DB 1편] - 스프링과 문제 해결(2)

Chooooo·2023년 1월 19일
0

스프링 DB 1편

목록 보기
11/11
post-thumbnail

이 글은 강의 : 김영한님의 - "[스프링 DB 1편 - 데이터 접근 핵심 원리]"을 듣고 정리한 내용입니다. 😁😁


직접 만든 데이터 계층 예외의 문제점

이전까지 DB에서 발생하는 예외들 중 원하는 예외만 서비스 계층에서 잡아서 처리할 수 있도록 사용자 정의 예외를 만들었다. 그렇지만 이 때 한 가지 문제점이 발생한다. 처리하고 싶은 DB 예외를 만들기 위해서는 각 DB에서 발생하는 예외들을 예외 변환 처리를 해줘야한다. 데이터 접근 계층이 수백개가 되면 이것은 아주 큰 문제로 다가온다. 따라서 이 부분의 해결이 필요해진다.

스프링 데이터 접근 예외 계층

스프링은 이미 데이터 접근 예외 계층을 추상화해서 제공해준다. 따라서 개발자는 해당 예외 계층을 사용하기만 하면 된다 !!

  • 스프링 데이터 접근 예외 계층

🧨 스프링은 데이터 접근 계층에 대한 수많은 예외를 정리해서 일관된 예외 계층을 제공해 준다.

🧨 스프링이 제공하는 예외는 순수 특정 기술에 종속적이지 않다. 따라서 서비스 계층(Service)에서 스프링이 제공하는 예외를 사용했을 때, DB 접근 기술에 대한 종속성 문제는 해결된다.

🧨 스프링은 JDBC / JPA를 사용할 때 발생하는 예외를 스프링 예외 계층으로 변환해 준다.

🧨 스프링 데이터 접근 예외 계층의 최상위 클래스는 DataAcessException이다. 이 예외는 Runtime 예외를 상속받았으며, 따라서 스프링이 제공하는 모든 데이터 접근 계층 예외는 런타임 예외다.

🧨 DataAccessException의 분류

  • NonTransisentDataAccessException Transient : 일시적.

    • 일시적이지 않은 예외다. 따라서 어플리케이션 단에서는 해결될 방법이 없을 가능성이 높고, 개발자가 의도적으로 개입해서 수정이 필요한 예외다. 예를 들어 잘못된 SQL 구문을 반복한다고 실행이 되진 않는다. (retry했을 때 성공 가능성 X)
  • TransisentDataAccessException

    • Transisent는 일시적으로 발생하는 Exception을 의미한다. 이 에러는 시간이 조금 흐른 뒤 다시 시도하면, 성공할 가능성이 있는 예외를 의미한다. 예를 들어 락이 걸린 경우, 시간이 조금 흐르면 락이 풀리고 이 때 시도하면 다시 성공할 수 있다. (retry했을 때 성공 가능성 O)

DataAccessException 클래스를 타고 들어가서 해당 클래스를 구현한 하위 계층을 확인할 수 있다. (수많은 예외가 존재한다.)

스프링이 제공하는 예외 변환기

앞서 스프링은 다양한 DB에서 발생하는 예외를 특정 DB 기술에 종속되지 않는 런타임 예외 계층을 제공해준다고 했다. 그렇지만 이 모든 예외 계층을 개발자가 하나씩 분석해서 만드는 것은 리소스 낭비다. 따라서 스프링은 DB에서 발생하는 예외를 ErrorCode를 바탕으로 스프링이 제공하는 DB 계층 예외로 변경해주는 예외 변환기를 제공해준다. 예외 변환기를 사용하면 예외 변환도 자동으로 되고, 만들어지는 예외 모두 특정 DB 기술에 종속적이지 않기 때문에 DI를 유지하면서 개발 생산성도 높일 수 있다.

// 예외 변환기 선언
public MemberRepositoryV4_2(DataSource dataSource) {
    this.dataSource = dataSource;
    this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
}
  • 예외 변환기는 DB에서 발생하는 ErrorCode를 바탕으로 예외를 변환한다. → 따라서 정보를 얻기 위해 반드시 DB Connection이 필요. 이 때 DataSource를 넘겨줘서 정보를 받아온다.
// 예외 변환기 : 예외 변환
throw exTranslator.translate("find", sql, e);

🎈 translate를 이용해 발생한 예외를 분석해서 스프링이 제공하는 DB 계층 예외로 변경해준다.

  • Task : 현재 작업하고 있는 내용을 정리하면 된다. 로그에 표시되는 용도
  • SQL : DB에 접근했을 때 사용한 SQL을 넣어준다.
  • Exception : Catch 문에서 잡은 Exception을 넣어준다.

  • 실행하면 여기서 task [select]라고 되어 있는데 내가 task에 select를 입력했기에 이렇게 발생 ! → 따라서 로그에 남길 값을 task에 작성해두면 된다.

  • DataSource에서 얻어온 값을 바탕으로 ErrorCode를 분석해서 필요한 형식의 예외를 만들어준다. 이 때, BadSqlGrammerException으로 예외가 변환된다.

스프링은 어떻게 모든 DB 예외를 변환하는가?

  • 설정 파일이 존재해 !
  • org.springframework.jdbc.support 경로로 가보면 sql-error-codes.xml 파일이 저장되어있다.
    • 이 파일에는 각 DB마다 어떤 ErrorCode가 어떤 Error에 대응되는지를 확인할 수 있다.
    • 위 이미지에서 볼 수 있듯이 여러 RDBMS(H2, MY-SQL 등)을 지원하는 것을 확인할 수 있다.

테스트

SpringExceptionTranslator를 이용한 테스트 코드

//SpringExceptionTranslatorTest.java

@Test
void sqlExceptionErrorCode() {
    String sql = "select bad grammer";
    try {
        Connection con = dataSource.getConnection();
        PreparedStatement pstmt = con.prepareStatement(sql);
        pstmt.executeQuery();
    } catch (SQLException e) {
        assertThat(e.getErrorCode()).isEqualTo(42122);
        int errorCode = e.getErrorCode();
        log.info("errorCode = {}", errorCode);

        SQLErrorCodeSQLExceptionTranslator translator
                = new SQLErrorCodeSQLExceptionTranslator(dataSource);
        DataAccessException resultEx = translator.translate("select", sql, e);

        log.info("resultEx = {}", resultEx);
        assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);

    }
}

🎈 SQLExceptionTranslator를 만들어준다.
🎈 SQLExceptionTranslator에 발생한 Exception과 SQL을 넘겨주면, 현재 어떤 이유로 Exception이 발생했는지를 분석해서 Spring이 제공하는 DB 계층으로 변경해서 제공해준다.

  • 실행 결과 Exception이 BadSqlGrammerException으로 변경된 것을 확인할 수 있다.

MemberServiceV4_2

이제 해당 SpringExceptionTranslator를 실제 코드에 적용해보자

// MemberServiceV4_2 필드 코드 수정
private final DataSource dataSource;
private final SQLExceptionTranslator exTranslator;

public MemberRepositoryV4_2(DataSource dataSource) {
    this.dataSource = dataSource;
    this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
}
  • 먼저 해당 클래스 전체에서 재사용하기 위해 필드 영역 + 생성자에 DI 처리해준다.
  • 앞서 사용방법대로 Translator를 생성할 때 DataSource 정보를 넘겨준다.
// MemberServiceV4_2 : 코드 수정 
@Override
public Member findById(String memberId){
    String sql = "select * from member where member_id = ?";

    ...
    
    } catch (SQLException e) {
        log.error("error", e);
        throw exTranslator.translate("find", sql, e); //이 부분 !!!
    }finally {
        
        ...
        
    }
}
  • 스프링이 제공하는 예외 변환기를 적용했다.
  • translate를 할 때, task + Sql + Exception을 넘겨줘서 에러 코드를 분석해서 넘겨주도록 한다.
  • 이 때, SpringExceptionTranslator가 만들어주는 예외는 런타임 예외이기 때문에 Service 계층에서는 해당 예외를 체크하지 않아도 된다.

정리

🎈 스프링은 데이터 접근 계층에 대해서 일관된 예외 추상화를 제공해준다. 이 때 제공하는 예외는 DB 기술 종속적이지 않으며, 모두 런타임 예외다.

🎈 스프링은 예외 변환기를 제공해주고, 예외 변환기는 DataSource / SQL / Exception을 입력받아 ErrorCode에 맞는 적절한 스프링 데이터 접근 예외 계층으로 변환해준다.

🎈 스프링 예외 변환기로 만들어지는 예외는 런타임 예외 + 특정 기술 종속적이지 않으므로, 필요 시 Controller / Service 계층에서 사용해도 문제가 없다.

🎈 서비스 계층에서 발생하는 예외를 잡아서 복구해야하는 경우, 예외가 스프링이 제공하는 데이터 접근 계층 예외로 변환되서 넘어오기 때문에 필요한 경우 예외를 잡아서 복구하면 된다.

🧨 남은 문제
지금까지 Connection과 DB를 사용하는 방법을 추상화 했고, Transaction을 추상화했다. 그리고 마지막으로 예외까지 추상화를 했다. 그럼에도 불구하고 코드 상에 해결 여지가 남아있다. 바로 JDBC를 사용하기 때문에 발생하는 반복 문제의 해결이 남아있다.

JDBC

리포지토리의 각각의 메서드를 살펴보면 상당히 많은 부분이 반복된다.
아직까지도 개선의 여지가 있음.

@Override
public Member findById(String memberId){
    String sql = "select * from member where member_id = ?";

    PreparedStatement pstmt = null;
    ResultSet rs = null;

    try {

        Connection conn = getConnection();
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, memberId);

        rs = pstmt.executeQuery();
        log.info("Connection = {}, class = {}", conn,conn.getClass());

        if (rs.next()) {
            Member member = new Member();
            member.setMemberId(rs.getString("member_id"));
            member.setMoney(rs.getInt("money"));
            return member;
        }else{
            throw new NoSuchElementException("member not found memberId = " + memberId);
        }
    } catch (SQLException e) {
        log.error("error", e);
        throw exTranslator.translate("find", sql, e);
    }finally {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(pstmt);
    }
}

코드를 보면 계속 반복되는 부분이 존재.

  • 커넥션 조회, 커넥션 동기화
  • PreparedStatement 생성 및 파라미터 바인딩
  • 쿼리 실행
  • ResultSet 바인딩
  • 예외 발생 시, 스플이 예외 변환기 실행
  • 리소스 종료

🎈 스프링은 JDBC의 반복 문제를 해결하기 위해 **JdbcTemplate 템플릿 제공**.

  • 템플릿이란 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법

위에서 발생하는 반복들은 대부분 JDBC 기술을 사용하기 때문에 발생하는 것이다. 이런 반복적인 부분들을 해결할 수 있다면, 코드가 아주 아름답게 리팩토링 될 것이다. 그렇다면 어떤 방식으로 접근할 수 있을까?

먼저 위 구문들은 실제 비즈니스 로직 사이에 적용되어있다. 따라서 이런 것들은 따로 메서드로 빼서 처리를 할 수 없다. 이런 경우에는 템플릿 콜백 패턴을 이용해서 처리할 수 있다. 다행히 이런 문제를 해결하기 위한 템플릿 콜백 패턴이 구현되어있다. 바로 JdbcTemplate이다. 위에서 발생한 문제들은 JdbcTemplate을 이용하면 깔끔하게 처리할 수 있다.

JdbcTemplate를 이용한 코드 리팩토링

@Slf4j
public class MemberRepositoryV5 implements MemberRepository{

    private final JdbcTemplate jdbcTemplate;

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

    @Override
    public Member save(Member member) {
        String sql = "insert into member(member_id, money) values(?,?)";
        jdbcTemplate.update(sql,member.getMemberId(), member.getMoney());
        return member;
    }

    @Override
    public Member findById(String memberId){
        String sql = "select * from member where member_id = ?";
        return jdbcTemplate.queryForObject(sql, memberRowMapper(), memberId);
    }

    @Override
    public void update(String memberId, int money){
        String sql = "update member set money = ? where member_id = ?";
        jdbcTemplate.update(sql, memberId, money);
    }

    @Override
    public void delete(String memberId){
        String sql = "delete from member where member_id=?";
        jdbcTemplate.update(sql, memberId);
    }

    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setMemberId(rs.getString("member_id"));
            member.setMoney(rs.getInt("money"));
            return member;
        };
    }

}

코드를 보면 JdbcTemplate는 JDBC로 개발할 때 발생하는 반복을 대부분 해결해준다 ! 그 뿐만 아니라 지금까지 학습했던, 트랜잭션을 위한 커넥션 동기화는 물론, 그 예외 발생시 스프링 예외 변환기도 자동으로 실행해준다 !!!

🎃 참고

  • 템플릿 콜백 패턴에 대해서 지금은 자세히 이해하지 못해도 괜찮다. 스프링이 JdbcTemplate 이라는 편리한 기능을 제공하는 구나 정도로 이해해도 된다.

🎈 정리

  • JdbcTemplate은 트랜잭션을 위한 커넥션 동기화 / 예외 발생 시 스프링 예외 변환기도 자동으로 실행해준다.

  • 서비스 계층의 순수성

    • 트랜잭션 추상화 + 트랜잭션 AOP 덕분에 서비스 계층의 순수성을 최대한 유지하면서 서비스 계층에서 트랜잭션을 사용할 수 있다.
    • 스프링에 제공하는 예외 추상화 + 예외 변환기 덕분에, DB 접근 기술이 변경되어도 서비스 계층의 순수성을 유지하면서 예외도 사용할 수 있다.
    • 서비스 계층이 리포지토리 인터페이스에 의존한 덕분에 향후 리포지토리가 다른 구현 기술로 변경되어도 서비스 계층을 순수하게 유지할 수 있다.
  • 리포지토리에서 JDBC를 사용하는 반복 코드가 JdbcTemplate에 의해 제거되었다.

한번은 이렇게 전체 과정을 직접 해보는 것이 나중에 간단하게 쓸 때 문제가 생길 때 깊이있게 이해할 수 있어 !!!

profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글