@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를 더 편리하게 사용하는 메더스 제공
서비스 계층에서 요청을 받을 객체 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에 매핑한다.
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;
}
}