ron_nie 2024. 1. 3. 18:44
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니까

 

반응형