카테고리 없음

spring-boot(6)

rjqnrdl83 2025. 2. 11. 20:49

6. 블로그 기획하고 API 만들기

 

6.1 API와 REST API

API: 클라이언트의 요청을 서버에 전달하고 서버의 결과물을 클라이언트에게 돌려주는 역할.

REST API: 웹의 장점을 최대한 활용하는 API. 자원의 이름으로 구분해 자원의 상태를 주고받는 API.

 

REST API의 장점은 URL만 보고도 무슨 행동을 하는 API인지 명확하게 알 수 있다는 겁니다. 상태가 없다는 특징이 있어서 클라이언트와 서버의 역활이 명확하게 분리됩니다. 또한 HTTP 표준을 사용하는 모든 플랫폼에서 사용 가능합니다.

단점으로는 HTTP메서드인 GET, POST와 같은 방식의 개수에 제한이 있고 설계를 하기 위한 공식적으로 제공되는 표준 규약이 없다는 겁니다.

 

REST API를 사용하는 방법

규칙 1. URL에는 동사를 쓰지 말고 자원을 표시해야 한다.

여기서 자원은 가져오는 데이터를 말합니다. 학생중에 id가 1인 학생의 정보를 가져온다면 /student/1 과 같이 설계할 수 있습니다.

예문 적합성 설명
/articles/1 적합 동사 없음, 1번 글을 가져온다는 명확한 의미
/articles/show/1
/show/articles/1
부적합 show라는 동사가 있음

 

규칙 2. 동사는 HTTP 메서드로.

 

6.2 블로그 개발을 위한 엔티티 구성하기

칼럼명 자료형 null 허용 설명
id BIGINT N 기본키 일련번호. 기본키
title VARCHAR(255) N   게시물의 제목
content VARCHAR(255) N   내용

 

Article.java

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="id", updatable = true)
    private Long id;

    @Column(name="title", nullable = false)
    private String title;

    @Column(name="content", nullable = false)
    private String content;

    @Builder
    public Article(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

@NoArgsConstrucor 어노테이션을 선언해 접근 제어자가 protected인 기본 생성자를 생성했습니다.

@Getter 어노테이션으로 모든 필드에 대한 접근자 메서드를 만들었습니다.

 

BlogRepository.java

public interface BlogRepository extends JpaRepository<Article, Long> {
}

JpaRepository 클래스를 상속받을 때 엔티티 Article과 엔티티의 PK타입 Long을 인수로 넣었습니다. JpaRepository에서 제공하는 여러 메서드를 사용할 수 있습니다.

 

6.3 블로그 글 작성을 위한 API 구현하기

 

AddArticleRequest.java

@NoArgsConstructor //기본 생성자 추가
@AllArgsConstructor  //모든 필드 값을 파라미터로 받는 생성자 추가
@Getter
public class AddArticleRequest {
    private String title;
    private String content;

    public Article toEntity(String author){
        return Article.builder()
                .title(title)
                .content(content)
                .author(author)
                .build();
    }
}

toEntity()는 빌더 패턴을 사용해 DTO를 엔티티로 만들어주는 메서드입니다.

 

BlgoService.java

@RequiredArgsConstructor
@Service
public class BlogService {

    private final BlogRepository blogRepository;

    public Article save(AddArticleRequest request) {
        return blogRepository.save(request.toEntity());
    }
}

@RequiredArgsConstructor는 final, @NotNull이 붙은 필드를 생성자로 만들어줍니다.

@Service는 해당 클래스를 빈으로 서블릿 컨테이너에 등록해줍니다.

save()는 JpaRepository에서 지원하는 저장메서드로 AddArticleRequest클래스에 저장된 값들을 article 데이터베이스에 저장합니다.

 

컨트롤러 메서드 코드 작성하기

BlogApiController.java

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

@RestController 어노테이션을 클래스에 붙이면 HTTP 응답으로 객체 데이터를JSON 형식으로 반환합니다.

@PostMapping() 어노테이션은 HTTP 메서드가 POST일 때 요청받은 URL과 동일한 메서드와 매핑합니다.

@RequestBody 어노테이션은 HTTP를 요청할 때 응답에 해당하는 값을 @RequestBody어노테이션이 붙은 대상 객체인 AddArticleRequest에 매핑합니다.

 

BlogApiControllerTest.java

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

ObjectMapper 클래스로 만든 객체는 자바 객체를 JSON 데이터로 변환하는 직렬화 또는 역직렬화 할 때 사용합니다.

 

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

    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);
    assertThat(articles.get(0).getTitle()).isEqualTo(title);
    assertThat(articles.get(0).getContent()).isEqualTo(content);
}

given: 블로그 글 추가에 필요한 요청 객체를 만듭니다.

when: 블로그 글 추가 API에 요청을 보냅니다. 이때 요청 타입은 JSON이고 given에서 만들어진 객체를 요청 본문으로 함께 보냅니다.

then: 응답 코드가 201 Created인지 확인합니다. Blog 전체 조회해 크기가 1인지 확인하고 실제로 저장된 요청 값을 비교합니다.

writeValueAsString() 메서드를 사용해서 객체를 JSON으로 직렬화해줍니다.

contentType() 메서드는 요청을 보낼 떄 JSON, XML 등 다양한 타입 중 하나를 골라 요청을 보냅니다.

assertThat() 메서드로는 글의 개수가 1인지 확인합니다.

 

6.4 블로그 글 목록 조회를 위한 API 구현하기

 

BlogService.java 

데이터베이스에 저장되어 있는 글을 모두 가져오는 findAll() 추가

public List<Article> findAll() {
    return blogRepository.findAll();
}

 

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

 

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

/api/article GET 요청이 오면 글 전체를 조회하는 findAll() 메서드를 호출한 뒤 응답용 객체인 AtricleResponse로 파싱해 body에 담아 클라이언트에게 전송합니다.

 

글 조회 테스트

BlogApiControllerTest.java

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

 

6.5 블로그 글 조회 API 구현하기

BlogService.java

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

findById() 메서드는 JPA에서 제공하는 메서드를 사용해 ID를 받아 엔티티를 조회하고 없으면 예외를 발생시킵니다.

 

 

BlogApiController.java

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

/api/articles/{id} GET 요청이 오면 블로그 글을 조회하기 위해 매핑할 findArticle() 메서드를 작성했습니다.

@PathVariable 에너테이션은 URL에서 값을 가져오는 에너테이션입니다.

/api/articles/3 GET 요청이 들어오면 id에 3이 들어옵니다. 서비스 클래스의 findById로 넘어가 3번 블로그 글을 찾고 글의 정보를 body에 담아 웹 브라우저로 전송합니다.

 

블로그 글 삭제 기능

BlogService.java

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

블로그 글의 id를 받은 뒤 JPA에서 제공하는 deleteById()메서드를 이용해 데이터베이스에서 삭제.

 

BlogApiController.java

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

/api/articles/{id} DELETE 요청이 들어오면 글을 삭제하기 위한 findArticles 추가

 

BlogApiControllerTest.java

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

given: 블로그 글을 저장합니다.

when: 저장한 블로그 글의 id값으로 삭제 API를 호출합니다.

then: 응답 코드가 200 OK이고 블로그 글 리스트를 전체 조회한 배열 크기가 0인지 확인합니다.

 

6.7 블로그 글 수정 API 구현하기

 

Articles.java

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

요청받은 내용으로 값을 수정하는 메서드를 추가합니다.

 

UpdateArticleReuqest.java

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

블로그 글 수정 요청을 받을 DTO를 작성했습니다.

 

BlogService.java

@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 어노테이션은 매칭한 메서드를 하나의 트랜잭션으로 묶는 역할을 합니다.

 

BlogApiController.java

@PutMapping("/api/articles/{id}")
public ResponseEntity<Article> updateArticle(@PathVariable("id") 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를 넘겨줍니다.

 

BlogApiControllerTest.java

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

given: 블로그 글을 저장하고 수정에 필요한 요청 객체를 만듭니다.

when: UPDATE API로 수정 요청을 보냅니다.

then: 응답 코드가 200 OK인지 확인하고 id로 조회한 후에 값이 수정되었는지 확인합니다.

 

요약

1. REST API 웹의 장점을 최대한 활용한 API로 자원을 이름으로 구분해 자원의 상태를 주고받는 방식입니다.

2. JpaRepository를 상속받으면 Spring Data JPA에서 지원하는 여러 메서드를 간편하게 사용할 수 있습니다.

3. 롬복을 사용하면 더 깔끔하게 코드를 작성할 수 있습니다.

4. 테스트 코드를 작성하면 코드의 기능이 제대로 작동한다는 것을 검증할 수 있습니다.