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);
}
'SpringBoot' 카테고리의 다른 글
[스프링 부트 3 백엔드 개발자 되기] ch 8. 스프링 시큐리티로 로그인/로그아웃, 회원가입 구현하기 (28) | 2024.07.19 |
---|---|
[스프링 부트 3 백엔드 개발자 되기] ch 7. 블로그 화면 구성하기 (0) | 2024.07.17 |
[스프링 부트 3 백엔드 개발자 되기] ch 5. 데이터베이스 조작이 편해지는 ORM (0) | 2024.07.12 |
[스프링 부트 3 백엔드 개발자 되기] ch 4. 스프링 부트 3과 테스트 (0) | 2024.07.11 |
[스프링 부트 3 백엔드 개발자 되기] ch 3. 스프링 부트 3 구조 이해하기 (0) | 2024.07.11 |