SERVER

[3기 SERVER 스터디] 3주차 - 블로그 기획하고 API 만들기

jimin924 2024. 10. 9. 21:19

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

 

1. API와 REST API

  • 네트워크에서 API는 프로그램 간에 상호작용하기 위한 매개체
    • 클라이언트의 요청을 서버에 잘 전달하고 서버의 결과물을 클라이언트에게 잘 돌려주는 역할
  • REST API는 웹의 장점을 최대한 활용하는 API
    • REST: Representational State Transfer
    • 자원을 이름으로 구분해 자원의 상태를 주고받는 API 방식, 명확하고 이해하기 쉬운 API
    • REST API는 URL의 설계 방식임.
  • REST API의 장단점
    • ✅ URL만 보고도 무슨 행동을 하는 API인지 명확히 알 수 있음
    • ✅ 상태가 없다는 특징이 있어 클라이언트와 서버의 역할이 명확히 분리됨
    • ✅ HTTP 표준을 사용하는 모든 플랫폼에서 사용 가능
    • ❌ HTTP 메서드, 즉, GET, POST와 같은 방식의 개수에 제한이 있음
    • ❌ 설계를 하기 위해 공식적으로 제공되는 표준 규약이 없음
    • 그럼에도 REST API는 주소와 메서드만 보고 요청의 내용을 파악할 수 있다는 강력한 장점이 있어 많은 개발자가 사용함
  • RESTful API: REST하게 디자인한 API
  • REST API를 사용하는 방법
    • 규칙 1. URL에는 동사를 쓰지 말고, 자원을 표시해야 한다.
      /articles/1 적합 동사 없음, 1번 글을 가져온다는 의미가 명확
      /articles/show/1 부적합 show라는 동사가 있음
    • 규칙 2. 동사HTTP 메서드
      id가 1인 블로그 글을 조회하는 API GET /articles/1
      블로그 글을 추가하는 API POST /articles
      블로그 글을 수정하는 API PUT /articles/1
      블로그 글을 삭제하는 API DELETE /articles/1
      • HTTP 메서드는 서버에 요청을 하는 방법을 나눈 것
      • POST, GET, PUT, DELETE → 만들고(create), 읽고(read), 업데이트하고(update), 삭제(delete)
      •  CRUD(크루드)

 

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

  • 엔티티 구성하기
  • 컬럼명 자료형 null 허용 설명
    id BIGINT N 기본키 일련번호, 기본키
    title VARCHAR(255) N   게시물의 제목
    content VARCHAR(255) N   내용
// 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)
    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 애너테이션
    • 롬복에서 지원하는 애너테이션
    • 생성자 위에 입력하면 빌더 패턴 방식으로 객체를 생성할 수 있어 편리함
    • 빌더 패턴을 사용하면 객체를 유연하고 직관적으로 생성할 수 있기에 개발자들이 애용하는 디자인 패턴임
    • 어느 필드에 어떤 값이 들어가는지 명시적으로 파악 가능
    // 빌더 패턴을 사용하지 않았을 때
    new Article("abc", "def");
    
    // 빌더 패턴을 사용했을 때
    Article.builder()
    	.title("abc")
    	.content("def")
    	.build();
    • Article 객체를 생성할 때 title에는 abc를, content에는 def값으로 초기화한다고 했을 때, 빌더 패턴을 사용하지 않으면 abc와 def가 어느 필드에 들어가는 값인지 파악하기 어려움
    • 빌더 패턴을 사용하면 어느 필드에 어느 값이 매칭되는지 바로 보이므로 객체 생성 코드의 가독성이 높음
  • 롬복의 애너테이션을 사용하여 가독성 향상
// Article.java

@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() 같이 필드의 값을 가져오는 게터 메서드들을 @Getter 애너테이션, @NoArgsConstructor 애너테이션으로 대치
  • protected Article() {} 코드 블록, get 관련 메서드는 모두 삭제
  • @NoArgsConstructor 애너테이션을 선언해 접근 제어자가 protected인 기본 생성자를 별도의 코드 없이 생성
  • @Getter 애너테이션으로 클래스 필드에 대해 별도 코드 없이 모든 필드에 대한 접근자 메서드를 만들 수 있게 됨

 

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

 

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

  • 블로그에 글을 추가하는 코드를 서비스 계층에 작성
  • 서비스 계층에서 요청을 받을 객체인 AddArticleRequest 객체를 생성하고, Blogservice 클래스를 생성한 다음에 블로그 글 추가 메서드인 save()를 구현
  • DTO: 계층끼리 데이터를 교환하기 위해 사용하는 객체
  • DAO: 데이터베이스와 연결되고 데이터를 조회하고 수정하는 데 사용하는 객체
  • DAO는 데이터 수정과 관련된 로직이 포함되지만 DTO는 단순하게 데이터를 옮기기 위해 사용하는 전달자 역할을 하는 객체이기에 별도의 비즈니스 로직을 포함하지 않음
// AddArticleRequest.java

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

    public Article toEntity() {
    // toEntity()는 빌더 패턴을 사용해 DTO를 엔티티로 만들어주는 메서드
        return Article.builder()
                .title(title)
                .content(content)
                .build();
    }
}
//BlogService.java

@RequiredArgsConstructor // 빈을 생성자로 생성하는 롬복에서 지원하는 애너테이션 (final 키워드나 @NotNull이 붙은 필드로 생성자 생성)
@Service // 해당 클래스를 빈으로 서블릿 컨테이너에 등록해줌
public class BlogService {

    private final BlogRepository blogRepository;

    public Article save(AddArticleRequest request) {
    // save() 메서드는 JpaRepository에서 지원하는 저장 메서드로 AddArticleRequest 클래스에 저장된 값들을 article 데이터베이스에 저장
        return blogRepository.save(request.toEntity());
    }
}

 

 

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

  • URL에 매핑하기 위한 컨트롤러 메서드를 추가
  • URL 매핑 애너테이션 @GetMapping, @PostMapping, @PutMapping, @DeleteMapping 등을 사용할 수 있음 → 각 메서드는 HTTP 메서드에 대응
  • /api/articles에 POST 요청이 오면 @PostMapping을 이용해 요청을 매핑한 뒤, 블로그 글을 생성하는 BlogService의 save() 메서드를 호출한 뒤, 생성된 블로그 글을 반환하는 작업을 할 addArticle() 메서드를 작성
// BlogApiController.java

@RequiredArgsConstructor
@RestController // 클래스에 붙이면 HTTP 응답으로 객체 데이터를 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);
    }
}
  • ✅ 꼭 알아두면 좋을 응답 코드
    • 200 OK: 요청이 성공적으로 수행되었음
    • 201 Created: 요청이 성공적으로 수행되었고, 새로운 리소스가 생성되었음
    • 400 Bad Request: 요청 값이 잘못되어 요청에 실패했음
    • 403 Forbidden: 권한이 없어 요청에 실패했음
    • 404 Not Found: 요청 값으로 찾은 리소스가 없어 요청에 실패했음
    • 500 Internal Server Error: 서버 상에 문제가 있어 요청에 실패했음

 

3. API 실행 테스트하기

// application.yml

spring:
  jpa:
  
// 생략

  datasource: // 들여쓰기 주의! (공백 두번, jpa와 동일 선상)
    url: jdbc:h2:mem:testdb

  h2:
    console:
      enabled: true
  • 포스트맨 실행

포스트맨 실행(작성)

  • 웹 브라우저 실행

 

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

// 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 데이터로 변환하는 직렬화 또는 반대로 JSON 데이터를 자바에서 사용하기 위해 자바 객체로 변환하는 역직렬화할 때 사용
Given 블로그 글 추가에 필요한 요청 객체를 만듭니다.
When 블로그 글 추가 API에 요청을 보냅니다. 이때 요청 타입은 JSON이며, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 보냅니다.
Then 응답 코드가 201 Created인지 확인합니다. Blog를 전체 조회해 크기가 1인지 확인하고, 실제로 저장된 데이터와 요청 값을 비교합니다.
// BlogApiControllerTest.java에 추가

@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {

// 생략

    @DisplayName("addArticle: 블로그 글 추가에 성공한다.")
    @Test
    public void addArticle() throws Exception {
        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);
        // writeValueAsString() 메서드를 사용해서 객체를 JSON으로 직렬화

        ResultActions result = mockMvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(requestBody));

        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);
    }
}
  • assertThat() 메서드
assertThat(articles.size()).isEqualTo(1); 블로그 글 크기가 1이어야 합니다.
assertThat(articles.size()).isGreaterThan(2); 블로그 글 크기가 2보다 커야 합니다.
assertThat(articles.size()).isLessThan(5); 블로그 글 크기가 5보다 작아야 합니다.
assertThat(articles.size()).isZero(); 블로그 글 크기가 0이어야 합니다.
assertThat(articles.size()).isEqualTo(”제목”); 블로그 글의 title값이 “제목”이어야 합니다.
assertThat(articles.size()).isNotEmpty(); 블로그 글의 title값이 비어 있지 않아야 합니다.
assertThat(articles.size()).cpntains(”제”); 블로그 글의 title값이 “제”를 포함해야 합니다.

 

4 - 7. 블로그 글 목록 조회, 삭제, 수정을 위한 API 구현하기

  • 조회 : findAll() , findAllArticles() 추가
  • 하나만 조회 : findById()
  • 삭제 : delete(), deleteById()
  • 수정 : update()

 

1. 포스트맨 실행 테스트하기

포스트맨 실행(조회)

포스트맨 실행(삭제)

포스트맨 실행(수정)
포스트맨 실행(수정)

 

2. 테스트 코드 작성하기

  • 글 목록 조회
    Given 블로그 글을 저장합니다.
    When 목록 조회 API를 호출합니다.
    Then 응답 코드가 200 OK이고, 반환받은 값 중에 0번째 요소의 content와 title이 저장된 값과 같은지 확인합니다.
  • 글 조회
    Given 블로그 글을 저장합니다.
    When 저장한 블로그 글의 id값으로 API를 호출합니다.
    Then 응답 코드가 200 OK이고, 반환받은 content와 title이 저장된 값과 같은지 확인합니다.
  • 글 삭제
    Given 블로그 글을 저장합니다.
    When 저장한 블로그 글의 id값으로 삭제 API를 호출합니다.
    Then 응답 코드가 200 OK이고, 블로그 글 리스트를 전체 조회해 조회한 배열 크기가 0인지 확인합니다.
  • 글 수정
    Given 블로그 글을 저장하고, 블로그 글 수정에 필요한 요청 객체를 만듭니다.
    When UPDATE API로 수정 요청을 보냅니다. 이때 요청 타입은 JSON이며, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 보냅니다.
    Then 응답 코드가 200 OK인지 확인합니다. 블로그 글 id로 조회한 후에 값이 수정되었는지 확인합니다.

테스트 성공

 

트랜잭션

  • 트랜잭션은 데이터베이스의 데이터를 바꾸기 위해 묶은 작업의 단위