- 예외 추상화에 대해
- 체크 예외, 언체크 예외
- 예외를 변환하는 구조를 설계하는 능????력??
- 프레임워크에 의존하지 않고 일반 애플리케이션에 적용 가능한 예외 처리 구조에 대해
예외 추상화
Spring은 기술, 라이브러리를 통합하는 프레임워크임
예외 처리 방식도 서로 다르고 고유한 예외 체계도 가지고 있는데,
JDBC는 SQLException, JPA는 PersistenceException을 던지며 각각 별도로 처리해야함
이런 문제를 처리하기 위해 Spring에선 예외 추상화(Exception Abstraction) 계층을 제공한다고 함.
이를 통해 여러 기술의 예외를 일관된 방식으로 처리할 수 있음
계층으로 따지면.. Exception - RuntimeException이 있는데, 이 안에 DataAccessException으로 추상 예외가 있다고 함
* 런타임 예외는 실행되기 전까지 모른다. 이 예외가 발생했을때를 대비해 GlobalException으로 예방할 수 있다.
예외 예시
// JDBC에서 발생하는 SQLException 처리의 예시
try {
Connection connection = DriverManager.getConnection(url,username,password);
// 이후에 JDBC 작업? 수행로직 작성
} catch(SQLException e) {
// JDBC 관련 예외 처리 코드 작성하기 (로직이 중복되고, 복잡도가 증가함)
}
// JPA의 PersistenceException 처리의 예시
try {
entityManager.persist(entity);
} catch(PersistenceException e) {
// JPA 관련 예외 처리
}
* 여기선 try - catch를 썼지만 try - with - resource를 쓰는게 훨씬 좋다고 한다
예외를 예시처럼 직접 처리하는 경우 예외 처리 코드가 중복되고, 비즈니스 로직과 섞이는게 필연적이라서 가독성도 떨어진다.
이게 예외코드야? 로직코드야? 할 수 있기 때문
또한 기술을 변경할 때 처리코드도 함께 수정해야 하기 때문에 매우 불편하다.
이를 생각해서 Spring에선 DataAccessException등의 공통 예외 계층을 제공하며, 추상화를 해서 예외를 처리할 수 있음
// Spring JDBC 템플릿 사용시
try {
jdbcTemplate.query("SELECT * FROM users", resultSetExtractor); // 쿼리문?
} catch(DataAccessException e) {
log.error("데이터 접근중에 오류가 발생함", e);
throw new CustomException("DB오류 발생!");
}
// 기술에 관계없이 공통된 예외처리가 가능하다고 함
예외 추상화의 동작 구조
JDBC : 원래는 SQLException
JPA : 원래는 PersistenceException
Hibernate : 원래는 HibernateException
이 3개를 Spring으로 변환하면 DataAccessException 및 하위 클래스로 처리할 수 있다!
SQLException / PersistenceException / HibernateException - > DataAccessException - >
개발자 정의 예외 처리(CustomException 등등..)
* 서비스 계층에서는 기술 종속적인 예외 대신 Spring 추상화 예외만 다루는게 좋다고 함
* 유지보수, 이식성이 향상되니까!
* 예외 메시지를 로그로 반드시 남기기
체크 예외, 언체크 예외
| 체크 예외(Check Exception) | 언체크 예외(Uncheck Exception) | |
| 상속 | java.lang.Exception | java.lang.RuntimeException |
| 예외처리 강제여부 | 반드시 처리해야함(try-catch or throw) | 처리 선택가능(컴파일 오류 발생 x) |
| 예시 | IOException, SQLException | NPE, IllegalArgumentException |
| 발생위치 | 주로 외부 자원 접근시 | 내부 비즈니스 로직 검증, Null값일때 등 |
| 설계 의도 | 외부 시스템 오류에 대비해 컴파일 시점에서 강제처리함 | 로직 오류(런타임)는 개발자 문제니까 우리가 처리하기 |
checked는 오탈자 등 빨간줄이 뜰 때 발생한다, try - catch 처리를 하거나 throw를 해줘야한다
unchecked는 runtimeException에 해당함
체크 예외 예시, 외부 자원(파일 시스템) ?
public class FileLoader {
public String readFile(String path) throws IOException { // throw 사용
BufferedReader reader = new BufferedReader(new FileReader(Path));
return reader.readLine();
}
}
// 호출할 때
try {
String result = fileLoader.readFile("test.txt"); // 위의 클래스를 불러오는건가?
} catch (IOException e) {
System.err.println("파일을 읽을 수 없음!" + e.getMessage());
}
반드시? 예외를 처리하도록 강제해야한다고 한다.
언체크 예외 예시, 내부 유효성 검증
public class UserValidator {
public void validate(String name) {
if(name == null || name.isBlank()) { // name이 null이거나 비어있으면
throw new IllegalArgumentException("이름은 필수에요");
}
}
}
// 호출할 때
userValidator.validate(null); // 예외가 발생할 수 있지만 try-catch는 쓰지 않는다
* 체크 예외는 외부 시스템과의 연동에서 주로 사용한다고 함
- DB, 파일, 네트워크, API 호출 등은 예측 불가능한 오류 발생 가능성이 높기 때문에 예외처리를 강제함으로 안정성을 높임
try {
connection = DriverManager.getConnection(??);
} catch(SQLException e) {
throw new DatabaseAccessException("DB연결 실패!", e); // 커스텀 언체크 예외로 랩핑한다고함
}
* 커스텀 언체크 예외로 변환, 비즈니스 로직 간결하게 유지
public void saveUser(User user) {
try {
userRepository.save(user); // SQLException에게 catch에서 던짐
} catch(SQLException e) {
throw new DatabaseAccessException("회원등록 싶패!", e); //언체크 예외로 랩핑한거라는데..
}
}
- 이렇게 설계하면 서비스 계층 이상에서는 SQLException에 대해서 전~혀 몰라도 된다고 함
- 예외는 전역 핸들러, @ControllerAdvice에서 처리한다고 함
* 비즈니스 로직에서는 언체크 예외를 주로 사용한다. 체크 예외는 빨간줄 등으로 이미 처리되기 때문
* 체크 예외는 주로 외부 시스템 접근(DB, 파일, 네트워크 등)에 사용하고, 내부에서는 CustomException으로 변환하여 던진다..
예외 변환 매커니즘
예외를 변환한다..?
Spring 앱은 보통 Controller - Service - > Repository로 이루어지는데, 각 계층은 관심사가 분리되어야하며
특히, 하위 계층의 기술 세부사항이 상위 계층에 전파되지 않도록 설계하는게 매우매우 중요하다고 한다.
하위 계층에서 발생한 예외가 상위 계층에도 퍼지게 되면.. 아~ 좀 복잡해질것 같다.
- > 서킷 브레이커(Circuit Breaker) 등으로 예외나 장애가 전파되는걸 막는다.
해결책 : 하위 계층에서 발생한 저수준의 예외를 커스텀 예외나 Spring 추상화 예외로 변환해서
상위 계층은 기술에 독립적인 예외처리만 하게끔 한다.
Repository -> Service 예외 변환, 예제(기술 종속 노출)
public class UserRepository {
public User findById(String id) throws IOException { // throws
// 대충 I/O 기반 유저 로딩
return readUserFromFile(id); // IOException 발생 가능
}
}
public class UserService {
public UserDto getUser(String id) {
try {
return convertToDto(userRepository.findById(id)); // User를 UserDto로 변환
} catch(IOException e) {
throw new AppException(ErrorCode.FILE_IO_ERROR); // catch에서 예외처리
}
}
}
위의 코드가 왜 문제인가?
- Service 계층(UserService)이 IOException을 처리하고 있음 - > 관심사 분리 위반
- 기술 변경을 할 때 Service 계층까지 수정할 필요가 있어서 매우 번거로움
개선된 코드(예외 변환 적용)
public class UerRepository {
public User findById(String id) {
try { // Repository에 try-catch문을 통해 예외를 잡는다
return readUserFromFile(id);
} catch(IOException e) {
throw new CustomException(ErrorCode.DATA_NOT_FOUND, e); // 예외 변환?
}
}
}
public class UserService {
public UserDto getUser(String id) {
return convertToDto(userRepository.findById(id)); // 기술 종속이 제거된 모습
}
}
- IOException은 사라지고 Repository에 CustomException으로 변환하여 처리함
- Service계층은 커스텀 예외만 처리한다.. 기술에 독립적임
| 위치 | 처리 대상 예외 | 변환 대상 | 설명 |
| DAO / Repository | IOException, SQLException, PersistenceException |
DataAccessException, 커스텀 예외 |
기술 종속 제거 |
| Service | NPE, IllegalStateException 등 | 커스텀 비즈니스 예외 | 로직 오류를 명확하게 전달 |
| 전역 예외 처리기 | 커스텀 예외, 런타임 예외 | @ControllerAdvice 처리 | HTTP 응답 변환 담당 |
실습을 통해 직접 해봐야 예외 변환에 대해 이해할 수 있을것 같다..
Repository의 역할이 늘어난 기분?
'Spring Boot' 카테고리의 다른 글
| 로깅 : 로깅(Logging)의 필요성에 대해 (2) | 2025.08.12 |
|---|---|
| 예외 처리 : 예외 계층 구조 설계 (5) | 2025.08.12 |
| 예외 처리 : 예외 처리의 필요성 (1) | 2025.08.12 |
| Spring 안정성 높이기 : 애플리케이션 안정성 (3) | 2025.08.11 |
| Repository에서 @Query, QueryDSL 작성에 대해 (2) | 2025.08.02 |