김영한 님의 스프링 DB 1편 - 데이터 접근 핵심 원리 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
public class MemberServiceV3_3 {
private final MemberRepositoryV3 memberRepository;
private void bizLogic(String fromId, String toId, int money) throws SQLException { ... }
...
}
MemberRepositoryV3
와 SQLException
에 의존하는 문제
➜ 순수한 Service를 위해 MemberRepository
인터페이스를 만든다
public class MemberRepositoryV3 {
public Member save(Member member) throws SQLException { ... }
...
}
Repository의 메서드는 SQLException
이라는 체크 예외를 던지고 있는데 인터페이스의 구현체가 체크 예외를 던지려면 인터페이스의 메서드가 체크 예외를 던지도록 선언되어 있어야한다
but> 인터페이스의 메서드에 throws
로 예외를 던지는 부분이 선언되어 있으면 이는 특정 기술에 의존적인 인터페이스
➜ 특정 기술에 의존하지 않도록 구현체는 체크 예외를 런타임 예외로 전환해서 서비스 계층에 던져야 한다
public class MemberRepositoryV4_1 implements MemberRepository{
@Override
public Member save(Member member) {
try {
...
} catch (SQLException e) {
throw new MyDbException(e);
} finally {
close(con, pstmt, null);
}
}
}
MemberRepository
인터페이스를 구현
메서드에 선언되어 있던 throws SQLException
제거
catch
에서 체크 예외를 잡아 RuntimeException
을 상속받은 MyDbException
을 반환
➡️체크 예외를 런타임 예외로 변환하면서 예외 의존 제거
public class MemberServiceV4 {
private final MemberRepository memberRepository;
private void bizLogic(String fromId, String toId, int money) { ... }
...
}
MemberRepository
인터페이스에 의존
Repository의 메서드를 사용하는 bizLogic()
에서 SQLException
이 제거됨
➡️특정 구현체 의존 제거 및 예외 의존 제거
예외에 의존하는 것을 제거하기 위해 체크 예외를 런타임 예외로 변환하면서 인터페이스와 서비스 계층의 순수성을 유지할 수 있게 되었다
서비스가 인터페이스에 의존하도록 변경하여 데이터 접근 기술을 변경해도 서비스의 코드는 변경하지 않는다
Repository가 JDBC 드라이버에 insert 쿼리 전달 ➜ JDBC 드라이버가 DB에 쿼리 전달
상황에 따라 DB 내부에 있는 오류 코드를 반환
JDBC 드라이버가 SQLException
을 만드는데 내부에 오류 코드를 넣어놓는다
Repository로 예외가 넘어오면 SQLException
내부의 오류 코드를 확인할 수 있다
e.getErrorCode()
로 오류 코드를 확인할 수 있다
SQLException
내부의 errorCode를 활용하면 DB에 발생한 문제를 알 수 있다
오류 코드는 DB 마다 다르며, 같은 오류 코드여도 DB 마다 다르다
특정 ID로 가입을 시도 했는데, 이미 같은 아이디가 있으면 뒤에 임의의 숫자를 붙여서 가입한다고 가정했을 때 이러한 흐름이 예외를 확인해서 복구하는 과정이다
오류 복구를 위해 서비스 계층에서는 예외 복구를 위해 키 중복 오류를 확인할 수 있어야한다
but> SQLException 내부의 오류 코드를 활용하기 위해 예외를 서비스로 던지게 되면 서비스가 JDBC 기술에 의존하게 된다
➡️Repository에서 예외를 변환해서 서비스로 던진다
키 중복일 때 발생시킬 예외를 MyDuplicateKeyException
을 생성한다
➜ 직접 만든 예외이기 때문에 특정 데이터 접근 기술에 종속적이지 않다
➜ 즉, 서비스 계층의 순수성을 유지할 수 있다
MyDuplicateKeyException
은 이전에 만들었던 MyDbException
을 상속받는다
static class Repository {
public Member save(Member member) {
try {
...
} catch (SQLException e) {
if (e.getErrorCode() == 23505) {
throw new MyDuplicateKeyException(e);
}
throw new MyDbException(e);
} finally {
...
}
}
}
예외 코드가 23505라면 MyDuplicateKeyException
을 새로 만들어서 서비스에 던진다
그 외의 예외는 MyDbException
을 만들어서 던진다
static class Service {
public void create(String memberId) {
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;
}
}
private String generateNewId(String memberId) {
return memberId + new Random().nextInt(10000);
}
}
저장을 시도했는데 MyDuplicateKeyException
예외가 올라오면 이 예외를 잡아서 generateNewId()
로 새로운 ID 생성을 시도
그리고 다시 저장하는 메서드를 호출하는데 여기가 예외를 복구하는 부분이다
복구할 수 없는 예외( MyDbException
)면 로그만 남기고 다시 예외를 던진다
위에서 에러 코드에 따른 예외 복구 코드를 작성했는데 SQL ErrorCode는 각각의 데이터베이스 마다 다르다
또한 DB에 정의된 오류 코드는 중복 키 오류를 제외하고도 많은 오류 코드가 존재한다
DB가 변경될 때마다 코드를 변경하는 것은 현실성이 없다
또한 모든 오류 코드를 확인해서 직접 예외를 변환시키는 것도 불가능
➡️스프링은 이런 문제들을 해결하기 위해 데이터 접근과 관련된 예외를 추상화해서 제공
스프링은 데이터 접근 계층에 대한 수십 가지 예외를 정리해서 일관된 예외 계층을 제공
각 예외는 특정 기술에 종속되지 않기 때문에 서비스 계층에서 스프링이 제공하는 예외를 사용하면 된다
JDBC나 JPA를 사용할 때 발생하는 예외를 스프링이 제공하는 예외로 변환해주는 역할도 스프링이 한다
런타임 예외를 상속받았기 때문에 스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외
예외의 최고 상위는 DataAccessExcepton
이고 Transient
와 NonTransient
로 나뉜다
Transient
: 일시적이라는 의미로 이 하위 예외들은 동일한 SQL을 다시 시도하면 성공할 가능성이 있다
NonTransient
: 동일한 SQL을 다시 시도해도 실패한다
SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);
스프링은 DB에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동으로 변환해주는 변환기를 제공한다
translate()
메서드의 첫번째 파라미터는 읽을 수 있는 설명이고, 두번째는 실행한 sql, 마지막은 발생된 SQLException
을 전달하면 된다
이렇게 하면 SQL을 분석해 적절한 스프링 데이터 접근 계층의 예외로 변환해서 반환한다
➜ 서비스, 컨트롤러에서 예외 처리가 필요하면 특정 기술에 종속적인 SQLException
같은 예외를 직접 사용하는 것이 아니라, 스프링이 제공하는 데이터 접근 예외를 사용
<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>
DB마다 SQL ErrorCode가 다른데 스프링이 각각의 DB가 제공하는 ErrorCode까지 고려해서 예외를 변환할 수 있는 이유는 sql-error-codes.xml
org.springframework.jdbc.support.sql-error-codes.xml
스프링 SQL 예외 변환기는 SQL ErrorCode를 위의 파일에 대입해서 어떤 스프링 데이터 접근 예외로 전환해야 할지 찾아낸다
ex> H2 데이터베이스에서 42000 이 발생하면 badSqlGrammarCodes
이기 때문에 BadSqlGrammarException
을 반환
public class MemberRepositoryV4_2 implements MemberRepository{
private final DataSource dataSource;
private final SQLExceptionTranslator exceptionTranslator;
public MemberRepositoryV4_2(DataSource dataSource) {
this.dataSource = dataSource;
this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
}
@Override
public Member save(Member member) {
...
catch (SQLException e) {
throw exTranslator.translate("save", sql, e);
}
...
}
}
SQLExceptionTranslator
: 인터페이스
SQLErrorCodeSqlExceptionTranslator
: 구현체
translate()
가 예외를 반환하기 때문에 바로 throw
를 통해 예외 발생 가능
Repository
에서 JDBC 를 사용하면서 아래 과정( 코드 )들이 계속 반복
커넥션 조회, 커넥션 동기화
PreparedStatement
생성 및 파라미터 바인딩
쿼리 실행, 결과 바인딩
예외 발생 시, 스프링 예외 변환기 실행
리소스 종료
➡️템플릿 콜백 패턴을 이용해 반복을 처리할 수 있다
➡️스프링은 JDBC 반복 문제 해결을 위해 JdbcTemplate
을 제공
public class MemberRepositoryV5 implements MemberRepository{
private final JdbcTemplate template;
public MemberRepositoryV5(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
String sql = "insert into member(member_id, money) values (?, ?)";
template.update(sql, member.getMemberId(), member.getMoney());
return member;
}
@Override
public Member findById(String memberId) {
String sql = "select * from member where member_id = ?";
return template.queryForObject(sql, memberRowMaper(), memberId);
}
private RowMapper<Member> memberRowMaper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
}
};
}
위처럼 작성하면 기존에 try, catch 내부에서 수행하던 모든 것을 자동으로 해준다
트랜잭션을 위한 커넥션 동기화, 스프링 예외 변환기도 자동으로 실행
update()
sql 문과 ?
에 들어갈 파라미터 정보를 넘겨준다
영향 받은 행 수를 반환
queryObject()
한 건을 조회할때 사용
쿼리 결과로 어떻게 객체를 만들 것인지에 대한 매핑 정보를 파라미터로 추가
memberRowMaper()
가 매핑 정보, ResultSet을 활용
마지막 파라미터는 ?
에 들어갈 정보