API와 REST API
API를 구성한다는건 스프링의 3계층인 컨트롤러, 서비스, 리포지토리를 구성하는 것과 같다. URL을 구성하는 것이 API의 전부는 아니다. URL을 통해 들어온 요청을 처리하는 것까지가 API의 역할이다.

엔티티와 리포지토리 구성하기
1️⃣ 엔티티 만들기
JPA를 통해 데이터베이스와 연결할 엔티티 객체를 만든다.
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
@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;
}
}
엔티티에서 매번 getter 함수와 생성자를 구현하는건 반복되는 업무에 코드 가독성을 떨어뜨린다. Spring에서는 Lombok이라는 레포지토리를 통해 반복되는 코드를 줄여 가독성을 높이고, 디자인 패턴을 사용해 코드의 성능과 재사용성을 높일 수 있다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
@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;
}
}
@Builder 어노테이션은 GoF(Gang of Four) 디자인 패턴 중 생성 패턴으로 객체를 생성할 때 매개변수에 들어가는 값을 분리해 코드의 가독성을 높이고, 초기조건이 다른 객체를 만들 때 유용하게 쓸 수 있다.
2️⃣ 리포지토리 구성하기
자바의 3계층에서 리포지토리는 데이터베이스와 상호작용하는 비즈니스 계층이다. 다음의 코드를 보자.
public interface BlogRepository extends JpaRepository<Article, Long> {
}
BlogRepository는 JpaRepository를 확장한 것이다. 예전 소프트웨어공학 수업에서 자바의 인터페이스는 C++의 순수 가상 함수와 비슷하다고 했다. JpaRepository도 인터페이스이기 때문에 단순히 확장한 것밖에 지나지 않는다. 여기서는 별도의 메서드를 추가하지 않았으므로, 단순히 JpaRepository<Article, Long>이 가진 메서드들(findAll, findById, save, delete 등)을 사용할 수 있는 빈 껍데기 인터페이스다.
📍 BlogRepository의 구현은 따로 해야하는가?
💡ChatGPT 질문
그럼 BlogRepository 인터페이스를 구현하고 싶다면, public example class implements BlogRepository() {} 이렇게 새로운 클래스를 만들어야 하는건가?
✅ 정리
- 네, 맞습니다. 원한다면 public class BlogRepositoryImpl implements BlogRepository { ... } 이렇게 직접 구현할 수 있습니다.
- 하지만 그렇게 하면 JpaRepository의 모든 메서드를 전부 구현해야 하기 때문에 실무에서는 거의 하지 않습니다.
- 대신 스프링이 자동으로 구현체를 만들어 주고, 우리는 필요할 때만 @Query나 메서드 이름 기반 쿼리를 인터페이스에 선언해서 확장합니다.
글 추가 API 제작하기
엔티티를 구성하면 API를 구현한다. 구현 과정은 서비스 클래스의 메소드를 구현하고, 컨트롤러에서 사용할 메소드를 구현한 다음 API를 테스트한다. 서비스와 컨트롤러는 스프링 부트의 계층으로 다음 게시글에 설명해놨다.
https://brotherjeantech.tistory.com/48
[스프링 부트 3 백엔드 개발자 되기] 4. 스프링 부트의 구조
스프링 부트 내부 3계층 구조 스프링 부트 애플리케이션은 내부를 3계층으로 나눠 비즈니스 로직을 구성한다. 각각 프레젠테이션, 비즈니스, 퍼시스턴스 계층으로 불리지만 컨트롤러, 서비스.
brotherjeantech.tistory.com
1️⃣ 서비스 메소드 코드 작성
서비스 메소드를 구현하기 전 계층들 간의 정보전달을 하는 DTO 객체를 만든다. Data Transfer Object의 약자로 단순하게 데이터를 옮기기 위해 사용하는 전달자 역할만 하기 때문에 별도의 비즈니스 로직 메소드를 포함하지 않는다.
@NoArgsConstructor // 기본 생성자 추가
@AllArgsConstructor // 모든 필드 값을 파라미터로 받는 생성자 추가
@Getter
public class AddArticleRequest {
private String title;
private String content;
public Article toEntity(){ // 생성자를 사용해 객체 생성
return Article.builder()
.title(title)
.content(content)
.build();
}
}
@NoArgsConstructor, @AllArgsConstructor는 파라미터의 갯수에 따라 다르게 생성하는 생성자를 어노테이션을 통해 구현한 것이다. 코드 없이 어노테이션으로만 구현할 수 있어 가독성이 향상된다.
DTO 클래스는 Article형의 toEntity 메소드를 통해 파라미터가 들어간 객체를 빌더 패턴으로 생성한다.
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service // 빈에 자동 등록
public class BlogService {
private final BlogRepository blogRepository;
// 블로그글 추가 메소드
public Article save(AddArticleRequest request) {
return blogRepository.save(request.toEntity());
}
}
서비스 계층 클래스와 메소드를 구현한다. save는 비즈니스 로직에서 블로그 생성에 해당한다. 사용자 요청으로 받은 블로그 정보들을 toEntity() 메소드를 통해 Article 객체로 만들고 이를 리포지토리로 전달에 데이터베이스에 저장한다.
2️⃣ 컨트롤러 메소드 구현
HTTP 요청을 받은 컨트롤러 클래스를 구현한다. RESTful API에 맞춰 API를 설계하고 해당 요청이 들어왔을 때 동작하는 메소드를 구현한다.
@RequiredArgsConstructor
@RestController // HTTP Response Body에 객체 데이터를 JSON 형식으로 변환하는 컨트롤러
public class BlogApiController {
private final BlogService blogService;
// HTTP 메소드가 POST일 때 매핑
@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에 매핑한다.
- RensponseEntity.status().body()는 응답코드를 반환한다.
HTTP Request가 오면 Header와 Body정보가 있다. addArticle 메소드는 파라미터로 @RequestBody가 매핑된 DTO 객체에 body값을 넣고 이를 리포지토리 계층으로 보내 데이터베이스에 저장한다.
리포지토리 클래스를 실행했을 때 반환값을 Article자료형의 savedArticle 객체에 넣어 동작이 완료됐는지 알리는 ResponseEntity.status().body() 메소드에 넣는다. 해당 메소드는 응답 코드를 반환한다.
3️⃣ API 실행 테스트하기
Postman을 이용해 HTTP Request를 보내면 정상적으로 응답하는지 테스트한다.

만들어둔 API로 요청이 들어오는지 확인한다. 만들어둔 컨트롤러, 서비스, 리포지토리 계층이 연계하는지 테스트한다.
응답값을 봤을 때 savedArticle값이 잘 들어온 것을 확인할 수 있다. 직접 확인하고 싶다면 localhost:8080/h2-console에 접속해 SQL 값을 확인하면 된다. (참고로 H2 데이터베이스는 메모리로 이루어진 가상 데이터베이스다.)

들어온 것을 확인할 수 있다.
4️⃣ 반복 작업을 줄여 줄 테스트 코드 작성하기
테스트 코드를 통해 복잡한 과정 없이 API가 잘 작동하는지 테스트할 수 있다.
@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 클래스로 만든 객체는 직렬화와 역직렬화에 사용한다.
테스트 코드 중 objectMapper를 알아야 한다. 자바 객체와 외부 형태, 여기서는 JSON 포맷으로 서로 변환을 해야한다. 자바 객체에서 JSON으로 변환하는 걸 직렬화, 외부 데이터 포맷으로부터 자바 객체로 변환하는 것을 역직렬화라 한다.
mockMvcSetup() 메소드는 mockMvc 객체를 생성하고 리포지토리를 통해 데이터베이스를 초기화 한다.
@SpringBootTest
@AutoConfigureMockMvc // MockMvc 생성 및 자동 구성
class BlogApiControllerTest {
// 생략 ..
@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);
}
}
- writeValueAsString() 메소드를 사용해 객체를 JSON으로 직렬화한다.
- MockMvc를 사용해 HTTP 메소드, URL, 요청 본문, 요청 타입 등을 설정한 뒤 설정한 내용을 바탕으로 테스트 내용을 보낸다.
- contentType() 메소드는 요청을 보낼 때 JSON, XML등 다양한 타입 중 하나를 골라 요청을 보낸다.
- assertThat() 메소드는 블로그 글의 개수가 1개인지 확인한다.
addArticle() 메소드는 DTO 객체를 생성하고 JSON 포맷으로 역직렬화한다. 이를 HTTP POST 메소드를 통해 API로 전송한다. API는 JSON을 받고 리포지토리를 통해 데이터베이스에 글을 적을 것이다. 이를 리포지토리의 findAll() 메소드를 통해 제대로 들어갔는지 확인한다.
글 전체 목록 조회 API 제작하기
추가 API를 작성한 것과 마찬가지로 이후의 과정은 모두 똑같다. 컨트롤러를 설계하고 서비스 로직을 설계하며 리포지토리 코드를 작성한다. 테스트 코드를 작성해 이를 테스트하면 하나의 요구사항에 맞는 API를 설계할 수 있다.
1️⃣ 서비스 메소드 코드 작성
글 목록을 조회하는 서비스 로직을 작성한다.
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service // 빈에 자동 등록
public class BlogService {
private final BlogRepository blogRepository;
// 생략..
public List<Article> findAll(){
return blogRepository.findAll();
}
}
서비스 계층의 findAll 메소드는 리포지토리에 findAll() 메소드를 실행하는 메소드다. 글 목록 전체 조회를 담당한다.
2️⃣ 컨트롤러 메소드 코드 작성
컨트롤러 계층을 만들 때는 DTO 객체도 함께 만든다. 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();
}
}
전에 생성했던 클래스와 다른 점은 builder로 객체를 생성하지 않는 점이다. @Getter 어노테이션을 통해 반환 메소드는 자동으로 생성한다. API를 통해 GET 요청을 받으면 DTO 객체가 계층 사이를 오가며 데이터를 전달한다.
💡두 DTO 객체의 차이점
두 DTO 객체의 차이
네, 두 DTO 객체인 ArticleResponse와 AddArticleRequest의 가장 큰 차이점은 목적과 사용 방향에 있습니다.
- AddArticleRequest: 클라이언트로부터 요청을 받을 때 사용합니다.
- @NoArgsConstructor와 @AllArgsConstructor를 사용하여, 클라이언트가 보낸 JSON 데이터를 객체로 변환(역직렬화)하는 데 필요합니다.
- toEntity() 메서드는 이 객체를 Article 엔티티로 변환하는 역할을 합니다. 클라이언트로부터 받은 데이터를 가공하여 실제 DB에 저장될 엔티티 객체를 생성하는 것이 주 목적입니다. 이 과정에서 Article.builder()를 사용하는 것이 일반적인 패턴입니다.
- ArticleResponse: 클라이언트에게 응답을 보낼 때 사용합니다.
- Article 엔티티를 ArticleResponse 객체로 변환하는 생성자를 가지고 있습니다.
- getter만 있고 setter가 없으며, 필드가 final로 선언되어 있습니다. 이는 응답용 DTO 객체는 한번 생성되면 값이 변경되지 않아야 한다는 불변성(Immutability) 원칙을 따르는 것입니다.
ArticleResponse에 빌더 패턴을 사용하지 않은 이유
질문하신 대로 ArticleResponse에 굳이 빌더 패턴을 사용하지 않은 것은, 불필요한 복잡성을 피하기 위함입니다.
빌더 패턴은 주로 객체에 설정해야 할 필드가 많거나, 선택적으로 필드를 설정해야 할 때 유용합니다. 하지만 ArticleResponse의 경우 이미 완성된 Article 엔티티 객체에서 필요한 데이터를 가져와 필드가 모두 확정된 상태로 생성됩니다.
ArticleResponse는 다음과 같은 이유로 빌더 패턴이 필요 없습니다.
- 간결한 생성자: 모든 필드가 Article 객체로부터 명확하게 결정되기 때문에, 모든 필드를 받는 생성자를 사용하는 것이 코드를 더 간결하고 직관적으로 만듭니다.
- 불변성: final 키워드와 생성자를 통해 객체를 한번에 안전하게 생성하고, 이후에 수정될 여지가 없음을 보장합니다.
반면 AddArticleRequest에서 엔티티를 생성할 때 빌더 패턴을 사용하는 것은 좋은 설계입니다. 이는 코드가 더 읽기 쉽고, 추후에 Article 엔티티의 필드가 추가되더라도 유연하게 대응할 수 있기 때문입니다.
@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에 담아 클라이언트에 전송한다.
"/api/articles" URL에 GET요청을 보내면 findAllArticles 메소드를 실행한다. 우선 서비스 계층의 findAll 메소드를 실행해 리포지토리를 통해 데이터베이스의 모든 글 목록을 가져온다. 반환 타입은 List<Article>이다.
List<Article>을 stream 형태로 받고 각 데이터를 매핑해 ArticleResponse 객체로 변환한다. 이를 다시 List의 형태로 articles 변수에 담는다.
그다음 클라이언트에 articles를 전송한다.
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));
}
직접 리포지토리에 글을 넣고 get 요청이 왔을 경우 데이터베이스 안에 있는 객체를 전달한다.
단일 글 조회 API 제작하기
글 하나를 조회할 때는 앞서 auto_increment로 만들었던 id를 사용해 조회한다.
1️⃣ 서비스 계층 코드 작성하기
public Article findById(long id){
return blogRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("not found: " + id));
}
- findById() 메소드는 JPA의 findById() 메소드를 사용해 ID를 받아 엔티티를 조회하고 없으면 Exception을 반환한다.
2️⃣ 컨트롤러 계층 코드 작성하기
@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에서 값을 가져오는 어노테이션이다. /api/articles/3 GET 요청을 받으면 id에 3이 들어온다.
- id 값이 findById() 값으로 넘어가 데이터베이스에서 3번 블로그 글을 찾는다. 글을 찾으면 3번 글의 정보를 body에 담아 웹 브라우저로 전송한다.
3️⃣ 테스트 코드 작성하기
@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("$[0].content").value(content))
.andExpect(jsonPath("$[0].title").value(title));
}
그동안 배웠던 내용과 같다. 리포지토리를 이용해 데이터베이스에 글을 저장하고 id를 통해 글을 조회한다.
삭제 API 제작하기
1️⃣ 서비스 계층 코드 작성하기
public void delete(long id){
blogRepository.deleteById(id);
}
- delete 메소드는 반환값이 없기에 void
2️⃣ 컨트롤러 계층 코드 작성하기
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable long id){
blogService.delete(id);
return ResponseEntity.ok()
.build();
}
- @PathVariable 어노테이션은 URL에서 값을 가져오는 어노테이션이다. /api/articles/3 HTTP DELETE 요청을 받으면 id에 3이 들어온다.
- id 값을 받고 리포지토리 계층을 이용해 해당 글을 지운다.
3️⃣ 테스트 코드 작성하기
@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());
List<Article> articles = blogRepository.findAll();
assertThat(articles).isEmpty();
}
그동안 배웠던 내용과 같다. 리포지토리를 이용해 데이터베이스에 글을 저장하고 삭제한다.
수정 API 제작하기
1️⃣ 서비스 계층 코드 작성하기
수정 API는 엔티티의 내용을 바꾸기 때문에 엔티티에 수정 메소드를 적는다. 그리고 블로그 글 수정 요청을 받는 DTO를 생성한다. 엔티티와 DTO를 이용해 서비스 계층에 리포지토리를 이용하는 코드를 만든다.
public void update(String title, String content){
this.title = title;
this.content = content;
}
Article.java 엔티티 파일에 객체의 멤버 변수값을 바꾸는 메소드를 추가한다.
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class UpdateArticleRequest {
private String title;
private String content;
}
수정할 내용을 담아 계층에 전달하는 DTO 객체인 UpdateArticleRequest를 만든다. 글 추가와 글 수정은 다른 요구사항이니 그거에 맞게 객체를 분리시킨다.
@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;
}
새로운 수정사항을 DTO 객체로 받고, 이를 엔티티의 수정 메소드를 통해 수정한다. 수정하기 전 해당되는 글을 찾기 위해 리포지토리의 findById 메소드를 사용한다.
2️⃣ 컨트롤러 계층 코드 작성하기
@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);
}
HTTP PUT 요청이 오면 이 API로 연결한다. 요청의 id는 DTO 객체에 넣어 서비스 계층의 수정 메소드로 전달한다.
3️⃣ 테스트 코드 작성하기
@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));
// then
result.andExpect(status().isOk());
Article article = blogRepository.findById(savedArticle.getId()).get();
assertThat(article.getTitle()).isEqualTo(newTitle);
assertThat(article.getContent()).isEqualTo(newContent);
}
수정된 글이 잘 반영이 됐는지 API를 테스트한다.
'Backend' 카테고리의 다른 글
| [스프링 부트 3 백엔드 개발자 되기] 4. 스프링 부트의 구조 (0) | 2025.08.18 |
|---|---|
| [스프링 부트 3 백엔드 개발자 되기] 5-1. 데이터베이스와 ORM (0) | 2025.07.31 |