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로 접속

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


'개발 > Spring' 카테고리의 다른 글
| [SpringBoot] 머스테치 Mustache (0) | 2022.09.08 |
|---|---|
| [SpringBoot] JPA Auditing으로 생성시간/수정시간 자동화하기 (0) | 2022.09.08 |
| [SpringBoot] Spring Data JPA (0) | 2022.09.07 |
| [SpringBoot] JPA (0) | 2022.09.06 |
| [SpringBoot] test 코드 작성하기, Lombok (0) | 2022.09.02 |