관리 메뉴

bright jazz music

blog04: 작성글 저장1 - 게시글 저장 구현 본문

Projects/blog

blog04: 작성글 저장1 - 게시글 저장 구현

bright jazz music 2022. 12. 28. 09:43

포스트 저장을 위한 클래스 생성과 설정

  • service 패키지 생성: PostService.java 생성
  • repository 패키지 생성: PostRepository.java 인터페이스 생성( JPARepository 인터페이스 구현(여기서는 extends). JPARepository 인터페이스의 제네릭 파라미터로는 Post 엔티티와 Long형식의 PK 전달)
  • domain 패키지 생성:
  • Post 클래스 생성(@Entity 애노테이션 부착)

 

기본적인 흐름

  • Json 데이터 ==> PostController(PostCreate 타입으로 바인딩) ==> PostService(일반 클래스인 PostCreate타입을 엔티티로 변환) ==> PostRepository 호출해서 save

 

 

프로젝트 구조

 

 

//PostController.java


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

    // 생성자를 통한 주입. 여기서는 롬복 @RequiredArgsConstructor를 사용한다.
    //    public PostController(PostService postService){
    //        this.postService = postService;
    //    }

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

}

 

 

//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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class PostService {
//    @Autowired // 필드 인젝션을 사용하는 것은 바람직하지 않다. 따라서 생성자를 사용해서 생성해준다.
//    private PostRepository postRepository;

    private final PostRepository postRepository;

//    /생성자를 사용한 필드 인젝션을 사용하면 생성자가 새로 생긴다. 그러나 롬복의 @RequiredArgsConstructor를 사용할 수도 있다.
//    public PostService(PostRepository postRepository) {
//        this.postRepository = postRepository;
//    }


    public void write(PostCreate postCreate) {
//        postRepository.save(postCreate); 이렇게는 들어가지 않는다. postCreate는 dto 형태이지 엔티티가 아니기 때문이다.
//        따라서 일반 클래스인 postCreate를 엔티티 형태로 변환해 줘야 한다

        Post post = new Post(postCreate.getTitle(), postCreate.getContent());
//        post.title = postCreate.getTitle(); Post의 접근 제한자를 public으로 바꿔서 이렇게 하는 것은 좋지 않다. 값이 변경될 수 있다.
//        따라서 필드의 접근 제한자를 private으로 바꾼 뒤 생성자를 통해 생성하도록 만드는 것이 좋다.

        postRepository.save(post);
    }
}

 

 

//Post.java : 엔티티

package com.endofma.blog.domain;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import javax.persistence.*;

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

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

}

 

 

//PostRepository.java

package com.endofma.blog.repository;

import com.endofma.blog.domain.Post;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Post, Long> { //Post 엔티티와 primary key 형식이 들어간다.

}

 

//PostControllerTest.java

//@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 {
        //expected

        //기본적으로 Content-Type을 application/json으로 보냄. 예전에는 application/x-www-form-urlencoded를 썼다.
        mockMvc.perform(post("/posts")
                        .contentType(MediaType.APPLICATION_JSON) //기본값이라 주석처리
                        .content("{\"title\": \"제목입니다.\", \"content\": \"내용입니다.\"}") //json 형태로 값 넣어주기
                )
                .andExpect(status().isOk()) //http response 가 200인지
                .andExpect(content().string("{}")) // 내용이 hello world인지
                .andDo(print()); //요청에 대한 전반적인 요약을 출력해준다.

    }

//PostControllerTest.java

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


        mockMvc.perform(post("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"title\": null, \"content\": \"내용입니다.\"}") //title 값을 빈 스트링으로 설정하였다.
//                        .content("{\"title\": \"안녕\", \"content\": \"내용입니다.\"}") //title 값을 빈 스트링으로 설정하였다.
                )
                .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 {
        //when
        mockMvc.perform(post("/posts")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"title\": \"제목입니다.\", \"content\": \"내용입니다.\"}") //title 값을 빈 스트링으로 설정하였다.
                )
                .andExpect(status().isOk())
                .andDo(print());

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

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

 

결과

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

2022-12-28 10:30:48.027  INFO 22288 --- [    Test worker] c.e.blog.controller.PostControllerTest   : Starting PostControllerTest using Java 11.0.12 on DESKTOP-8H1PTVG with PID 22288 (started by markany-hjcha in D:\personal\blog)
2022-12-28 10:30:48.028  INFO 22288 --- [    Test worker] c.e.blog.controller.PostControllerTest   : No active profile set, falling back to 1 default profile: "default"
2022-12-28 10:30:48.593  INFO 22288 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2022-12-28 10:30:48.636  INFO 22288 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 36 ms. Found 1 JPA repository interfaces.
2022-12-28 10:30:49.057  INFO 22288 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-12-28 10:30:49.252  INFO 22288 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2022-12-28 10:30:49.319  INFO 22288 --- [    Test worker] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2022-12-28 10:30:49.377  INFO 22288 --- [    Test worker] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.6.14.Final
2022-12-28 10:30:49.564  INFO 22288 --- [    Test worker] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2022-12-28 10:30:49.741  INFO 22288 --- [    Test worker] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2022-12-28 10:30:50.360  INFO 22288 --- [    Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2022-12-28 10:30:50.368  INFO 22288 --- [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2022-12-28 10:30:50.852  WARN 22288 --- [    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
2022-12-28 10:30:51.377  INFO 22288 --- [    Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2022-12-28 10:30:51.377  INFO 22288 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2022-12-28 10:30:51.378  INFO 22288 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 1 ms
2022-12-28 10:30:51.397  INFO 22288 --- [    Test worker] c.e.blog.controller.PostControllerTest   : Started PostControllerTest in 3.678 seconds (JVM running for 5.993)

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /posts
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"60"]
             Body = {"title": "제목입니다.", "content": "내용입니다."}
    Session Attrs = {}

Handler:
             Type = com.endofma.blog.controller.PostController
           Method = com.endofma.blog.controller.PostController#post(PostCreate)

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 = {}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
2022-12-28 10:30:51.896  INFO 22288 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-12-28 10:30:51.896  INFO 22288 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2022-12-28 10:30:51.902  INFO 22288 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-12-28 10:30:51.905  INFO 22288 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
BUILD SUCCESSFUL in 7s
4 actionable tasks: 2 executed, 2 up-to-date
AM 10:30:52: Execution finished ':test --tests "com.endofma.blog.controller.PostControllerTest.test3"'.

 

개별 테스트가 서로에게 영향을 주지 않도록 처리

 

그런데 개별 테스트 메소드가 아니라 클래스 전체 테스트를 돌렸을 때는 에러가 발생한다. 왜냐하면 test1()에서 이미 한 번 데이터를 넣고 test3()에서 한 번 더 넣기 때문에 저장된 값이 2개가 되어 기대값이 1개와 달라지기 때문이다.

 

별 문제가 아닌 것처럼 보일 수 있지만 이는 나중에 작성되는 모든 테스트 케이스에 영향을 미치기 때문에 테스트 클래스 전체를 테스트 할 때 문제가 될 수 있다. 

 

개별 테스트를 하기 전에 저장공간에서 데이터를 삭제함으로써 영향을 제거할 수 있다. 각 테스트 메소드마다 삭제 문구를 넣어주는 것이다. 이는 코드가 지저분해 지고 개발자가 신경을 써줘야 하는 단점이 있다.

 

이는 @BeforeEach를 사용해서 해결할 수 있다. @BeforeEach는 각각의 메소드를 실행하기 전에 특정 구문을 실행해 준다. 아래는 예시다.

 

@AutoConfigureMockMvc
@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();
    }
//...
}

//이렇게 구문을 추가하고 클래스 전체 테스트를 실행하면 오류가 발생하지 않는다.

 

 

 

 

 

 

Comments