너가 써야될거

[MyBatis/JPA] (코드) MyBatis -> JPA 전환 과정

suoop 2024. 7. 2. 04:48

💡목표

앞선 포스팅에서,

어댑터 패턴을 이용하여 OrderRepository와 OrderJPARepository의 메서드가 호환될 수 있도록 한다고 하였다.

 

즉, OrderJPARepository'Adapter' 클래스를 이용하여, 호환되지 않는 OrderJPARepository를 OrderRepository 인터페이스로 변환시켜 OrderJPARepository와 OrderRepository 인터페이스가 호환되도록 한다.

 

 

📗 과정

1. 기존 OrderMyBatisRepository만 있던 코드에서, 아래 클래스를 생성한다.

  • OrderJpaRepository 인터페이스 (JPA 레포지토리)
  • OrderRepository 인터페이스 (상위 레포지토리)
  • OrderJPARepositoryAdapter 클래스 (어댑터 클래스)

-> MyBatis 레포지토리와 JPA 레포지토리는 상위 인터페이스를 상속 및 구현하도록 한다.

 

 

< OrderRepository >

// ...import문 생략

public interface OrderRepository {

    Optional<Order> getOrder(Long orderId);

    int saveOrder(Order order);

    void deliveredToConfirmOrder(Long orderId);

    void updateOrderPrices(Order order);
}

 

< OrderMyBatisRepository >

// ...import문 생략

@Mapper
// OrderRepository를 상속한다.
public interface MybatisOrderRepository extends OrderRepository {

    Optional<Order> getOrder(Long orderId);

    int saveOrder(Order order);

    void deliveredToConfirmOrder(Long orderId);

    void updateOrderPrices(Order order);
}

 

< OrderJPARepositoryAdapter >

// ...import문 생략

@Slf4j
@Repository
// OrderRepository를 구현한다.
public class JpaOrderRepositoryAdapter implements OrderRepository {
    ...
}

 

 

2. 어댑터의 내용을 작성한다.

1) 현재 OrderService에서는 여전히 MyBatis 레포지토리 메서드의 반환값을 쓰고 있다.

2) 또한 DIP 준수를 위해, JPA가 도입되었다고 해서 서비스 코드가 바뀌면 안 된다.

 

=> 따라서 어댑터를 통해, 서비스의 메서드를 오버라이딩하고 로직은 JPA 레포지토리를 이용한다. 

이후 JPA 레포지토리 메서드의 반환값은 서비스의 반환값 타입으로 변환시킨다.

그래야 서비스 코드를 수정하지 않고도 JPA 레포지토리를 이용하여 코드를 실행할 수 있다.

 

< OrderJPARepositoryAdapter >

// ...import문 생략

@Slf4j
@Repository
public class JpaOrderRepositoryAdapter implements OrderRepository {
    MemberJpaRepository memberJpaRepository;
    AddressJpaRepository addressJpaRepository;
    JpaOrderRepository jpaOrderRepository;

    JpaOrderRepositoryAdapter(JpaOrderRepository jpaOrderRepository
            , MemberJpaRepository memberJpaRepository
            , AddressJpaRepository addressJpaRepository) {
        this.jpaOrderRepository = jpaOrderRepository;
        this.memberJpaRepository = memberJpaRepository;
        this.addressJpaRepository = addressJpaRepository;
    }

// 서비스의 메서드를 오버라이딩한다.
    @Override
    public Optional<Order> getOrder(Long orderId) {
        // 메서드 안의 로직은 jpaOrderRepository 메서드를 사용한다.
        return jpaOrderRepository.findById(orderId)
                // 반환값은 서비스의 반환값인 Optional<Order>에 맞춰 변환한다.
                .map(Order::new);
    }

    @Override
    public int saveOrder(Order order) {
        OrderEntity orderEntity = convertToOrderEntity(order);
        jpaOrderRepository.save(orderEntity);
        return 1;
    }

    @Override
    public void deliveredToConfirmOrder(Long orderId) {
        jpaOrderRepository.deliveredToConfirmOrder(orderId);
    }

    @Override
    public void updateOrderPrices(Order order) {
        OrderEntity orderEntity = jpaOrderRepository.findById(order.getOrderId())
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "주문 정보를 찾을 수 없습니다."));

        orderEntity.setTotalProductsPrice(order.getTotalProductsPrice());
        orderEntity.setTotalPaymentPrice(order.getTotalPaymentPrice());

        jpaOrderRepository.save(orderEntity);
    }
}

 

 

3. 서비스에서 OrderRepository의 Bean 객체 주입 대상을 정한다(@Qualifier 어노테이션 이용)

서비스 코드에서 아무런 조치 없이 바로 OrderRepository의 메서드를 사용하게 되면, 

아래 사진과 같이 "Could not autowire. There is more than one bean of 'OrderRepository' type." 라는 문구가 나온다.

 

이는 'OrderRepository'를 상속하고 구현하는 대상이 Jpa어댑터와 MyBatis레포지토리, 2개이므로

OrderRepository 타입의 빈이 여러 개 있어서 어떤 빈을 주입해야 할지 알 수 없다는 의미이다.

 

따라서 @Qualifier 어노테이션을 이용하여 bean 객체 주입의 대상을 정한다.

만약 jpaRepositoryAdapter를 주입하면, JPA 레포지토리 메서드를 이용한 서비스 로직이 수행된다.

 

< OrderService >

// ...import문 생략

@Slf4j
@Service
public class OrderService {

    private final UpperOrderRepository upperOrderRepository;
    private final MemberRepository memberRepository;
    private final AddressRepository addressRepository;

    public OrderService(
            @Qualifier("jpaOrderRepositoryAdapter") UpperOrderRepository upperOrderRepository
            ,@Qualifier("memberJpaRepositoryAdapter") MemberRepository memberRepository
            ,@Qualifier("addressJpaRepositoryAdapter") AddressRepository addressRepository
//            @Qualifier("mybatisOrderRepository") UpperOrderRepository upperOrderRepository
//            ,@Qualifier("memberMybatisRepository") MemberRepository memberRepository
//            ,@Qualifier("addressMybatisRepository") AddressRepository addressRepository
    ) {
        this.upperOrderRepository = upperOrderRepository;
        this.memberRepository = memberRepository;
        this.addressRepository = addressRepository;
    }

    @Transactional(readOnly = true)
    public OrderResponse getOrder(Long orderId) {

        return upperOrderRepository.getOrder(orderId)
                .map(OrderResponse::fromOrder)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "존재하지 않는 주문번호입니다."));
    }

    @Transactional
    public Long saveOrder(CreateOrderRequest createOrderRequest) {

        Member member = memberRepository.findById(createOrderRequest.getMemberId())
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "회원 정보를 찾을 수 없습니다."));

        Address address = addressRepository.findById(createOrderRequest.getAddressId())
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "배송지 정보를 찾을 수 없습니다."));

        Order order = createOrderRequest.toOrder(member, address);
        upperOrderRepository.saveOrder(order);

        return order.getOrderId();
    }

    @Transactional
    public void deliveredToConfirmOrder(Long orderId) {

        Order order = upperOrderRepository.getOrder(orderId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "주문 정보를 찾을 수 없습니다."));

        if (order.getStatus() != Status.DELIVERED) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "주문 상태가 '배송완료'가 아니기 때문에 주문확정이 불가능합니다.");
        }

        upperOrderRepository.deliveredToConfirmOrder(orderId);
    }
}

 

 

 

📕 정리

앞에 나온 과정들을 정리해보면 다음과 같다.

 

 

1. JPA 레포지토리, JPA 레포지토리 어댑터, 상위 레포지토리 인터페이스를 생성(MyBatis 레포지토리는 기존에 존재).

2. 어댑터 내용을 작성.
    -> 상위 레포지토리 메서드를 오버라이딩하여 JPA 레포지토리를 이용한 로직 작성
    -> 그 후 반환값은 서비스 메서드의 반환값으로 변환하여 반환.

3. 서비스에서  @Qualifier 어노테이션을 이용하여 bean 객체 주입의 대상을 정함.

 

 

이렇게 하면 

다른 레이어의 코드 변경 없이 & 기존 시스템의 기능은 모두 유지하면서

MyBatis와 JPA 레포지토리 메서드, 둘 중 하나를 선택하여 사용할 수 있게 된다!