1) 서비스 메소드 구현
2) 컨트롤러 메소드 구현
3) API 테스팅
1) main/java/me.xxxxx/프로젝트명 패키지에 dto 패키지를 생성한다.
2) 해당 패키지에 AddArticleRequest클래스를 생성한다.
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class AddArticleRequest {
private String title;
private String content;
public Article toEntity(){
return Article.builder()
.title(title)
.content(content)
.build();
}
}
- @NoArgsConstructor: Lombok 어노테이션으로, 파라미터가 없는 기본 생성자를 자동으로 생성한다.
- @AllArgsConstructor: Lombok 어노테이션으로, 모든 필드를 매개변수로 받는 생성자를 자동으로 생성한다.
- @Getter: Lombok 어노테이션으로, 모든 필드에 대한 getter 메서드를 자동으로 생성한다.
- public Article toEntity(): AddArticleRequest 객체를 Article 엔티티 객체로 변환하는 메서드이다.
이 메서드는 Article 클래스의 빌더 패턴을 사용하여 title과 content 필드를 설정한 후, Article 객체를 생성하여 반환한다.
1) main/java/me.xxxx/프로젝트명 패키지에 service 패키지를 생성한다.
2) 해당 패키지에 BlogService 클래스를 구현한다.
@RequiredArgsConstructor
@Service // 빈으로 등록
public class BlogService {
private final BlogRepository blogRepository;
// 블로그 글 추가 메소드
public Article save(AddArticleRequest request){
return blogRepository.save(request.toEntity());
}
}
- @RequiredArgsConstructor: Lombok 어노테이션으로, final이나 @NotNull로 선언된 필드에 대한 생성자를 자동으로 생성한다. 이를 통해 의존성 주입(Dependency Injection)이 가능하다.
- @Service: Spring의 어노테이션으로, 이 클래스가 서비스 레이어의 컴포넌트임을 나타내며, 스프링 컨테이너에 의해 빈으로 등록된다.
- private final BlogRepository blogRepository: 데이터베이스 작업을 처리하기 위해 BlogRepository를 의존성으로 가진다. final 키워드를 사용하여 생성 시에만 할당되며, 변경되지 않는다.
- public Article save(AddArticleRequest request): AddArticleRequest 객체를 받아서, 그 객체의 toEntity() 메서드를 호출하여 Article 엔티티로 변환한다. 변환된 Article 엔티티를 blogRepository의 save 메서드를 통해 데이터베이스에 저장하고, 저장된 Article 객체를 반환한다.
컨트롤러 메소드는 다음과 같은 URL 매핑 어노테이션을 사용할 수 있다.
1. @GetMapping
2. @PostMapping
3. @PutMapping
4. @DeleteMapping
이 상황에서는 다음과 같은 흐름을 가진다.
1) @PostMapping으로 요청을 매핑
2) BlogService의 save()를 호출
3) 생성된 블로그 글을 반환
1) main/java/me.xxxxx 패키지에 controller 패키지를 생성
2) 해당 패키지에 BlogApiController.java 파일을 생성한다.
@RequiredArgsConstructor
@RestController // HTTP Response Body에 객체데이터를 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);
}
}
1) @RestController: Spring MVC의 어노테이션으로, 이 클래스가 RESTful 웹 서비스의 컨트롤러임을 나타낸다. 이 어노테이션은 @Controller와 @ResponseBody를 결합한 형태로, 메서드에서 반환된 객체는 JSON 형식으로 HTTP 응답 body에 직접 작성된다.
2) private final BlogService blogService: 블로그 관련 비즈니스 로직을 처리하는 BlogService를 의존성으로 가진다. @RequiredArgsConstructor에 의해 생성자가 자동으로 생성되며, 이 생성자를 통해 BlogService가 주입된다.
3) @RequestBody AddArticleRequest request: HTTP 요청 본문을 AddArticleRequest 객체(dto)로 변환하여 매개변수로 받는다. 이는 클라이언트가 JSON 형식으로 보낸 데이터를 Java 객체로 변환한다.
4) Article savedArticle = blogService.save(request): AddArticleRequest 객체를 BlogService의 save 메서드를 통해 저장하고, 저장된 Article 객체를 반환받는다.
5) return ResponseEntity.status(HttpStatus.CREATED).body(savedArticle): HTTP 응답으로 상태 코드 201(CREATED)와 함께 저장된 Article 객체를 JSON 형식으로 반환한다.
application.yml 파일에 다음 코드를 추가한다.
spring:
...
datasource:
url: jdbc:h2:mem:testdb
h2:
console:
enabled: true

localhost:8080/h2-console에 접속 후 JDBC URL을 다음과 같이 수정한다.

1) BlogApiController 클래스에
Alt+Enter를 누르고 [Create Test]를 통해 테스트코드 파일을 생성한다.
@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();
}
}
1) @SpringBootTest: 이 어노테이션은 스프링 부트 애플리케이션의 전체 컨텍스트를 로드하여 통합 테스트를 수행하도록 설정한다. 이를 통해 실제 애플리케이션과 동일한 환경에서 테스트를 수행할 수 있다.
2) @AutoConfigureMockMvc: 이 어노테이션은 MockMvc 객체를 자동으로 구성하고 생성한다. MockMvc는 스프링 MVC 애플리케이션의 웹 계층을 테스트하는 데 사용된다.
3) @Autowired protected MockMvc mockMvc: MockMvc 객체를 주입받는다. MockMvc는 컨트롤러의 동작을 테스트할 수 있게 해준다.
4) @Autowired protected ObjectMapper objectMapper: ObjectMapper 객체를 주입받는다. ObjectMapper는 JSON 데이터를 Java 객체로 변환하거나 그 반대로 변환하는 데 사용된다.(직렬화 & 역직렬화)
5) @Autowired private WebApplicationContext context: WebApplicationContext 객체를 주입받는다. 이 객체는 스프링의 웹 애플리케이션 컨텍스트로, MockMvc를 설정하는 데 사용된다.
6) @Autowired BlogRepository blogRepository: BlogRepository 객체를 주입받는다. 테스트 중 데이터베이스 작업을 수행하는 데 사용된다.
7) this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build():
MockMvc 객체를 WebApplicationContext를 사용하여 설정한다. 이를 통해 MockMvc가 스프링의 웹 애플리케이션 컨텍스트와 함께 초기화된다.
8) blogRepository.deleteAll(): 테스트 시작 전에 BlogRepository의 모든 데이터를 삭제하여, 각 테스트가 깨끗한 상태에서 시작되도록 한다.
@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)
.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);
}
1) @DisplayName 어노테이션은 테스트의 설명을 지정한다. 이 경우 "블로그 글 추가에 성공한다."는 테스트의 목적을 설명한다.
2) requestBody: userRequest 객체를 JSON 문자열로 직렬화한다.
3) mockMvc를 사용하여 post 요청을 실행한다. 요청의 Content-Type을 application/json으로 설정한다. requestBody를 요청의 Body로 설정한다. ResultActions 객체를 통해 요청의 결과를 받아온다.
4) result.andExpect(status().isCreated()): 요청에 대한 응답 상태가 201(Created)인지 확인한다.
5)
blogRepository.findAll(): 데이터베이스에서 모든 블로그 글을 가져온다.
assertThat(articles.size()).isEqualTo(1): 데이터베이스에 글이 하나 추가되었는지 확인한다.
assertThat(articles.get(0).getTitle()).isEqualTo(title): 추가된 글의 제목이 요청한 제목과 일치하는지 확인한다.
assertThat(articles.get(0).getContent()).isEqualTo(content): 추가된 글의 내용이 요청한 내용과 일치하는지 확인한다.
BlogService.java에 findAll()메소드를 추가한다.
@RequiredArgsConstructor
@Service // 빈으로 등록
public class BlogService {
private final BlogRepository blogRepository;
...
public List<Article> findAll(){
return blogRepository.findAll();
}
}
응답을 위한 DTO를 먼저 작성한다.
dto 패키지에 ArticleResponse.java를 생성한다.
@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에 findAllArticles메소드를 추가한다.
@GetMapping("/api/articles")
public ResponseEntity<List<ArticleResponse>> findAllArticles(){
List<ArticleResponse> articles = blogService.findAll()
.stream()
.map(ArticleResponse::new)
.toList();
return ResponseEntity.ok()
.body(articles);
}
1)
public ResponseEntity<List<ArticleResponse>> findAllArticles(): 이 메서드는 ResponseEntity 객체를 반환하며, 이 객체는 HTTP 응답의 상태 코드, 헤더 및 바디를 포함할 수 있습니다. 반환 타입은List<ArticleResponse>입니다.
2)
가) blogService.findAll(): 블로그 서비스의 findAll 메서드를 호출하여 모든 블로그 글 목록을 가져온다. 이 메서드는List<Article>타입을 반환한다.
나) .stream():List<Article>을 스트림으로 변환한다.
다) .map(ArticleResponse::new): 각 Article 객체를 ArticleResponse 객체로 변환한다. 이 과정에서 ArticleResponse의 생성자를 호출한다.
라) .toList(): 변환된 ArticleResponse 객체들을 리스트로 수집한다.
3)
가) ResponseEntity.ok(): HTTP 상태 코드 200 OK를 설정합니다.
나) .body(articles): 응답의 바디에 articles 리스트를 설정합니다.
-> 최종적으로 ResponseEntity 객체를 반환하여 클라이언트에게 응답합니다.
INSERT INTO article (title, content) VALUES ('제목1', '내용1')
INSERT INTO article (title, content) VALUES ('제목2', '내용2')
INSERT INTO article (title, content) VALUES ('제목3', '내용3')

@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));
}
1) blogRepository.save(...): 데이터베이스에 테스트용 블로그 글을 저장한다. Article 객체를 빌더 패턴을 사용하여 생성하고, title과 content 값을 설정하여 저장한다.
2) mockMvc.perform(get(url).accept(MediaType.APPLICATION_JSON)): 지정된 URL로 HTTP GET 요청을 보내고, 응답을 JSON 형식으로 받도록 설정한다.
ResultActions resultActions: 요청의 결과를 캡처하여 resultActions 객체에 저장한다.
3)
가. resultActions.andExpect(status().isOk()): 응답 상태 코드가 200 OK인지 확인한다.
나. resultActions.andExpect(jsonPath("[0].title").value(title)):
응답 JSON 배열의 첫 번째 객체의 title 필드가 title 변수의 값과 일치하는지 확인한다.
jsonPath("$.content").value("Sample Content"): JSON 응답의 content 필드가 "Sample Content"인지 확인합니다.
글 하나를 조회하는 API 개발
public Article findById(long id){
return blogRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("not found: " + id));
}
@GetMapping("/api/articles/{id}")
public ResponseEntity<ArticleResponse> findArticle(@PathVariable long id){
Article article = blogService.findById(id);
return ResponseEntity.ok()
.body(new ArticleResponse(article));
}
@PathVariable: URL에서 값을 가져오는 어노테이션이다.
즉 id값을 추출한다.
@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));
}
public void delete(long id){
blogRepository.deleteById(id);
}
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable long id){
blogService.delete(id);
return ResponseEntity.ok().build();
}
import static org.assertj.core.api.Assertions.assertThat;
@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();
}
Article 클래스에 업데이트하는 메소드를 직접 추가한다.
public void update(String title, String content){
this.title=title;
this.content = content;
}
dto 패키지에서 UpdateArticleRequst.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;
}
1) @Transactional: 매칭한 메소드를 하나의 트랙잭션으로 묶어버린다.
2) update(): 엔티티의 필드값이 바뀌면 중간에 에러가 발생해도 제대로된 값 수정을 보장한다.
@PutMapping("/api/articles/{id}")
public ResponseEntity<Article> updateArticle(@PathVariable long id,
@RequestBody UpdateArticleRequest request)
{
Article updateArticle = blogService.update(id, request);
return ResponseEntity.ok()
.body(updateArticle);
}
@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);
}
objectMapper.writeValueAsString(request): request 객체를 JSON 문자열로 변환한다.
objectMapper는 Jackson 라이브러리의 ObjectMapper 객체로, Java 객체를 JSON으로 변환하거나 JSON을 Java 객체로 변환하는 데 사용된다.
request는 PUT 요청으로 전송할 데이터입니다. 이 데이터는 JSON 형식으로 변환되어 요청 본문에 포함된다.