스프링 DB - 예외처리, 반복

Heeeoh·2024년 2월 27일
0

스프링 DB

목록 보기
6/9
post-thumbnail

🌿 시작하기 앞서


스프링 부트 3.2.2 버전을 기준으로 작성됨
H2 데이터베이스 Version 2.2.224 (2023-09-17)


🌱 예외 처리, 반복 문제 해결


✔️ 체크 예외와 인터페이스

서비스 계층은 가급적 특정 구현 기술에 의존하지 않고,
순수하게 유지하는 것이 좋다.

인터페이스 도입

항상 느끼는 것이 인터페이스 즉, 추상화에 의존하는 것이 얼마나 중요한지 매 강의를 들을 때 마다 느껴진다.
변경에 아주 유연하게 작용한다는 것이 너무 큰 메리트이다.

체크 예외와 인터페이스 문제

public interface MemberRepositoryEx {
	Member save(Member member) throws SQLException;
    ...
}

인터페이스 메서드에 체크 예외 선언을 다 해주어야한다.
구현 클래스에서는 해당 메서드의 예외 선언은 부모 타입에서 던진 예외와 같거나 하위 타입이여야한다.

특정 기술에 종속되는 인터페이스

JDBC의 SQLException 체크 예외에 종속적이다.
인터페이스는 변경에 유연해야하는데
Member save(Member member) throws SQLException;
이러면 한 기술에만 종속적으로 되어 버리는 문제가 있다.

런타임 예외는 자유롭기에 예외 선언을 하지 않아도 되고 인터페이스가 특정 기술에 종속적일 필요가 없어진다.

✔️ 런타임 예외 적용

인터페이스

public interface MemberRepository {
	Member save(Member member); // 예외 선언 생략
    ...
}

런타임 예외

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

}

MemberRepositoryV4_1

public class MemberRepositoryV4_1 implements MemberRepository {
	//...  
    
    @Override
    public Member save(Member member) {
    // sql 문 작성 및 con, pstmt 객체 선언 
    
    try {
    	// 커넥터 연결 및 SQL 문 보내기
    } catch (SQLException e) { // 기존 체크 예외 잡고
    	throw new MyDbException(e); 
        // 런타임 예외로 전환 
        // 스택 트레이스를 위해 기존 예외를 넘기자
        
    }
}

강조할 것은 꼭 기존 예외를 포함하자

서비스 계층


public class MemberServiceV4 {

	private final MemberRepository memberRepository;
    
    @Transactional
    public void accountTransfer(String fromId, String toId, int money) {
    	// 트랜잭션, 예외 선언 등 사라지고 오직 순수한 비지니스 코드만 남았다. 
    	bizLogic(fromId, toId, money);
    }
}


	private void bizLogic(String fromId, String toId, int money) {
		Member fromMember = memberRepository.findById(fromId);
		Member toMember = memberRepository.findById(toId);
        
		memberRepository.update(fromId, fromMember.getMoney() - money);
		memberRepository.update(toId, toMember.getMoney() + money);
 }

정리

  • 체크 예외를 런타임 예외로 변환
    -> 인터페이스와 서비스 계층의 순수성을 유지 가능

  • 향후 JDBC에서 다른 구현 기술로 변경하더라도 서비스 게층의 코드를 변경하지 않고 유지 가능

남은 문제

리포지토리에서 넘어오는 특정한 예외의 경우 복구를 시도할 수도 있다. 지금 방식은 MyDbException 이라는 예외만 넘어오기 때문에 예외를 구분할 수 없는 단점이 있다.



✔️ 데이터 접근 예외 직접 만들기


데이터베이스 오류에 따라서 특정 예외는 복구하고 싶을 수 있다.
ex) 회원 가입 시 중복 ID 존재 -> 임의의 숫자를 붙여서 가입 시도

SQLException 에는 데이터베이스가 제공하는 errorCode 라는 것이 들어 있다.

e.getErrorCode() : 에러 코드 조회

Repository

// ...
} catch (SQLException e) {
	//h2 db
	if (e.getErrorCode() == 23505) {
        // MyDbException을 상속받은 런타임 예외
		throw new MyDuplicateKeyException(e);
	}
	throw new MyDbException(e);	
}

Service

try {
	repository.save(new Member(memberId, 0));
	log.info("saveId={}", memberId);
    
} catch (MyDuplicateKeyException e) {
	log.info("키 중복, 복구 시도");
	String retryId = generateNewId(memberId);
	log.info("retryId={}", retryId);
	repository.save(new Member(retryId, 0));
    
} catch (MyDbException e) {
	log.info("데이터 접근 계층 예외", e);
	throw e;
}

복구할 수 없는 예외면 던진다.
복구할 수 없는 예외는 공통으로 예외를 처리하는 곳에서 예외 로그를 남기는 것이 좋다.

남은 문제

SQL ErrorCode 는 데이터베이스 마다 다르다.
데이터베이스가 전달하는 오류는 키 중복 뿐만 아니라 락이 걸린경우, SQL 문법 오류 등 많다.



✔️ 스프링 예외 추상화 이해


  • 스프링은 데이터 접근 계층에 대한 수십 가지 예외를 정리해서 일관된 예외 계층을 제공

  • 각각의 예외는 특정 기술에 종속적이지 않게 설계되어 있음

  • JDBC나 JPA를 사용할 때 발생하는 예외를 스프링이 제공하는 예외로 변환해주는 역할도 스프링이 제공

  • DataAccessException은 크게 2가지로 구분

    • Transient 예외

      • 일시적이라는 뜻
      • 하위 예외는 동일한 SQL을 다시 시도했을 때 성공 가능성 있음
      • ex) 쿼리 타임아웃, 락 관련 오류
    • NonTransient 예외

      • 일시적이지 않다는 뜻
      • 같은 SQL 반복 실행 시 실패
      • ex) SQL 문법 오류, 데이터베이스 제약 조건 위배 등

exceptionTranslator

SQLExceptionTranslator exTranslator = 
		new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);
  • translate()
    • 처음 파라미터 : 읽을 수 있는 설명
    • 두 번째 파라미터 : 실행한 sql
    • 마지막 파라미터 : 발생된 SQLException 전달

해당 함수를 실행하면 적절한 스프링 접근 계층의 예외로 변환해서 반환해준다.

각 DB마다 SQL ErrorCode를 XML로 가지고 있다.
<bean id="H2" class="org.springframework.jdbc.support.SQLErrorCodes">
	<property name="badSqlGrammarCodes">
		<value>42000,42001,42101,42102,42111,42112,42121,42122,42132</value>
	</property>
	<property name="duplicateKeyCodes">
		<value>23001,23505</value>
	</property>
</bean>
<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
	<property name="badSqlGrammarCodes">
		<value>1054,1064,1146</value>
	</property>
	<property name="duplicateKeyCodes">
		<value>1062</value>
	</property>
</bean>

정리

  • 스프링은 데이터 접근 계층에 대한 일관된 예외 추상화를 제공
  • 스프링은 예외 변환기를 통해서 SQLExceptionErrorCode에 맞는 적절한 스프링 데이터 접근 예외로 변환
  • 특정 기술에 종속적 X
  • 스프링에 대한 기술 종속성은 발생한다.

적용

 try {
 // 커넥터 및 sql 파라미터 작성 및 커밋 
 } catch (SQLException e) {
 throw exTranslator.translate("update", sql, e);


✔️ JDBC 반복 문제 해결 - JDBCTemplate


JDBC 반복 문제

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

이런 반복을 효과적으로 처리하는 방법
'템플릿 콜백 패턴'

스프링은 JDBC 반복 문제를 해결하기 위해 JDBCTemplate 를 제공

public class MemberRepositoryV5 implements MemberRepository{

    private final JdbcTemplate template;

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

    public Member save(Member member) {
        String sql = "insert into member(member_id, money) values (?, ?)";
        template.update(sql, member.getMemberId(), member.getMoney());
        return member;
    }
    
	public Member findById(String memberId) {
        String sql = "select  * from member where member_id = ?";
        return template.queryForObject(sql, memberRowMapper(), 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로 개발할 때 발생하는 반복을 대부분 해결해준다. 뿐만 아니라 트랜잭션을 위한 커넥션 동기화, 스프링 예외 변환기도 자동으로 실행해준다.


🔖 학습내용 출처

스프링 DB 1편 - 데이터 접근 핵심 원리

profile
열심히 살자

0개의 댓글