API를 만들기 위해서는 3개의 클래스가 필요하다.
1) Request 데이터를 받을 Dto
2) API 요청을 받을 Controller
3) 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
1. Spring 웹 계층
1) Web Layer
- 흔히 사용하는 컨트롤러(Controller)와 JSP/Freemarker 등의 뷰 템플릿 영역
- 이외에도 필터(@Filter), 인터셉터, 컨트롤러(@ControllerAdvice) 등 외부 요청과 응답에 대한 전반적인 영역을 의미
2) Service Layer
- @Service에 사용되는 서비스 영역
- 일반적으로 Controller와 Dao의 중간 영역에서 사용된다
- @Transactional이 사용되어야 하는 영역
* DAO : Data Access Object의 약자로, DB의 데이터에 접근하기 위한 객체
3) Repository Layer
- Database와 같이 데이터 저장소에 접근하는 영역
- Dao 영역으로 이해하면 된다.
4) Dtos
- Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체를 의미, Dtos는 이들의 영역을 의미한다
ex) 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등 이들을 이야기 한다.
5) Domain Model
- 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고, 고유할 수 있도록 단순화시킨 것을 도메인 모델이라고 한다.
- 비지니스 로직을 처리하는 영역
ex) 택시 앱이라고 하면, 배차, 탑승, 요금 등이 모두 도메인이 될 수 있다.
- @Entity가 사용된 영역 역시 도메인 모델
- 다만, 무조건 데이터베이스의 테이블과 관계가 있어야 하는 것은 아니다. VO 처럼 값 객체들도 이 영역에 해당하기 때문
2. 트랜잭션과 도메인 모델
- Domain : Web(controller), Service, Repository, Dto, Domain 5가지 레이어에서 비즈니스 처리를 담당
- 트랜잭션 스크립트 : 절차지향적으로 도메인 레이어를 설계
1) sudo code
@Transactional
public Order cancelOrder(int orderId){
1) 데이터베이스로부터 주문정보(Orders), 결제정보(Billing), 배송정보(Delivery) 조회
2) 배송 취소를 해야 하는지 확인
3) if(배송중이라면){
배송 취소로 변경
}
4) 각 테이블에 취소 상태 update
}
2) 실제 코드
- 서비스 내부에서 처리 -> 서비스 계층이 무의미하며, 객체란 단순히 데이터 덩어리 역할
@Transactional
public Order cancelOrder(int orderId){
//1)
OrdersDto order = orderDao.selecctOrders(orderId);
BillingDto billing = billingDao.selectBilling(orderId);
DeliveryDto delivery = deliveryDao.selectDelivery(orderId);
//2)
String deliveryStatus = delivery.getStatus();
//3)
if("IN_PROGRESS".equals(deliveryStatus)){
deliveryStatus.setStatus("CANCEL");
deliveryDao.update(delivery);
}
//4)
order.setStatus("CANCEL");
orderDao.update(order);
billing.setStatus("CANCEL");
deliveryDao.update(billing);
return order;
}
- order, billing, delivery가 각자 본인의 취소 이벤트 처리를 하며, 서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장
@Transactional
public Order cancelOrder(int orderId){
//1)
Orders order = ordersRepository.findById(orderId);
Billing billing = billingRepository.findById(orderId);
Delivery delivery = deliveryRepository.findById(orderId);
//2-3)
delivery.cancel();
//4)
order.cancel();
billing.cancel();
return order;
}
3. 등록 API 생성
1) PostsApiController
package com.freitag.admin.web;
import com.freitag.admin.web.domain.posts.PostsService;
import com.freitag.admin.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PutMapping("/api/v1/posts")
public Long save (@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
}
2) PostsService
package com.freitag.admin.web.domain.posts;
import com.freitag.admin.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
}
롬복 어노테이션이 있으면 해당 컨트롤러에 새로운 서비스를 추가하거나, 기존 컴포넌트를 제거하는 등의 상황이 발생해도 생성자 코드는 전혀 손을 대지 않아도 된다.
- 스프링에서 Bean을 주입받는 방법
* Bean : 스프링 컨테이너가 관리하는 자바 객체
@Autowired (권장하지 않음)- setter
- 생성자
이 위의 코드에서는 @RequiredArgsConstructor를 통해 해결한다. final이 선언된 모든 필드를 인자값으로 하는 생성자를 롬복의 @RequiredArgsConstructor가 대신 생성해준다. 이는 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함이다. 롬복 어노테이션이 있으면 해당 컨트롤러에 새로운 서비스를 추가하거나, 기존 컴포넌트를 제거하는 등의 상황이 발생해도 생성자 코드는 전혀 손을 대지 않아도 된다.
3) PostsSaveRequestDto
package com.freitag.admin.web.dto;
import com.freitag.admin.web.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
@Builder
public PostsSaveRequestDto(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
public Posts toEntity(){
return Posts.builder()
.title(title)
.author(author)
.content(content)
.build();
}
}
Entity클래스를 Request/Response 클래스로 사용해서는 안된다
Entity 클래스와 거의 유사한 형태인 Dto 클래스를 추가로 생성하였다. Entity클래스를 Request/Response 클래스로 사용해서는 안된다. Entity 클래스는 데이터베이스와 맞다은 핵심 클래스로 이 클래스를 기준으로 테이블이 생성되고 스키마가 변경된다. 화면 변경은 아주 사소한 기능 변경인데, 이를 위해 테이블과 연결된 Entity 클래스를 변경하는 것은 아주 큰 변경이다. Request/Response용 Dto는 View를 위한 클래스라 정말 자주 변경이 필요하기 때문에 꼭 Entity 클래스와 Controller에서 쓸 Dto는 분리해서 사용해야한다.
4. Test Code 작성
1) PostsApiControllerTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest{
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private PostsRepository postsRepository;
@AfterEach
public void tearDown(){
postsRepository.deleteAll();
}
@Test
public void Posts_cre() throws Exception{
String title = "title";
String content = "content";
String author = "author";
PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
.title(title)
.content(content)
.author(author)
.build();
String url = "http://localhost:"+port+"/api/v1/posts";
ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> postsList = postsRepository.findAll();
assertThat(postsList.get(0).getTitle()).isEqualTo(title);
assertThat(postsList.get(0).getContent()).isEqualTo(content);
assertThat(postsList.get(0).getAuthor()).isEqualTo(author);
}
}
- @SpringBootTest와 TestRestTemplate을 사용한 이유?
@WebMvcTest를 사용하면 JPA기능이 작동하지 않고, 외부 연동과 관련된 부분만 활성화 되기때문에 JPA 기능까지 한번에 테스트하기 위해 @SpringBootTest와 TestRestTemplate를 사용
5. 수정/조회 기능 작성
1) PostApiCotroller.java
@RequiredArgsConstructor
@RestController
public class PostsApiController {
...
@PutMapping("/api/v1/posts/{id}")
public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto){
return postsService.update(id, requestDto);
}
@GetMapping("/api/v1/posts/{id}")
public PostsResponseDto findById(@PathVariable Long id){
return postsService.findById(id);
}
}
2) PostResponseDto.java
@Getter
public class PostsResponseDto {
private Long id;
private String title;
private String content;
private String author;
public PostsResponseDto(Posts entity){
this.id = entity.getId();
this.title = entity.getTitle();
this.author = entity.getAuthor();
this.content = entity.getContent();
}
}
- Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣는다. 굳이 모든 필드를 가진 생성자가 필요하지 않으므로 Dto는 Entity를 받아 처리한다.
3) PostsService.java
@RequiredArgsConstructor
@Service
public class PostsService {
...
@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;
}
public PostsResponseDto findById(Long id){
Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
return new PostsResponseDto(entity);
}
}
- update기능에서 쿼리날리는 부분이 없는데 그 이유는 ?
JPA의 영속성 컨텍스트때문
- 영속성 컨텍스트란 ?
- 엔티티를 영구 저장하는 환경이라는 뜻으로 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스같은 역할을 한다.
- JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐
- 더티 체킹 ?
- JPA의 엔티티 매니저가 활성화된 상태로 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면 -> 영속성 컨텍스트 유지 -> 이 때 해당 데이터 값을 변경 -> 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영
- dirty는 "엔티티 데이터의 변경된 부분"을 뜻하고, dirty checking은 변경된 부분을 감지한다는 의미
4) PostUpdateRequestDto.java
@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
private String title;
private String content;
@Builder
public PostsUpdateRequestDto(String title, String content){
this.content = content;
this.title = title;
}
}
5) PostsApiControllerTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest{
...
@Test
public void Posts_re() throws Exception{
//given
Posts savedPosts = postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
Long updateId = savedPosts.getId();
String expectedTitle = "title2";
String expectedContent = "content2";
PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
.title(expectedTitle)
.content(expectedContent)
.build();
String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;
HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);
// when
ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT,
requestEntity, 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(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
}
6. 조회기능 톰캣 실행
- 로컬 환경에선 데이터베이스로 H2 사용 -> 메모리에서 실행하기에 직접 접근시 웹 콘솔 사용
//application.properties
spring.h2.console.enabled=true
- 추가한 뒤 Application 클래스의 main 메소드 실행후 http://localhost:8080/h2-console로 접속
- 이 때 JDBC URL을 jdbc:h2:mem:testdb로 작성후 실행해주어야하는데 오류 발생
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "CREATE TABLE POSTS (ID BIGINT NOT NULL AUTO_INCREMENT, AUTHOR VARCHAR(255), CONTENT TEXT NOT NULL, TITLE VARCHAR(500) NOT NULL, PRIMARY KEY (ID)) ENGINE=[*]INNODB"; expected "identifier"; SQL statement: create table posts (id bigint not null auto_increment, author varchar(255), content TEXT not null, title varchar(500) not null, primary key (id)) engine=InnoDB [42001-200]
- spring.datasource.hikari.jdbc-url=jdbc:h2:mem:testdb;MODE=MYSQL을 추가하여 오류 해결
- select * from posts; 실행
- insert into posts(author, content, title) values ('author', 'content', 'title'); 실행
- http://localhost:8080/api/v1/posts/1 -> API 조회기능
'스프링부트와 AWS로 혼자 구현하는 웹서비스' 카테고리의 다른 글
Part 8. 게시글 화면 만들기 (1) (2) | 2024.01.13 |
---|---|
Part 7. JPA Auditing으로 생성시간/수정시간 자동화하기 (1) | 2024.01.11 |
Part5. 프로젝트에 Spring Data JPA 적용 (0) | 2023.12.14 |
Part 4. JPA (0) | 2023.12.08 |
Part 3. 롬복 (0) | 2023.12.07 |