API 만들어 보기/게시판 API

[작성글 저장] 게시글 저장 구현

midcon 2023. 9. 22. 23:51

게시글 저장 구현

이제부터 게시판의 가장 기본적인 기능인 CRUD 의 시작이다. 우선 게시글 저장부터 구현해보자. 

JpaRepository 인터페이스를 사용하여 자동 주입되는 JpaRepository 구현체의 단건 저장 기능을 사용할것이다.

컨트롤러에서 바로 Repository 를 호출해도 되지만 PostService 를 만들어서 서비스 계층에서 호출하는게 바람직하다.

가장 간단한 방법으로 게시글 저장을 구현하고 테스트까지 해보자.

 

컨트롤러에서 클라이언트로 어떻게 응답을 내려줄것인가?

[클라이언트에서 요구하는 응답 케이스]

  1.  저장한 데이터 Entity 를 response 로 반환한다.
  2.  저장한 데이터의 primary_id 를 response 로 반환한다.
     -> Client 에서는 수신한 id 로 글 조회 API 를 통해서 데이터를 수신받음
  3.  응답 필요 없다.
     -> 클라이언트에서 모든 글 데이터 context 를 잘 관리함

이처럼 클라이언트 개발자마다 실력도 스타일도 다 다르기 때문에 요구하는 응답값이 다 다르다.

따라서 서버에서 반드시 이렇게 할겁니다 !! 라고 하는건 안좋다. 서버에서 유연하게 대응하는게 좋다.
한 번에 일괄적으로 잘 처리되는 케이스는 없다. 결국 잘 관리되는 형태가 중요하기 때문에 코드를 잘 짜야한다.

 

PostController

요청 데이터를 PostCreate 객체로 받아 서비스로 넘겨주고 등록한 Post 정보를 반환한다.

엔티티 자체를 응답으로 보내주는건 좋지 않지만 우선은 간략하게 구현하겠다.

@RestController
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

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

 

PostCreate

@Getter
@ToString
@NoArgsConstructor
public class PostCreate {

    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    @NotBlank(message = "내용을 입력해주세요.")
    private String content;

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

ㄴ @NoArgsConstructor

기본 생성자를 만들어주는 Lombok 애노테이션이다.

Build설정이 Gradle 로 되어있으면 기본 생성자 없이도 데이터를 객체에 매핑시켜주는 역직렬화가 잘 되지만

Build설정이 IntelliJ 로 되어 있으면 기본 생성자 없이는 역직렬화가 안된다.

 

PostService

컨트롤러에서 받은 요청데이터를 바탕으로 Post 객체를 만들어 Repository 로 넘겨준다.

그 후 컨트롤러에 저장한 Post 정보를 반환한다.

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public Post write(PostCreate request) {
        Post post = new Post(request.getTitle(), request.getContent());
        return postRepository.save(post);
    }
}

 

Post

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @Lob
    private String content;

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

ㄴ @NoArgsConstructor

기본 생성자를 만들어주는 Lombok 애노테이션이다.

JPA 에서 엔티티는 기본 생성자가 있어야 DB 와 매핑을 할 수 있기 때문에 필요하다.

다른 곳에서 남용되는것을 막기 위해 제한자 설정을 AccesLevel.PROTECTED 로 한다.

 

PostRepository

JpaRepository 인터페이스에 의존성 주입된 JpaRepository 구현체에 구현돼있는 save() 메서드를 사용하여

DB에 Post 를 저장한다.

public interface PostRepository extends JpaRepository<Post, Long> {

}

 


PostServiceTest

글 작성 시 DB에 Post 를 저장하고 제대로 저장됐는지 확인하는 테스트이다.

@SpringBootTest
class PostServiceTest {

    @Autowired
    private PostService postService;

    @Autowired
    private PostRepository postRepository;

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

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

        // when
        postService.write(postCreate);
        Post post = postRepository.findAll().get(0);

        // then
        assertEquals(1L, postRepository.count());
        assertEquals("글 제목입니다", post.getTitle());
        assertEquals("글 내용입니다 하하", post.getContent());
    }
}

 

테스트를 수행하면 테스트가 성공함을 확인할 수 있다.

 


PostControllerTest

처음 테스트는 컨트롤러에서 응답이 변경됐기 때문에 글 작성 후 제대로 응답을 내려주는지 확인하는 테스트이다.

2번째 테스트는 DB에 Post 를 저장하고 제대로 저장됐는지 확인하는 테스트이다.

@SpringBootTest
@AutoConfigureMockMvc
class PostControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private PostRepository postRepository;

    @BeforeEach
    void clean() {
        postRepository.deleteAll();
    }
    
    @DisplayName("/posts 요청 시 저장한 Post 를 출력한다.")
    @Test
    void post1() throws Exception {
        // when
        mockMvc.perform(
                MockMvcRequestBuilders.post("/posts")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"title\" : \"글 제목입니다\", \"content\" : \"글 내용입니다 하하\"}")
            )
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.title").value("글 제목입니다"))
            .andExpect(jsonPath("$.content").value("글 내용입니다 하하"))
            .andDo(print());
    }

    @DisplayName("/posts 요청 시 DB에 값이 저장된다.")
    @Test
    void post3() throws Exception {
        // when
        mockMvc.perform(
                MockMvcRequestBuilders.post("/posts")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"title\" : \"글 제목입니다\", \"content\" : \"글 내용입니다 하하\"}")
            )
            .andExpect(status().isOk())
            .andDo(print());
        Post post = postRepository.findAll().get(0);

        // then
        assertEquals(1L, postRepository.count());
        assertEquals("글 제목입니다", post.getTitle());
        assertEquals("글 내용입니다 하하", post.getContent());
    }
}

ㄴ @SpringBootTest

기존의 @WebMvcTest 는 프레젠테이션 계층과 관련된 빈들만 의존성 주입을 해준다.

테스트를 진행하기 위해서는 Repository 호출이 필요하기 때문에 해당 애노테이션을 사용하였다.

ㄴ @AutoConfigureMockMvc

@SpringBootTest 에는 MockMvc 에 의존성을 주입해주는 기능이 없다.

따라서 테스트를 진행하기 위해 MockMvc 의존성 주입이 가능한 해당 애노테이션을 사용하였다.

ㄴ @BeforeEach

모든 테스트 간의 초기 조건은 항상 동일하고 순서가 달라져도 결과는 항상 같아야하기 때문에

각각의 테스트가 시작하기 전에 Repository 의 데이터를 모두 삭제해야한다.

해당 애노테이션을 사용하면 각각의 테스트를 시작하기 전에 지정해둔 메서드를 실행할 수 있다.

 

 

테스트를 수행하면 테스트가 성공함을 확인할 수 있다.