엔티티 구성/ build.gradle 세팅/ 퍼시스턴스 계층 구성
참고 : https://jhzlo.tistory.com/30
1. 블로그 글 목록 조회 API 구현
1-1. 서비스 메서드 코드
service / BlogService.java
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service // 빈으로 등록
public class BlogService {
private final BlogRepository blogRepository;
// 블로그 글 추가 메서드
public Article save(AddArticleRequest request){
return blogRepository.save(request.toEntity());
}
// 블로그 글 모두 가져오기
public List<Article> findAll(){
return blogRepository.findAll();
}
}
위의 코드와 같이 데이터베이스에 저장되어 있는 글을 모두 가져오는 findAll() 메서드를 추가한다.
JPA 지원 메서드인 findAll()을 호출해 article 테이블에 저장되어 있는 모든 데이터를 조회하는 로직이다.
1-2. 컨트롤러 메서드 코드
/api/articles GET 요청이 오면 글 목록을 조회할 findAllArticles() 메서드 생성
해당 메서드는 전체 글 목록을 조회하고 응답하는 역할을 한다.
dto / ArticleResponse.java
Getter
public class ArticleResponse {
private final String title;
private final String content;
public ArticleResponse(Article article){
this.title = article.getTitle();
this.content = article.getContent();
}
}
글은 제목과 내용 구성이므로 해당 필드를 가지는 클래스를 만든 다음,
엔티티를 인수로 받는 생성자를 추가해준다.
controller / BlogApiController.java
@RequiredArgsConstructor
@RestController // HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {
private final BlogService blogService;
---생략---
@GetMapping("/api/articles")
public ResponseEntity<List<ArticleResponse>> findAllArticles(){
List<ArticleResponse> articles = blogService.findAll()
.stream()
.map(ArticleResponse::new)
.toList();
return ResponseEntity.ok()
.body(articles);
}
}
controller 디렉터리에 있는 BlogApiController.java 파일을 열어 전체 글을 조회한 뒤 반환하는 findAllArticles() 메서드를 추가한다.
- /api/articles GET 요청이 오면 글 전체를 조회하는 findAll() 메서드를 호출한다.
- 응답용 객체인 ArticleResponse로 파싱해 body에 담아 클라이언트에게 전송한다 -> stream 사용
- stream() 메서드는 List<Article>을 스트림으로 변환한다.
- map(ArticleResponse::new)는 스트림의 각 Article 객체를 ArticleResponse 객체로 변환한다.
ArticleResponse::new는 ArticleResponse 클래스의 생성자를 참조하여 각 Article 객체를 생성자 인자로 사용해 새로운 ArticleResponse 객체를 생성한다. - toList()는 변환된 스트림을 다시 리스트로 수집한다. 이 리스트의 타입은 List<ArticleResponse>이다.
1-3. 실행 테스트
resources / data.sql
INSERT INTO article (title, content) VALUES ('제목 1', '내용 1');
INSERT INTO article (title, content) VALUES ('제목 2', '내용 2');
INSERT INTO article (title, content) VALUES ('제목 3', '내용 3');
테스트를 쉽게 하기 위해 위와 같이 data.sql 코드를 작성한다.
포스트맨으로 확인하기
/api/articles로 GET 요청을 하면 다음과 같이 입력한 데이터가 조회되는 것을 확인할 수 있다.
1-4. 테스트 코드 작성하기
Given | 블로그 글을 저장합니다. |
When | 목록 조회 API를 호출합니다. |
Then | 응답 코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 content와 title이 저장된 값가 같은지 확인한다. |
BlogApiControllerTest.java
@SpringBootTest // 테스트용 애플리케이션 컨텍스트
@AutoConfigureMockMvc // MockMvc 생성 및 자동 구성
class BlogApiControllerTest {
--- 생략 ---
@DisplayName("findAllArticles: 블로그 글 목록 조회에 성공한다.")
@Test
public void findAllArticles() throws Exception{
// given
final String url = "/api/articles";
final String title = "title";
final String content = "content";
blogRepository.save(Article.builder()
.title(title)
.content(content)
.build());
// when
final ResultActions resultActions = mockMvc.perform(get(url)
.accept(MediaType.APPLICATION_JSON));
// then
resultActions
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].content").value(content))
.andExpect(jsonPath("$[0].title").value(title));
}
위와 같이 코드를 작성하고 실행해보면,
다음과 같이 테스트 코드도 잘 동작함을 확인할 수 있다.
2. 블로그 단일 글 조회 API 구현
블로그 글 전체를 조회할 API를 구현했으니 이번에는 글 하나를 조회하는 API를 구현할 것이다.
2-1. 서비스 메서드 코드
service / BlogService.java
블로그 글 하나를 조회하는 메서드인 findById() 메서드를 추가한다.
이 메서드는 데이터베이스에 저장되어 있는 글의 ID를 이용해 글을 조회한다.
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service // 빈으로 등록
public class BlogService {
--- 생략 ---
// 블로그 글 단일 조회
public Article findById(long id){
return blogRepository.findById(id)
.orElseThrow(()-> new IllegalArgumentException("not found: " + id));
}
}
여기서 구현한 findById() 메서드는 JPA에서 제공하는 findById() 메서드를 사용해 ID를 받아 엔티티를 조회한다.
만약 없으면 IllegalArgumentException 예외를 발생한다.
2-2. 컨트롤러 메서드 코드
controller / BlogApiController.java
/api/articles/{id} GET 요청이 오면 블로그 글을 조회하기 위해 매핑할 finArticle() 메서드 작성
@RequiredArgsConstructor
@RestController // HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {
--- 생략 ---
@GetMapping("/api/articles/{id}")
// URL 경로에서 값 추출
public ResponseEntity<ArticleResponse> findArticle(@PathVariable long id){
Article article = blogService.findById(id);
return ResponseEntity.ok()
.body(new ArticleResponse(article));
}
}
- @PathVariable
URL에서 값을 가져오는 애너테이션이다.
URL에서 {id}에 해당하는 값이 long id로 들어온다.
2-3. 테스트 코드 작성하기
Given | 블로그 글을 저장합니다. |
When | 저장한 블로그 글의 id값으로 API를 호출합니다. |
Then | 응답 코드가 200 OK이고, 반환받은 content와 title이 저장된 값과 같은지 확인한다. |
BlogApiControllerTest.java
@SpringBootTest // 테스트용 애플리케이션 컨텍스트
@AutoConfigureMockMvc // MockMvc 생성 및 자동 구성
class BlogApiControllerTest {
--- 생략 ---
@DisplayName("findArticle: 블로그 글 조회에 성공한다.")
@Test
public void findArticle() throws Exception {
// given
final String url = "/api/articles/{id}";
final String title = "title";
final String content = "content";
Article savedArticle = blogRepository.save(Article.builder()
.title(title)
.content(content)
.build());
// when
final ResultActions resultActions = mockMvc.perform(get(url, savedArticle.getId()));
// then
resultActions
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").value(content))
.andExpect(jsonPath("$.title").value(title));
}
}
위와 같이 작성하고 테스트 코드를 실행하면
다음과 같이 정상적으로 실행함을 확인할 수 있다.
3. 블로그 글 삭제 API 구현
3-1. 서비스 메서드 코드
service / BlogService.java
delete() 메서드를 추가한다.
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service // 빈으로 등록
public class BlogService {
--- 생략 ---
// 블로그 글 삭제
public void delete(long id){
blogRepository.deleteById(id);
}
}
해당 메서드는 블로그 글의 ID를 받은 뒤 JPA에서 제공하는 deleteById() 메서드를 이용해 데이터베이스에서 데이터를 삭제한다.
3-2. 컨트롤러 메서드 코드
controller / BlogApiController.java
@RequiredArgsConstructor
@RestController // HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {
--- 생략 ---
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable long id){
blogService.delete(id);
return ResponseEntity.ok()
.build();
}
}
ResponseEntity< ? > 알아보기
1. ResponseEntity<Void>
ResponseEntity<Void>는 HTTP 응답 본문이 없음을 나타냅니다. 즉, 클라이언트에게 상태 코드와 헤더만을 보내고, 실제로 본문 데이터는 포함하지 않습니다. 주로 상태 코드만을 전달하고자 할 때 사용됩니다. 예를 들어, 삭제 작업이 성공했음을 알리는 응답에서는 본문이 필요 없기 때문에 Void를 사용합니다.
2. ResponseEntity<List<ArticleResponse>>
ResponseEntity<List<ArticleResponse>>는 HTTP 응답 본문이 List<ArticleResponse> 타입임을 나타냅니다. 주로 여러 개의 ArticleResponse 객체를 클라이언트에게 전달할 때 사용됩니다. 예를 들어, 모든 기사를 조회하는 경우 각 기사를 ArticleResponse 객체로 변환하여 리스트 형태로 반환합니다.
3. ResponseEntity<ArticleResponse>
ResponseEntity<ArticleResponse>는 HTTP 응답 본문이 단일 ArticleResponse 객체임을 나타냅니다. 주로 특정 ID의 기사를 조회하거나, 단일 기사를 생성하거나 업데이트한 후 해당 기사를 클라이언트에게 반환할 때 사용됩니다.
요약
- ResponseEntity<Void>: 응답 본문이 없을 때 사용. 주로 삭제 작업이나 상태 코드만 전달할 때 사용.
- ResponseEntity<List<ArticleResponse>>: 응답 본문이 List<ArticleResponse>일 때 사용. 여러 개의 기사를 반환할 때 사용.
- ResponseEntity<ArticleResponse>: 응답 본문이 ArticleResponse일 때 사용. 단일 기사를 반환할 때 사용.
이렇게 각 경우에 따라 적절한 제네릭 타입을 사용하여 클라이언트에게 필요한 정보를 효율적으로 전달할 수 있습니다.
- /api/articles/{id} DELETE 요청이 오면 {id}에 해당하는 값이 @PathVariable 애너테이션을 통해 들어온다.
- JPA에서 지원하는 delete 메서드를 이용하여 해당 id를 삭제한다
- build() 메서드를 통해 http 응답의 본문을 비우고 상태 코드와 헤더만 포함하는 'ResponseEntity'를 반환한다.
3-3. 실행 테스트
포스트맨
api/articles/1
id가 1인 데이터를 삭제하는 요청을 보내고
글 목록 전체를 조회하는 api/articles로 get 요청을 보내면
다음과 같이 id가 1에 해당하는 데이터가 삭제되었음을 확인할 수 있다.
3-4. 테스트 코드 작성하기
Given | 블로그 글을 저장합니다. |
When | 저장한 블로그 글의 id값으로 삭제 API를 호출합니다. |
Then | 응답 코드가 200 OK이고, 블로그 글 리스트를 전체 조회해 조회한 배열 크기가 0인지 확인한다. |
BlogApiControllerTest.java
@SpringBootTest // 테스트용 애플리케이션 컨텍스트
@AutoConfigureMockMvc // MockMvc 생성 및 자동 구성
class BlogApiControllerTest {
--- 생략 ---
@DisplayName("deleteArticle: 블로그 글 삭제에 성공한다.")
@Test
public void deleteArticle() throws Exception{
// given
final String url = "/api/articles/{id}";
final String title = "title";
final String content = "content";
Article savedArticle = blogRepository.save(Article.builder()
.title(title)
.content(content)
.build());
//when
mockMvc.perform(delete(url, savedArticle.getId()))
.andExpect(status().isOk());
// then
List<Article> articles = blogRepository.findAll();
assertThat(articles).isEmpty();
}
}
위와 같이 테스트 코드를 작성하고 실행하면
다음과 같이 정상적으로 실행됨을 확인할 수 있다.
4. 블로그 글 수정 API 구현
4-1. 서비스 메서드 코드
update()메서드는를 구현하기 위해 Article.java와 dto에 해당 로직을 추가해준다.
domain / Article.java
update()메서드는 특정 아이디의 글을 수정한다.
@Entity // 엔티티로 지정
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {
--- 생략 ---
public void update(String title, String content){
this.title = title;
this.content = content;
}
}
dto / UpdateArticleRequest.java
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class UpdateArticleRequest {
private String title;
private String content;
}
글에서 수정해야 하는 내용은 제목과 내용이므로 그에 맞게 제목과 내용 필드로 구성한다.
service / BlogService.java
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service // 빈으로 등록
public class BlogService {
--- 생략 ---
@Transactional // 트랜잭션 메서드
public Article update(long id, UpdateArticleRequest request){
Article article = blogRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("not found: " + id));
article.update(request.getTitle(), request.getContent());
return article;
}
}
- @Transactional
매칭한 메서드를 하나의 트랜잭션으로 묶는 역할을 한다.
update()메서드는 엔티티의 필드값이 바뀌면 중간에 에러가 발생해도 제대로 된 값 수정을 보장하게 된다. - jpa에서 제공하는 findById()를 이용하여 id에 해당하는 글을 수정한다.
4-2. 컨트롤러 메서드 코드
controller / BlogApiController.java
@RequiredArgsConstructor
@RestController // HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {
--- 생략 ---
@PutMapping("/api/articles/{id}")
public ResponseEntity<Article> updatableArticle(@PathVariable long id,
@RequestBody UpdateArticleRequest request){
Article updatedArticle = blogService.update(id, request);
return ResponseEntity.ok()
.body(updatedArticle);
}
}
- /api/articles/{id} PUT 요청이 오면 Request Body 정보가 request로 넘어온다.
- 서비스 클래스의 update() 메서드에 id와 request를 넘겨주고 응답 값은 body에 담아 전송한다.
4-3. 실행 테스트
포스트맨
위와 같이 포스트맨으로 PUT 요청을 하면 다음과 같이 새로운 데이터 값으로 Body 응답이 오는 것을 확인할 수 있다.
GET 요청으로 블로그 글 목록을 전체 조회하면 데이터가 바뀌었음을 확인할 수 있다.
4-4. 테스트 코드 작성하기
Given | 블로그 글을 저장하고, 블로그 글 수정에 필요한 요청 객체를 만든다. |
When | UPDATE API로 수정 요청을 보낸다. 이때 요청 타입은 JSON이며, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 보낸다. |
Then | 응답 코드가 200 OK인지 확인한다. 블로그 글 id로 조회한 후에 값이 수정되었는지 확인한다. |
BlogApiControllerTest.java
@SpringBootTest // 테스트용 애플리케이션 컨텍스트
@AutoConfigureMockMvc // MockMvc 생성 및 자동 구성
class BlogApiControllerTest {
--- 생략 ---
@DisplayName("updatableArticle: 블로그 글 수정에 성공한다.")
@Test
public void updateArticle() throws Exception{
// given
final String url = "/api/articles/{id}";
final String title = "title";
final String content = "content";
Article savedArticle = blogRepository.save(Article.builder()
.title(title)
.content(content)
.build());
final String newTitle = "new title";
final String newContent = "new content";
UpdateArticleRequest request = new UpdateArticleRequest(newTitle, newContent);
// when
ResultActions result = mockMvc.perform(put(url, savedArticle.getId())
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(request)));
// then
result.andExpect(status().isOk());
Article article = blogRepository.findById(savedArticle.getId()).get();
assertThat(article.getTitle()).isEqualTo(newTitle);
assertThat(article.getContent()).isEqualTo(newContent);
}
}
위와 같이 테스트 코드를 작성하면
위와 같이 정상적으로 작동함을 알 수 있다.
<다음 글>
'JAVA > SpringBoot 3' 카테고리의 다른 글
[스프링 부트 3] 블로그 만들기 - 3 (타임리프를 활용하여 블로그 뷰 구현하기) (0) | 2024.08.12 |
---|---|
[스프링 부트 3] 템플릿 엔진, 타임리프란? (0) | 2024.08.12 |
[스프링 부트 3] 블로그 만들기 - 1 (블로그 글 작성 API 구현) (4) | 2024.08.07 |
[스프링 부트 3] ORM, JPA, 하이버네이트 (0) | 2024.07.30 |
[스프링 부트 3] 테스트 코드란? / 테스트 코드 작성하기 (0) | 2024.07.29 |