스프링 부트 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
예외
NonTransient
예외
SQLExceptionTranslator exTranslator =
new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);
translate()
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>
SQLException
의 ErrorCode
에 맞는 적절한 스프링 데이터 접근 예외로 변환 try {
// 커넥터 및 sql 파라미터 작성 및 커밋
} catch (SQLException e) {
throw exTranslator.translate("update", sql, e);
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로 개발할 때 발생하는 반복을 대부분 해결해준다. 뿐만 아니라 트랜잭션을 위한 커넥션 동기화, 스프링 예외 변환기도 자동으로 실행해준다.
🔖 학습내용 출처