관리 메뉴

bright jazz music

blog10: 게시글 수정 1 본문

Projects/blog

blog10: 게시글 수정 1

bright jazz music 2023. 1. 9. 08:52

어차피 수정할 필드가 같으므로 Post.java를 사용할 수 있다. 그러나 다른 기능별로 나눠 주는 것이 좋다.

 

 

게시글을 수정하기 위해 사용할 클래스를 만들어준다.

 

//PostEdit.java

package com.endofma.blog.request;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;

@Getter
@Builder
public class PostEdit {
    @NotBlank(message = "타이틀을 입력하세요.")
    private String title;

    @NotBlank(message = "콘텐츠를 입력해 주세요.")
    private String content;

    @Builder
    public PostEdit(String title, String content){
        this.title = title;
        this.content = content;
    }
}

 

 

postService에 게시글 수정 로직을 넣는다.

기본적은 순서는 아래와 같다.

 

1. 게시글 아이디(Long id)를 파라미터로 사용하여 DB에서 게시글을 가져온다.

2. 게시글 값을 변경해 준다.

3. 값을 저장한다.

 

값을 변경해 줄 때, Post 클래스에 Setter를 만들어주고, setter를 사용해서 변경해 주어도 된다. 또는 개별 세터 대신 한 번에 값을 변경해 줄 수 있는 change()와 같은 메소드를 Post에 만들어 주어도 된다. 

 

 

이래는 change()를 사용하여 값을 변경하고 저장하는 코드가 적혀있다. 

PostRepository의 save()를 사용하였지만, 주석처리하고 @Transactional을 사용하였다.

//PostService.java

package com.endofma.blog.service;

import com.endofma.blog.domain.Post;
import com.endofma.blog.repository.PostRepository;
import com.endofma.blog.request.PostCreate;
import com.endofma.blog.request.PostEdit;
import com.endofma.blog.request.PostSearch;
import com.endofma.blog.response.PostResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Service
//@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;
    public PostService(PostRepository postRepository){
        this.postRepository = postRepository;
    }

    public void write(PostCreate postCreate) {
        //PostCreate 일반 클래스 ==> Post 엔티티
        Post post = Post.builder()
                .title(postCreate.getTitle())
                .content(postCreate.getContent())
                .build();

        postRepository.save(post);
    }

    //단건 조회
    public PostResponse get(Long id) {

        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글입니다.")); //있으면 post반환 없으면 에러 반환

        //응답 클래스를 분리
        return PostResponse.builder()
                .id(post.getId())
                .title(post.getTitle())
                .content(post.getContent())
                .build();

    }

//    public List<Post> getList() {
//        return postRepository.findAll();
//    }

//    public List<PostResponse> getList(Pageable pageable){
    public List<PostResponse> getList(PostSearch postSearch){

//        return postRepository.findAll(pageable).stream() //pageable
        return postRepository.getList(postSearch).stream() //QueryDsl사용
                .map(PostResponse::new)
                .collect(Collectors.toList());
    }

    //게시글 수정
    @Transactional //알아서 커밋
    public void edit(Long id, PostEdit postEdit){
        Post post = postRepository.findById(id)
                .orElseThrow(()-> new IllegalArgumentException("존재하지 않는 글입니다."));

//        post.setTitle(postEdit.getTitle());
//        post.setContent(postEdit.getContent());
        post.change(postEdit.getTitle(), postEdit.getContent());

//        postRepository.save(post); 사실상 적어주지 않아도 된다. 대신 @Transactional을 사용
    }

}

 

//PostServiceTest.java
//...

@SpringBootTest
class PostServiceTest {

    @Autowired
    private PostService postService;

    @Autowired
    private PostRepository postRepository;

    @BeforeEach
    void clean(){
        postRepository.deleteAll();
    }

  //...

    @Test
    @DisplayName("글 제목 수정")
    void test4(){
        //given
        Post post = Post.builder()
                .title("블로그 ")
                .content("제이드빌 ")
                .build();

        postRepository.save(post);

        PostEdit postEdit = PostEdit.builder().title("블로그 수정").build();

        //when
        postService.edit(post.getId(), postEdit);

        //then
        Post chengedPost = postRepository.findById(post.getId())
                .orElseThrow(() -> new RuntimeException("글이 존재하지 않습니다. id=" + post.getId()));

        Assertions.assertEquals("블로그 수정", chengedPost.getTitle());
    }

}

 

결과 성공

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.6)

2023-01-09 08:51:34.141  INFO 1300 --- [    Test worker] c.endofma.blog.service.PostServiceTest   : Starting PostServiceTest using Java 11.0.12 on DESKTOP-8H1PTVG with PID 1300 (started by markany-hjcha in D:\personal\blog)
2023-01-09 08:51:34.142  INFO 1300 --- [    Test worker] c.endofma.blog.service.PostServiceTest   : No active profile set, falling back to 1 default profile: "default"
2023-01-09 08:51:34.705  INFO 1300 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-01-09 08:51:34.756  INFO 1300 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 44 ms. Found 1 JPA repository interfaces.
2023-01-09 08:51:35.181  INFO 1300 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-01-09 08:51:35.379  INFO 1300 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-01-09 08:51:35.451  INFO 1300 --- [    Test worker] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-01-09 08:51:35.510  INFO 1300 --- [    Test worker] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.6.14.Final
2023-01-09 08:51:35.699  INFO 1300 --- [    Test worker] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2023-01-09 08:51:35.825  INFO 1300 --- [    Test worker] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2023-01-09 08:51:36.370  INFO 1300 --- [    Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-01-09 08:51:36.378  INFO 1300 --- [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-01-09 08:51:36.886  WARN 1300 --- [    Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-01-09 08:51:37.137  INFO 1300 --- [    Test worker] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:blog'
2023-01-09 08:51:37.423  INFO 1300 --- [    Test worker] c.endofma.blog.service.PostServiceTest   : Started PostServiceTest in 3.598 seconds (JVM running for 5.893)
2023-01-09 08:51:37.793  INFO 1300 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-01-09 08:51:37.793  INFO 1300 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2023-01-09 08:51:37.795  WARN 1300 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 90121, SQLState: 90121
2023-01-09 08:51:37.795 ERROR 1300 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-01-09 08:51:37.796  WARN 1300 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 90121, SQLState: 90121
2023-01-09 08:51:37.796 ERROR 1300 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-01-09 08:51:37.796  WARN 1300 --- [ionShutdownHook] o.s.b.f.support.DisposableBeanAdapter    : Invocation of destroy method failed on bean with name 'entityManagerFactory': org.hibernate.exception.JDBCConnectionException: Unable to release JDBC Connection used for DDL execution
2023-01-09 08:51:37.796  INFO 1300 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2023-01-09 08:51:37.801  INFO 1300 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
BUILD SUCCESSFUL in 9s
4 actionable tasks: 3 executed, 1 up-to-date
AM 8:51:38: Execution finished ':test --tests "com.endofma.blog.service.PostServiceTest.test4"'.

 

 

--------------------

 

그런데 Post.change()에 들어갈 인자가 늘어나게 될 경우, 인자의 순서를 잘못 배치하는 등의 실수할 가능성이 높아진다.

따라서 아래와 같이 하나의 타입을 인자로 넘겨 인자의 수를 1개로 줄이는 방법을 생각해볼 수 있다.

 

도메인에 패키지에 PostEditor.java 생성

//PostEditor.java

package com.endofma.blog.domain;

import lombok.Builder;
import lombok.Getter;

@Getter
public class PostEditor {
    //수정해야할 필드만 좁혀서 수정하기 때문에 이 값들만 수정 가능하다는 것을 직관적으로 알 수 있다.
    private final String title;
    private final String content;

    @Builder
    public PostEditor(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

 

 

//PostService.java

package com.endofma.blog.service;

import com.endofma.blog.domain.Post;
import com.endofma.blog.domain.PostEditor;
import com.endofma.blog.repository.PostRepository;
import com.endofma.blog.request.PostCreate;
import com.endofma.blog.request.PostEdit;
import com.endofma.blog.request.PostSearch;
import com.endofma.blog.response.PostResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Service
//@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;
    public PostService(PostRepository postRepository){
        this.postRepository = postRepository;
    }

    public void write(PostCreate postCreate) {
        //PostCreate 일반 클래스 ==> Post 엔티티
        Post post = Post.builder()
                .title(postCreate.getTitle())
                .content(postCreate.getContent())
                .build();

        postRepository.save(post);
    }

    //단건 조회
    public PostResponse get(Long id) {

        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글입니다.")); //있으면 post반환 없으면 에러 반환

        //응답 클래스를 분리
        return PostResponse.builder()
                .id(post.getId())
                .title(post.getTitle())
                .content(post.getContent())
                .build();

    }

//    public List<Post> getList() {
//        return postRepository.findAll();
//    }

//    public List<PostResponse> getList(Pageable pageable){
    public List<PostResponse> getList(PostSearch postSearch){

//        return postRepository.findAll(pageable).stream() //pageable
        return postRepository.getList(postSearch).stream() //QueryDsl사용
                .map(PostResponse::new)
                .collect(Collectors.toList());
    }

    //게시글 수정
    @Transactional //알아서 커밋
    public void edit(Long id, PostEdit postEdit){
        Post post = postRepository.findById(id)
                .orElseThrow(()-> new IllegalArgumentException("존재하지 않는 글입니다."));

//        post.setTitle(postEdit.getTitle());
//        post.setContent(postEdit.getContent());

//        post.change(postEdit.getTitle(), postEdit.getContent());

//        postRepository.save(post); 사실상 적어주지 않아도 된다. 대신 @Transactional을 사용

        PostEditor.PostEditorBuilder editorBuilder =  post.toEditor();
        PostEditor postEditor = editorBuilder
                .title(postEdit.getTitle())
                .content(postEdit.getContent())
                .build();

        post.edit(postEditor);

    }

}

 

//Post.java : 엔티티

package com.endofma.blog.domain;

import lombok.*;

import javax.persistence.*;

@Getter
//@Setter
@Entity
@NoArgsConstructor(access = AccessLevel.PUBLIC) //엔티티는 기본 생성자가 필요하다.
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @Lob //DB에서 큰 크기의 파일을 저장할 때 사용하는 타입(Large Object). 자바에서 긴 스트링 값으로 넘어가는 값을 DB에서 저장할 수 있도록 사용.
    private String content;



    @Builder
    public Post(String title, String content){
        this.title = title;
        this.content = content;
    }

//    public void change(String title, String content){
//        this.title = title;
//        this.content = content;
//    }

    //빌더 클래스를 넘겨준다.
    public PostEditor.PostEditorBuilder toEditor(){
        return PostEditor.builder()
                .title(title)
                .content(content);
//                .build(); //빌드를 해버리면 PostEditor가 생성된다. 우리가 필요한 건 빌더이다.
    }

    public void edit(PostEditor postEditor) { //빌더를 사용하여 고정된 PostEditor가 넘어온다. 인자 수가 1개로 줄어든다.
        title = postEditor.getTitle();
        content = postEditor.getContent();
    }


//    public String getTitle(){
//        return this.title.substring(0, 10); //10글자만 반환
        //그런데 Post.java에서 수정하게 되면 나중에 다른 기능이 추가되었을 때 기능이 충돌할 수 있다.
        //입력값 전부가 필요한 경우가 있는데 10글자밖에 반환하지 않는 경우가 있기 때문. 문제발견이 늦어질 수 있다.
        //엔티티에 getter 메서드를 만들 때에는 절대 서비스 정책을 넣지 말 것.
        //그럼 어떻게 10글자만 반환할까? 서비스 정책에 맞는 응답 클래스를 만들어주는 것이다.
//        return this.title;
//    }



}

 

위와 같은 테스트를 수행했을 때 성공한다.

---

 

그런데 문제가 하나 있다. 테스트에서 content의 값을 지정해 주지 않았을 때 null이 들어갈 가능성이 있다. 사실 이 사항은 클라이언트와의 조정이 필요하다. 수정할 것만 보낼 것인지 또는 원본데이터를 다 보낼 것인지.

만약 필요한 데이터만 보낸다고 하면 아래와 같이 if문을 통해 null을 걸러줄 수 있다.

//PostService.java

package com.endofma.blog.service;

import com.endofma.blog.domain.Post;
import com.endofma.blog.domain.PostEditor;
import com.endofma.blog.repository.PostRepository;
import com.endofma.blog.request.PostCreate;
import com.endofma.blog.request.PostEdit;
import com.endofma.blog.request.PostSearch;
import com.endofma.blog.response.PostResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Service
//@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;
    public PostService(PostRepository postRepository){
        this.postRepository = postRepository;
    }

    public void write(PostCreate postCreate) {
        //PostCreate 일반 클래스 ==> Post 엔티티
        Post post = Post.builder()
                .title(postCreate.getTitle())
                .content(postCreate.getContent())
                .build();

        postRepository.save(post);
    }

    //단건 조회
    public PostResponse get(Long id) {

        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글입니다.")); //있으면 post반환 없으면 에러 반환

        //응답 클래스를 분리
        return PostResponse.builder()
                .id(post.getId())
                .title(post.getTitle())
                .content(post.getContent())
                .build();

    }

//    public List<Post> getList() {
//        return postRepository.findAll();
//    }

//    public List<PostResponse> getList(Pageable pageable){
    public List<PostResponse> getList(PostSearch postSearch){

//        return postRepository.findAll(pageable).stream() //pageable
        return postRepository.getList(postSearch).stream() //QueryDsl사용
                .map(PostResponse::new)
                .collect(Collectors.toList());
    }

    //게시글 수정
    @Transactional //알아서 커밋
    public void edit(Long id, PostEdit postEdit){
        Post post = postRepository.findById(id)
                .orElseThrow(()-> new IllegalArgumentException("존재하지 않는 글입니다."));

//        post.setTitle(postEdit.getTitle());
//        post.setContent(postEdit.getContent());

//        post.change(postEdit.getTitle(), postEdit.getContent());

//        postRepository.save(post); 사실상 적어주지 않아도 된다. 대신 @Transactional을 사용

        PostEditor.PostEditorBuilder editorBuilder =  post.toEditor();

        if(postEdit.getTitle() != null) {
            editorBuilder.title(postEdit.getTitle());
        }

        if(postEdit.getContent() != null) {
            editorBuilder.content(postEdit.getContent());
        }


//        post.edit(postEditor);
        post.edit(editorBuilder.build());

    }

}

 

그러나 나중에 어떤 데이터가 필요할지 모르므로 다 넘겨주는 것도 괜찮다.

 

---------

여긴 컨트롤러까지 만들었다. 수정 내용을 확인할 수 있도록 제이슨을 반환하는 기능 역시 존재한다.

그러나 테스트 용이며, 최종적으로는 수정 내용을 반환하지 않는 것으로 한다.

 

//PostController.java

package com.endofma.blog.controller;

//...


@Slf4j
@RestController
@RequiredArgsConstructor
public class PostController {
    private final PostService postService;


    @PostMapping("/posts")
    public void post(@RequestBody @Valid PostCreate request) {
        postService.write(request);
//        return Map.of();
    }

    //단건 조회
    @GetMapping("/posts/{postId}")
    public PostResponse get(@PathVariable Long postId){

        PostResponse response = postService.get(postId);
        return response;
    }

    //여러 글 조회(글 목록 가져오기)
    //  /posts
    @GetMapping("/posts")

    //글이 너무 많은 경우 비용이 너무 많이 든다.
    //DB가 뻗을 수 있음.
    //DB -> 애플리케이션 서버로 전달하는 시간, 트래픽 비용이 많이 발생할 수 있다.
    //따라서 페이지 설정

//    원래는 int로 받았음
//   public List<PostResponse> getList(@RequestParam int page){

// 그러나 사용의 용이성을 위해 pageable을 사용함
//   public List<PostResponse> getList(@PageableDefault Pageable pageable){ //1로 넘겨도 0으로 보정해서 넣어줌.
    //근데 PageableDefault의 기본 size가 10이라 yml에서 설정해도 먹히지 않는다.
    //이 떄는 어노테이션을 그대로 유지하면서 size를 파라미터로 넣어주는 방법이 있다.
    //public List<PostResponse> getList(@PageableDefault(size=10) Pageable pageable){

    //또는 어노테이션을 빼고 application.yml에서 default-page-size를 설정하여 해결할 수 수있다.

    //전에는 Pageable을 사용했으나 여러 요구사항을 수용 할 수 있는 클래스를 사용하기 위해
    //postSearch 클래스 사용


   public List<PostResponse> getList(@ModelAttribute PostSearch postSearch){//따로 만든 요청클래스 사용하려고 함
//   public List<PostResponse> getList(Pageable pageable){
        return postService.getList(postSearch);

    }

    @PatchMapping("/posts/{postId}")
    public PostResponse edit(@PathVariable Long postId, @RequestBody @Valid PostEdit request){
        return postService.edit(postId, request);
    }

}

 

 

//PostService.java

//...

@Slf4j
@Service
//@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;
    public PostService(PostRepository postRepository){
        this.postRepository = postRepository;
    }

    public void write(PostCreate postCreate) {
        //PostCreate 일반 클래스 ==> Post 엔티티
        Post post = Post.builder()
                .title(postCreate.getTitle())
                .content(postCreate.getContent())
                .build();

        postRepository.save(post);
    }

    //단건 조회
    public PostResponse get(Long id) {

        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글입니다.")); //있으면 post반환 없으면 에러 반환

        //응답 클래스를 분리
        return PostResponse.builder()
                .id(post.getId())
                .title(post.getTitle())
                .content(post.getContent())
                .build();

    }

//    public List<Post> getList() {
//        return postRepository.findAll();
//    }

//    public List<PostResponse> getList(Pageable pageable){
    public List<PostResponse> getList(PostSearch postSearch){

//        return postRepository.findAll(pageable).stream() //pageable
        return postRepository.getList(postSearch).stream() //QueryDsl사용
                .map(PostResponse::new)
                .collect(Collectors.toList());
    }

    //게시글 수정
    @Transactional //알아서 커밋
    public PostResponse edit(Long id, PostEdit postEdit){
        Post post = postRepository.findById(id)
                .orElseThrow(()-> new IllegalArgumentException("존재하지 않는 글입니다."));

//        post.setTitle(postEdit.getTitle());
//        post.setContent(postEdit.getContent());

//        post.change(postEdit.getTitle(), postEdit.getContent());

//        postRepository.save(post); 사실상 적어주지 않아도 된다. 대신 @Transactional을 사용

        PostEditor.PostEditorBuilder editorBuilder =  post.toEditor();

        PostEditor postEditor = editorBuilder
                .title(postEdit.getTitle())
                .content(postEdit.getContent())
                .build();

        post.edit(postEditor);

        return new PostResponse(post); //수정된 내용 반환

    }

}

 

//PostServiceTest.java

package com.endofma.blog.service;

//import...

@SpringBootTest
class PostServiceTest {

    @Autowired
    private PostService postService;

    @Autowired
    private PostRepository postRepository;

    @BeforeEach
    void clean(){
        postRepository.deleteAll();
    }

    @Test
    @DisplayName("글 작성")
    void test1() {
        //given
        PostCreate postCreate = PostCreate.builder()
                .title("제목입니다.")
                .content("내용입니다.")
                .build();

        //when
        postService.write(postCreate);

        //then
        Assertions.assertEquals(1L, postRepository.count());
        Post post = postRepository.findAll().get(0);
        assertEquals("제목입니다.", post.getTitle());
        assertEquals("내용입니다.", post.getContent());

    }

    @Test
    @DisplayName("글 1개 조회")
    void test2(){
        //given
        Post requestPost = Post.builder()
                .title("foo")
                .content("bar")
                .build();
        postRepository.save(requestPost);

        //when
        PostResponse response = postService.get(requestPost.getId());

        //then
        Assertions.assertNotNull(response);
        assertEquals(1L, postRepository.count());
        assertEquals("foo", response.getTitle());
        assertEquals("bar", response.getContent());
    }

    @Test
    @DisplayName("글 1페이지 조회")
    void test3(){
        //given
        List<Post> requestPost = IntStream.range(1, 20) //for (int =0; i<30; i++)
                .mapToObj(i -> Post.builder()
                        .title("foo " + i)
                        .content("bar " + i)
                        .build())
                .collect(Collectors.toList());

        postRepository.saveAll(requestPost);


        // sql -> select, limit, offset 알아야 함.

//        Pageable pageableRequest = PageRequest.of(0, 5, Sort.Direction.DESC, "id");
        PostSearch postSearch = PostSearch.builder()
                .page(1)
                .size(10)
                .build();


        //when
        List<PostResponse> posts = postService.getList(postSearch);

       //then
        assertEquals(10L, posts.size());
        assertEquals("foo 19", posts.get(0).getTitle());

    }



    @Test
    @DisplayName("글 제목 수정")
    void test4(){
        //given
        Post post = Post.builder()
                .title("블로그 ")
                .content("제이드빌 ")
                .build();

        postRepository.save(post);

        PostEdit postEdit = PostEdit.builder()
                .title("블로그 수정")
                .content("제이드빌 ")
                .build();

        //when
        postService.edit(post.getId(), postEdit);

        //then
        Post chengedPost = postRepository.findById(post.getId())
                .orElseThrow(() -> new RuntimeException("글이 존재하지 않습니다. id=" + post.getId()));

        Assertions.assertEquals("블로그 수정", chengedPost.getTitle());
        Assertions.assertEquals("제이드빌 ", chengedPost.getContent());
    }

    @Test
    @DisplayName("글 제목 수정")
    void test5(){
        //given
        Post post = Post.builder()
                .title("블로그 ")
                .content("제이드빌 ")
                .build();

        postRepository.save(post);

        PostEdit postEdit = PostEdit.builder()
                .title("블로그 수정")
                .content("부림동 ")
                .build();

        //when
        postService.edit(post.getId(), postEdit);

        //then
        Post chengedPost = postRepository.findById(post.getId())
                .orElseThrow(() -> new RuntimeException("글이 존재하지 않습니다. id=" + post.getId()));

        Assertions.assertEquals("블로그 수정", chengedPost.getTitle());
        Assertions.assertEquals("부림동 ", chengedPost.getContent());
    }

}

 

 

//PostControllerTest.java

//import...


//@WebMvcTest //간단한 웹테스트만 가능. 우린 스프링 전반에 걸쳐 여러 가지를 만들었기 때문에 @SpringBootTest가 필요
@AutoConfigureMockMvc //@WebMvcTest가 없어지면 기존 테스트가 안되므로 @WebMvcTest를 구성하는 애노테이션을 떼내어 붙였다.
@SpringBootTest
class PostControllerTest {



    @Autowired
    private MockMvc mockMvc; ////Could not autowire. No beans of 'MockMvc' type found.

    @Autowired
    private PostRepository postRepository; //DB저장 테스트를 위해 주입

    @BeforeEach
    void clean() {
        postRepository.deleteAll();
    }

    @Test
    @DisplayName("/posts 요청시 hello world를 출력한다")
    void test() throws Exception {
        //given
//        PostCreate request = new PostCreate("제목입니다.", "내용입니다.");
        PostCreate request = PostCreate.builder()
                .title("제목입니다.")
                .content("내용입니다.")
                .build();

        //jackson을 사용하여 객체를 Json 형태로 바꿔준다.(json을 처리해 줌)
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(request);
//        @Autowired private ObjectMapper objectMapper; 이렇게 주입해서도 쓸 수 있다.

        System.out.println(json);

        //expected
        //기본적으로 Content-Type을 application/json으로 보냄. 예전에는 application/x-www-form-urlencoded를 썼다.
        mockMvc.perform(post("/posts")
                                .contentType(APPLICATION_JSON) //기본값이라 주석처리
//                        .content("{\"title\": \"제목입니다.\", \"content\": \"내용입니다.\"}") //json 형태로 값 넣어주기
                                //.content에는 byte나 String만 들어가진다. 따라서 jackson으로 Json변환처리를 하지 않은 request를 넣을 수는 없다.

                                .content(json) //jackson을 사용하여 json이 된 객체. 클래스에 게터가 존재해야 한다.
                )
                .andExpect(status().isOk()) //http response 가 200인지
                .andExpect(content().string("")) // 내용이 hello world인지
                .andDo(print()); //요청에 대한 전반적인 요약을 출력해준다.

    }


    @Test
    @DisplayName("/posts 요청시 title 값은 필수다")
    void test2() throws Exception {

        //given
        PostCreate request = PostCreate.builder()
                //.title("제목입니다.")
                .content("내용입니다.")
                .build();

        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(request);

        //expected
        mockMvc.perform(post("/posts")
                        .contentType(APPLICATION_JSON)
                        .content(json)

                )
                .andExpect(status().isBadRequest()) //.OK()
                .andExpect(jsonPath("$.code").value("400")) //json 검증
                .andExpect(jsonPath("$.message").value("잘못된 요청입니다!"))
//                .andExpect(jsonPath("$.validation.title").value("타이틀을 입력하세요!"))
                .andDo(print());

    }



    @Test
    @DisplayName("/posts 요청시 DB에 값이 저장된다.")
    void test3() throws Exception {
        //given
        PostCreate request = PostCreate.builder()
                .title("제목입니다.")
                .content("내용입니다.")
                .build();

        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(request);

        //when
        mockMvc.perform(post("/posts")
                        .contentType(APPLICATION_JSON)
                        .content(json)
                )
                .andExpect(status().isOk())
                .andDo(print());

        //then
        assertEquals(1L, postRepository.count()); //하나의 값이 있을 거라고 예상. 일치

        //DB에 잘 들어갔는지 확인
        Post post = postRepository.findAll().get(0); //가장 처음 데이터 가져옴
        assertEquals("제목입니다.", post.getTitle());
        assertEquals("내용입니다.", post.getContent());
    }

    @Test
    @DisplayName("글 1개 조회")
    void test4() throws Exception{
        //given
        Post post = Post.builder()
                .title("123456789012345")
                .content("bar")
                .build();
        System.out.println("post.getId!!1="+ post.getId()); //==null

        postRepository.save(post);
        System.out.println("post.getId!!2="+ post.getId()); //==1

        //expected
        mockMvc.perform(get("/posts/{postId}", post.getId())
                        .contentType(APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(post.getId()))
                .andExpect(jsonPath("$.title").value("1234567890"))
                .andExpect(jsonPath("$.content").value("bar"))
                .andDo(print());
    }

    @Test
    @DisplayName("글 여러 개 조회")
    void test5() throws Exception{
        //given
        List<Post> requestPost = IntStream.range(1, 20) //for (int =0; i<30; i++)
                .mapToObj(i -> Post.builder()
                        .title("foo " + i)
                        .content("bar " + i)
                        .build()).collect(Collectors.toList());
        postRepository.saveAll(requestPost);


        //expected
//        mockMvc.perform(get("/posts?page=1")
        mockMvc.perform(get("/posts?page=1&size=10")
//        mockMvc.perform(get("/posts?page=1&sort=id,desc&size=5")
                //size를 매번 넣어주기 귀찮으면 application.yml에 가서 default-page-size를 설정해 준다.
                //보통 현업에서는 10개보기 ,20개 보기 등의 옵션이 없으면 그냥 서버가 주는대로 받기 때문에
                //size를 파라미터로 넘기는 경우가 많지는 않다.
                        .contentType(APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()", is(10)))
//                .andExpect(jsonPath("$[0].id").value(30))
                .andExpect(jsonPath("$[0].title").value("foo 19"))
                .andExpect(jsonPath("$[0].content").value("bar 19"))
                .andDo(print());



    }
    @Test
    @DisplayName("페이지를 0으로 요청해도 첫 페이지를 가져온다.")
    void test6() throws Exception{
        //given
        List<Post> requestPost = IntStream.range(1, 20) //for (int =0; i<30; i++)
                .mapToObj(i -> Post.builder()
                        .title("foo " + i)
                        .content("bar " + i)
                        .build()).collect(Collectors.toList());
        postRepository.saveAll(requestPost);


        //expected
//        mockMvc.perform(get("/posts?page=1")
        mockMvc.perform(get("/posts?page=0&size=10")
//        mockMvc.perform(get("/posts?page=1&sort=id,desc&size=5")
                        //size를 매번 넣어주기 귀찮으면 application.yml에 가서 default-page-size를 설정해 준다.
                        //보통 현업에서는 10개보기 ,20개 보기 등의 옵션이 없으면 그냥 서버가 주는대로 받기 때문에
                        //size를 파라미터로 넘기는 경우가 많지는 않다.
                        .contentType(APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()", is(10)))
//                .andExpect(jsonPath("$[0].id").value(30))
                .andExpect(jsonPath("$[0].title").value("foo 19"))
                .andExpect(jsonPath("$[0].content").value("bar 19"))
                .andDo(print());
    }

    @Test
    @DisplayName("페이지를 0으로 요청해도 첫 페이지를 가져온다.")
    void test7() throws Exception{
        //given
        ObjectMapper objectMapper = new ObjectMapper();

        Post post = Post.builder()
                .title("블로그 ")
                .content("제이드빌 ")
                .build();

        postRepository.save(post);

        PostEdit postEdit = PostEdit.builder()
                .title("블로그 수정")
                .content("부림동 ")
                .build();

        //expected
        mockMvc.perform(patch("/posts/{postId}", post.getId())
                        .contentType(APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(postEdit)))
                .andExpect(status().isOk())
                .andDo(print());



    }
}

 

결과

 

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.6)

2023-01-09 22:02:19.643  INFO 30996 --- [    Test worker] c.e.blog.controller.PostControllerTest   : Starting PostControllerTest using Java 11.0.12 on DESKTOP-Q7HBM41 with PID 30996 (started by user in D:\personal\blog)
2023-01-09 22:02:19.645  INFO 30996 --- [    Test worker] c.e.blog.controller.PostControllerTest   : No active profile set, falling back to 1 default profile: "default"
2023-01-09 22:02:20.672  INFO 30996 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-01-09 22:02:20.764  INFO 30996 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 77 ms. Found 1 JPA repository interfaces.
2023-01-09 22:02:21.485  INFO 30996 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-01-09 22:02:21.812  INFO 30996 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-01-09 22:02:21.918  INFO 30996 --- [    Test worker] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-01-09 22:02:21.990  INFO 30996 --- [    Test worker] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.6.14.Final
2023-01-09 22:02:22.263  INFO 30996 --- [    Test worker] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2023-01-09 22:02:22.515  INFO 30996 --- [    Test worker] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2023-01-09 22:02:23.421  INFO 30996 --- [    Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-01-09 22:02:23.434  INFO 30996 --- [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-01-09 22:02:24.269  WARN 30996 --- [    Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-01-09 22:02:24.692  INFO 30996 --- [    Test worker] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:blog'
2023-01-09 22:02:25.216  INFO 30996 --- [    Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2023-01-09 22:02:25.216  INFO 30996 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2023-01-09 22:02:25.219  INFO 30996 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 1 ms
2023-01-09 22:02:25.251  INFO 30996 --- [    Test worker] c.e.blog.controller.PostControllerTest   : Started PostControllerTest in 6.106 seconds (JVM running for 8.78)

MockHttpServletRequest:
      HTTP Method = PATCH
      Request URI = /posts/1
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"51"]
             Body = {"title":"블로그 수정","content":"부림동 "}
    Session Attrs = {}

Handler:
             Type = com.endofma.blog.controller.PostController
           Method = com.endofma.blog.controller.PostController#edit(Long, PostEdit)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"id":1,"title":"블로그 수정","content":"부림동 "}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
2023-01-09 22:02:26.048  INFO 30996 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-01-09 22:02:26.050  INFO 30996 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2023-01-09 22:02:26.053  WARN 30996 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 90121, SQLState: 90121
2023-01-09 22:02:26.053 ERROR 30996 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-01-09 22:02:26.054  WARN 30996 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 90121, SQLState: 90121
2023-01-09 22:02:26.054 ERROR 30996 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-01-09 22:02:26.055  WARN 30996 --- [ionShutdownHook] o.s.b.f.support.DisposableBeanAdapter    : Invocation of destroy method failed on bean with name 'entityManagerFactory': org.hibernate.exception.JDBCConnectionException: Unable to release JDBC Connection used for DDL execution
2023-01-09 22:02:26.055  INFO 30996 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2023-01-09 22:02:26.059  INFO 30996 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
BUILD SUCCESSFUL in 23s
4 actionable tasks: 3 executed, 1 up-to-date
PM 10:02:26: Execution finished ':test --tests "com.endofma.blog.controller.PostControllerTest.test7"'.

 

위에서도 말했지만 이건 반환 확인용이며 최종적으로는 수정된 내용을 반환하지 않는 것으로 하였다.

'Projects > blog' 카테고리의 다른 글

blog11: 게시글 삭제  (0) 2023.01.10
blog10: 게시글 수정 2 (오류수정, 보충)  (0) 2023.01.09
blog09: 페이징 처리(QueryDSL) 2  (0) 2023.01.04
blog09: 페이징 처리(QueryDSL) 1  (0) 2023.01.04
blog08: 페이징 처리  (0) 2023.01.04
Comments