본문 바로가기

SpringBoot

[스프링 부트 3 백엔드 개발자 되기] ch 6. 블로그 기획하고 API 만들기

1. API와 REST API

 

API

- 프로그램 간에 상호작용하기 위한 매개체 

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

 

REST API

- Representational State Transfer

- 웹의 장점을 최대한 활용하는 API

- 자원을 이름으로 구분해 자원의 상태를 주고받는 API 방식

-  URL 설계 방식 

-  주소와 메서드만 보고 요청의 내용을 파악할 수 있다. 

 

1) 특징

 

- 서버/클라이언트 구조, 무상태, 캐시 처리 기능, 계층화, 인터페이스 일관성 

 

2) REST API를 사용하는 방법 

 

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

 

 자원 = 가져오는 데이터 

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

 

 

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

 

- 동사는 HTTP 메서드로 해결한다. 

- HTTP 메서드는 POST,GET,PUT,DELETE이고 각각 만들고(create), 읽고(read), 업데이트하고(update), 삭제(delete)하는 역할을 담당한다. 이것들을 묶어서 CRUD라고 부른다.

 

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

설명 적합한 HTTP 메서드와 URL
id가 1인 블로그 글을 조회하는 API GET/articles/1
블로그 글을 추가하는 API POST/articles
블로그 글을 수정하는 API PUT/articles/1
블로그 글을 삭제하는 API DELETE/articles/1

 

 

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

 

- 계층별로 코드를 디렉터리에 넣어 분리 

 

프레젠테이션 계층 controller
비즈니스 계층 service
퍼시스턴스 계층 repository
데이터베이스와 연결되는 DAO domain

 

 

1) 엔티티 구성하기 

 

- 엔티티와 매핑되는 테이블 구조 

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

 

 

domain/Article.java

@Entity //엔티티로 지정 
public class Article {
    
    @Id  //id 필드를 기본키로 지정
    @GeneratedValue(strategy= GenerationType.IDENTITY) //기본키를 자동으로 1씩 증가 
    @Column(name="id", updatable=false)
    private Long id;
    
    @Column(name="title", nullable=false) //'title'이라는 not null 컬럼과 매핑 
    private String title;
    
    @Column(name="content", nullable=false)
    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 애너테이션은 롬복에서 지원하는 애너테이션으로 생성자 위에 입력하면 빌더 패턴 방식으로 객체를 생성할 수 있어 편리하다. 

- 빌더 패턴을 사용하면 객체를 유연하고 직관적으로 생성할 수 있기 때문에 개발자들이 애용하는 디자인 패턴이다. 

- 빌더 패턴을 사용하면 어느 필드에 어떤 값이 들어가는지 명시적으로 파악할 수 있다. 

 

ex)

- Article 객체를 생성할 때 title은 abc를, content에는 def값으로 초기화 

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

//빌더 패턴을 사용하지 않았을 떄
new Article("abc","def");

//빌더 패턴을 사용했을 때
Article.builder()
    .title("abc")
    .content("def")
    .build();

 

 

- 롬복 사용시 getId(), getTitle() 같이 필드의 값을 가져오는 게터 메서드들을 public class Article 위에 @Getter 애너테이션, @NoArgsConstructor 애너테이션으로 대치한다. 

 

domain/Article.java

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

    @Id  //id 필드를 기본키로 지정
    @GeneratedValue(strategy= GenerationType.IDENTITY) //기본키를 자동으로 1씩 증가
    @Column(name="id", updatable=false)
    private Long id;

    @Column(name="title", nullable=false) //'title'이라는 not null 컬럼과 매핑
    private String title;

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

    @Builder //빌더 패턴으로 객체 생성
    public Article(String title, String content) {
        this.title = title;
        this.content = content;
    }
    

}

 

 

2) 리포지터리 만들기 

 

repository/BlogRepository.java

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

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

 

 

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

 

- 서비스 클래스에서 메서드를 구현하고, 컨트롤러에서 사용할 메서드를 구현한 다음, API를 실제로 테스트 

 

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

 

- 블로그에 글을 추가하는 코드를 서비스 계층에 작성

- 서비스 계층에서 요청을 받을 객체인 AddArticleRequest 객체를 생성하고, BlogService 클래스를 생성한 다음 블로그 글 추가 메서드인 save()를 구현 

 

 

dto/AddArticleRequest.java

- 컨트롤러에서 요청한 본문을 받을 객체

- DTO(datat Transfer Object)는 계층끼리 데이터를 교환하기 위해 사용하는 객체, DAO는 데이터베이스와 연결되고 데이터를 조회하고 수정하는 데 사용하는 객체라 데이터 수정과 관련된 로직이 포함되지만 DTO는 데이터를 옮기기 위해 사용하는 전달자 역할 

 

@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를 엔티티로 만들어주는 메서드이다. 이 메서드는 블로그 글을 추가할 때 저장할 엔티티로 변환하는 용도로 사용한다. 

 

service/BlogService.java

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

    private final BlogRepository blogRepository;

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

 

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

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

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

 

 

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

 

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

- 컨트롤러 메서드에는 URL 매핑 애너테이션인 @GetMapping, @PostMapping, @PutMapping, @DeleteMapping 등을 사용할 수 있다. 

 

- /api/articles에 POST 요청이 오면 @PostMapping을 이용해 요청을 매핑한 뒤, 블로그 글을 생성하는 BlogService의 save() 메서드를 호출한 뒤, 생성된 블로그 글을 반환하는 작업을 할 addArticle() 메서드를 작성 

 

controller/BlogApiControlle.java

@RequiredArgsConstructor
@RestController //HTTP Response Body에 객체 데이터를 JSON 형태로 반환하는 컨트롤러
public class BlogApiController {
    
    private final BlogService blogService;
    
    //HTTP 메서드가 POST일 때 전달받은 URL과 동일하면 메서드로 매핑
    @PostMapping("/api/articles")
    //@RequestBody로 요청 본문 값 매핑
    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에 매핑한다. ResponseEntity.status().body()는 응답 코드로 201. 즉, Created를 응답하고 테이블에 저장된 객체를 반환한다. 

 

꼭 알아두면 좋을 응답코드 

200 OK  요청이 성공적으로 수행되었음
201 Created 요청이 성공적으로 수행되었고, 새로운 리소스가 생성되었음
400 Bad Reuqest 요청 값이 잘못되어 요청에 실패했음
403 Forbidden 권한이 없어 요청에 실패했음
404 Not Found 요청 값으로 찾은 리소스가 없어 요청에 실패했음
500 Internal Server Error 서버 상에 문제가 있어 요청에 실패했음 

 

 

3) API 실행 테스트

 

- 실제 데이터를 확인하기 위해 H2 콘솔을 활성화 해야함 

 

application.yml

...
  datasource:
    url: jdbc:h2:mem:testdb
  h2:
    console:
      enabled: true
...

 

 

- 스프링 부트 서버를 실행 하고 포스트맨을 실행하여 HTTP 메서드는 [POST]로, URL에는 http://localhost:8080/api/articles, [Body]는 [raw->JSON]으로 변경한 다음 요청창에 다음과 같이 작성 

 

 

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

- 웹 브라우저에서 localhost:8080/h2-console에 접속 

 

 

 

4) 반복 작업을 줄여 줄 테스트 코드 작성 

 

BlogApiControllerTest.java

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

}

 

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

 

 

- 블로그 글 생성 API를 테스트하는 코드 작성

 

Given 블로그 글 추가에 필요한 요청 객체를 만든다. 
When 블로그 글 추가 API에 요청을 보낸다. 이때 요청 타입은 JSON이며, given절에서 미리 만들어준 객체를 요청 본문으로 함께 보낸다. 
Then 응답 코드가 201 Created인지 확인한다. Blog를 전체 조회해 크기가 1인지 확인하고, 실제로 저장된 데이터와 요청 값을 비교한다. 

 

 

test/BlogApiController.java

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

 

- writeValueAsString() 메서드를 사용해서 객체를 JSON으로 직렬화해준다. 그 이후에는 MockMvc를 사용해 HTTP 메서드, URL, 요청 본문, 요청 타입 등을 설정한 뒤 설정한 내용을 바탕으로 테스트 요청으로 보낸다. 

 

 

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

 

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

 

BlogService.java

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

...
//db에 저장되어 있는 글을 모두 가져오는 findAll() ㅔㅁ서드 
public List<Article> findAll() {
        return blogRepository.findAll();
}
...

 

 

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

 

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

 

dto/ArticleResponse.java

- 응답을 위한 DTO 작성

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

@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

- 전체 글을 조회한 뒤 반환하는 findAllArtilces() 메서드를 추가 

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

 

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

 

 

3) 테스트 코드 작성하기 

 

Given 블로그 글을 저장한다. 
When 목록 조회 API를 호출한다. 
Then  응답 코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 content와 title이 저장된 값과 같은지 확인한다. 

 

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

 

 

5. 블로그 글 조회 API 구현하기 

 

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

 

 BlogService.java

 - 블로그 글 하나를 조회하는 메서드인 findById() 추가 

 - 이 메서드는 데이터베이스에 저장되어 있는 글의 ID를 이용해 글을 조회 

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

 

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

 

 

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

 

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

 

BlogApiController.java

....
@GetMapping("api/articles/{id}")
    //URL 경로에서 값 추출
    public ResponseEntity<ArticleResponse> findArticle(@PathVariable long id) {
        Article article = blogService.findById(id);
        
        return ResponseEntity.ok()
                .body(new ArticleResponse(article));
 }
...

 

- @PathVariable 애너테이션은 URL에서 값을 가져오는 애너테이션이다. 이 애너테이션이 붙은 메서드의 동작 원리는 /api/articles/3 GET 요청을 받으면 id에 3이 들어온다. 그리고 값을 앞서 만든 서비스 클래스의 findById()메서드로 넘어가 3번 블로그 글을 찾는다. 글을 찾으면 3번 글의 정보를 body에 답아 웹 브라우저로 전송한다. 

 

 

3) 테스트 코드 작성

 

Given 블로그 글을 저장한다. 
When 저장한 블로그 글의 id값으로 API를 호출한다.
Then 응답 코드가 200 OK이고, 반환값은 content와 title이 저장된 갑소가 같은지 확인한다. 

 

BlogApiController.java

...
@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. 블로그 글 삭제 API 구현하기 

 

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

 

BlogService.java

- delete() 메서드를 추가한다. 이 메서드는 그 글의 ID를 받은 뒤 JPA에서 제공하는 deleteById() 메서드를 이용해 데이터베이스에서 데이터를 삭제한다. 

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

 

 

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

 

- /api/articles/{id} DELETE 요청이 오면 글을 삭제하기 위한 findArticles() 메서드를 작성 

 

BlogApiController.java

...
@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) 테스트 코드 작성

 

Given 블로그 글을 저장한다. 
When 저장한 블로그 글의 id값으로 삭제 API를 호출한다. 
Then 응답 코드가 200 OK이고, 블로그 글 리스트 전체를 조회해 배열 크기가 0인지 확인 

 

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

 

 

7. 블로그 글 수정 API 구현하기 

 

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

 

- update() 메서드는 특정 아이디의 글을 수정한다. 

 

Article.java

- 엔티티에 요청받은 내용으로 값을 수정하는 메서드 작성 

...
//엔티티에 요청받은 내용으로 값을 수정하는 메서드 
 public void update(String title, String content) {
    this.title = title;
    this.content = content;
}

 

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

- 글에서 수정해야 하는 내용은 제목과 내용이므로 그에 맞게 제목과 내용 필드로 구성 

 

dto/UpdateArticleRequest.java

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

 

 

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

 

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

- 스프링에서는 트잭션을 적용하기 위해 다른 작업을 할 필요 없이 @Transactional 애너테이션만 사용하면 된다. 

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

 

 

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

 

BlogApiController.java

- /api/articles/{id} PUT 요청이 오면 글을 수정하기 위한 updateArticle() 메서드를 작성

...
@PutMapping("/api/articles/{id}")
    public ResponseEntity<Article> updateArticle(@PathVariable 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를 넘겨준다. 응답 값은 body에 담아 전송한다. 

 

 

3) 테스트 코드 작성 

 

Given 블로그 글을 저장하고, 블로그 글 수정에 필요한 요청 객체를 만듣나.
When UPDATE API로 수정 요청을 보낸다. 이때 요청 타입은 JSON이며, given 절에서 미리 만들어둔 객체를 요청 본문으로 함께 보낸다. 
Then 응답 코드가 200 OK인지 확인한다. 블로그 글 id로 조회한 후에 값이 수정되었는지 확인한다. 

 

 

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