본문 바로가기
개발/TIL

[TIL]방법론 스터디_TDD 테스트 예제 & [failed to load applicationcontext] 에러 & [Error creating bean with name] 에러

by 반비🥰 2024. 5. 20.
반응형
 

[TIL]방법론스터디_주문요청 API & JPA console 설정

[TIL]TDD스터디_Test 메소드 접근제어자(Public) & Record 클래스 타입[TIL]JAVA사전스터디_연산자, 조건문, 반복문, 배열, 컬렉션[TIL]JAVA사전스터디_기본형 변수, 참조형 변수, 형변환📌 자바 개발 환경JRE(

success-notes.tistory.com

 


 

 

해당 포스트는 "실전! 스프링부트 상품 주문 API 개발로 알아보는 TDD" 강의를 수강하여 해당 내용을 정리한 글입니다.

 

📌 오류 해결


[Error][failed to load applicationcontext]

failed to load applicationcontext for webmergedcontextconfiguration

해당 오류는 Bean을 생성하지 않아 발생한 문제였다. 그래서 Bean 객체를 생성해 주어 해결하였다.

 

[Error][Error creating bean with name]

Error creating bean with name 'XXX.XXXX.productorderservice.order.OrderApiTest': Injection of autowired dependencies failed

테스트 요청하는 Service에 @SpringBootTest 어노테이션을 붙여, Bean을 못 찾아서 발생하는 오류

 

📌 TDD 코드 구현 과정


1. Test에 통과 가능한 테스트 코드 작성(순수 JAVA로 구현)

public class PaymentServiceTest {
    private PaymentService paymentService;
    private PaymentPort paymentPort;

// Test 실행 전, 필요한 클래스 생성(인스턴스 생성)
    @BeforeEach
    void setUp() {
        PaymentGateway paymentGateway = new ConsolePaymentGateway();
        PaymentRepository paymentRepository = new PaymentRepository();
        paymentPort = new PaymentAdapter(paymentGateway, paymentRepository);
        paymentService = new PaymentService(paymentPort);
    }

// 상품주문 테스트
    @Test
    void 상품주문() {
        final Long orderId = 1L;
        final String cardNumber = "1234-1234-1234-1234";
        final PaymentRequest request = new PaymentRequest(orderId, cardNumber);
        paymentService.payment(request);
    }

// Test를 위한 데이터 전송 객체 생성 클래스(DTO)
    private record PaymentRequest(Long orderId, String cardNumber) {

        public PaymentRequest{
            Assert.notNull(orderId, "주문 ID는 필수입니다.");
            Assert.hasText(cardNumber, "카드 번호는 필수입니다.");
        }
    }

// 상품결제 서비스 
    private class PaymentService {
        private final PaymentPort paymentPort;

        private PaymentService(PaymentPort paymentPort) {
            this.paymentPort = paymentPort;
        }

        public void payment(final PaymentRequest request) {
            Order order = paymentPort.getOrder(request.orderId());

            final Payment payment = new Payment(order, request.cardNumber());

            paymentPort.pay(payment);
            paymentPort.save(payment);
        }
    }

    private interface PaymentPort {

        Order getOrder(Long orderId);

        void save(Payment payment);

        void pay(Payment payment);
    }

// 결제 Entity
    private class Payment {
        private Long id;
        private final Order order;
        private final String cardNumber;


        public Payment(final Order order, final String cardNumber) {
            Assert.notNull(order, "주문은 필수입니다.");
            Assert.hasText(cardNumber, "카드 번호는 필수입니다.");
            this.order = order;
            this.cardNumber = cardNumber;
        }

        public void assignId(Long id) {
            this.id = id;
        }

        public Long getId() {
            return id;
        }
    }


    private class PaymentAdapter implements PaymentPort {
        private final PaymentGateway paymentGateway;
        private final PaymentRepository paymentRepository;

        private PaymentAdapter(PaymentGateway paymentGateway, PaymentRepository paymentRepository) {
            this.paymentGateway = paymentGateway;
            this.paymentRepository = paymentRepository;
        }

        @Override
        public Order getOrder(Long orderId) {
            return new Order(new Product("상품1", 1000, DiscountPolicy.NONE),2 );
        }

        @Override
        public void save(Payment payment) {
            paymentRepository.save(payment);
        }

        @Override
        public void pay(Payment payment) {
            paymentGateway.execute(payment);
        }
    }

    private interface PaymentGateway {
        void execute(Payment payment);
    }

// 실제 결제 API 구현 구간
    public class ConsolePaymentGateway implements PaymentGateway{
        @Override
        public void execute(Payment payment) {
            System.out.println("결제 완료");
        }
    }

    private class PaymentRepository {
        private Map<Long, Payment> persistence = new HashMap<>();
        private Long sequence = 0L;

        public void save(Payment payment) {
            payment.assignId(++sequence);
            persistence.put(payment.getId(), payment);
        }
    }
}

 

Test할 기능을 하나의 Test 클래스에 구현하여 테스트 통과 후, Refactor를 통해 Inner Class를 분리한다.

Inner Class를 분리할 때는 부모 클래스 먼저 분리한 후, 자식 클래스를 분리해야 한다.

 

2. 스프링부트 Test로 변환

스프링부트 Test로 변환하기 위해서는 Test 클래스에 @SpringBootTest 어노테이션을 붙이고, @Autowired 및 @Component를 붙여 해당 테스트에 필요한 설정을 자동으로 제공해 준다.

 

@SpringBootTest

@SpringBootTest는 통합테스트를 위한 어노테이션으로 모든 Bean들을 스캔하고 애플리케이션 컨텍스트를 생성한 후, 테스트를 진행한다. 

@WebMvcTest

@WebMvcTest는 컨트롤러 테스트를 위한 어노테이션으로 내장된 서블릿 컨테이너가 랜덤 포트로 실행된다. 컨트롤러와 연관된 Bean들만을 제한적으로 찾아서 등록하며, @Component나 @ConfigurationProperties Bean들은 스캔되지 않는다.

@DataJpaTest

@DataJpaTest는 JPA 레포지토리 테스트를 위한 어노테이션으로 @Entity가 있는 클래스들을 스캔하며 테스트를 위한 TestEntityManager를 사용해 JPA 설정을 해준다. @WebMvcTest 어노테이션과 같이 @Component나 @ConfigurationProperties Bean들은 스캔되지 않는다.

3. API로 전환

[ API로 전환 전, PaymentService.java ] 

@Component
public class PaymentService {
    private final PaymentPort paymentPort;

    public PaymentService(PaymentPort paymentPort) {
        this.paymentPort = paymentPort;
    }

    public void payment(final PaymentRequest request) {
        final Order order = paymentPort.getOrder(request.orderId());

        final Payment payment = new Payment(order, request.cardNumber());

        paymentPort.pay(payment.getPrice(), payment.getCardNumber());
        paymentPort.save(payment);
    }
}

 

[ API로 전환 후, PaymentService.java ] 

@Component에서 @RestController와 @RequestMapping으로 변환

@RestController는 RESTful 웹 서비스를 제공하는 컨트롤러를 정의할 때 사용되며, @RequestMapping은 경로를 지정한다.

@RestController
@RequestMapping("/payments")
public class PaymentService {
    private final PaymentPort paymentPort;

    PaymentService(final PaymentPort paymentPort) {
        this.paymentPort = paymentPort;
    }

    @PostMapping
    @Transactional
    public ResponseEntity<Void> payment(@RequestBody final PaymentRequest request) {
        final Order order = paymentPort.getOrder(request.orderId());

        final Payment payment = new Payment(order, request.cardNumber());

        paymentPort.pay(payment.getPrice(), payment.getCardNumber());
        paymentPort.save(payment);

        return ResponseEntity.status(HttpStatus.OK).build();
    }
}

 

4. JPA 적용

[ JPA 적용 전, PaymentRepository.java ]

@Repository
public class PaymentRepository {
    private Map<Long, Payment> persistence = new HashMap<>();
    private Long sequence = 0L;

    public void save(Payment payment) {
        payment.assignId(++sequence);
        persistence.put(payment.getId(), payment);
    }
}

 

 

[ JPA 적용 후, PaymentRepository.java ]

public interface PaymentRepository extends JpaRepository<Payment, Long> {
}

기존 테스트를 위해 임의로 생성한 데이터를 interface로 변경하며 JPA를 통해 실제 데이터를 적용할 수 있다. 또한, 임의로 생성한 getter 등 불필요한 메서드를 삭제하였다.

 

👩🏻‍💻 전체 코드


https://github.com/BoHyun-Choi-0320/TDDStudy.git

 

GitHub - BoHyun-Choi-0320/TDDStudy: 항해플러스 - TDD 사전스터디 기록

항해플러스 - TDD 사전스터디 기록. Contribute to BoHyun-Choi-0320/TDDStudy development by creating an account on GitHub.

github.com

 

참고 사이트

https://mangkyu.tistory.com/242

반응형