REST API
REST API는 웹의 장점을 최대한 활용하는 API이다. 쉽게 말해 명확하고 이해하기 쉬운 API를 말한다. RESR API는 URL의 설계 방식을 말한다.
REST API의 특징
REST API는 서버/클라이언트 구조, 무상태, 캐시 처리기능, 계층화, 인터페이스 일관성과 같은 특징이 있다.
REST API의 장점과 단점
REST API의 장점은 URL만 보고도 무슨 행동을 하는 API인지 명확하게 알 수 있다는 것이다. 그리고 상태가 없다는 특징이 있어서 클라이언트와 서버의 역할이 명확하게 분리된다. HTTP 표준을 사용하는 모든 플랫폼에서 사용 할 수 있다. 단점으로는 HTTP 메서드, 즉 GET, POST와 같은 방식의 개수에 제한이 있고, 설계를 하기 위해 공식적으로 제공되는 표준 규약이 없다는 것이다. 그럼에도 REST API는 주소와 메서드만 보고 요청의 내용을 파악할 수 있다는 강력한 장점이 있어서 개발자들이 많이 사용한다. 심지어 'REST 하게 디자인한 API'를 RESTful API라 부른다.
규칙 1. URL에는 동사를 쓰지 말고, 자원을 표시해야 한다.
예문 | 적합성 | 설명 |
/article/1 | 적합 | 동사 없음, 1번 글을 가져온다ㅏ는 의미가 명확, 적합 |
/article/show/1 /show/article/1 |
부적합 | show라는 동사가 있음, 부적합 |
규칙 2. 동사는 HTTP 메서드로
설명 | 적합한 HTTP 메서드와 URL |
id가 1인 블로그 글을 조회하는 API | GET/articles/1 |
블로그 글을 추가하는 API | POST/article |
블로그 글을 수정한느 API | PUT/article |
블로그 글을 삭제하는 API | DELETE/article/1 |
빌더 패턴의 장점
- 가독성 향상:
- 복잡한 객체를 생성할 때, 어떤 값이 어떤 필드에 해당하는지 명확히 알 수 있어 가독성이 높아집니다.
- 예: Article article = Article.builder().title("제목").content("내용").build();
- 불변성 (Immutability):
- 객체를 불변으로 만들 수 있습니다. 빌더 패턴을 사용하면 객체 생성 후에 필드를 변경할 수 없게 설계할 수 있습니다.
- 유연한 객체 생성:
- 필수 필드와 선택 필드를 쉽게 구분할 수 있습니다. 필수 필드는 빌더 메서드로 강제하고, 선택 필드는 필요에 따라 추가할 수 있습니다.
- 안전한 객체 생성:
- 빌더 패턴을 사용하면 생성자에 전달되는 매개변수의 순서나 타입에 대한 오류를 방지할 수 있습니다.
- 메서드 체이닝:
- 메서드 체이닝을 통해 객체 생성 시 일관된 형태로 코드를 작성할 수 있습니다.
@Getter의 장점
- 코드 간소화:
- 각 필드에 대한 getter 메서드를 자동으로 생성하여 코드 중복을 줄여줍니다.
- 가독성:
- 코드가 간결해져 클래스의 의도가 명확히 드러납니다. 코드를 읽는 사람이 핵심 로직에 집중할 수 있습니다.
- 유지보수 용이성:
- Lombok이 자동으로 생성해주는 메서드를 사용하여 수동으로 작성하는 실수를 줄이고, 유지보수를 더 쉽게 할 수 있습니다.
@NoArgsConstructor의 장점
- 간편한 객체 생성:
- 매개변수가 없는 기본 생성자를 자동으로 생성하여, JPA와 같은 프레임워크에서 프록시 객체를 생성하거나 리플렉션을 통해 객체를 생성할 때 유용합니다.
- 호환성:
- JPA 엔티티나 스프링 빈과 같은 일부 프레임워크에서는 기본 생성자가 필요합니다. @NoArgsConstructor를 사용하면 이러한 요구사항을 쉽게 충족할 수 있습니다.
- 테스트 편의성:
- 테스트 코드에서 객체를 쉽게 생성하여 테스트할 수 있습니다. 기본 생성자를 사용해 기본값으로 객체를 생성한 후, 필요한 값만 setter로 설정할 수 있습니다.
엔티티 작성
@Entity //엔티티로 지정
@Getter
@NoArgsConstructor
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;
}
public void update(String title, String content){
this.title=title;
this.content=content;
}
}
리포지터리 작성
public interface BlogRepository extends JpaRepository<Article, Long> {
}
dto작성
@NoArgsConstructor//기본 생성자 추가
@Getter
@AllArgsConstructor//모든 필드 값을 파라미터로 받는 생성자 추가
public class AddArticleRequest {
private String title;
private String content;
public Article toEntity(){ // 생성자를 사용해 객체 생성
return Article.builder()
.title(title)
.content(content)
.build();
}
}
@Getter
public class ArticleResponse {
private final String title;
private final String content;
public ArticleResponse(Article article){
this.title=article.getTitle();
this.content=article.getContent();
}
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UpdateArticleRequest {
private String title;
private String content;
}
서비스 작성
@RequiredArgsConstructor // final이 붙거나 @Not Null이 붙은 필드의 생성자 추가
@Service // 빈으로 둥록
public class BlogService {
private final BlogRepository blogRepository;
//블로그 글 추가 메서드
public Article save(AddArticleRequest request){
return blogRepository.save(request.toEntity());
}
public List<Article> findAll(){
return blogRepository.findAll();
}
public Article findById(long id){
return blogRepository.findById(id).orElseThrow(()->new IllegalArgumentException("not found: " + id));
}
public void delete(long id){
blogRepository.deleteById(id);
}
@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;
}
}
controller 작성
@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);
}
@GetMapping("/api/articles")
public ResponseEntity<List<ArticleResponse>> findAllArticles(){
List<ArticleResponse>articles = blogService.findAll()
.stream()
.map(ArticleResponse::new)
.toList();
return ResponseEntity.ok().body(articles);
}
@GetMapping("/api/articles/{id}")
//URL 경로에서 값 추출
public ResponseEntity<ArticleResponse> findArticles(@PathVariable long id){
Article article = blogService.findById(id);
return ResponseEntity.ok().body(new ArticleResponse(article));
}
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Void> delete(@PathVariable long id){
blogService.delete(id);
return ResponseEntity.ok().build();
}
@PutMapping("/api/articles/{id}")
public ResponseEntity<Article> update(@PathVariable long id,
@RequestBody UpdateArticleRequest request){
Article updateArticle = blogService.update(id, request);
return ResponseEntity.ok().body(updateArticle);
}
}
꼭 알아두면 좋은 응답 코드
200 OK | 요청이 성공적으로 수행되었음 |
201 Created | 요청이 성곡적으로 수행되었고, 새로운 리소스가 생성되었음 |
400 Bad Request | 요청 값이 잘못되어 요청에 실패했음 |
403 Forbidden | 권한이 없어 요청에 실패했음 |
404 Not Found | 요청 값으로 찾은 리소스가 없어 요청에 실패했음 |
500 Internal Server Error | 서버 상에 문제가 있어 요청에 실패했음 |
테스트코드 작성
@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();;
}
@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.APPLICATION_JSON_VALUE).content(requestBody));
//then
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);
}
@DisplayName("findAllArticles : 블로그 글 목록 조회에 성공한다.")
@Test
public void findAllArticle() 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));
}
@DisplayName("findArticle : 블로그 글 조회 성공")
@Test
public void findArticle() throws Exception{
//given
final String url = "/api/articles/{id}";
final String title = "title";
final String content = "content";
Article saveArticle = blogRepository.save(Article.builder()
.content(content)
.title(title)
.build());
//when
final ResultActions resultActions = mockMvc.perform(get(url,saveArticle.getId()).accept(MediaType.APPLICATION_JSON));
//then
resultActions.andExpect(status().isOk())
.andExpect(jsonPath("$.content").value(content))
.andExpect(jsonPath("$.title").value(title));
}
@DisplayName("deleteArticle : 블로그 글 삭제에 성공한다.")
@Test
public void deleteArticle() throws Exception{
//given
final String url = "/api/articles/{id}";
final String title = "title";
final String content = "content";
Article saveArticle = blogRepository.save(Article.builder()
.title(title)
.content(content)
.build());
//when
mockMvc.perform(delete(url,saveArticle.getId())).andExpect(status().isOk());
//then
List<Article> articles =blogRepository.findAll();
assertThat(articles).isEmpty();
}
@DisplayName("updateArticle : 블로그 글 수정에 성공한다.")
@Test
public void updateArticle()throws Exception{
//given
final String url ="/api/articles/{id}";
final String title = "title";
final String content ="content";
Article saveArticle = blogRepository.save(Article.builder()
.content(content)
.title(title)
.build());
final String newTitle = "new title";
final String newContent = "new Content";
UpdateArticleRequest request = new UpdateArticleRequest(newTitle,newContent);
//when
ResultActions result = mockMvc.perform(put(url,saveArticle.getId())
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(request)));
//then
result.andExpect(status().isOk());
Article article = blogRepository.findById(saveArticle.getId()).get();
assertThat(article.getTitle()).isEqualTo(newTitle);
assertThat(article.getContent()).isEqualTo(newContent);
}
}
- 직렬화 (Serialization): 자바 객체를 JSON 형식의 문자열로 변환하여 파일이나 네트워크로 전송하기 쉽게 합니다.
- 역직렬화 (Deserialization): JSON 형식의 문자열을 다시 자바 객체로 변환하여 사용합니다.
- 목적: 객체 상태를 저장하거나 전송하고, 이를 다시 원래 객체로 복원하는 데 사용됩니다.
given | 블로그 글 추가에 필요한 요청 객체를 만든다. |
when | 블로그 글 추가 API에 요청을 보낸다. 이때 요청 타입은 JSON이며, given절에서 미리 만들어둔 객체를 요청 본문으로 함꼐 보낸다. |
then | 응답 코드가 201 Created인지 확인 한다. Blog를 전체 조회해 크기가 1인지 확인하고, 실제로 저장된 데이터와 요청 값을 비교한다. |
코드 | 성명 |
assertThat(articles.size()).isEqualTo(1); | 블로그 글 크기가 1이어야 한다. |
assertThat(articles.size()).isgreaterThan(2); | 블로그 글 크기가 2보다 커야 한다. |
assertThat(articles.size()).isLessThan(5); | 블로그 글 크기가 5보다 작아야 한다. |
assertThat(articles.size()).isZero(); | 블로그 글 크기가 0이어야 한다. |
assertThat(article.title()).isEqualTo("제목") | 블로그 글의 title값이 "제목"이어야 한다. |
assertThat(article.title()).isNotEmpty(); | 블로그 글의 title값이 비어 있지 않아야 한다. |
assertThat(article.title()).contains("제"); | 블로그 글의 title값이 "제"를 포함해야 한다. |
'Spring공부' 카테고리의 다른 글
블로그의 인증, 인가 쿠키&세션 (0) | 2024.05.23 |
---|---|
240520_TIL (0) | 2024.05.20 |
데이터베이스 (0) | 2024.05.17 |
테스트 코드 (0) | 2024.05.16 |
스프링 부트 구조 살펴보기 (0) | 2024.05.14 |