[스프링부트3 백엔드 개발자 되기] part 6

CodeKong의 기술 블로그·2023년 11월 14일
1

SPRING BOOT

목록 보기
17/24

📌 학습목표

블로그 글을 만들고, 조회하고, 업데이트하고, 삭제하는 RESTful API를 만들고 스프링 부트 3와 JPA를 어떻게 사용하는지 알아봅니다.


💡 API

식당으로 알아보는 API

✅ 식당에서는 점원에게 요리를 주문하고, 점원은 주방에 가서 조리를 요청합니다.
✅ 여기서 손님은 클라이언트, 요리사를 서버라고 합니다.
✅ 그리고 중간에 있는 점원은 API입니다.

REST API

✅ REST API는 웹의 장점을 최대한 활용하는 API입니다.
REST는 Representational State Transfer를 줄인 표현입니다.
✅ 즉, URL의 설계 방식을 말합니다.

REST API의 특징

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

REST API의 장점과 단점

장점

✅ URL만 보고도 무슨 행동을 하는 API인지 명확하게 알 수 있음!
✅ 서버와 클라이언트의 역할이 명확하게 분리된다!
✅ HTTP 표준을 사용하는 모든 플랫폼에서 사용 가능!

단점

✅ HTTP 메서드 방식 개수에 제한이 있다
✅ 공식적으로 제공되는 표준 규약이 없다

REST API를 사용하는 방법

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

2.동사는 HTTP 메서드로


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

프로젝트 준비하기

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    //스프링 데이터 JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2' //인메모리 데이터베이스
    compileOnly 'org.projectlombok:lombok' //롬복
    annotationProcessor 'org.projectlombok:lombok'
}

엔티티 구성하기

@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 interface BlogRepository extends JpaRepository<Article, Long> {
}

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

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

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class AddArticleRequest {

    private String title;
    private String content;

    public Article toEntity() {
        return Article.builder()
                .title(title)
                .content(content)
                .build();
    }
    
}
@RequiredArgsConstructor
@Service
public class BlogService {

    private final BlogRepository blogRepository;

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

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

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

}

API 실행 테스트하기

spring:
  datasource:
    url: jdbc:h2:mem:testdb

  h2:
    console:
      enabled: true


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

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

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

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

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

@RequiredArgsConstructor
@Service
public class BlogService {

    ...

    public List<Article> findAll() {
        return blogRepository.findAll();
    }
}

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

@Getter
public class ArticleResponse {

   ...

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

        return ResponseEntity.ok()
                .body(articles);
    }
}

실행 테스트하기

INSERT INTO article (title, content) VALUES ('제목 1', '내용 1');
INSERT INTO article (title, content) VALUES ('제목 2', '내용 2');
INSERT INTO article (title, content) VALUES ('제목 3', '내용 3');

@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {

    ...

    @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)
                .contentType(MediaType.APPLICATION_JSON));

        //then
        resultActions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].title").value(title))
                .andExpect(jsonPath("$[0].content").value(content));
    }
}

💡 블로그 글 조회 API 구현하기

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

@RequiredArgsConstructor
@Service
public class BlogService {

    ...

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

}

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

@RequiredArgsConstructor
@RestController
public class BlogApiController {

    ...

    @GetMapping("/api/articles/{id}")
    public ResponseEntity<ArticleResponse> findArticleById(@PathVariable Long id) {
        Article article = blogService.findById(id);
        return ResponseEntity.ok()
                .body(new ArticleResponse(article));
    }
}

테스트 코드 작성하기

@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {

    ...

    @DisplayName("findArticleById: 블로그 글 조회에 성공한다.")
    @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("$.title").value(title))
                .andExpect(jsonPath("$.content").value(content));
    }
}

💡 블로그 글 삭제 API 구현하기

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

@RequiredArgsConstructor
@Service
public class BlogService {

    ...

    public void deleteById(Long id) {
        blogRepository.deleteById(id);
    }

}

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

@RequiredArgsConstructor
@RestController
public class BlogApiController {

    ...

    @DeleteMapping("/api/articles/{id}")
    public ResponseEntity<Void> deleteArticleById(@PathVariable Long id) {
        blogService.deleteById(id);
        
        return ResponseEntity.ok()
                .build();
    }
}

실행 테스트하기


테스트 코드 작성하기

@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {

    ...

    @DisplayName("deleteArticleById: 블로그 글 삭제에 성공한다.")
    @Test
    public void deleteArticleById() 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(delete(url, savedArticle.getId()))
                .andExpect(status().isOk());

        //then
        List<Article> articles = blogRepository.findAll();

        assertThat(articles).isEmpty();
    }
}

💡 블로그 글 수정 API 구현하기

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

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {

    ...

    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }

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

    ...

    @Transactional
    public Article update(long id, AddArticleRequest request) {
        Article article = blogRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("not found" + id));

        article.update(request.getTitle(), request.getContent());
        
        return article;
    }

}

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

@RequiredArgsConstructor
@RestController
public class BlogApiController {

    ...

    @PutMapping("/api/articles/{id}")
    public ResponseEntity<Article> updateArticleById(@PathVariable Long id,
                                                              @RequestBody AddArticleRequest request) {
        Article updatedArticle = blogService.update(id, request);

        return ResponseEntity.ok()
                .body(updatedArticle);
    }
}

실행 테스트하기

테스트 코드 작성하기

@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {

    ...

    @DisplayName("updateArticleById: 블로그 글 수정에 성공한다.")
    @Test
    public void updateArticleById() 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 = "newTitle";
        final String newContent = "new content";

        UpdateArticleRequest request = new UpdateArticleRequest(newTitle, newContent);

        //when
        final 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);
    }
}

💡 핵심요약

✅ REST API는 웹의 장점을 최대한 활용하는 API로, 자원을 이름으로 구분해 자원의 상태를 주고받는 방식입니다.
✅ JpaRepository를 상속받으면 Spring JPA에서 지원하는 여러 메서드를 간편하게 사용할 수 있습니다.
✅ 롬복을 사용하면 더 깔끔하게 코드를 작성할 수 있습니다.
✅ 테스트 코드를 작성하면 코드의 기능이 제대로 작동한다는 것을 검증할 수 있습니다.

0개의 댓글