일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- LG
- deep learning
- 오블완
- 머신러닝
- LLM
- 회귀
- AI
- Machine Learning
- 티스토리챌린지
- ChatGPT
- OpenAI
- LG Aimers 4th
- PCA
- regression
- Classification
- 지도학습
- 해커톤
- supervised learning
- LG Aimers
- GPT-4
- 딥러닝
- gpt
- 분류
- Today
- Total
SYDev
[스프링 부트 3 백엔드 개발자 되기] Chapter 6. 블로그 기획하고 API 만들기 본문
자바 애너테이션
- 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에 담아 전송
참고자료
- 스프링 부트 3 백엔드 개발자 되기, 신선영, 골든래빗, 2023.05.12
- https://velog.io/@nias0327/REST-API%EC%9D%98-%EC%A0%95%EC%9D%98%EC%99%80-%ED%8A%B9%EC%A7%95
'대딩코딩 > 웹개발 스터디' 카테고리의 다른 글
JWT의 개념 및 활용 (0) | 2024.05.18 |
---|---|
[스프링 부트 3 백엔드 개발자 되기] Chapter 7. 블로그 화면 구성하기 (2) | 2024.05.01 |
[Express] 19. React Nodejs Start (1) | 2023.12.05 |
[Express] 18. Delete Comment Fetch (1) | 2023.12.04 |
[Express] 17. Display Comment Fetch (0) | 2023.12.04 |