본문 바로가기
Backend/<스프링 부트 3 백엔드 개발자 되기>

06장 블로그 기획하고 API 만들기

by ccooll 2023. 11. 2.

학습 목표 : 블로그 글을 만들고(create), 조회하고(read), 업데이트하고(update), 삭제하는(delete) RESTful API를 만들고 스프링 부트 3와 JPA를 어떻게 사용하는지 알아보기

 

 

** 목차 **
6.1 사전지식 : API와 REST API

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

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

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

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

6.6 블로그 글 삭제 API 구현하기

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

 

- API 구현 순서 : 서비스 메서드 코드 작성 → 컨트롤러 메서드 코드 작성 → 실행테스트 → 테스트 코드 작성

 


 

 

6.1 사전지식 : API와 REST API

1. 식당으로 알아보는 API

손님(클라이언트) ↔ 점원(API) ↔ 주방(서버)

ex) 웹 사이트에 방문하는 상황

: 우리는 웹 사이트의 주소를 입력해서 '구글 메인 화면을 보여줘'라고 요청

  → API는 이 요청을 받아서 서버에게 가져다 줌

  → 서버는 API가 준 요청을 처리해 결과물을 만들고 이것을 다시 API로 전달함

  → API는 최종 결과물을 브라우저에 보내주고 우리는 화면을 볼 수 있게 됨.

 

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

 

2. 웹의 장점을 최대한 활용하는 REST API

REST(Representational State Transfer) : 자원을 이름으로 구분해 자원의 상태를 주고받는 API 방식, URL의 설계 방식

 

- REST API의 특징

: 서버/클라이언트 구조, 무상태, 캐시 처리 가능, 계층화, 인터페이스 일관성과 같은 특징

 

- REST API의 장점과 단점

 장점 : URL만 보고도 무슨 행동을 하는 API인지 명확하게 알 수 있음.

           상태가 없다는 특징 → 클라이언트와 서버의 역할이 명확하게 분리됨.

           HTTP 표준을 사용하는 모든 플랫폼에서 사용할 수 있음.

 단점 : HTTP 메서드(GET, POST와 같은 방식의 개수)에 제한이 있음.

          설계를 하기 위해 공식적으로 제공되는 표준 규약이 없음.

 

- REST API를 사용하는 방법

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

ex) 학생 중에 id가 1인 학생의 정보를 가져오는 URL

: /students/1, /get.student?student_id=1 → 둘 중 RESTful API는 '/students/1' (동사 있으면 안됨)

 

2. 동사는 HTTP 메서드

HTTP 메서드 : 서버에 요청을 하는 방법을 나눈 것.

 → POST(만들기), GET(읽기), PUT(업데이트하기), DELETE(삭제하기) 

 → 묶어서 CRUD라고 부름

 

ex) 블로그에 글을 쓰는 설계

GET/articles/1 : id가 1인 블로그 글을 조회하는 API

POST/articles/1 : 블로그 글을 추가하는 API

PUT/articles/1 : 블로그 글을 수정하는 API

DELETE/articles/1 : 블로그 글을 삭제하는 API

 

 

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

1. 프로젝트 준비

2. 엔티티 구성하기

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

만들 엔티티와 매핑되는 테이블의 구조

 

① src/main/java/me.cool/springbootdeveloper/domain/Article.java

@Entity // 엔티티로 지정
public class Article {

    @Id  // id 필드를 기본키로 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY)	// 기본키 자동으로 1씩 증가
    private Long id;
    
    @Column(name = "title", nullable = false)	// 'title'이라는 not null 컬럼과 매핑
    private String title;
    
    @Column(name = "content", nullable = false)	// 'content'라는 not null 컬럼과 매핑
    private String content;
    
    @Builder	// 빌더 패턴으로 객체 생성
    public Article(String title, String content){
        this.title = title;
        this.content = content;
    }
    
    // 기본 생성자
    protected Article() {
    }
    
    // 게터
    public Long getId() {
        return id;
    }
    
    public String getTitle() {
        return title;
    }
    
    public String getContent() {
        return content;
    }
}

 

- @Builder 애너테이션 : 생성자 위에 입력하면 빌더 패턴 방식으로 객체를 생성할 수 있어 편리함.

                                       어느 필드에 어떤 값이 들어가는지 명시적으로 파악할 수 있음.

→ @Builder 애너테이션을 사용하면 빌더 패턴을 사용하기 위한 코드를 자동으로 생성, 간편하게 빌더 패턴을 사용해 객체를 만들 수 있음.

 

 

// 빌더 패턴을 사용하지 않았을 때

new Article("abc", "def");

 

// 빌더 패턴을 사용했을 때

Article.builder()
    .title("abc")
    .content("def")
    .build();

 → 빌더 패턴을 사용하면 어느 필드에 어느 값이 매칭되는지 바로 보이므로 객체 생성 코드의 가독성을 높일 수 있음

 

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false)
    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;
    }

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

 

- getId(), getTitle(), getContent()와 같은 게터 메서드들은 @Getter 애너테이션으로 대치.

- @NoArgsConstructor 애너테이션을 선언해 접근 제어자가 protected인 기본 생성자를 별도의 코드 없이 생성.

 

 

3. Repository 만들기

src/main/java/me.cool/springbootdeveloper/repository/BlogRepository.java

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

 

- JpaRepository 클래스를 상속받을 때 엔티티 Article과 엔티티의 PK 타입 Long을 인수로 넣음.

 

 

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

클라이언트 ↔ 컨트롤러 ↔ 서비스 ↔ 리포지터리

 

1. 서비스 메서드 코드 작성하기

서비스 계층에서 요청을 받을 객체인 AddArticleRequest 객체 생성, BlogService 클래스 생성

→ 블로그 글 추가 메서드인 save() 구현

 

① src/main/java/me.cool/springbootdeveloper/dto/AddArticleRequest.java

- DTO(Data Transfer Object) : 계층끼리 데이터를 교환하기 위해 사용하는 객체

                                               → 단순하게 데이터를 옮기기 위해 사용하는 전달자 역할(별도의 비즈니스 로직 없음)

- DAO : 데이터베이스와 연결되고 데이터를 조회하고 수정하는데 사용하는 객체(데이터 수정과 관련된 로직 포함)

 

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

    private String title;
    private String content;
    
    public Article toEntity() {	// 생성자를 사용해 객체 생성
        return Article.builder()
                .title(title)
                .content(content)
                .build();    
    }
}

 

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

  → 이 메서드는 추후에 블로그 글을 추가할 때 저장할 엔티티로 변환하는 용도로 사용함.

 

② src/main/java/me.cool/springbootdeveloper/service/BlogService.java

@RequiredArgsConstructor	// final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service	// 빈으로 등록
public class BlogService {

    private final BlogRepository blogRepository;
    
    // 블로그 글 추가 메서드
    public Article save(AddArticleRequest request) {
        return blogRepository.save(request.toEntity());    
    }
}

 

- @RequiredArgsConstructor는 빈을 생성자로 생성하는 롬복에서 지원하는 애너테이션. final 키워드나 @NotNull이 붙은 필드로 생성자를 만들어줌.

- @Service 애너테이션은 해당 클래스를 빈으로 서블릿 컨테이너에 등록해줌.

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

 

 

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

URL에 매핑하기 위한 컨트롤러 메서드 추가

URL 매핑 애너테이션 : @GetMapping, @PostMapping, @PutMapping, @DeleteMapping

 

/api/articles에 POST 요청이 오면 @PostMapping을 이용해 요청을 매핑

→ 블로그 글을 생성하는 BlogService의 save() 메서드를 호출

→ 생성된 블로그 글을 반환하는 작업을 할 addArticle() 메서드 작성

 

① src/main/java/me.cool/springbootdeveloper/controller/BlogApiController.java

 

@RequiredArgsConstructor
@RestController	// HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {

    private final BlogService blogService;
    
    // HTTP 메서드가 POST일 때 전달받은 URL와 동일하면 메서드로 매핑
    @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와 동일한 메서드와 매핑함.

  위 코드의 경우 /api/articles는 addArticle() 메서드에 매핑함.

- @RequestBody 애너테이션은 HTTP를 요청할 때 응답에 해당하는 값을 @RequestBody 애너테이션이 붙은 대상 객체인 AddArticleRequest에 매핑함.

- ResponseEntity.status().body() 는 응답 코드로 201, 즉 Created를 응답하고 테이블에 저장된 객체를 반환함.

 

3. API 실행 테스트하기

- H2 콘솔 활성화

① src/main/resources/application.yml

spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true
    defer-datasource-initialization: true
  datasource:
    url: jdbc:h2:mem:testdb
  h2:
    console:
      enabled: true

 

② 스프링 부트 서버 실행

→ 포스트맨 실행

→ HTTP 메서드 : POST

    URL : http://localhost:8080/api/articles

    Body : JSON

    요청 창 :

{
    "title": "제목",
    "content": "내용"
}

 

 

③ H2 데이터베이스에 잘 저장됐는지 확인

localhost:8080/h2-console 접속 → Connect

 

 

④ SQL statement: 입력창에 SELECT * FROM ARTICLE 입력 → Run

결과확인

: ARTICLE 이라는 테이블 존재.

 애플리케이션을 실행하면 자동으로 생성한 엔티티 내용을 바탕으로 테이블이 생성되고, 우리가 요청한 POST 요청에 의해 데이터가 실제로 저장됨.

 

4. 반복 작업을 줄여 줄 테스트 코드 작성하기

① src/test/java/me.cool/springbootdeveloper/controller/BlogApiControllerTest.java

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

    @Autowired
    protected MockMvc mockMvc;
    
    @Autowired
    protected ObjectMapper objectMapper;	// 직렬화, 역직렬화를 위한 클래스
    
    @Autowired
    private WebAppicationContext context;
    
    @BeforeEach	// 테스트 전 실행하는 메서드
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
        		.build();
        blogRepository.deleteAll();
    }
}

 

- ObjectMapper 클래스로 만든 객체는 자바 객체를 JSON 데이터로 변환하는 직렬화 또는 반대로 JSON 데이터를 자바에서 사용하기 위해 자바 객체로 변환하는 역직렬화할 때 사용함.

 

 

② 블로그 글 생성 API 테스트 코드 작성

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

when : 블로그 글 추가 API에 요청을 보낸다. 이때 요청 타입은 JSON이며, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 보낸다.

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

 

@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {
    // 생략
    
    @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.APPICATION_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);   
    }
}

 

- writeValueAsString() 메서드를 사용해서 객체를 JSON으로 직렬화해줌.

- MockMvc를 사용해 HTTP 메서드, URL, 요청 본문, 요청 타입 등을 설정한 뒤 설정한 내용을 바탕으로 테스트 요청을 보냄.(HTTP 메서드 : post, 요청 타입: JSON)

 

③ 테스트 실행

 

 

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

블로그 글 조회하기 위한 API 구현 - 모든 글을 조회하는 API, 글 내용을 조회하는 API

 

1. 서비스 메서드 코드 작성하기

① src/main/java/me.cool/springbootdeveloper/service/BlogService.java

@RequiredArgsConstructor
@Service
public class BlogService {

    private final BlogRepository blogRepository;
    
    // 생략
    
    public List<Article> findAll() {
        return blogRepository.findAll();
    }
}

 

- JPA 지원 메서드인 findAll()을 호출해 article 테이블에 저장되어 있는 모든 데이터를 조회함.

 

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

/api/articles GET 요청이 오면 글 목록을 조회할 findAllArticles() 메서드

→ 전체 글 목록을 조회하고 응답하는 역할

 

① src/main/java/me.cool/springbootdeveloper/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();    
    }
}

 

- 글은 제목과 내용 구성이므로 해당 필드를 가지는 클래스를 만든 다음, 엔티티를 인수로 받는 생성자 추가

 

src/main/java/me.cool/springbootdeveloper/controller/BlogApiController.java

- 파일을 열어 전체 글을 조회한 뒤 반환하는 findAllArticles() 메서드 추가

@RequiredArgsConstructor
@RestController
public class BlogApiController {

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

 

- /api/articles GET 요청이 오면 글 전체를 조회하는 findAll() 메서드 호출

 → 응답용 객체인 ArticleResponse로 파싱해 body에 담아 클라이언트에게 전송함, 이 코드에는 스트림 적용함

 

 

 

3. 실행 테스트하기

① src/main/resource/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')

 

 

② 포스트맨

HTTP 메서드 : GET

Params → URL : http://localhost:8080/api/articles → Send

 

 

4. 테스트 코드 작성하기

given : 블로그 글을 저장한다

when : 목록 조회 API를 호출한다

then : 응답 코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 content와 title이 저장된 값과 같은지 확인한다

 

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

 

 

 

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

글 하나를 조회하는 API 구현

 

1. 서비스 메서드 코드 작성

① src/main/java/me.cool/springbootdeveloper/service/BlogService.java

@RequiredArgsConstructor
@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. 컨트롤러 메서드 코드 작성

① src/main/java/me.cool/springbootdeveloper/controller/BlogApiController.java

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

 

- @PathVariable 애너테이션은 URL에서 값을 가져오는 애너테이션.

- 이 애너테이션이 붙은 메서드의 동작 원리

: /api/articles/3 GET 요청을 받으면 id에 3이 들어옴.

  → 이 값은 앞서 만든 서비스 클래스의 findById() 메서드로 넘어가 3번 블로그 글을 찾음.

  → 글을 찾으면 3번 글의 정보를 body에 담아 웹 브라우저로 전송함.

 

 

3. 테스트 코드 작성하기

given : 블로그 글을 저장한다

when : 저장한 블로그 글의 id 값으로 API를 호출한다

then : 응답 코드가 200 OK이고, 반환받은 content와 title이 저장된 값과 같은지 확인한다

 

① src/test/java/me.cool/springbootdeveloper/controller/BlogApiControllerTest.java

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

 

 

 

6.6 블로그 글 삭제 API 구현하기

1. 서비스 메서드 코드 작성하기

① src/main/java/me.cool/springbootdeveloper/service/BlogService.java

@RequiredArgsConstructor
@Service
public class BlogService {
    // 생략
    
    public void delete(long id){
        blogRepository.deleteById(id);
    }
}

 

 

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

① src/main/java/me.cool/springbootdeveloper/controller/BlogApiController.java

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

 

- /api/articles/{id} DELETE 요청이 오면 {id}에 해당하는 값이 @PathVariable 애너테이션을 통해 들어옴

 

 

3. 실행 테스트하기

① 블로그 글 삭제

포스트맨 실행

HTTP 메서드 : DELETE

URL : http://localhost:8080/api/articles/1

→ Send

 

 

② 블로그 글 조회

HTTP 메서드 : GET

URL : http://localhost:8080/api/articles

→ ID가 1인 글은 삭제됨을 확인할 수 있음

 

 

4. 테스트 코드 작성하기

given : 블로그 글을 저장한다

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

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

 

@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {
    // 생략
    
    @DisplayName("deleteArticle: 블로그 글 삭제에 성공한다.")
    @Test
    public void deleteArticle() {
        // 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()    
    }
}

 

 

 

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

1. 서비스 메서드 코드 작성하기

src/main/java/me.cool/springbootdeveloper/domain/Article.java

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Article{
    // 생략
    
    public void update(String title, String content){
        this.title = title;
        this.content = content;
    }
}

 

 

② src/main/java/me.cool/springbootdeveloper/dto/UpdateArticleRequest.java

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

 

- 블로그 글 수정 요청을 받을 DTO

 

 

③ src/main/java/me.cool/springbootdeveloper/service/BlogService.java

@RequiredArgsConstructor
@Service
public class BlogService {
    // 생략
    
    @Transactional	// 트랜잭션 메서드
    public Article update(long id, UpdateArticleRequest request) {
        Article article = blogRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("not found: " + id));
        
        article.updqte(request.getTitle(), request.getContent());
        
        return article;    
    }
}

 

- @Transactional 애너테이션은 매칭한 메서드를 하나의 트랜잭션으로 묶는 역할을 함.

  → update() 메서드는 엔티티의 필드 값이 바뀌면 중간에 에러가 발생해도 제대로 된 값 수정을 보장하게 되었음.

 

 

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

src/main/java/me.cool/springbootdeveloper/controller/BlogApiController.java

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

 

 

- /api/articles/{id} PUT 요청이 오면 Request Body 정보가 request로 넘어옴.

- 다시 서비스 클래스의 update() 메서드에 id와 request를 넘겨줌.

- 응답 값은 body에 담아 전송함.

 

 

3. 실행 테스트하기

① 블로그 글 수정

포스트맨 실행

HTTP 메서드 : PUT

URL : http://localhost:8080/api/articles/1

Body(수정 내용 입력) :

{ "title": "새 블로그", "content": "새로운 글을 써볼까?" }

→ Send

 

② 글 전체 조회 API로 블로그 글 수정 확인

HTTP 메서드 : GET

 

 

4. 테스트 코드 작성하기

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

when : UPDATE API로 수정 요청을 보낸다. 이때 요청 타입은 JSON이며, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 보낸다

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

 

①  src/test/java/me.cool/springbootdeveloper/controller/BlogApiControllerTest.java

@SpringBootTest
@AutoConfigureMockMvc
public class BlogApiControllerTest {
    // 생략
    
    @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.writewValueAsString(request)));
            
        // then
        result.andExpect(status.isOk());
        
        Article article = blogRepository.findById(savedArticle.getId().get());
        
        assertThat(article.getTitle()).isEqualTo(newTitle);
        assertThat(article.getContent()).isEqualTo(newContent);    
    }
}