728x90
Service
예외 처리법
public Article findById(Long id) {
return blogRepository.findById(id)
// 해당 글이 없을 때 예외 발생
.orElseThrow(() -> new IllegalArgumentException("not found: " + id));
}
조회했는데 없을 때 예외처리법
업데이트
// 글 수정]
@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;
}
업데이트 로직은 따로 만들어야한다
Controller
dto, entity 반환타입
//HTTP 메서드가 POST일때 전달받은 URL과 동일하면 메서드로 매핑
@PostMapping("/api/articles")
public ResponseEntity<Article> addArticles(@RequestBody AddArticleRequest request) {
Article savedArticle = blogService.save(request);
// 요청한 자원이 성공적으로 생성 되었으며 저장된 블로그 글 정보를 응답 객체에 담아 전송
return ResponseEntity.status(HttpStatus.CREATED)
.body(savedArticle);
}
// 글 조회 컨트롤러
@GetMapping("/api/articles")
public ResponseEntity<List<ArticleResponse>> findAllArticles() {
List<ArticleResponse> articles = blogService.findAll()
.stream()
.map(ArticleResponse::new)
.toList();
return ResponseEntity.ok()
.body(articles);
}
반환타입을 엔티티 자체로 하냐, 아니면 dto를 하냐의 차이는 결론부터 말하자면 dto로 반환하는게 좋다 엔티티로 설정했을 경우에는 유저가 db구조를 알기도 쉽고(보안) 불필요한 정보까지 전부 넘겨줄 수도 있다. 아마 이 책에서는 테이블 요소가 많지도 않고 글 작성 시에 들어가는 요소가 많지 않기 때문에 엔티티를 바로 반환시킨 듯 하다.
dto
request, response
나누는 기준은 간단하다. 어떤 글을 새로 만들어서 데이터베이스에 저장한 다음에 다시 요청을 보내줘야한다면 request, 글을 조회하는게 목적이라면 response
package com.rowoon.myblog.dto;
import com.rowoon.myblog.domain.Article;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class AddArticleRequest {
private String title;
private String content;
// 빌더 패턴을 이용해 DTO를 엔티티로 만들어준다.
public Article toEntity() {
return Article.builder()
.title(title)
.content(content)
.build();
}
}
package com.rowoon.myblog.dto;
import com.rowoon.myblog.domain.Article;
import lombok.Getter;
@Getter
public class ArticleResponse {
private final String title;
private final String content;
public ArticleResponse(Article article) {
this.title = article.getTitle();
this.content = article.getContent();
}
}
response는 getTitle로 초기화를 진행한다(이미 가지고있는 걸로)
package com.rowoon.myblog.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class UpdateArticleRequest {
private String title;
private String content;
}
업데이트도 글 작성과 같은 원리이기 때문에 request로 만든 모습
또한 request는 요청을 클라이언트로 받아야하기 때문에 인자에 넣어주지만 조회와 삭제는 이미 가지고있는 데이터로 진행하기 때문에 인자에 넣지 않는다
@PostMapping("/api/articles")
public ResponseEntity<Article> addArticles(@RequestBody AddArticleRequest request) {
Article savedArticle = blogService.save(request);
// 요청한 자원이 성공적으로 생성 되었으며 저장된 블로그 글 정보를 응답 객체에 담아 전송
return ResponseEntity.status(HttpStatus.CREATED)
.body(savedArticle);
}
// 글 조회 컨트롤러
@GetMapping("/api/articles")
public ResponseEntity<List<ArticleResponse>> findAllArticles() {
List<ArticleResponse> articles = blogService.findAll()
.stream()
.map(ArticleResponse::new)
.toList();
return ResponseEntity.ok()
.body(articles);
}
오늘의 테스트코드 리뷰
설정
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper; // 직렬화, 역직렬화를 위한 클래스
@Autowired
private WebApplicationContext context;
@Autowired
BlogRepository blogRepository;
@BeforeEach
public void mockMvcSetUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.build();
blogRepository.deleteAll();
}
글 작성
@DisplayName("addArticle: 블로그 글 추가에 성공한다.")
@Test
public void addArticle() throws Exception {
// given
final String url = "/api/articles";
final String title = "title";
final String content = "content";
final AddArticleRequest userRequest = new AddArticleRequest(title, content);
// 객체 JSON으로 직렬화
final String requestBody = objectMapper.writeValueAsString(userRequest);
// when
// 설정한 내용을 바탕으로 요청 전송
ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(requestBody)
);
// then
result.andExpect(status().isCreated());
List<Article> articles = blogRepository.findAll();
assertThat(articles.size()).isEqualTo(1);// 크기가 1인가
assertThat(articles.get(0).getTitle()).isEqualTo(title);
assertThat(articles.get(0).getContent()).isEqualTo(content);
}
글 조회
@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));
}
MediaType.APPLICATION_JSON 과 MediaType.APPLICATION_JSON_VALUE의 차이
HTTP 헤더에 미디어 타입을 직접 삽입하려면 MediaType.APPLICATION_JSON_VALUE를 사용하고, 미디어 타입에 추가적인 정보를 포함하려면 MediaType.APPLICATION_JSON를 사용함
하지만 대부분의 test 에서는 MediaType.APPLICATION_JSON_VALUE 를 사용함
$[0]
jsonPath로 $는 json 객체의 루트를 이야기함 $[0]은 배열의 첫번째 요소임 목록은 list니까
반응형
'Spring, Spring boot' 카테고리의 다른 글
스프링 시큐리티 config (0) | 2024.01.10 |
---|---|
템플릿엔진 - 타임리프 (0) | 2024.01.04 |
JPA와 엔티티 (0) | 2023.12.24 |
Spring Boot3 와 테스트 (0) | 2023.12.24 |
JPA를 활용한 기본적인 CRUD 해보기 (0) | 2023.10.26 |