Notice
Recent Posts
Recent Comments
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

SYDev

[스프링 부트 3 백엔드 개발자 되기] Chapter 6. 블로그 기획하고 API 만들기 본문

대딩코딩/웹개발 스터디

[스프링 부트 3 백엔드 개발자 되기] Chapter 6. 블로그 기획하고 API 만들기

시데브 2024. 4. 7. 00:42

자바 애너테이션

  • Java Annotation: 자바로 작성한 코드에 추가하는 표식, 보통 @기호 사용
  • 메타 데이터로 사용하는 경우가 가장 많음
@Override: 선언된 메서드가 오버라이드 됨
@Deprecated: 더 이상 사용되지 않음
@SuppressWarnings: 컴파일 경고를 무시함

 

Spring Concept

제어의 역전과 의존성 주입

  • 스프링은 모든 기능의 기반을 제어의 역전(IoC)의존성 주입(DI)에 두고 있다.

IoC(Inversion of Control)

  • 외부에서 관리하는 객체를 가져와 사용하는 것을 말한다.
  • 스프링은 스프링 컨테이너가 객체를 관리, 제공하는 역할을 한다.
public class A {
    b = new B();	//객체 새로 생성
}
public class A {
    private B b;	//어디선가 받아온 객체를 b에 할당
}

 

DI(Dependency Injection)

  • 어떤 클래스가 다른 클래스에 의존한다는 뜻
public class A {
    //A에서 B를 주입받음
    @Autowired
    B b;
}
  • Autowired: 스프링 컨테이너에 있는 빈을 주입하는 역할 -> 빈: 스프링 컨테이너에서 관리하는 객체
  • 클래스 A에서 객체 B를 쓰고 싶은 경우, 직접 생성하는 것이 아니라 스프링 컨테이너에서 객체를 주입 받아 사용 -> IoC/DI 개념

 

빈과 스프링 컨테이너

스프링 컨테이너

  • 스프링 컨테이너는 빈을 생성하고 관리한다.
  • 빈이 생성되고 소멸되기까지의 생명주기를 해당 스프링 컨테이너가 관리

  • 빈은 스프링 컨테이너가 생성하고 관리하는 객체 -> 스프링에서 제공해주는 객체!
  • 앞 코드의 B -> 빈
  • 빈을 스프링 컨테이너에 등록하기 위해 XML 파일 설정, annotation 추가 등 여러 가지 방법 제공
@Component	//클래스 MyBean을 빈으로 등록
public class MyBean {
}

 

 

관점 지향 프로그래밍

  • AOP(Aspect Oriented Programming): 관점 지향 프로그래밍. 프로그래밍에 대한 관심을 핵심 관점, 부가 관점으로 나누어 관심 기준으로 모듈화하는 것을 의미
  • 계좌 이체 -> 핵심 관점: 계좌 이체, 부가 관점: 로깅, DB 연결
  • 고객 관리 -> 핵심: 고객 관리, 부가 관점: 로깅, DB 연결

 

이식 가능한 서비스 추상화

  • PSA(Portable Service Abstraction): 스프링에서 제공하는 다양한 기술들을 추상화해 개발자가 쉽게 사용하는 인터페이스

 

REST API

  • Representational State Transfer API: 자원을 이름으로 구분해 자원의 상태를 주고받는 API 방식
  • URL의 설계 방식을 의미함

REST API의 특징

  • 서버/클라이언트 구조
  • 무상태(Stateless): HTTP 프로토콜은 Stateless Protocol이므로 REST 역시 무상태성을 갖는다. Client의 context를 server에 저장하지 않는다.
  • 캐시 처리 기능(Cacheable): HTTP의 특징인 캐싱 기능을 적용할 수 있다. 캐시 사용을 통해 응답 시간 빨라진다.
  • 계층화(Layered System): Client는 REST API Server만 호출하고, REST Server는 다중 계층으로 구성될 수 있다.
  • 인터페이스 일관성(Uniform Interface): URL로 지정한 Resource에 대한 조작을 통일되고 한정적인 인터페이스로 수행한다.

REST API 장점과 단점

  • URL마 보고 무슨 행동을 하는 API인지 명확하게 파악 가능
  • Stateless -> 클라이언트와 서버 역할이 명확히 분리
  • HTTP 표준을 사용하는 모든 플랫폼에서 사용 가능
  • HTTP 메서드 방식의 개수에 제한이 있다.
  • 설계를 위해 공식적으로 제공되는 표준 규약이 없다.

 

REST API를 사용하는 방법

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

  • 자원은 가져오는 데이터를 말한다. 
URL 설계 ex)
/students/1
/get-student?student_id=1

-> id가 1인 학생의 정보
-> 2번의 경우 동사를 사용해서 추후 개발 시에 혼란 유발 가능

 

규칙2. 동사는 HTTP 메서드로

  • HTTP 메서드: 서버에 요청을 하는 방법을 나눈 것
  • 주로 사용하는 HTTP 메서드: POST, GET, PUT, DELETE -> 각각 만들고, 읽고, 업데이트하고, 삭제하는 역할 담당
  • 보통 이것들을 묶어 CRUD라 부른다.
id가 1인 블로그 글을 조회하는 API -> GET/articles/1
블로그 글을 추가하는 API -> POST/articles
블로그 글을 수정하는 API -> PUT/articles/1
블로그 글을 삭제하는 API -> DELETE/articles/1

 

 

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

  • 프레젠테이션 계층: controller
  • 비즈니스 계층: service
  • 퍼시스턴스 계층: repository
  • 데이터베이스와 연결되는 DAO: domain
프레젠테이션, 비즈니스, 퍼시스턴스 계층
- 프레젠테이션 계층: HTTP 요청을 받고, 이 요청을 비즈니스 계층으로 전송하는 역할. -> Controller
비즈니스 계층: 모든 비즈니스 로직(서비스를 만들기 위한 로직)을 처리한다. -> Service
- 퍼시스턴스 계층: 모든 데이터베이스 관련 로직을 처리한다. 이 과정에서 데이터베이스에 접근하는 DAO 객체(데이터베이스 계층과 상호작용하기 위한 객체)를 사용할 수도 있다. ->  Repository

 

엔티티 구성하기

컬럼명 자료형 null 허용 설명
id BIGINT N 기본키 일련번호. 기본키
title VARCHAR(255) N   게시물의 제목
content VARCHAR(255) N   내용

 

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

 

롬복을 사용해 코드 개편

  • @Getter: 게터 메소드들 대치
  • @NoArgsConstructor: 기본 생성자 대치

Article.java

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity //엔티티로 지정
@Getter //getId(), getTitle()같은 게터 메스드들을 대치
@NoArgsConstructor(access = AccessLevel.PROTECTED)  //기본 생성자 대치
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;
    }
}

 

Repository 만들기

ORM(Object-relational Mapping): 자바의 객체와 데이터베이스를 연결하는 프로그래밍 기법. 객체와 데이터베이스를 연결해 자바 언어로만 데이터베이스를 다룰 수 있게 하는 도구. -> 데이터베이스 시스템에 대한 종속성이 줄어든다.

JPA(Java Persistence API): 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스 -> 인터페이스이므로, ORM 프레임워크를 추가로 선택해야사용할 수 있다. -> 대표적으로 하이버네이트를 많이 사용한다. 내부적으로는 JDBC API를 사용

하이버네이트(Hibernate): JPA 인터페이스를 구현한 구현체이자 자바용 ORM 프레임워크.

엔티티(entity): 데이터베이스의 테이블과 매핑되는 객체. 본질적으로는 자바의 객체이나, 데이터베이스의 테이블과 직접 연결된다는 특징이 있어 구분지어 부른다.

엔티티 매니저(entity manager): 앤티티를 관리해 데이터베이스와 애플리케이션 사이에서 객체를 생성, 수정, 삭제하는 등의 역할을 한다. -> 엔티티 매니저 팩토리: 엔티티 매니저를 만드는 곳 -> 여러 사용자가 데이터베이스에 접근할 때 각각에 엔티티 매니저를 생성해준다. 
=> 스프링 부트 내부에서는 매니저 팩토리를 하나만 생성해서 관리하고 @Persistence Context 또는 @Autowired 애너테이션을 사용해서 엔티티 매니저를 사용한다.
-> 동시성 문제가 발생할 수 있으니, 실제로는 엔티티 매니저와 연결하는 프록시(가짜) 엔티티 매니저를 사용 -> 필요할 때 데이터베이스 트랜잭션과 관련된 실제 엔티티 매니저 호출

영속성 컨텍스트: 엔티티를 관리하는 가상의 공간. 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩이라는 특징
- 1차 캐시: 영속성 컨텍스트는 내부에 1차 캐시를 가지는데, 이때 캐시의 키는 엔티티의 @Id 애너테이션이 달린 기본키 역할을 하는 index이며 값은 엔티티 -> 1차 캐시에서 데이터 조회하고 반환 -> 값이 없으면 데이터베이스에서 조회하고 1차 캐시에 저장한 다음 반환
- 쓰기 지연(transaction write-behind): 트랜잭션 커밋 전에는 데이터베이스에 실제로 질의문을 보내지 않고 쿼리를 모았다가, 트랜잭션을 커밋하면 모았던 쿼리를 한 번에 실행하는 것을 의미
- 변경 감지: 트랜잭션을 커밋하면 1차 캐시에 저장된 엔티티 값과 현재 엔티티 갑승ㄹ 비교해서 변경을 감지해, 변경된 값을 데이터베이스에 자동으로 반영
- 지연 로딩(lazy loading): 쿼리로 요청한 데이터를 애플리케이션에 바로 로딩하는 것이 아니라 필요할 때 쿼리를 날려 데이터를 조회하는 것

엔티티의 상태
- 분리(detached) 상태: 영속성 컨텍스트가 관리하고 있지 않는
- 관리(managed) 상태: 영속성 컨텍스트가 관리하는 상태
- 비영속(transient) 상태: 영속성 컨텍스트와 전혀 관계가 없는 상태
- 삭제된(removed) 상태

스프링 데이터 JPA: 스프링 데이터의 공통적인 기능에서 JPA의 유용한 기술이 추가된 기술. 스프링 데이터의 인터페이스인 PagingAndSortingRepository를 상속받아 Jpa 인터페이스를 만들었으며, JPA를 더 편리하게 사용하는 메더스 제공

 

BlogRepository.java

package me.shinsunyoung.springbootdeveloper.repository;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BlogRepository extends JpaRepository<Article, Long> {

}

 

 

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

 

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

  • 서비스 계층에서 요청을 받을 객체 AddArticleRequet 객체를 생성 -> BlogService 클래스를 생성 -> 블로그 글 추가 메서드인 save() 구현

1단계

  • dto 패키지 생성 -> dto 패키지를 컨트롤러에서 요청한 본문을 받을 객체인 AddArticleRequest.java 파일 생성
  • DTO(data transfer object): 계층끼리 데이터를 교환하기 위해 사용하는 객체

AddArticleRequest.java

package me.shinsunyoung.springbootdeveloper.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor //모든 필드 값을 파라미터로 받는 생성자
@Getter
public class AddArticleRequest {

    private String title;
    private String content;

    public Article toEntity() { //생성자를 사용해 객체 생성
        return Article.builder()
                .title(title)
                .content(content)
                .build();
    }
}

-> 이 메서드는 추후에 블로그 글을 추가할 때 저장할 엔티티로 변환하는 용도

 

2단계

  • service 패키지 생성 -> BlogService.java 클래스 구현

BlogService.java

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.repository.BlogRepository;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor    //final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service    //빈으로 등록
public class BlogService {

    private final BlogRepository blogRepository;

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

-> @RequiredArgsConstructor: 빈을 생성자로 생성하는 애너테이션(롬복 지원) -> final 키워드나 @NotNull이 붙은 필드로 생성자 추가

-> @Service: 해당 클래스를 빈으로 서블릿 컨테이너에 등록

-> Save(): JpaRepository에서 지원하는 저장 메서드. AddArticleRequest 클래스에 저장된 값들을 article 데이터베이스에 저장

BlogRepository는 JpaRepository를 상속받는데, JpaRepository의 부모 클래스인 CrudRepository에 save() 메서드가 선언되어 있다. -> 해당 메서드로 데이터베이스에 Article 엔티티 저장 가능!

 

컨트롤러 메서드 코드 작성

  • URL에 매핑하기 위한 컨트롤러 메서드 추가
  • 컨트롤러 메서드 -> URL 매핑 애너테이션 @GetMapping, @PostMapping, @PutMapping, @DeleteMapping 사용 가능
  • /api/articles에 POST 요청 -> @PostMapping을 이용해 요청을 매핑 -> 블로그 글을 생성하는 BlogService의 save() 메서드 호출 -> 생성된 블로그 글을 반환하는 작업을 할 addArticle()메서드 작성
package me.shinsunyoung.springbootdeveloper.controller;

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.service.BlogService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@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 saveArticle = 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 Request: 요청 값이 잘못되어 요청에 실패했음
403 Forbidden: 권한이 없어 요청에 실패했음
404 Not Found: 요청 값으로 찾은 리소스가 없어 요청에 실패했음
500 Internal Server Error: 서버 상에 문제가 있어 요청에 실패했음

 

API 실행 테스트

  • 실제 데이터를 확인하기 위해 H2 콘솔을 활성화해야 한다.
  • 속성 파일(application.yml) 수정

1단계

application.yml

spring:
  jpa:
    #전송 쿼리 확인
    show-sql: true
    properties:
      hibernate:
        format_sql:true
  datasource:
    url: jdbc:h2:mem:testdb

  h2:
    console:
      enabled: true

# 테이블 생성 후에 data.sql 실행
    defer-datasource-initialization: true

 

 

2단계

  • 스프링 부트 서버 실행 -> 포스트맨 실행
  • localhost:8080/api/articles
  • [Body]: [raw -> JSON]으로 변경

-> 요청 전송 및 응답

 

3단계

  • localhost:8080/h2-console에서 H2 콘솔 로그인 

4단계

  • SQL statement로 데이터베이스 확인

 

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

BlogApiControllerTest.java

package me.shinsunyoung.springbootdeveloper.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import me.shinsunyoung.springbootdeveloper.repository.BlogRepository;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.junit.jupiter.api.Assertions.*;

@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)할 때 사용

 

자바 직렬화, 역직렬화

Article.java의 객체
title: "제목"
content: "내용"

<->

{
    "title": "제목"
    "content": "내용"
}

 

Given-When-Then 패턴

  • Given: 블로그 글 추가에 필요한 요청 객체 생성
  • When: 블로그 글 추가 API에 요청을 전송. 이때 요청 타입은 JSON, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 전송
  • Then: 응답 코드가 201 created인지 확인. Blog를 전체 조회해 크기가 1인지 확인 -> 실제로 저장된 데이터와 요청 값을 비교
package me.shinsunyoung.springbootdeveloper.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.repository.BlogRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import java.util.List;

import static org.assertj.core.api.FactoryBasedNavigableListAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

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

 

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

  • 클라이언트는 데이터베이스에 직접 접근할 수 없다. 그러니 이 역시도 API를 구현해볼 수 있도록 해야 한다. -> 모든 글을 조회하는 API, 글 내용을 조회하는 API를 순서대로 구현

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

  • 저장되어 있는 글을 모두 가져오는 findAll() 메서드 추가

BlogService.java

package me.shinsunyoung.springbootdeveloper.service;

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.repository.BlogRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@RequiredArgsConstructor    //final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service    //빈으로 등록
public class BlogService {

    private final BlogRepository blogRepository;

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

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

-> JPA 지원 메서드인 findAll()을 호출해 article 테이블에 저장되어 있는 모든 데이터를 조회

 

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

  • /api/articles GET 요청이 오면 글 목록을 조회할 findAllArticles() 메서드를 작성 -> 전체 글 목록을 조회하고 응답하는 역할

1단계

  • 응답을 위한 DTO 작성
  • dto 디렉터리에 ArticleResponse.java 파일 생성

ArticleResponse.java

package me.shinsunyoung.springbootdeveloper.dto;

import lombok.Getter;
import me.shinsunyoung.springbootdeveloper.domain.Article;

@Getter
public class ArticleResponse {

    private final String title;
    private final String content;

    public ArticleResponse(Article article) {
        this.title = article.getTitle();
        this.content = article.getContent();
    }
}

-> 글을 제목과 내용 구성이므로 해당 필드를 가지는 클래스를 생성 -> 엔티티를 인수로 받는 생성자 추가

 

2단계

  • controller 디렉토리의 BlogApiController.java 파일 -> 전체 글을 조회한 뒤 반환하는 findAllArticles() 메서드 추가

BlogApiController.java

package me.shinsunyoung.springbootdeveloper.controller;

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.dto.ArticleResponse;
import me.shinsunyoung.springbootdeveloper.service.BlogService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

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

-> /api/articles GET 요청 -> 글 전체를 조회하는 findAll() 메서드를 호출 -> 응답용 객체인 ArticleResponse로 파싱해 body에 담아 클라이언트에게 전송

 

6.5. 블로그 글 조회를 위한 API 구현하기

  • 블로그 글 하나를 조회하는 API 구현

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

BlogService.java

package me.shinsunyoung.springbootdeveloper.service;

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.repository.BlogRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@RequiredArgsConstructor    //final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@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));
    }
}

-> 데이터베이스에 저장되어 있는 글의 ID를 이용해 글을 조회

-> 여기서 구현한 findById() 메서드는 JPA에서 제공하는 findById() 메서드를 사용해 ID를 받아 엔티티를 조회 -> 없으면 IllegalArgumentException 예외 발생

 

컨트롤러 메서드 코드 작성

BlogApiController.java

package me.shinsunyoung.springbootdeveloper.controller;

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.dto.ArticleResponse;
import me.shinsunyoung.springbootdeveloper.service.BlogService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@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> findArticle(@PathVariable long id) {
        Article article = blogService.findById(id);
        
        return ResponseEntity.ok()
                .body(new ArticleResponse(article));
    }
}

-> @PathVariable Annotation은 URL에서 값을 가져오는 Annotation이다.

-> id에 해당하는 글의 정보를 body에 담아 웹 브라우저로 전송

 

6.6. 블로그 글 삭제 API 구현하기

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

  • BlogService.java 파일에 delete() 메서드 추가
  • 블로그 글의 ID를 받은 뒤 JPA에서 제공하는 deleteById() 메서드를 이용해 데이터베이스에서 데이터를 삭제

BlogService.java

package me.shinsunyoung.springbootdeveloper.service;

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.repository.BlogRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@RequiredArgsConstructor    //final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service    //빈으로 등록
public class BlogService {

	...

    public void delete(long id) {
        blogRepository.deleteById(id);
    }
}

 

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

BlogApiController.java

package me.shinsunyoung.springbootdeveloper.controller;

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.dto.ArticleResponse;
import me.shinsunyoung.springbootdeveloper.service.BlogService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RestController //HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {

    private final BlogService blogService;

	...

    @DeleteMapping("/api/articles/{id}")
    public ResponseEntity<Void> deleteArticle(@PathVariable long id) {
        blogService.delete(id);

        return ResponseEntity.ok()
                .build();
    }
}

-> /api/articles/{id} DELETE 요청이 들어오면 {id}에 해당하는 값이 @PathVariable annotation을 통해 들어옴

 

6.7. 블로그 글 수정 API 구현하기

서비스 메서드 코드 작성

1단계

  • Article에 update 메서드 작성

Article.java

package me.shinsunyoung.springbootdeveloper.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity //엔티티로 지정
@Getter //getId(), getTitle()같은 게터 메스드들을 대치
@NoArgsConstructor(access = AccessLevel.PROTECTED)  //기본 생성자 대치
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;
    }
}

 

2단계

  • 블로그 글 수정 요청을 받을 DTO

UpdateArticleRequest.java

package me.shinsunyoung.springbootdeveloper.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

-> 글에서 수정해야 하는 내용은 제목과 내용이므로, 그에 맞게 제목과 내용 필드로 구성

 

3단계

BlogService.java

package me.shinsunyoung.springbootdeveloper.service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.dto.UpdateArticleRequest;
import me.shinsunyoung.springbootdeveloper.repository.BlogRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@RequiredArgsConstructor    //final이 붙거나 @NotNull이 붙은 필드의 생성자 추가
@Service    //빈으로 등록
public class BlogService {

    private final BlogRepository blogRepository;

	...

    @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: 매칭한 메서드를 하나의 트랜잭션으로 묶는 역할

(A 계좌에서 출금하고 B 계좌에 입금을 실패하면, 고객 입장에서는 출금은 됐는데 입금이 안 된 심각한 상황 연출 -> 이 두 과정을 하나로 묶은 것이 트랜잭션)

 

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

BlogApiController.java

package me.shinsunyoung.springbootdeveloper.controller;

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.Article;
import me.shinsunyoung.springbootdeveloper.dto.AddArticleRequest;
import me.shinsunyoung.springbootdeveloper.dto.ArticleResponse;
import me.shinsunyoung.springbootdeveloper.dto.UpdateArticleRequest;
import me.shinsunyoung.springbootdeveloper.service.BlogService;
import org.apache.coyote.Response;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RestController //HTTP Response Body에 객체 데이터를 JSON 형식으로 반환하는 컨트롤러
public class BlogApiController {

    private final BlogService blogService;

	...

    @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에 담아 전송

 

 


참고자료

 

REST API의 정의와 특징

📒 API의 정의 Application Programming Interface 로 애플리케이션 소프트웨어를 구축하고 통합하는 정의 및 프로토콜의 세트이다. API 사용 시 장점 :: 데이터, 애플리케이션 및 기기를 연결하여 기술 전

velog.io