본문 바로가기

개발/Spring

[SpringBoot] API, API Test

 

 

API를 만들기 위한 클래스

  • Request 데이터를 받을 Dto
  • API 요청을 받을 Controller
  • 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service

 

 

  • Web Layer
    • @Controller와 JSP/Freemaker등의 뷰 템플릿 영역
    • @Filter, 인터셉터, @ControllerAdvice 등 외부 요청과 응답에 대한 전반적인 영역
  • Service Layer
    • Controller와 Dao의 중간 영역에서 사용되는 서비스 영역
    • @Transactional이 사용되어야 하는 영역
  • Repository Layer
    • Database와 같이 데이터 저장소에 접근하는 영역(Data Access Object 영역)
  • Dtos
    • Data Transfer Object, 계층 간에 데이터 교환을 위한 객체들의 영역
    • 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등
  • Domain Model
    • 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것
    • @Entity가 사용된 영역 포함
    • VO처럼 값 객체들도 이 영역에 해당
    • Domain 영역에서 비즈니스 처리

 


 

 

* 트랜잭션 스크립트

  • 서비스 클래스 내부에서 모든 비즈니스 로직을 처리
  • 서비스 계층이 무의미하며, 객체란 단순히 데이터 덩어리 역할만 하게 된다.

 

- 슈도코드

public Order cancelOrder(int orderId) {
  1) 데이터베이스로부터 주문정보(Orders), 결제정보(Billing), 배송정보(Delivery) 조회
  2) 배송 취소를 해야 하는지 확인
  3) if(배송 중이라면) {
       배송 취소로 변경
     }
  4) 각 테이블에 취소 상태 Update
}

- 기존 Service 코드

@Transactional
public Order cancelOrder(int orderId) {

  //1)
  OrdersDto order = ordersDao.selectOrders(orderId);
  BillingDto billing = billingDao.selectBilling(orderId);
  DeliveryDto delivery = deliveryDao.selectDelivery(orderId);
  
  //2)
  String deliveryStatus = delivery.getStatus();
  
  //3)
  if("IN_PROGRESS".equals(deliveryStatus)) {
    delivery.setStatus("CANCEL");
    deliveryDao.update(delivery);
  }
  
  //4)
  order.setStatus("CANCEL");
  orderDao.update(order);
  
  billing.setStatus("CANCEL");
  deliveryDao.update(billing);
  
  return order;
}

- 수정된 코드

@Transactional
public Order cancelOrder(int orderId) {

  //1)
  Orders order = ordersRepository.findById(orderId);
  Billing billing = billingRepository.findByOrderId(orderId);
  Delivery delivery = deliveryRepository.findByOrderId(orderId);
  
  //2-3)
  delivery.cancel();
  
  //4)
  order.cancel();
  billing.cancel();
  
  return order;
}

* order, billing, delivery가 각자 본인의 취소 이벤트 처리

  -> 서비스 메서드는 트랜잭션과 도메인 간의 순서만 보장

 

 


 

 

 

* 스프링에서 Bean을 주입 받는 방식

  • @Autowired: 권장하지 않음
  • setter
  • 생성자: 권장

 

* @RequiredArgsConstructor

  • final이 선언된 모든 필드를 인자값으로 하는 생성자를 롬복이 대신 생성해줌
  • 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 수정하는 번거로움 해결

 

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;
    
    ...
 }

 

 


 

 

* Entity 클래스를 Request/Response 클래스로 사용하면 안된다.

  • Entity 클래스는 데이터베이스와 맞닿는 핵심 클래스
  • Entity 클래스를 기준으로 테이블이 생성되고, 스키마가 변경 -> 여러 클래스에 영향
  • Request/Response용 Dto는 View를 위한 클래스라 자주 변경이 필요
  • Controller에서 결과값으로 여러 테이블을 조인해서 줘야 할 경우에 Entity 클래스만으로 한계가 있다.
  • View Layer와 DB Layer의 역할 분리를 철저히 하는 게 좋다.

 

 


 

* ApiController 테스트

  • @WebMvcTest의 경우 JPA 기능이 작동하지 않음
    • Controller와 ControllerAdvice 등 외부 연동과 관련된 부분만 활성화
  • JPA 기능을 한번에 테스트할 때는 @SpringBootTest TestRestTemplate를 사용

 

import com.talk.about.domain.posts.Posts;
import com.talk.about.domain.posts.PostsRepository;
import com.talk.about.web.dto.PostsSavsRequestDto;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PostsApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @AfterEach
    public void tearDown() throws Exception {
        postsRepository.deleteAll();
    }

    @Test
    public void TestRegisterPosts() throws Exception {
        //given
        String title = "title";
        String content = "content";
        PostsSavsRequestDto requestDto =
                PostsSavsRequestDto.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();

        String url = "http://localhost:"+port+"/api/v1/posts";

        //when
        ResponseEntity<Long> responseEntity =
                restTemplate.postForEntity(url, requestDto, Long.class);

        //then
        assertThat(responseEntity.getStatusCode())
                .isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody())
                .isGreaterThan(0L);

        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);
    }

}

 

 

 


 

 

- Service에서 update

// 글수정
@Transactional
public Long update(Long id, PostsUpdateRequestDto requestDto) {

  Posts posts = postsRepository.findById(id).orElseThrow(
      () -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
        
  posts.update(requestDto.getTitle(), requestDto.getContent());
        
  return id;
}

* JPA의 영속성 컨텍스트

  • 엔티티를 영구 저장하는 환경 -> 논리적 개념
  • update에서 데이터베이스에 쿼리를 날리는 부분이 없다.
  • JPA의 핵심 내용: 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐
  • JPA의 EntityManager가 활성화된 상태로 트랜잭션 안에서 DB로부터 가져온 데이터는 영속성 컨텍스트가 유지된 상태
  • Entity 객체의 값만 변경하면 별도로 쿼리를 실행할 필요 없이 트랜잭션이 끝나는 시점에 해당 테이블에 변경된 값을 반영
    • 이를 dirty checking이라고 한다.

 

 

 


 

 

h2로 조회기능 확인

- application.yml -> h2 설정 추가

datasource:
  hikari:
    jdbc-url: jdbc:h2:mem:testdb;MODE=MYSQL

h2:
  console:
    enabled: true

- http://localhost:8080/h2-console로 접속

JDBC URL 확인

- connect 클릭 -> 관리 페이지로 이동

POSTS 테이블 확인

 

조회 테스트 완료