| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 코딩공부
- 자바개발
- HashMap
- 알고리즘
- 프로그래밍기초
- 알고리즘공부
- 객체지향
- 자료구조
- 자바공부
- 백준
- 코딩테스트준비
- 멀티스레드
- JVM
- 개발공부
- 프로그래머스
- 코딩테스트
- Java
- 가비지컬렉션
- 정렬
- 파이썬
- 자바프로그래밍
- 코딩테스트팁
- 예외처리
- 개발자취업
- 자바
- 개발자팁
- 메모리관리
- 코딩인터뷰
- 클린코드
- 자바기초
- Today
- Total
코드 한 줄의 기록
Java 사용자 정의 예외와 예외 전환·포장 전략 완벽 가이드: 실전 예제와 베스트 프랙티스 본문
Java 애플리케이션을 개발하다 보면 표준 예외만으로는 상황을 충분히 설명하기 어려울 때가 많습니다. 특히 도메인 로직에서 발생하는 특정 오류를 명확히 드러내고, 상위 계층으로 안전하게 전달하거나 은닉해야 할 경우가 그렇습니다. 이 글에서는 Java 사용자 정의(Custom) 예외를 만들고, 예외 전환(Exception Translation) 및 예외 포장(Exception Wrapping) 전략을 활용하여 안정적이고 유지보수하기 용이한 예외 처리 구조를 설계하는 방법을 단계별로 살펴봅니다.
예외 처리 기본 복습: Checked vs Unchecked
Java 예외는 크게 두 가지로 나눌 수 있습니다.
- Checked Exception: 컴파일 시점에 처리(try-catch 혹은 throws) 여부를 강제합니다. 예)
IOException,SQLException - Unchecked Exception:
RuntimeException을 상속한 예외로, 컴파일러가 처리 여부를 검사하지 않습니다. 예)NullPointerException,IllegalArgumentException
Checked 예외는 외부 자원 접근 오류와 같이 반드시 처리해야 할 상황에, Unchecked 예외는 프로그래밍 로직상의 버그나 잘못된 인자 전달을 표현할 때 주로 사용합니다.
사용자 정의 예외 만들기
Checked 사용자 정의 예외
public class InsufficientBalanceException extends Exception {
private final BigDecimal deficit;
public InsufficientBalanceException(BigDecimal deficit) {
super("잔액이 부족합니다. 부족 금액: " + deficit);
this.deficit = deficit;
}
public BigDecimal getDeficit() {
return deficit;
}
}
Exception을 상속해 Checked 예외로 만든다.- 생성자에서 메시지와 추가 데이터(부족 금액)를 보관.
Unchecked 사용자 정의 예외
public class DataValidationException extends RuntimeException {
public DataValidationException(String field, String error) {
super(String.format("검증 실패: 필드=%s, 오류=%s", field, error));
}
}
RuntimeException상속 시 Unchecked 예외가 된다.- 잘못된 인자 검증 로직에 적합.
예외 전환(Exception Translation)
외부 라이브러리나 하위 계층에서 던진 예외를 상위 계층에 적합한 예외로 바꿔 던지는 기법입니다.
public class UserRepository {
public User findById(Long id) {
try {
return jdbcTemplate.queryForObject("SELECT * FROM users WHERE id=?", rowMapper, id);
} catch (EmptyResultDataAccessException e) {
throw new UserNotFoundException(id, e);
} catch (DataAccessException e) {
throw new RepositoryException("데이터베이스 오류", e);
}
}
}
EmptyResultDataAccessException→UserNotFoundException으로 전환DataAccessException→RepositoryException으로 래핑
이 전략을 쓰면 상위 계층(서비스나 컨트롤러)이 하위 라이브러리 예외에 종속되지 않고, 도메인 의미에 맞춰 처리할 수 있습니다.
예외 포장(Exception Wrapping)
기존 예외를 새로운 예외 내부에 포함시키는 방법으로, 원인을 보존하면서 계층별로 적절한 의미를 덧씌울 수 있습니다.
public class PaymentService {
public void processPayment(Order order) {
try {
billingApi.charge(order);
} catch (ExternalServiceException e) {
throw new PaymentProcessingException("결제 처리 중 오류 발생", e);
}
}
}
- 원래 예외(
ExternalServiceException)를PaymentProcessingException에 wrap getCause()로 내부 예외를 추적 가능
언제 전환하고, 언제 포장할까?
- 전환(Translation): 하위 계층 예외를 전혀 공개하지 않고 도메인적 의미로 완전 교체 - 하위 오류를 은닉하고, 공개 API 관점에서 일관된 예외 타입 제공
- 포장(Wrapping): 원인 예외를 보존하면서 추가 정보(문맥)를 덧붙임 - 디버깅 시 스택 트레이스 상에서 원인 파악이 중요할 때
일반적으로 Repository → Service 계층에서는 전환을, Service → Controller 계층에서는 포장을 통해 사용자 메시지를 덧붙이는 식으로 계층별 패턴을 정하면 좋습니다.
실전 코드 예제: 계층별 예외 설계
// 1. 도메인 계층: 사용자 정의 예외
public class UserNotFoundException extends RuntimeException {
private final Long userId;
public UserNotFoundException(Long userId) {
super("사용자를 찾을 수 없습니다. ID=" + userId);
this.userId = userId;
}
public Long getUserId() { return userId; }
}
// 2. Repository 계층: 예외 전환
public class UserRepository {
public User find(Long id) {
try {
// ... JDBC 조회
} catch (SQLException e) {
if (isNotFound(e)) {
throw new UserNotFoundException(id);
} else {
throw new RepositoryException("DB 오류", e);
}
}
}
}
// 3. Service 계층: 예외 포장
public class UserService {
public UserDto getUserProfile(Long id) {
try {
User user = userRepository.find(id);
return UserMapper.toDto(user);
} catch (RepositoryException e) {
throw new ServiceException("프로필 조회 실패(ID=" + id + ")", e);
}
}
}
// 4. Controller 계층: 예외 핸들링
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
try {
UserDto dto = userService.getUserProfile(id);
return ResponseEntity.ok(dto);
} catch (UserNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
} catch (ServiceException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
}
베스트 프랙티스 및 주의사항
- 구체적인 예외 메시지 작성: 사용자와 개발자 모두 이해할 수 있게.
- 불필요한 래핑 지양: 중첩이 깊어지면 디버깅이 어려워짐.
- 체계적인 예외 계층 구조 유지: 공통 베이스 예외(
ApplicationException) 두고 분류하면 관리 용이. - 로그와 연계: 예외 발생 시 적절한 로그 레벨(DEBUG/INFO/WARN/ERROR) 설정.
- API 문서 반영: 공개 API 예외 타입과 의미를 명세에 명확히 기재.
사용자 정의 예외와 예외 전환·포장 전략을 활용하면, 애플리케이션의 예외 처리 로직이 더욱 명확해지고 유지보수가 쉬워집니다. 각 계층별 역할에 맞춰 전환과 포장을 적절히 섞어 쓰면, 도메인 의미를 보존하면서도 하위 구현에 종속되지 않는 견고한 설계를 완성할 수 있습니다. Java 예외 처리의 정석을 숙지하고, 실제 프로젝트에 바로 적용해 보세요!
Java try-catch-finally와 try-with-resources로 자원 누수 완벽 방지하기
자바로 개발하면서 가장 중요하지만 종종 놓치기 쉬운 부분이 바로 자원 관리입니다. 파일을 열거나 데이터베이스에 연결할 때, 사용이 끝나면 반드시 닫아줘야 하는데 이를 제대로 하지 않으
byteandbit.tistory.com
'JAVA' 카테고리의 다른 글
| Java 로깅 완벽 가이드: SLF4J와 로그 레벨 이해 (0) | 2025.10.19 |
|---|---|
| Java 스택 트레이스와 브레이크포인트 완벽 해설 (0) | 2025.10.18 |
| Java try-catch-finally와 try-with-resources로 자원 누수 완벽 방지하기 (0) | 2025.10.15 |
| Java 예외 처리 마스터하기: 체크/언체크 예외부터 throw/throws까지 완전 정복 (0) | 2025.10.14 |
| Java 설계 원칙 완전 정복: 초보자를 위한 SOLID 입문 가이드 (0) | 2025.10.13 |