1. 블로그 기획하고 API 만들기
[되기] 스프링 부트 3 백엔드 개발자 되기(자바 편) - 골든래빗
자바 백엔드 개발자가 되고 싶다면 자바 그다음에 꼭 보세요! 입문자에게 백엔드 개발의 필수 지식을 학습 로드맵 중심으로 설명합니다. 스프링 부트 3 개발에 꼭 필요한 JPA ORM, OAuth2 인증, AWS 배
goldenrabbit.co.kr
이번 시간에는 블로그에서 글작성,수정,삭제,조회 등을 해보겠다.
1. 프로젝트 설정하기
프로젝트 설정이 처음 이신 분들은 다음 링크를 참고해주세요!
https://changuk0308.tistory.com/18
[SpringBoot-JPA 활용편1] 자바 ORM 표준 JPA 프로그래밍 - 활용편1: 1. 프로젝트 환경설정
강의 출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 강의 - 인프런 실무에 가까운 예제로, 스프링 부트
changuk0308.tistory.com
스프링 스타터 방문
Application.yml
spring: #띄어쓰기 없음
datasource: #띄어쓰기 2칸
url: jdbc:h2:tcp://localhost/~/blog1 #4칸
username: sa
password: 123
driver-class-name: org.h2.Driver
jpa: #띄어쓰기 2칸
hibernate: #띄어쓰기 4칸
ddl-auto: create #띄어쓰기 6칸 !!!none or create
properties: #띄어쓰기 4칸
hibernate: #띄어쓰기 6칸
#전송 쿼리확인
show_sql: true #띄어쓰기 8칸
format_sql: true #띄어쓰기 8칸
# 테이블 생성 후 에 data.sql 실행
defer-datasource-initialization: true #-> 추가
# open-in-view: false # OSIV OFF
sql:
init:
mode: always #-> 추가
jwt:
issuer: changuk0308@gmail.com
secret_key: study-springboot
logging.level: #띄어쓰기 없음
org.hibernate.SQL: debug #띄어쓰기 2칸
org.hibernate.orm.jdbc.bind: trace #파라미터에 머가 들어갈지 로그에 찍어줌
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'jpabook'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-devtools' //이것을 통해 코드 수정시 컴파일만하면 변경 업데이트 해줌
//쿼리 파라미터를 로그로 남기는 외부 라이브러리
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
// Hibernate5JakartaModule 등록
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//JUnit4 추가
testImplementation("org.junit.vintage:junit-vintage-engine") {
exclude group: "org.hamcrest", module: "hamcrest-core"
}
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
//스프링 시큐리티를 사용하기 위한 스타터 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
//타임리프 에서 스프링 시큐리티를 사용하기 위한 의존성추가
implementation 'org.thymeleaf.extras::thymeleaf-extras-springsecurity6'
//스프링 시큐리티를 테스트 하기 위한 의존성 추가
testImplementation 'org.springframework.security:spring-security-test'
//JWT라이브러리
implementation 'io.jsonwebtoken:jjwt:0.9.1' //자바 JWT 라이브러리
implementation 'javax.xml.bind:jaxb-api:2.3.1' //XML 문서와 Java 객체 간 매핑 자동화
}
//junit
tasks.named('test') {
useJUnitPlatform()
}
//querydsl 추가
def querydslDir = 'src/main/generated'
clean {
delete file('src/main/generated')
}
tasks.withType(JavaCompile){
options.generatedSourceOutputDirectory=file(querydslDir)
}
h2 데이터베이스 설정
디렉토리구조
2. Article 엔티티 구성하기
Article
을 domain
패키지에 추가
package blogExample.blog.domain;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.cglib.core.Local;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import java.time.LocalDateTime;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@EntityListeners(AuditingEntityListener.class)
@Entity //빈으로 등록
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)//기본생성자를 protected로 생성하는 롬복
public class Article {
//기본키
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id",updatable = false)
private Long id;
@Column(name="title",nullable = false)
private String title;
@Column(name="content",nullable = false)
private String content;
@CreatedDate //엔티티가 생성될 때 생성 시간 저장
@Column(name = "created_at")
private LocalDateTime createdAt;
@LastModifiedDate //엔티티가 수정될 때 수정 시간 저장
@Column(name = "updated_at")
private LocalDateTime updatedAt;
//setter대신 생성자로 값 설정
@Builder //★★Builder 패턴으로 생성자 객체 정의★★★
public Article(String title,String content)
{
this.title=title;
this.content=content;
}
// //기본 생성자 (롬복으로 설정
// JPA에서는 기본 생성자를 통해 접근하기에
// protected Article(){
//
// }
}
JPA에서는 기본 생성자를 이용하여 프록시 객체를 생성한다. 이전까지는 기본생성자를 안만들고 setter로만 값을 세팅했다.
만약 클래스에 아무 생성자도 정의하지 않으면, 자바는 기본적으로 묵시적으로 기본 생성자를 제공_한다. 이 기본 생성자는 public 또는 protected 접근 제한을 가질 수 있다.
JPA 스펙에서는 엔티티가 protected 접근 제한을 가진 기본 생성자를 가져야 한다고 명시하고 있어서, JPA 구현체들이 프록시 생성 같은 기능을 사용할 때 이를 활용할 수 있게 한다.
만약 위와 같이 생성자가 정의 되면 JPA 사용을 위해 기본 생성자를 만들어야한다.
Builder 패턴 적용 예:
기본적으로 객체를 생성할 때 사용하는 방법과 비교해서, Builder 패턴은 객체의 생성 과정을 명확하게 해줘. 예를 들어, Article 객체에 대해 설명할 때: *
- 기존 생성자 방식:이 방식은 간단하지만, 매개변수가 많아지면 어떤 값이 어떤 필드에 해당하는지 파악하기 어려워져.파라미터 삽입시 위치에 상관없이 주입가능(메서드가 아니라 객체에서만 사용가능)
new Article("abc","def"); builder 패턴: Article.builder() .title("abc") .content("def") .build();
Builder 패턴 적용 예:
기본적으로 객체를 생성할 때 사용하는 방법과 비교해서, Builder 패턴은 객체의 생성 과정을 명확하게 해준다. 예를 들어, Article 객체에 대해 설명할 때:
- 기존 생성자 방식:이 방식은 간단하지만, 매개변수가 많아지면 어떤 값이 어떤 필드에 해당하는지 파악하기 어려워진다.
new Article("abc","def");
- Builder 패턴 방식:Builder 패턴을 사용하면, 각 매개변수의 의미를 명확하게 알 수 있고, 필요한 매개변수만 선택적으로 설정할 수 있다. 또한, 객체의 필드 값들을 메서드 체인 방식으로 순서에 상관없이 설정할 수 있어서 유연성이 높아진다.
//builder 패턴:
Article.builder()
.title("abc")
.content("def")
.build();
Builder 패턴은 특히 매개변수가 많거나, 객체 생성 과정이 복잡할 때 코드의 가독성과 유지보수성을 크게 향상시켜주는 장점이 있다. 따라서 복잡한 객체를 생성해야 하는 상황에서 이 패턴의 사용을 고려해보는 것이 좋다.
(앞으로 Builder 패턴으로 프로젝트 진행 할 예정이니 완전히 이해해주시길 바랍니다.)
3. Article 리포지토리
BlogRepository
를 repository에 생성
package blogExample.blog.repository;
import blogExample.blog.domain.Article;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BlogRepository extends JpaRepository<Article,Long> {
}
4. 블로그 글 작성(save) API 만들기
1. BlogService 만들기
BlogService
@RequiredArgsConstructor
@Service //빈으로 등록
@Transactional(readOnly = true)
public class BlogService {
private final BlogRepository blogRepository;
//비즈니스 로직:
// 1. 블로그 글 추가하기
// 2. 블로그 글 목록 조회하기
// 3. 블로그 글 단건 조회하기
// 4. 블로그 글 삭제하기
// 5. 블로그 글 수정하기
//1. 블로그 글 추가 메서드
@Transactional
public Article save(AddArticleRequestDto request)
{
return blogRepository.save(request.toEntity());
}
}
2. 요청 DTO, 응답 DTO만들기
AddArticleRequestDto
을 Dto
패키지에 추가
package blogExample.blog.Dto;
import blogExample.blog.domain.Article;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor //기본 생성자
@AllArgsConstructor //모든 필드를 생성자로 받음.(+a, Required는 final 지시어가 붙어야함.)
@Getter
public class AddArticleRequestDto {
private String title;
private String content;
//콘트롤러에서 요청한 Body를 받을 객체 생성
//DTO를 엔티티로 변환함.(DTO는 단순히 데이터 전달 객체임)
public Article toEntity()
{
//생성자로 엔티티 세팅
return Article.builder()
.title(title)
.content(content)
.build();
}
}
Jpa 활용편을 보면 등록이나 수정 요청 (POST,PUT,PATCH) 수행시 콘트롤러 단에서는 엔티티
를 파라미터
로 입력 받으면 안된다고 했다. 이는 API 요청 스펙
에 맞추어 별도의 DTO
를 파라미터로 받아야 하기 때문이고, 엔티티가 바뀌면 API 스펙 전체가 바뀌어 버리기에 DTO로 받아야한다.
그러면 콘트롤러에서 등록 요청시 DTO를 받으면 리포지토리에 저장하기 위해선 엔티티로 변경해야한다.
(+a, 조회는 반대로 진행한다.왜냐하면 리포지토리에서 읽은 값은 엔티티이기 때문이다.)
지금까지는 콘트롤러 단에서 Dto를 엔티티에 setting하면서 변경했다. 다음은 회원 등록 API 예이다.
@PostMapping("/api/v2/members")
public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) {
Member member = new Member();
member.setName(request.getName());
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
그러면 위의 Dto는 콘트럴로 단에서 엔티티를 안바꾸고 Dto단에서 바꿔준다.
AddArticleResponseDto
를 Dto 패키지에 추가한다.
@Getter
public class AddArticleResponseDto {
private final Long id;
private final String title;
private final String content;
public AddArticleResponseDto(Article article)
{
this.id=article.getId();
this.title=article.getTitle();
this.content=article.getContent();
}
}
3. Controller 작성하기
BlogApiController
@RequiredArgsConstructor
@RestController //HTTP response Bodyd에 객체 데이터를 JSON으로 변환하는 콘트롤러
public class BlogApiController {
private final BlogService blogService;
// DTO로 받은 후 엔티티로 변환 후 저장하기
//1. 글 작성하기
@PostMapping("/api/articles")
public ResponseEntity<AddArticleResponseDto> addArticle(
@RequestBody AddArticleRequestDto addArticleRequestDto
)
{
//요청에 응답하여 블로그 글 생성
Article saveArticle=blogService.save(addArticleRequestDto);
//요청된 자원이 성공적으로 생성 되었으며
// 저장된 블로그 글 정보를 응답 객체에 담아 전송
return ResponseEntity.status(HttpStatus.CREATED)
.body(new AddArticleResponseDto(saveArticle));
/** http 상태 응답 코드
* OK(200): 요청이 성공적으로 수행됨
* CREATED(201) : 요청이 성공적으로 수행되었고, 새로운 리소스가 생성되었음
*/
}
}
/api//articles
에 POST 요청이 오면 @PostMapping 을 이용해 요청을 매칭한 뒤, 블로그 글을 생성하는 BlogService
의 save()
메서드를 호출한 뒤, 생성된 블로그 글을 반환하는 작업을 할 `addArticle() 메소드`를 작성하였다.
@RestController
애너테이션을 클래스에 붙이면 HTTP 응답
으로 객체 데이터를 JSON 형식
으로 반환한다.
@RequestBody
애노테이션은HTTP를 요청
할 때 응답에 해당하는 값
을 @RequestBody 애노테이션
이 붙은 대상 객체인 AddArticleRequest
에 매핑
한다.
ResponseEntity.status().body()
는 응답코드로 201, 즉, Created를 응답하고 테이블에 저장된 객체를 반환한다.
결과 확인
BlogApiControllerTest
테스트 코드 작성
@SpringBootTest //스프링 부트 테스트
@AutoConfigureMockMvc //MockMvc 생성
public class BlogApiControllerTest {
/**
* 테스트 초기 설정
*/
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper; //직렬화,역지렬화를 위한 클래스
@Autowired
private WebApplicationContext context;
@Autowired
BlogRepository blogRepository;
@BeforeEach //테스트 실행전 실행하는 메서드
public void mockMvcSetUp()
{
//mockMvc 설정
this.mockMvc= MockMvcBuilders.webAppContextSetup(context)
.build();
//리포지토리 초기화
blogRepository.deleteAll();
}
/**
* 테스트 로직 시작
*/
//1. 글 작성하는 API 테스트
@DisplayName("addArticle: 블로그 글 추가에 성공한다.")
@Test
public void addArticle() throws Exception{
//given: 블로그 글 추가에 필요한 요청 객체를 만든다.
String url="/api/articles";
String title="title";
String content="content";
AddArticleRequestDto userRequest=new AddArticleRequestDto(title,content);
//객체 JSON으로 직렬화 (객체를 JSON으로 변환)
//writeValueAsString: 객체를 JSON으로 직렬화
String requestBody=objectMapper.writeValueAsString(userRequest);
//when: 블로그 글 추가 API 요청을 보낸다. 이때 요청 타입은 JSON이면,given절에서 미리 만들어둔
// 객체를 요청 본문으로 함께 보낸다.
// (Mockmvc(HTTP메서드,URL,요청 타입, 요청 타입 설정)를 활용하여
// url을 post로 JSON으로 요청 전송.)
ResultActions result=mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(requestBody));
//then
//응답 코드가 Created인지 확인
result.andExpect(status().isCreated());
List<Article> articles=blogRepository.findAll();
//검증 (저장된것이 1개인가, 제목이 같은가, 내용이 같은가)
assertThat(articles.size()).isEqualTo(1); //크기가 1인지 검증
assertThat(articles.get(0).getTitle()).isEqualTo(title);
assertThat(articles.get(0).getContent()).isEqualTo(content);
}
}
여기서 주의할점이 있다.
import org.junit.jupiter.api.Test;
ㄴ반드시 jupiter로 테스트 해야한다!! jupiter가 없으면 displayname이 실행이 안됩니다.
Given | 블로그 글 추가에 필요한 요청 객체를 만듭니다. |
---|---|
When | 블로그 글 추가 API 요청을 보냅니다. 이때 요청 타입은 JSON이며, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 보냅니다. |
Then | 응답 코드가 201 Created 인지 확인합니다. Blog 를 전체 조회해 크기가 1인지 확인하고, 실제 저장된 데이터와 요청된 값을 비교합니다. |
4. 직렬화 역직렬화(블로그 글 추가 테스트)
(추후 테스트 작성법에 대해서도 업로드 할 예정입니다.)
writeValueAsString() 메서드를 사용해서 객체를 JSON으로 직렬화해준다. 그 이후에는 MockMvc를 사용해 HTTP 메서드, URL, 요청 본문, 요청 타입 등을 설정한 뒤 설정한 내용을 바탕으로 테스트 요청을 보낸다. contentType() 메서드는 요청에 보낼 때 JSON.XML 등 다양한 타입 중 하나를 골라 요청을 보낸다. 여기서 JSON 타입의 요청을 보낸다고 명시했다. assertThat() 메서드로는 블로그 글의 개수가 1인지 확인한다.
다음은 자주 사용하는 메서드를 표로 정리 한 것이다.
- 코드설명
assertThat(article.size()).isEqualTo(); 블로그 글 크기가 1이어야 합니다. assertThat(article.size()).isGreaterThan(2); 블로그 글 크기가 2보다 커야 합니다. assertThat(article.title()).isLessThan(5); 블로그 글 크기가 5보다 작아야 합니다. assertThat(article.size()).isZero(); 블로그 글 크기가 0이어야 합니다. assertThat(article.size()).isEqualTo("제목"); 블로그 글의 title값이 "제목" 이어야 합니다. assertThat(article.size()).isNotEmpty(); 블로그 글의 title값이 비어 있지 않아야 합니다. assertThat(article.size()).contains("제"); 블로그 글의 title값이 "제"를 포함해야 합니다.
5. 블로그 글 목록 조회 API 구현하기
이번에는 작성한 글을 조회하는 것이다.
1. 모든 글을 조회하는 서비스 작성하기
BlogService
@RequiredArgsConstructor
@Service //빈으로 등록
@Transactional(readOnly = true)
public class BlogService {
private final BlogRepository blogRepository;
//비즈니스 로직:
// 1. 블로그 글 추가하기
// 2. 블로그 글 목록 조회하기
// 3. 블로그 글 단건 조회하기
// 4. 블로그 글 삭제하기
// 5. 블로그 글 수정하기
//1. 블로그 글 추가 메서드
@Transactional
public Article save(AddArticleRequestDto request)
{
return blogRepository.save(request.toEntity());
}
}
ArticleResponseDto
를 Dto 패키지에 추가한다
@Getter
public class ArticleResponseDto {
private final String title;
private final String content;
public ArticleResponseDto(Article article)
{
this.title=article.getTitle();
this.content=article.getContent();
}
}
BlogApiController
에 추가
//2. 블로그 글 목록 조회하기
@GetMapping("/api/articles")
public ResponseEntity<List<ArticleResponseDto>> findAllAriticles()
{
List<ArticleResponseDto> articlesCollect=blogService.findAll()
.stream()
.map(ArticleResponseDto::new)
.toList();
return ResponseEntity.ok()
.body(articlesCollect);
}
조회는 등록과 다르게 반대로 db에서 불러오는 것이다. 즉, db의 엔티티를 DTO로 변환해서 그 값을 조회하는 것이다.
위는 엔티티를 Dto로 변환하는 과정이다. 이때 콜렉션값이므로 stream으로 변경해서 Dto와 매핑하여 변경한다.
data.sql
작성
INSERT INTO article (title, content) VALUES ('제목 1', '내용 1')
INSERT INTO article (title, content) VALUES ('제목 2', '내용 2')
INSERT INTO article (title, content) VALUES ('제목 3', '내용 3')
data.sql
실행을 위해 다음과 같이 application.yml
파일 수정
테스트 하기
BlogApiControllerTest
에 글 목록 조회 API 테스트 로직 추가
//2. 글 목록 조회 API 테스트
@DisplayName("findAllArticles: 블로그 글 목록 조회에 성공한다.")
@Test
public void findAllArticles() throws Exception{
//given
//블로그 글을 저장
String url="/api/articles";
String title="title";
String content="content";
//블로그 글인 Article을 db에 저장.)
blogRepository.save(Article.builder()
.title(title)
.content(content)
.build());
//when
//목록 조회 API 호출
ResultActions resultActions= mockMvc.perform(get(url)
.accept(MediaType.APPLICATION_JSON));
//then
//응답이 Ok이고, 반환받은 값 중에 0번쩨 요소의 content와 title이 저장된 값과 같은지 확인.
resultActions
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].content").value(content))
.andExpect(jsonPath("$[0].title").value(title));
테스트 결과
6. 블로그 글 단건 조회 API 구현하기
BlogService
에 findById 메서드 추가
//3. 블로그 글 단건 조회하기
public Article findById(Long id)
{
// 만약 NULL값이면 에러처리
return blogRepository.findById(id)
.orElseThrow(()->new IllegalArgumentException("Not found: "+id));
// ()=""= 즉, Null 이면 에러처리
}
BlogController
findArticle 메서드 추가
//3. 블로그 글 단건 조회하기
@GetMapping("/api/articles/{id}")
public ResponseEntity<ArticleResponseDto> findArticle(
@PathVariable("id") Long id
)
{
Article article=blogService.findById(id);
return ResponseEntity.ok()
.body(new ArticleResponseDto(article));
}
단건 조회 테스트 코드
//3. 글 단건 조회 API 테스트
@DisplayName("findAllArticles: 블로그 글 조회에 성공한다.")
@Test
public void findArticle() throws Exception {
//given
String url="/api/articles/{id}";
String title="title";
String content="content";
// 블로그 글 저장
Article savedArticle=
blogRepository.save(Article.builder()
.title(title)
.content(content)
.build());
//when
//저장된 블로그 글의 id값으로 API 호출
ResultActions resultActions= mockMvc.perform(get(url,savedArticle.getId()));
//then
//응답이 Ok이고, 반환받은 값 중에 0번쩨 요소의 content와 title이 저장된 값과 같은지 확인.
resultActions
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").value(content))
.andExpect(jsonPath("$.title").value(title));
}
테스트 결과
7. 블로그 글 단건 삭제 API 구현하기
BlogService
에 delete 메서드 추가
//4. 블로그 글 삭제하기
@Transactional
public void delete(Long id)
{
blogRepository.deleteById(id);
}
BlogController
에 deleteArticle 메서드 추가
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable("id") Long id)
{
blogService.delete(id);
return ResponseEntity.ok()
.build();
}
여기서 id를 파라미터 값으로 입력했다. 따라서 @PathVariable
annotation을 사용하여 입력값을 파라미터로 받았다.
삭제 테스트 코드
//4. 블로그 글 삭제하기
@DisplayName("deleteArticle: 블로그 글 삭제에 성공한다.")
@Test
public void deleteArticle() throws Exception {
//given
String url="/api/articles/{id}";
String title="title";
String content="content";
//블로그 글을 저장
Article savedArticle=
blogRepository.save(Article.builder()
.title(title)
.content(content)
.build());
//when
//저장한 블로그글의 id값으로 삭제 API를 호출
mockMvc.perform(delete(url,savedArticle.getId()))
.andExpect(status().isOk());
//then
//응답이 Ok이고, 블로그 글 리스트를 전체 조회에 조회한 배열크기가 0인지 확인
List<Article> articles=blogRepository.findAll();
assertThat(articles).isEmpty();
}
포스트맨에서 실행및 삭제하며 목록 확인
테스트 코드 결과
8. 블로그 글 단건 수정 API 구현하기
1. Article
객체에서 값 재세팅을 위해 update
메서드 생성
/**
* 수정 메서드 작성(값을 재세팅 하기 위해서)
*/
public void update(String title,String content)
{
this.title=title;
this.content=content;
}
2. 수정을 받을 Dto 생성
@NoArgsConstructor //기본 생성자
@AllArgsConstructor //모든 필드를 생성자로 받음.(+a, Required는 final 지시어가 붙어야함.)
@Getter
public class UpdateArticleRequest {
private String title;
private String content;
}
3. BlogService
작성
//5.블로그 업데이트 하기
@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;
}
4. BlogController
작성
//5. 블로그 글 수정하기
@PatchMapping("/api/articles/{id}")
public ResponseEntity<AddArticleResponseDto> updateArticle(
@PathVariable("id") Long id,
@RequestBody UpdateArticleRequest request)
{
Article updateArticle = blogService.update(id, request);
return ResponseEntity.ok()
.body(new AddArticleResponseDto(updateArticle));
}
수정
은 데이터 JPA에서 메서드를 제공해주지는 않는다. 따라서 아이디를 찾고 해당 아이디에 대하 요청 Dto에 맞게 재세팅을 해줘야한다. 그리고 서비스가 dto의 값을 수정 한다.
콘트롤러는 수정한 값을 엔티티로 받고 그 엔티티를 다시 Dto로 형식으로 변경하여 반환한다.
(이때 수정은 그냥 등록과 같은 Dto로 사용했습니다. 원래는 id와 수정할 필드만 Dto로 선언해야합니다. 그러나 이번 시간에는 PATCH로 일부분을 수정했다 하더라도, 전체를 수정한 예이니 등록과 같은 Dto 형태로 반환했습니다. )
포스트맨에서 실행
빨간색으로 동그라미 친 부분은 직접 작성한 부분입니다.
직접 확인
테스트 코드
//5. 블로그 글 수정하기
@DisplayName("updateArticle: 블로그 글 수정에 성공한다.")
@Test
public void updateArticle() throws Exception {
//given
String url="/api/articles/{id}";
String title="title";
String content="content";
//블로그 글 저장
Article savedArticle=
blogRepository.save(Article.builder()
.title(title)
.content(content)
.build());
//블로그 글 수정에 필요한 요청 객체 생성
String newTitle="new Title";
String newContent="new Content";
//수정 요청 DTO 객체
UpdateArticleRequest request=new UpdateArticleRequest(newTitle,newContent);
//when
//UPDATE API로 수정요청을 보낸다. 이때 요청 타입 JSON이면
//given 절에서 미리 만들어둔 객체를 요청 본문으로 보냄
ResultActions resultActions=
mockMvc.perform(patch(url,savedArticle.getId())
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(request)));
//then
//응답코드가 Ok인지 확인
resultActions.andExpect(status().isOk());
//블로그글 id로 조회한 후에 값이 수정되었는지 확인
Article article=blogRepository.findById(savedArticle.getId()).get();
assertThat(article.getTitle()).isEqualTo(newTitle);
assertThat(article.getContent()).isEqualTo(newContent);
}
테스트 결과