#스프링 #강의

[토비의 스프링 6] 예외

1jeongg 1jeongg Follow 2024년 06월 29일 · 6 mins read
Share this

예외

예외는 정상적인 프로그램 흐름을 방해하는 사건이다. 말 그대로 예외적인 상황에서만 사용해야 한다.
많은 경우 예외는 포로그램 오류, 버그 때문에 발생한다.

예외가 발생하면?

➡️ 예외 상황을 복구해서 정상적인 흐름으로 전환할 수 있는가?

  1. 재시도: 일시적인 오류
  2. 대안: 다른 방식으로 해결 (ex. 캐시에 남겨놓은 정보를 사용)

➡️ 버그인가?

  1. 예외가 발생한 코드의 버그인가? (ex. 널체크 잘못한 경우)
  2. 클라이언트의 버그인가? (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으로 저장된다.
OrderEntityManager는 사용자마다 새로운 오브젝트를 만들기 때문에 공유되면 안되기에 스프링의 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);
    }
}



1jeongg
Written by 1jeongg Follow

I'm studying Android development by Kotlin and Spring by Java