본문 바로가기
공부/Spring

[Spring][인프런 스프링 DB] 스프링, 자바 예외 처리

by 웅대 2023. 7. 1.
728x90
반응형

본 포스팅은 김영한 강사님의 인프런 강의 "스프링 DB 1편"을 정리한 포스팅입니다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard

 

스프링 DB 1편 - 데이터 접근 핵심 원리 - 인프런 | 강의

백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., - 강의

www.inflearn.com

자바의 예외 계층은 다음과 같다.

Throwable 하위의 Error의 경우 애플리케이션에서는 복구 할 수 없는 에러이기 때문에 사실상 애플리케이션 개발자가 고려해야할 최상위 예외는 Exception이라고 봐도 무방하다.

 

예외는 잡아서 처리하거나 던져야 하며 그 때 해당 예외의 자식들도 함께 처리된다.

 

Exception에서는 크게 체크 예외 언체크 예외(런타임 예외)로 나뉜다.

 

체크 예외

컴파일러가 체크하는 예외이다.

 

에러를 잡아서 처리하거나 던지지 않는다면 컴파일 단계에서 오류가 발생한다.

 

에러를 던질 때는 throws를 반드시 작성해야 한다.

public void function() throws Exception {
	//만약 repository.call()에서 예외를 던진다면 throws를 붙여야 한다.
    //만약 try, catch 구문으로 직접 예외를 처리한다면 던질 필요가 없기 때문에 throws를 붙일 필요가 없다.
 	repository.call();
}

위 코드에서는 Exception을 던지고 있는데 사실 Exception을 던지는 것은 좋지 않다.

 

하위 예외까지 모두 던져버려서 중요한 예외를 전부 놓치기 때문이다.

 

체크 예외는 개발자의 실수를 방지해주지만 모든 예외를 하나씩 처리해야 하기 때문에 번거롭다.

 

또한 특정 예외의 의존 관계를 변경하면 파급 효과가 상당하다는 단점이 있다.

SQLException이 다른 체크 예외로 바뀌면 연관되어 있는 모든 계층을 변경해야 한다.

그리고 애플리케이션에서 복구 불가능한 에러도 모든 계층이 throws를 해야 하는 불필요한 일이 생긴다.

언체크 예외

컴파일러가 체크하지 않는 예외로 런타임 중에 발생한다.

 

역시 예외를 잡아서 처리하거나 던져야 하는데 던질 때 throws를 작성하지 않아도 자동으로 상위 계층으로 던지게 된다.

 

아래 그림과 같이 컨트롤러와 서비스가 예외에 의존하지 않는 모습을 볼 수 있다.

체크 예외를 언체크 예외로 바꾸는 방법도 사용한다.

 

참고로 체크 예외를 언체크 예외로 바꿀 때 체크 예외를 언체크 예외 안으로 반드시 넣어줘야 한다.

 

넣지 않으면 스택트레이스에서 예외 발생 원인을 정확하게 파악하기 어렵다.

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

이러한 언체크 예외는 잘 정리해둬야 한다. (주석으로 에러 정리 등등...)

예외 처리

이제 서비스에서 SQLException이라는 JDBC 관련 기술의 종속성을 없애는 방법이다.

 

먼저 레포지토리에 인터페이스를 도입하고 구현체에서 Jdbc를 하는데 이 구현체에서 SQLExcpetion을 언체크 예외로 바꿔서 반환하면 된다.

 

만약 구현체에서 그대로 throws SQLException을 통해 넘겨버릴 경우 인터페이스에서도 throws SQLException을 작성해야하고 인터페이스도 JDBC 기술에 종속적이게 되어 버리는 문제가 발생한다.

 

그래서 구현체에서 SLQExcpetion을 잡아서 언체크 예외로 변환해야한다.

 

우선 언체크 예외를 다음과 같이 만든다.

public class MyDbException extends RuntimeException {
 	public MyDbException(Throwable cause) {
 		super(cause);
    }
 }
@Slf4j
public class JdbcMemberRepository implements MemberRepository {
.
.
.
 	@Override
 	public Member save(Member member) {
    //커넥션 생성 등등..
 		try {
        //쿼리 실행 등등...
 		} catch (SQLException e) {
        //여기서 언체크 예외로 바꾼다.
 		throw new MyDbException(e);
        //생성한 언체크 에러에 반드시 실제 발생한 에러를 넣어줘야한다.
 		} finally {
        //리소스 정리 등등..
        }
 .
 .
 .
 }

이제 인터페이스에서 Jdbc 관련 기술 종속성을 제거하였고 자연스럽게 서비스에서도 Jdbc 관련 기술의 종속성이 제거되었다.

public interface MemberRepository {
//throws SQLException을 작성할 필요가 없음
 Member save(Member member);
 .
 .
 .
 
}

데이터 접근 예외

특정 오류가 발생했을 때 복구하고 싶을 수 있다. ex) PK 중복이 발생하면 랜덤 숫자 붙여서 다시 등록

 

만약 JDBC를 사용하고 있다면 SQLException이 발생할테고 여기에 정의된 error code를 사용해서 특정 예외를 복구할 수 있다.

 

레포지토리에서 error code를 보고 SQLException을 적절한 언체크 예외로 변환해서(종속성 제거) 서비스 계층으로 넘겨주면 서비스 계층에서 해당 에러를 복구한다.

그런데 이 error code의 종류는 다양하고 데이터베이스마다 error code가 다르기 때문에 직접 변환하는 것은 효율이 떨어진다.

 

그래서 스프링에서 데이터 접근 에러를 추상화해서 제공한다.

NonTransient : 일시적이지 않은 에러 ex) SQL 문법 오류 등등.

Transient : 일시적인 에러 ex) 락 등등..

 

SQLExceptionTranslator를 사용하여 스프링에서 제공해주는 언체크 예외로 쉽게 변환할 수 있다.

 

<레포지토리>

 try {
 	//커넥션 생성 및 sql 실행 등등...
 } catch (SQLException e) {
 	SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
 	DataAccessException resultEx = exTranslator.translate("select", sql, e);
    //첫 번째 파라미터는 설명, 두 번째는 sql 구문, 세 번째는 실제로 발생한 SQLException
    //만약 sql 문법 오류라면 resultEx는 DataAccessException을 상속받은 BadSqlGrammarException
 }

sql-error-codes.xml 파일에 여러 데이터베이스에 따른 에러 코드가 정의되어 있기 때문에 쉽게 변환할 수 있는 것이다.

 

JDBC에 대한 종속성이 제거되고 스프링에 대한 종속성이 생기지만 생산성이 향상된다.

 

JDBC 반복

 

템플릿 콜백 패턴을 통해 계속 연결을 맺는 반복 과정을 생략할 수 있다.

 

JdbcTemplate을 사용하면 쉽게 반복 코드를 제거할 수 있다.

public class JdbcMemberRepository implements MemberRepository {

 	private final JdbcTemplate template;
    
 	public JdbcMemberRepository(DataSource dataSource) {
 		template = new JdbcTemplate(dataSource);
 	}
    
 	@Override
 	public Member save(Member member) {
 		String sql = "CREATE 구문"
 		template.update(sql, member.getMemberId(), member.getMoney());
 		return member;
	}
    
 	@Override
 	public Member findById(String memberId) {
 		String sql = "READ 구문";
 		return template.queryForObject(sql, memberRowMapper(), memberId);
        
     private RowMapper<Member> memberRowMapper() {
 		return (rs, rowNum) -> {
        //ResultSet으로 부터 Member를 생성 후 반환
 		return member;
 	 };
 }
 }
728x90
반응형

댓글