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(크루드)
- 규칙 1. URL에는 동사를 쓰지 말고, 자원을 표시해야 한다.
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
- 포스트맨 실행
- 웹 브라우저 실행
- localhost:8080/h2-console 접속
- H2 데이터베이스에 저장된 데이터를 확인할 수 있음
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. 포스트맨 실행 테스트하기
- 글 목록 조회
- HTTP 메서드 → GET
- http://localhost:8080/api/articles
- 글 삭제
- HTTP 메서드 → DELETE
- http://localhost:8080/api/articles/1 → SEND
- HTTP 메서드 → GET
- http://localhost:8080/api/articles
- 글 수정
- HTTP 메서드 → PUT
- http://localhost:8080/api/articles/1
- 수정된 글은 글 전체 조회 API로 확인
- HTTP 메서드 → GET
- http://localhost:8080/api/articles
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로 조회한 후에 값이 수정되었는지 확인합니다.
✅ 트랜잭션
- 트랜잭션은 데이터베이스의 데이터를 바꾸기 위해 묶은 작업의 단위