#스프링 #강의

[토비의 스프링 6] 서비스 추상화

1jeongg 1jeongg Follow 2024년 06월 30일 · 11 mins read
Share this

서비스 추상화

서비스

계층 구조

서비스는 일반적인 용어이기에 쓰이는 곳에 따라 다른 의미를 가진다. 단순히 @Service 어노테이션이 붙은 것이 아닌 인프라 서비스를 추상화하는 것을 서비스 추상화라고 한다.

서비스(=서버)는 클라이언트에게 서비스를 제공해주는 오브젝트나 모듈을 말한다.

서비스는 일반적으로 상태를 가지지 않는다. 따라서 상태 없는 싱글톤 스프링 빈을 사용하기에 적합하다.

서비스의 종류

  • 애플리케이션 서비스: @Service 어노테이션이 붙는다. 비즈니스 로직의 경계에 있다.
  • 도메인 서비스: 비즈니스 로직을 엔티티 같은 도메인 오브젝트에 집어넣는다.
  • «인프라 서비스»: 도메인/애플리케이션 로직에 참여하지 않는, 기술을 제공하는 서비스를 말한다.
    • 서비스 추상화의 대상!
    • 메일, 캐시, 트랜잭션, 메시징, …

애플리케이션 서비스 도입

  • @Service 빈으로 구성된다.
  • Application/Service 계층에 존재한다.
  • 애플리케이션/도메인 로직 - 도메인 오브젝트/엔티티 활용
  • 인프라 서비스의 도움이 필요하다
  • 가장 중요한 도메인 / 애플리케이션 / 비즈니스 로직이 들어간다
  • 인프라 레이어에 존재하는 기술에 가능한 의존하지 않도록 만들어야 한다
    • 서비스 코드가 같이 따라서 바뀌면 안된다
  • 마치 PaymentService - ExRateService에 적용된 DIP
    • 인터페이스를 사용해서 분리했던 경험..

예제를 사용해보자! 구조는 다음과 같다 구조

OrderService

  • 데이터 액세스 기술의 하나인 JPA에 의존
  • JPA를 사용하는 Repository 클래스에 의존
  • JPA Transaction Manager에 의존 ```java @Service public class OrderService {

    private final OrderRepository orderRepository; private final JpaTransactionManager jpaTransactionManager;

    public OrderService(OrderRepository orderRepository1, JpaTransactionManager jpaTransactionManager) { this.orderRepository = orderRepository1; this.jpaTransactionManager = jpaTransactionManager; }

    public Order createOrder(String no, BigDecimal total) { Order order = new Order(no, total);

      return new TransactionTemplate(jpaTransactionManager).execute(status -> {
          this.orderRepository.save(order);
          return order;
      });   }
    

}


Order
- JPA 엔티티
- 컴파일 시점에만 JPA 라이브러리 의존
- 클래스 코드에는 JPA 기술과 관련된 내용이 들어가있지 않는다
- JPA를 사용하지 않으면 런타임에는 JPA 라이브러리가 존재하지 않음

```java
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    @Column(unique = true)
    private String no;

    private BigDecimal total;
    ...
}

@Configuration
@Import(DataConfig.class)
public class OrderConfig {
    @Bean
    public OrderService orderService(JpaTransactionManager jpaTransactionManager) {
        return new OrderService(orderRepository(), jpaTransactionManager);
    }

    @Bean
    public OrderRepository orderRepository() {
        return new OrderRepository();
    }
}
public class OrderRepository {
    @PersistenceContext
    private EntityManager entityManager;

    public void save(Order order) {
        entityManager.persist(order);
    }
}
public class OrderClient {
    public static void main(String[] args) {
        BeanFactory beanFactory = new AnnotationConfigApplicationContext(OrderConfig.class);
        OrderService service = beanFactory.getBean(OrderService.class);
        Order order = service.createOrder("0100", BigDecimal.TEN);
        System.out.println(order);
    }
}

기술에 독립적인 애플리케이션 서비스

Order에서 JPA 메타데이터 분리

  • 어노테이션(@Entity)은 컴파일타임 라이브러리 의존성만 가진다.
  • 엔티티의 동작에는 영향을 주지 않기 때문에 엔티티 클래스를 다른 데이터 기술에서 사용해도 된다
  • 그래도 제거하고 싶다면 외부 XML 디스크립터를 사용할 수 있다.
    • /META-INF/orm.xml 에 XML 정보를 넣으면 된다.

OrderRepository DIP

현재는 OrderService가 OrderRepository에 의존하고 있다. 이러한 문제를 해결하기 위해선 의존관계 역전 방식을 사용해서 OrderService와 JPAOrderRepository를 연결하는 인터페이스를 만들어서 사용하면 된다.

이렇게 하면 하위 모듈이 상위 모듈에 의존하는 식으로 변경된다!

인터페이스를 만들고..

public interface OrderRepository {
    void save(Order order);
}

이를 실제로 구현하는 JpaOrderRepository를 만든다

public class JpaOrderRepository implements OrderRepository {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public void save(Order order) {
        entityManager.persist(order);
    }
}

트랜잭션 서비스 추상화

하지만 현재는 JPA 를 사용하는 방식에 의존하고 있다는 문제점이 있다. JDBC 등 다른 기술을 사용했을 때도 많은 변화 없이 프로그램을 이용할 수 있도록 하려면 서비스 추상화 작업이 필요하다.

추상화

  • 구현의 복잡함과 디테일을 감추고 중요한 것만 남기는 방법
  • 여러 인프라 서비스 기술의 공통적이고 핵심적인 기능을 인터페이스로 정의하고 이를 구현하는 어댑터를 만들어 일관된 사용이 가능하게 만드는 것

PlatformTransactionManager는 공통적이고 핵심적인 기능을 정의하는 인터페이스다.

JPATransactionManagerDataSourceTransactionManager가 인터페이스를 구현하도록 하는 어댑터이다.

이미지

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final PlatformTransactionManager transactionManager;

    public OrderService(OrderRepository orderRepository, PlatformTransactionManager transactionManager) {
        this.orderRepository = orderRepository;
        this.transactionManager = transactionManager;
    }

    public Order createOrder(String no, BigDecimal total) {
        ...
    }
}

JDBC 데이터 액세스 기술

JDBC는 자바에서 제공하는 로우 레벨의 SQL을 이용하는 기술이다.

JdbcClient

  • SQL을 사용하는 JDBC 데이터 처리 코드를 유연하게 작성하도록 도와줌
  • 일종의 템플릿/콜백
  • 스프링의 JdbcTemplate 대체 기술

DataSourceTransactionManager

  • JDBC의 Connection을 이용하는 트랜잭션 매니저
  • Connection을 리턴하는 DataSource 오브젝트 필요
public class JdbcOrderRepository implements OrderRepository {

    private final JdbcClient jdbcClient;

    public JdbcOrderRepository(DataSource dataSource) {
        this.jdbcClient = JdbcClient.create(dataSource);
    }

    // Bean의 초기화 작업이 끝나면 자동으로 실행 - 테이블 생성
    @PostConstruct
    void init() {
        jdbcClient.sql("""
                create table orders (id bigint not null, no varchar(255), total numeric(38,2), primary key (id));
                alter table if exists orders drop constraint if exists UK43egxxciqr9ncgmxbdx2avi8n;
                alter table if exists orders add constraint UK43egxxciqr9ncgmxbdx2avi8n unique (no);
                create sequence orders_SEQ start with 1 increment by 50;
        """).update();
    }

    // Order 저장
    @Override
    public void save(Order order) {
        Long id = jdbcClient.sql("select next value for orders_SEQ;").query(Long.class).single();
        order.setId(id);
        
        jdbcClient.sql("insert into orders (no,total,id) values (?,?,?);")
                .params(order.getNo(), order.getTotal(), order.getId())
                .update();

        System.out.println(id);
    }
}

트랜잭션 테스트

데이터 기술 (JPA, jdbc 등)이 변경되어도 기존 코드는 영향을 받지 않지만 TransactionTemplate, PlatformTransactionManager와 같은 기술과 연관된 코드가 계속 등장한다.
트랜잭션의 시작과 종료는 보통 애플리케이션 서비스 메소드 실행 전호루 발생하기에 어쩔 수 없이 OrderService에 기술 관련 코드가 들어갈 수 밖에 없음

트랜잭션 테스트의 어려움

  • 트랜잭션이 필요한 곳에 정확하게 적용되었는지 테스트 하기 어려움
  • JDBC처럼 자동 커밋이 되거나 Spring Data JPA처럼 기본 리포지토리 구현에서 트랜잭션을 알아서 적용해주는 기술을 사용할 때 트랜잭션이 바르게 적용되지 않은 것을 놓치기 쉬움
  • 모든 작업이 성공하면 하나의 트랜잭션으로 진행된 것인지 여러 개의 트랜잭션으로 쪼개진 것인지 확인하기 어려움

=> 트랜잭션 중간에 실패하는 케이스를 만들어 롤백 여부로 확인하기

이번 예제에서는 여러 Orders 를 한 번에 저장하는데, 하나가 실패해도 시도했던 모든 Order 내용이 DB에 저장되지 않고 rollback 되도록 하는 코드와 테스트를 진행해본다.

@Service
public class OrderService {
    ...
    // 여러 Orders 한 번에 저장하기
    public List<Order> createOrders(List<OrderRequest> reqs) {
        return new TransactionTemplate(transactionManager).execute(status ->
                reqs.stream().map(req -> createOrder(req.no(), req.total())).toList()
        );
    }
}
@Test
void createDuplicatedOrders() {
    List<OrderRequest> orderReqs = List.of(
            new OrderRequest("0300", BigDecimal.ONE),
            new OrderRequest("0300", BigDecimal.TWO));

    // 실패하는가?
    assertThatThrownBy(() -> orderService.createOrders(orderReqs))
            .isInstanceOf(DataIntegrityViolationException.class);

    // 첫 번째가 성공해도 두 번째가 실패하면 둘 다 rollback 해줘야 함
    JdbcClient client = JdbcClient.create(dataSource);
    Long count = client.sql("select count(*) from orders where no = '0300'").query(Long.class).single();
    assertThat(count).isEqualTo(0L);
}

트랜잭션 프록시

그런데 OrderService 안에 관심사가 다른 Transaction 관련 메소드를 쓰고 싶지 않다면?? 그러면서 Transaction은 적용하고 싶은데..

=> 프록시를 사용해서 Transaction을 사용하는 부분을 분리하자

데코레이터 패턴

오브젝트의 코드를 변경하지 않고 새로운 기능을 런타임에 부여하는 디자인 패턴 이미지

프록시 패턴

타깃을 대신해서 존재하며 접근을 제어하거나 보안, 지연, 원격 접속 등의 기능을 제공 이미지

트랜잭션 프록시를 만들어보자. 먼저 OrderService 인터페이스를 추출하고 트랜잭션 부가 기능을 제공하는 OrderServiceTxProxy 프록시를 만든다.

OrderServiceImpl는 실제 OrderService를 구현한다.

OrderServiceTxProxy는 트랜잭션을 시작하고 에러가 없으면 커밋, 있으면 롤백하는 기능을 담당한다.

이미지

public class OrderServiceTxProxy implements OrderService {
  private final OrderService target;
  private final PlatformTransactionManager transactionManager;

  public OrderServiceTxProxy(OrderService target, PlatformTransactionManager transactionManager) {
    this.target = target;
    this.transactionManager = transactionManager;
  }

  @Override
  public Order createOrder(String no, BigDecimal total) {
    return new TransactionTemplate(transactionManager).execute(status ->
            target.createOrder(no, total)
    );
  }

  @Override
  public List<Order> createOrders(List<OrderRequest> reqs) {
    return new TransactionTemplate(transactionManager).execute(status ->
            target.createOrders(reqs)
    );
  }
}
public interface OrderService {
    Order createOrder(String no, BigDecimal total);

    List<Order> createOrders(List<OrderRequest> reqs);
}
@Service
public class OrderServiceImpl implements OrderService {

    private final OrderRepository orderRepository;

    public OrderServiceImpl(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public Order createOrder(String no, BigDecimal total) {
        Order order = new Order(no, total);
        orderRepository.save(order);
        return order;
    }

    @Override
    public List<Order> createOrders(List<OrderRequest> reqs) {
        return reqs.stream().map(req -> createOrder(req.no(), req.total())).toList();
    }
}

아주 굿!

@Transaction과 AOP

@Transaction 어노테이션이 붙은 클래스의 메소드가 트랜잭션 안에서 실행되도록 프록시를 만들어줌

이것만 하면 Transaction이 된다..!

@Configuration
@Import(DataConfig.class)
@EnableTransactionManagement
public class OrderConfig {

    @Bean
    public OrderRepository orderRepository(DataSource dataSource) {
        return new JdbcOrderRepository(dataSource);
    }

    @Bean
    public OrderService orderService(OrderRepository orderRepository) {
        return new OrderServiceImpl(orderRepository);
    }

}
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
  ...
}

AOP는 아니더라도 데코레이터/프록시 패턴의 동작원리를 이해하고 필요한 곳에 활용하면 된당



1jeongg
Written by 1jeongg Follow

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