예외
예외는 정상적인 프로그램 흐름을 방해하는 사건이다. 말 그대로 예외적인 상황에서만 사용해야 한다.
많은 경우 예외는 포로그램 오류, 버그 때문에 발생한다.
예외가 발생하면?
➡️ 예외 상황을 복구해서 정상적인 흐름으로 전환할 수 있는가?
- 재시도: 일시적인 오류
- 대안: 다른 방식으로 해결 (ex. 캐시에 남겨놓은 정보를 사용)
➡️ 버그인가?
- 예외가 발생한 코드의 버그인가? (ex. 널체크 잘못한 경우)
- 클라이언트의 버그인가? (ex. 적절하지 않은 API 요청을 보낸 경우)
➡️ 제어할 수 없는 상황인가?
사용자에게 알림을 주고 개발자가 빠르게 수정해야함
잘못된 예외처리
- catch문 안에 아무 작업을 처리하지 않고 정상적으로 처리된 것처럼 처리하는 경우
- 무의미하고 무책임한 Throws (ex. 냅다 throws Exception 적음)
예외의 종류
- Error
- 시스템에 비정상적인 상황이 발생: 캐치하면 안됨. 컨트롤 불가능
- OutOfMemoryError
- ThreadDeath
- Exception (checked)
- catch나 throws를 강요
- 초기 라이브러리의 잘못된 예외 설계/사용
- 복구할 수 없다면 RuntimeException이나 적절한 추상화 레벨의 예외로 전환해서 던질 것
- RuntimeException (unchecked)
예외의 추상화와 전환
자바에는 다양한 사용 기술을 사용하기 때문에 같은 문제에 대해 다른 종류의 예외가 발생한다.
적절한 예외 추상화와 예외 번역이 필요하다.
JPA Repository - 애플리케이션 인프라스트럭처 빈
JPA를 이용하여 Order를 저장해보는 예제를 작성해본다.
- Order: 주문 정보 저장
- OrderRepository: Order의 읽기 쓰기를 담당
- EntityManager: DB에서 온 정보를 다시 자바 오브젝트로 변환
- EntityManagerFactory: EntityManager를 생성한다.
- DataSource: 데이터베이스와의 연결을 담당한다.
이때 회색인 DataSource
, EntitymanagerFactory
, OrderRepository
는 딱 한 개만 존재해도 되므로 공유되어 사용되는 스프링 Bean으로 저장된다.
Order
나 EntityManager
는 사용자마다 새로운 오브젝트를 만들기 때문에 공유되면 안되기에 스프링의 Bean으로 등록되지 않는다.
따라서 위 예제를 코드로 구현하면 다음과 같다.
public class DataClient {
public static void main(String[] args) {
BeanFactory beanFactory = new AnnotationConfigApplicationContext(DataConfig.class);
OrderRepository repository = beanFactory.getBean(OrderRepository.class);
Order order = new Order("100", BigDecimal.TEN);
repository.save(order);
System.out.println(order);
}
}
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
private Long id;
@Column(unique = true)
private String no;
private BigDecimal total;
}
public class OrderRepository {
private final EntityManagerFactory emf;
public OrderRepository(EntityManagerFactory emf) {
this.emf = emf;
}
public void save(Order order) {
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
em.persist(order);
em.flush();
transaction.commit();
} catch(RuntimeException e) {
if (transaction.isActive()) transaction.rollback();
throw e;
} finally {
if (em.isOpen()) em.close();
}
}
}
@Configuration
public class DataConfig {
// data source
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
}
// entity manager factory
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(dataSource());
emf.setPackagesToScan("example.hellospring");
emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter() );
return emf;
}
@Bean
public OrderRepository orderRepository(EntityManagerFactory emf) {
return new OrderRepository(emf);
}
}
스프링의 데이터 액세스 예외처리
기술적인 변경이 일어났을 때도 똑같은 문제를 처리하는 코드가 일정한 방식으로 예외를 다루었으면 좋겠다
- JDBC SQLException
- JDBC를 기반으로 하는 모든 기술에서 발생하는 예외
- DB의 에러 코드에 의존하거나, 데이터 기술에 의존적인 예외처리 코드
DataAccessException
- DB의 에러코드와 데이터 액세스 기술에 독립적인 예외 구조
- 적절한 예외 번역 도구 제공
추상화된 예외로 변환해서 사용하도록 위의 코드를 변환해보자!
public class DataClient {
public static void main(String[] args) {
BeanFactory beanFactory = new AnnotationConfigApplicationContext(DataConfig.class);
OrderRepository repository = beanFactory.getBean(OrderRepository.class);
JpaTransactionManager transactionManager = beanFactory.getBean(JpaTransactionManager.class);
// transaction begin
try {
new TransactionTemplate(transactionManager).execute(status -> {
Order order = new Order("100", BigDecimal.TEN);
repository.save(order);
System.out.println(order);
// 데이터 무결성 에러 발생: no 가 unique 해야 하기 때문
Order order2 = new Order("100", BigDecimal.TEN);
repository.save(order2);
System.out.println(order2);
return null;
});
}
catch (DataIntegrityViolationException e) {
System.out.println("주문번호 중복 복구 작업");
}
}
}
public class OrderRepository {
@PersistenceContext
private EntityManager entityManager;
public void save(Order order) {
entityManager.persist(order);
}
}
@Configuration
public class DataConfig {
// data source..
// entity manager factory..
// orderRepository..
// persist 후처리 작업
@Bean
public BeanPostProcessor persistenceAnnotationBeanPostProcessor() {
return new PersistenceAnnotationBeanPostProcessor();
}
// Transaction 매니저 - Commit, begin 등을 자동으로 해줌
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}