API를 만들기 위한 3개의 클래스
- Request 데이터를 받을 Dto
- API 요청을 받을 Controller
- 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
- 기존의 스프링 프로젝트 : Service에서 비즈니스 로직을 처리하는 식
- Web Layer
- 흔히 사용하는 컨트롤러(@Controller)와 JSP/Freemarker 등의 뷰 템플릿 영역
- 이외에도 필터(@Filter), 인터셉터, 컨트롤러 어드바이스(@ControllerAdvice) 등 외부의 요청과 응답에 대한 전반적인 영역을 이야기한다.
- Service Layer
- @Service에 사용되는 서비스 영역이다.
- 일반적으로 Controller와 Dao의 중간 영역에서 사용된다.
- @Transactional이 사용되어야 하는 영역
- Repository Layer
- Database와 같이 데이터 저장소에 접근하는 영역
- 기존에 개발하셨던 분들이라면 Dao(Data Access Object)영역으로 이해하시면 쉬울 것이다.
- Dtos
- Dto(Data Transfer Object)는 계층 간의 데이터 교환을 위한 객체를 이야기하며 Dtos는 이들의 영역을 얘기한다.
- 예를 들어 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등이 이들을 이야기합니다.
- Domain Model
- 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것을 도메인 모델이라고 한다.
- 이를테면 택시 앱이라고 하면 배차, 탑승, 요금 등이 모두 도메인이 될 수 있다.
- @Entity를 사용해보신 분들은 @Entity가 사용된 영역 역시 도메인 모델이라고 이해해면 된다.
- 무조건 데이터베이스의 테이블과 관계가 있어야만 하는 것은 아니다.
- VO처럼 값 객체들도 이 영역에 해당하기 때문
- Web(Controller), Service, Repository, Dto, Domain 이 5가지 레이어에서 비즈니스 처리를 담당해야 할 곳은 어디?
-> Domain이다.
- 기존에 서비스로 처리하던 방식을 트랜잭션 스크립트라고 한다. 주문 취소 로직을 작성한다면 다음과 같다.
- 슈도코드
@Transactional
public Order cancelOrder(int orderId){
1) 데이터베이스로부터 주문정보(Orders), 결제정보(Billing), 배송정보(Delivery) 조회
2) 배송 취소를 해야 하는지 확인
3) if(배송중이라면){
배송취소로 변경
}
4) 각 테이블에 취소 상태 Update
}
- 실제 코드
@Transactional
public Order cancleOrder(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("CANCLE");
deliveryDao.update(delivery);
}
//4)
order.setStatus("CANCLE");
ordersDao.update(order);
billing.setStatus("CANCLE");
deliveryDao.update(billing);
return order;
}
-> 모든 로직이 서비스 클래스 내부에서 처리
- 서비스 계층이 무의미, 객체란 단순히 데이터 덩어리 역할만 하게 된다.
도메인 모델에서 처리할 경우
@Transactional
public Order cancleOrder(int orderId){
//1)
Orders order = ordersRepository.findById(orderId);
Billing billing = billingRepository.findById(orderId);
Delivery delivery = deliveryRepository.findByOrderId(orderId);
//2-3)
delivery.cancle();
//4)
order.cancle();
billing.cancle();
return order;
}
- order, billing, delivery가 각자 본인의 취소 이벤트 처리를 한다.
- 서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장해준다.
등록, 수정, 삭제 기능을 만들어보자
- 이 책에서 JPA를 처음 접하면서 의아했던 점 : 보통 프로젝트를 구성할 때 DBTable VO(Dto)를 먼저 만들고 DAO를 작성 -> Service 작성 -> Controller 순으로 코드를 작성하였다.
- JPA 방식인지는 모르겠지만 Controller를 먼저 만들어주고 위에서 아래로 내려가는 식으로 코드를 작성한다.
PostsApiController - web 패키지
PostsSaveRequestDto - web.dto 패키지
PostsService - service.posts 패키지
PostsApiController - web 패키지
- 간단한 설명 : PostApiController이다. save 메소드를 실행하는 파트로. 클라이언트에서 "/api/v1/posts"로 요청이 오면 service를 호출해서 Long 값을 리턴 받는다.
- RestController로 Json데이터를 리턴한다.
- Autowired를 사용하지 않고 private final을 사용한다. 뒤에 설명
PostsService - service.posts 패키지
- 스프링을 써보았기 때문에 @Autowired가 없는 것이 어색하다.
- 스프링에서 Bean을 주입받는 방식들은 3가지 이다.
- @Autowired
- setter
- 생성자
- 책의 저자는 가장 권장하는 방식이 생성자로 주입받는 방식이라고 한다.(@Autowired 권장하지 않는다??..)
- 생성자로 Bean 객체를 받도록 하면 @Autowired와 동일한 효과를 볼 수 있다는 것
- 어떻게 생성자로 Bean 객체를 받을수 있을까?
@RequiredArgsConstructor 해결
: final이 선언된 모든 필드를 인자값으로 하는 생성자를 롬복의 @RequiredArgsConstructor가 대신 생성해 준 것.
- 생성자를 직접 안 쓰고 롬복 어노테이션을 사용하는 이유 : 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함이다
PostsSaveRequestDto - web.dto 패키지
- Controller와 Service에서 사용할 Dto
- Entity클래스(posts)와 거의 유사한 형태임에도 Dto 클래스를 추가로 생성
- 하지만, 절대로 Entity 클래스를 Request/Response 클래스로 사용해서는 안 된다.
- Entity 클래스는 데이터베이스와 맞닿은 핵심 클래스이다.
- Entity클래스를 기준으로 테이블이 생성되고, 스키마가 변경된다.
- 화면변경은 사소한 기능 변경인데, 이를 위해 테이블과 연결된 Entity 클래스를 변경하는 것은 너무 큰 변경이다
- Entity 클래스가 변경되면 여러 클래스에 영향을 끼치지만, Request와 Response용 Dto는 View를 위한 클래스라 정말 자주 변경이 필요하다.
Test 코드 작성
PostsApiControllerTest(web 패키지 밑에)
- Api Controller를 테스트하는데 HelloController와 달리 @WebMvcTest를 사용하지 않았다.
- @WebMvcTest의 경우 JPA 기능이 작동하지 않기 때문, Controller와 ControllerAdvice등 외부 연동과 관련된 부분만 활성화되니 지금 같이 JPA 기능까지 한번에 테스트할 때는 @SpringBootTest와 TestRestTemplate를 사용하면 된다.
PostsApiController(main/java/web 패키지 밑에) update, findById 메서드 추가
- 책에는 PutMapping이라고 되어 있는데 PostMapping으로 작성하는 것이 맞다.(저자도 확인해주셨다)
- update와 findById메서드 추가
PostsResponseDto- web.dto 패키지
- PostsResponseDto는 Entity의 필드 중 일부만 사용
- 생성자로 Entity를 받아 필드에 값을 넣는다.
- 굳이 모든 필드를 가진 생성자가 필요하진 않으므로 Dto는 Entity를 받아 처리한다.
PostsUpdateRequestDto
- 게시글을 수정할 때에 사용하는 유저는 제목과, 내용만을 바꿀 수 있기 때문에 title과 content
Posts
PostsService
- update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없다.
- 가능한 이유? JPA의 영속성 컨텍스트 때문
- 영속성 컨텍스트란?
- 엔티티를 영구 저장하는 환경(논리적 개념)
- JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈린다
- JPA의 엔티티 매니저(Entity Manager)가 활성화된 상태로(Spring Data Jpa를 쓴다면 기본 옵션) 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태.
- 이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영한다.
- 즉, Entity 객체의 값만 변경하면 별도로 Update쿼리를 날릴 필요가 없다는 것
- 이 개념이 더티 체킹(dirty checking)
영속성 컨텍스트
영속성 컨텍스트
hckcksrl.medium.com
PostsApiControllerTest
- 조회 기능은 실제로 톰캣을 실행해서 확인
- 로컬 환경에선 데이터베이스로 H2를 사용한다. 메모리에서 실행하기 때문에 직접 접근하려면 웹 콘솔을 사용해야 한다.
- application.properties에 옵션 추가
spring.h2.console.enabled=true
- Application 클래스에 main 메서드를 실행하고 http://localhost:8080/h2-console로 접속하면
이렇게 뜨고 jdbc:h2:mem:testdb를 작성해서 Connect를 누른다.
- POSTS 테이블이 보여야 한다.
- 여기서 SELECT * FROM posts;
- insert into posts(author, content, title) values('author', 'content', 'title'); 쿼리를 실행해본다.
- 가끔 values뒤에 "author", 이렇게 " "을 써서 실행하는 사람이 있는데 그게 바로 나였다. 안 된다.
- ' '을 쓰자
JPA Auditing으로 생성시간/수정시간 자동화하기
- 보통 엔티티(entity)는 해당 데이터의 생성시간과 수정시간을 포함한다
- 언제 만들어졌는지, 언제 수정되었지 등은 차후 유지보수에 있어 굉장히 중요한 정보
- 그러다 보니 매번 DB에 삽입하기 전, 갱신하기 전에 날짜 데이터를 등록/수정하는 코드가 여기저기 들어가게 된다.
- 이 무제를 해결하고자 JPA Auditing를 사용
LocalDate 사용
- Java부터 LocalDate와 LocalDateTime이 등장했다.
- Java의 기본 날짜 타입인 Date의 문제점을 제대로 고친 타입이라 Java8일 경우 무조건 써야 한다고 생각하면 된다.
참고
- Java8이 나오기 전까지 사용되었던 Date, Calendar 클래스는 다음과 같은 문제점들이 있었다.
1. 불편(변경이 불가능한) 객체가 아닙니다.
- 멀티스레드 환경에서 언제든 문제가 발생할 수 있다.
2. Calendar는 월(Month) 값 설계가 잘못되었습니다.
- 10월을 나타내는 Calendar.OCTOBER의 숫자 값은 '9'이다
- 당연히 '10'으로 생각했던 개발자들에게는 큰 혼란이 왔다.
JodaTime이라는 오픈소스를 사용해서 문제점들을 피했었고, Java8에선 LocalDate를 통해 해결했다.
domain 패키지에 BaseTimeEntity 클래스 생성
- BaseTimeEntity 클래스는 모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리하는 역할
- @MappedSuperclass
- JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들(createdDate, modifiedDate)도 칼럼으로 인식하도록 한다.
- @EntityListeners(AuditingListener.class)
- BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다
- @CreatedDate
- Entity가 생성되어 저장될 때 시간이 자동 저장됩니다.
- @LastModifiedDate
- 조회한 Entity의 값을 변경할 때 시간이 자동 저장됩니다.
- Posts 클래스가 BaseTimeEntity를 상속받도록 변경
- 테스트 코드를 작성하기 전에 Application에 이 설정을 해주어야 한다.(책에 없음, 이거 없이 테스트 하면 실패)
JPA Auditing 테스트 코드 작성하기
테스트 성공!
'스프링부트와 AWS로 혼자 구현하는 웹 서비스' 카테고리의 다른 글
9. 머스테치로 화면 구성하기 (0) | 2022.01.22 |
---|---|
7. 요구사항 분석 (0) | 2022.01.18 |
6. JPA로 데이터베이스 다뤄보자 (0) | 2022.01.18 |
5. Hello Controller 테스트 코드 작성하기 (0) | 2022.01.17 |
4. 스프링부트에서 테스트 코드를 작성하자 (0) | 2022.01.17 |