블로그 API 개발

Sirius·2024년 7월 10일

블로그 글 작성 API 개발

1) 서비스 메소드 구현
2) 컨트롤러 메소드 구현
3) API 테스팅

1) 서비스 메소드 코드 작성

1> AddArticleRequest.java 생성

  • DTO: 계층끼리 데이터를 교환하기 위해 사용하는 객체(단순히 전달자 역할, 별도의 비즈니스 로직 포함X)
  • DAO: 데이터베이스와 연결되고 데이터를 조회하고 수정하는데 사용하는 객체(데이터 수정과 관련된 로직이 포함됨)

1) main/java/me.xxxxx/프로젝트명 패키지에 dto 패키지를 생성한다.
2) 해당 패키지에 AddArticleRequest클래스를 생성한다.

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class AddArticleRequest {
    private String title;
    private String content;
    
    public Article toEntity(){
        return Article.builder()
                .title(title)
                .content(content)
                .build();
    }
}
  1. @NoArgsConstructor: Lombok 어노테이션으로, 파라미터가 없는 기본 생성자를 자동으로 생성한다.
  2. @AllArgsConstructor: Lombok 어노테이션으로, 모든 필드를 매개변수로 받는 생성자를 자동으로 생성한다.
  3. @Getter: Lombok 어노테이션으로, 모든 필드에 대한 getter 메서드를 자동으로 생성한다.
  4. public Article toEntity(): AddArticleRequest 객체를 Article 엔티티 객체로 변환하는 메서드이다.
    이 메서드는 Article 클래스의 빌더 패턴을 사용하여 title과 content 필드를 설정한 후, Article 객체를 생성하여 반환한다.

2> BlogService.java 생성

1) main/java/me.xxxx/프로젝트명 패키지에 service 패키지를 생성한다.
2) 해당 패키지에 BlogService 클래스를 구현한다.

@RequiredArgsConstructor
@Service // 빈으로 등록
public class BlogService {
    private final BlogRepository blogRepository;
    
    // 블로그 글 추가 메소드
    public Article save(AddArticleRequest request){
        return blogRepository.save(request.toEntity());
    }
}
  1. @RequiredArgsConstructor: Lombok 어노테이션으로, final이나 @NotNull로 선언된 필드에 대한 생성자를 자동으로 생성한다. 이를 통해 의존성 주입(Dependency Injection)이 가능하다.

  2. @Service: Spring의 어노테이션으로, 이 클래스가 서비스 레이어의 컴포넌트임을 나타내며, 스프링 컨테이너에 의해 빈으로 등록된다.

  3. private final BlogRepository blogRepository: 데이터베이스 작업을 처리하기 위해 BlogRepository를 의존성으로 가진다. final 키워드를 사용하여 생성 시에만 할당되며, 변경되지 않는다.

  4. public Article save(AddArticleRequest request): AddArticleRequest 객체를 받아서, 그 객체의 toEntity() 메서드를 호출하여 Article 엔티티로 변환한다. 변환된 Article 엔티티를 blogRepository의 save 메서드를 통해 데이터베이스에 저장하고, 저장된 Article 객체를 반환한다.

2) 컨트롤러 메소드 코드 작성

컨트롤러 메소드는 다음과 같은 URL 매핑 어노테이션을 사용할 수 있다.
1. @GetMapping
2. @PostMapping
3. @PutMapping
4. @DeleteMapping

이 상황에서는 다음과 같은 흐름을 가진다.
1) @PostMapping으로 요청을 매핑
2) BlogService의 save()를 호출
3) 생성된 블로그 글을 반환

1> BlogApiController.java 생성

1) main/java/me.xxxxx 패키지에 controller 패키지를 생성
2) 해당 패키지에 BlogApiController.java 파일을 생성한다.

@RequiredArgsConstructor
@RestController // HTTP Response Body에 객체데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {
    private final BlogService blogService;
    
    @PostMapping("/api/articles")
    public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request){
        Article savedArticle = blogService.save(request);
        
        return ResponseEntity.status(HttpStatus.CREATED).body(savedArticle);
    }
}

1) @RestController: Spring MVC의 어노테이션으로, 이 클래스가 RESTful 웹 서비스의 컨트롤러임을 나타낸다. 이 어노테이션은 @Controller와 @ResponseBody를 결합한 형태로, 메서드에서 반환된 객체는 JSON 형식으로 HTTP 응답 body에 직접 작성된다.


2) private final BlogService blogService: 블로그 관련 비즈니스 로직을 처리하는 BlogService를 의존성으로 가진다. @RequiredArgsConstructor에 의해 생성자가 자동으로 생성되며, 이 생성자를 통해 BlogService가 주입된다.


3) @RequestBody AddArticleRequest request: HTTP 요청 본문을 AddArticleRequest 객체(dto)로 변환하여 매개변수로 받는다. 이는 클라이언트가 JSON 형식으로 보낸 데이터를 Java 객체로 변환한다.


4) Article savedArticle = blogService.save(request): AddArticleRequest 객체를 BlogService의 save 메서드를 통해 저장하고, 저장된 Article 객체를 반환받는다.


5) return ResponseEntity.status(HttpStatus.CREATED).body(savedArticle): HTTP 응답으로 상태 코드 201(CREATED)와 함께 저장된 Article 객체를 JSON 형식으로 반환한다.

  • 응답코드
    1) 200 OK: 요청이 성공적으로 수행됨
    2) 201 Created: 요청이 성공적으로 수행, 새로운 리소스가 생성됨
    3) 400 Bad Request: 요청 값이 잘못되어 요청에 실패함
    4) 404 Not Found: 요청 값으로 찾은 리소스가 없어 요청에 실패함
    5) 500 Internal Server Error: 서버 상에 문제가 있어 요청에 실패함

3) H2 콘솔활성화

application.yml 파일에 다음 코드를 추가한다.

spring:
  ...
  datasource:
    url: jdbc:h2:mem:testdb

   h2:
     console:
       enabled: true

4) API 테스팅

1> 포스트맨으로 요청 보내기

2> h2 console에서 확인하기

localhost:8080/h2-console에 접속 후 JDBC URL을 다음과 같이 수정한다.

5) 테스트 코드 작성

1> 테스트코드파일 생성

1) BlogApiController 클래스에 Alt+Enter를 누르고 [Create Test]를 통해 테스트코드 파일을 생성한다.

@SpringBootTest // 테스트용 애플리케이션 컨텍스트
@AutoConfigureMockMvc // MockMvc 생성 및 자동구성
class BlogApiControllerTest {

    @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();
    }
}

1) @SpringBootTest: 이 어노테이션은 스프링 부트 애플리케이션의 전체 컨텍스트를 로드하여 통합 테스트를 수행하도록 설정한다. 이를 통해 실제 애플리케이션과 동일한 환경에서 테스트를 수행할 수 있다.


2) @AutoConfigureMockMvc: 이 어노테이션은 MockMvc 객체를 자동으로 구성하고 생성한다. MockMvc는 스프링 MVC 애플리케이션의 웹 계층을 테스트하는 데 사용된다.


3) @Autowired protected MockMvc mockMvc: MockMvc 객체를 주입받는다. MockMvc는 컨트롤러의 동작을 테스트할 수 있게 해준다.


4) @Autowired protected ObjectMapper objectMapper: ObjectMapper 객체를 주입받는다. ObjectMapper는 JSON 데이터를 Java 객체로 변환하거나 그 반대로 변환하는 데 사용된다.(직렬화 & 역직렬화)


5) @Autowired private WebApplicationContext context: WebApplicationContext 객체를 주입받는다. 이 객체는 스프링의 웹 애플리케이션 컨텍스트로, MockMvc를 설정하는 데 사용된다.


6) @Autowired BlogRepository blogRepository: BlogRepository 객체를 주입받는다. 테스트 중 데이터베이스 작업을 수행하는 데 사용된다.


7) this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build():
MockMvc 객체를 WebApplicationContext를 사용하여 설정한다. 이를 통해 MockMvc가 스프링의 웹 애플리케이션 컨텍스트와 함께 초기화된다.


8) blogRepository.deleteAll(): 테스트 시작 전에 BlogRepository의 모든 데이터를 삭제하여, 각 테스트가 깨끗한 상태에서 시작되도록 한다.

2> 테스트코드 작성

  • Given: 블로그 글 추가에 필요한 요청 객체를 만든다.
  • When: 블로그 글 추가 API에 요청을 보낸다.(요청타입: JSON, Given에서 만든 객체를 보냄)
  • Then: 1) 응답코드가 201 Created인지 확인, 2) Blog 전체 조회해 크기가 1인지 확인, 3) 실제로 저장된 데이터와 요청값을 비교
 @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)
                .content(requestBody));

        //then
        result.andExpect(status().isCreated());

        List<Article> articles = blogRepository.findAll();

        assertThat(articles.size()).isEqualTo(1);
        assertThat(articles.get(0).getTitle()).isEqualTo(title);
        assertThat(articles.get(0).getContent()).isEqualTo(content);
    }

1) @DisplayName 어노테이션은 테스트의 설명을 지정한다. 이 경우 "블로그 글 추가에 성공한다."는 테스트의 목적을 설명한다.


2) requestBody: userRequest 객체를 JSON 문자열로 직렬화한다.


3) mockMvc를 사용하여 post 요청을 실행한다. 요청의 Content-Type을 application/json으로 설정한다. requestBody를 요청의 Body로 설정한다. ResultActions 객체를 통해 요청의 결과를 받아온다.


4) result.andExpect(status().isCreated()): 요청에 대한 응답 상태가 201(Created)인지 확인한다.


5)
blogRepository.findAll(): 데이터베이스에서 모든 블로그 글을 가져온다.
assertThat(articles.size()).isEqualTo(1): 데이터베이스에 글이 하나 추가되었는지 확인한다.
assertThat(articles.get(0).getTitle()).isEqualTo(title): 추가된 글의 제목이 요청한 제목과 일치하는지 확인한다.
assertThat(articles.get(0).getContent()).isEqualTo(content): 추가된 글의 내용이 요청한 내용과 일치하는지 확인한다.

블로그 글 목록 조회 API 개발

1) 서비스 메소드 코드 작성

BlogService.java에 findAll()메소드를 추가한다.

@RequiredArgsConstructor
@Service // 빈으로 등록
public class BlogService {
    private final BlogRepository blogRepository;
    ...
    public List<Article> findAll(){
        return blogRepository.findAll();
    }
}

2) 컨트롤러 메소드 코드 작성

1> ArticleResponse.java 생성

응답을 위한 DTO를 먼저 작성한다.
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();
    }
}

2> findAllArticles() 메소드 추가

BlogApiController.java에 findAllArticles메소드를 추가한다.

 @GetMapping("/api/articles")
    public ResponseEntity<List<ArticleResponse>> findAllArticles(){
        List<ArticleResponse> articles = blogService.findAll()
                .stream()
                .map(ArticleResponse::new)
                .toList();
        
        return ResponseEntity.ok()
                .body(articles);
    }

1) public ResponseEntity<List<ArticleResponse>> findAllArticles(): 이 메서드는 ResponseEntity 객체를 반환하며, 이 객체는 HTTP 응답의 상태 코드, 헤더 및 바디를 포함할 수 있습니다. 반환 타입은 List<ArticleResponse>입니다.


2)
가) blogService.findAll(): 블로그 서비스의 findAll 메서드를 호출하여 모든 블로그 글 목록을 가져온다. 이 메서드는 List<Article> 타입을 반환한다.
나) .stream(): List<Article>을 스트림으로 변환한다.
다) .map(ArticleResponse::new): 각 Article 객체를 ArticleResponse 객체로 변환한다. 이 과정에서 ArticleResponse의 생성자를 호출한다.
라) .toList(): 변환된 ArticleResponse 객체들을 리스트로 수집한다.


3)
가) ResponseEntity.ok(): HTTP 상태 코드 200 OK를 설정합니다.
나) .body(articles): 응답의 바디에 articles 리스트를 설정합니다.
-> 최종적으로 ResponseEntity 객체를 반환하여 클라이언트에게 응답합니다.

3) API 테스팅

1> 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')

2> 포스트맨으로 테스팅

4) 테스트 코드 작성

  • Given: 블로그 글을 저장
  • When: 목록조회 API 호출
  • Then: 1) 응답코드가 200 OK, 2) 반환받은 값 중에 0번째 요소의 content와 title이 저장된 값과 같은지 비교
@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));
}

1) blogRepository.save(...): 데이터베이스에 테스트용 블로그 글을 저장한다. Article 객체를 빌더 패턴을 사용하여 생성하고, title과 content 값을 설정하여 저장한다.


2) mockMvc.perform(get(url).accept(MediaType.APPLICATION_JSON)): 지정된 URL로 HTTP GET 요청을 보내고, 응답을 JSON 형식으로 받도록 설정한다.
ResultActions resultActions: 요청의 결과를 캡처하여 resultActions 객체에 저장한다.


3)
가. resultActions.andExpect(status().isOk()): 응답 상태 코드가 200 OK인지 확인한다.
나. resultActions.andExpect(jsonPath("[0].content").value(content)):응답JSON배열의첫번째객체의content필드가content변수의값과일치하는지확인한다..resultActions.andExpect(jsonPath("[0].content").value(content)): 응답 JSON 배열의 첫 번째 객체의 content 필드가 content 변수의 값과 일치하는지 확인한다. 다. resultActions.andExpect(jsonPath("[0].title").value(title)):
응답 JSON 배열의 첫 번째 객체의 title 필드가 title 변수의 값과 일치하는지 확인한다.

  • andExpect문법

    jsonPath("$.content").value("Sample Content"): JSON 응답의 content 필드가 "Sample Content"인지 확인합니다.

블로그 글 조회 API 개발

글 하나를 조회하는 API 개발

1) 서비스 메소드 코드 작성

public Article findById(long id){
        return blogRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("not found: " + id));
    }

2) 컨트롤러 메소드 코드 작성

@GetMapping("/api/articles/{id}")
    public ResponseEntity<ArticleResponse> findArticle(@PathVariable long id){
        Article article = blogService.findById(id);

        return ResponseEntity.ok()
                .body(new ArticleResponse(article));
    }

@PathVariable: URL에서 값을 가져오는 어노테이션이다.
즉 id값을 추출한다.

3) 테스트 코드 작성

@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));
    }

블로그 글 삭제 API 개발

1) 서비스 메소드 코드 작성

public void delete(long id){
        blogRepository.deleteById(id);
    }

2) 컨트롤러 메소드 코드 작성

@DeleteMapping("/api/articles/{id}")
    public ResponseEntity<Void> deleteArticle(@PathVariable long id){
        blogService.delete(id);
        return ResponseEntity.ok().build();
    }

3) 테스트 코드 작성

import static org.assertj.core.api.Assertions.assertThat;
@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();
    }

블로그 글 수정 API 개발

1) 서비스 메소드 코드 작성

1> Article.java 수정

Article 클래스에 업데이트하는 메소드를 직접 추가한다.

public void update(String title, String content){
        this.title=title;
        this.content = content;
    }

2> DTO 작성

dto 패키지에서 UpdateArticleRequst.java 생성

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class UpdateArticleRequest {
    private String title;
    private String content;
}

3> 서비스 메소드 코드 작성

BlogService.java에서 update() 메소드 추가

 @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;
    }

1) @Transactional: 매칭한 메소드를 하나의 트랙잭션으로 묶어버린다.
2) update(): 엔티티의 필드값이 바뀌면 중간에 에러가 발생해도 제대로된 값 수정을 보장한다.

2) 컨트롤러 메소드 코드 작성

@PutMapping("/api/articles/{id}")
public ResponseEntity<Article> updateArticle(@PathVariable long id,
                                                 @RequestBody UpdateArticleRequest request)
{
    Article updateArticle = blogService.update(id, request);
    return ResponseEntity.ok()
            .body(updateArticle);
}

3) 테스트 코드 작성

    @DisplayName("updateArticle: 블로그 글 수정에 성공한다.")
    @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);
    }

objectMapper.writeValueAsString(request): request 객체를 JSON 문자열로 변환한다.
objectMapper는 Jackson 라이브러리의 ObjectMapper 객체로, Java 객체를 JSON으로 변환하거나 JSON을 Java 객체로 변환하는 데 사용된다.
request는 PUT 요청으로 전송할 데이터입니다. 이 데이터는 JSON 형식으로 변환되어 요청 본문에 포함된다.

0개의 댓글